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

@@ -71,6 +71,12 @@ class AppConfig {
'api_key' => '', // À remplir avec la clé API SMS OVH
'api_secret' => '', // À remplir avec le secret API SMS OVH
],
'backup' => [
'encryption_key' => 'K8mN2pQ5rT9wX3zA6bE1fH4jL7oS0vY2', // Clé de 32 caractères pour AES-256
'compression' => true,
'compression_level' => 6,
'cipher' => 'AES-256-CBC'
],
];
// Configuration PRODUCTION
@@ -336,6 +342,24 @@ class AppConfig {
return $this->clientIp;
}
/**
* Retourne la configuration des backups
*
* @return array Configuration des backups
*/
public function getBackupConfig(): array {
return $this->getCurrentConfig()['backup'];
}
/**
* Retourne la clé de chiffrement des backups
*
* @return string Clé de chiffrement des backups
*/
public function getBackupEncryptionKey(): string {
return $this->getCurrentConfig()['backup']['encryption_key'];
}
/**
* Détermine l'adresse IP du client en tenant compte des proxys et load balancers
*

File diff suppressed because it is too large Load Diff

View File

@@ -157,7 +157,6 @@ class LoginController {
'first_name' => $user['first_name'] ?? '',
'fk_role' => $user['fk_role'] ?? '0',
'fk_entite' => $user['fk_entite'] ?? '0',
// 'interface' supprimée pour se baser uniquement sur le rôle
];
Session::login($sessionData);
@@ -229,13 +228,16 @@ class LoginController {
} elseif ($interface === 'admin' && $user['fk_role'] == 2) {
// Interface admin avec rôle 2 : les 3 dernières opérations dont l'active
$operationLimit = 3;
} elseif ($interface === 'admin' && $user['fk_role'] > 2) {
// Interface admin avec rôle > 2 : les 10 dernières opérations dont l'active
$operationLimit = 10;
} else {
// Autres cas : pas d'opérations
$operationLimit = 0;
}
if ($operationLimit > 0 && !empty($user['fk_entite'])) {
$operationQuery = "SELECT id, libelle, date_deb, date_fin
$operationQuery = "SELECT id, fk_entite, libelle, date_deb, date_fin, chk_active
FROM operations
WHERE fk_entite = ?";
@@ -254,9 +256,11 @@ class LoginController {
foreach ($operations as $operation) {
$operationsData[] = [
'id' => $operation['id'],
'name' => $operation['libelle'],
'fk_entite' => $operation['fk_entite'],
'libelle' => $operation['libelle'],
'date_deb' => $operation['date_deb'],
'date_fin' => $operation['date_fin']
'date_fin' => $operation['date_fin'],
'chk_active' => $operation['chk_active']
];
}
@@ -435,21 +439,29 @@ class LoginController {
// Déchiffrement du nom
if (!empty($membre['encrypted_name'])) {
$membreItem['name'] = ApiService::decryptData($membre['encrypted_name']);
} else {
$membreItem['name'] = '';
}
// Déchiffrement du nom d'utilisateur
if (!empty($membre['encrypted_user_name'])) {
$membreItem['username'] = ApiService::decryptSearchableData($membre['encrypted_user_name']);
} else {
$membreItem['username'] = '';
}
// Déchiffrement du téléphone
if (!empty($membre['encrypted_phone'])) {
$membreItem['phone'] = ApiService::decryptData($membre['encrypted_phone']);
} else {
$membreItem['phone'] = '';
}
// Déchiffrement du mobile
if (!empty($membre['encrypted_mobile'])) {
$membreItem['mobile'] = ApiService::decryptData($membre['encrypted_mobile']);
} else {
$membreItem['mobile'] = '';
}
// Déchiffrement de l'email
@@ -458,6 +470,8 @@ class LoginController {
if ($decryptedEmail) {
$membreItem['email'] = $decryptedEmail;
}
} else {
$membreItem['email'] = '';
}
$membresData[] = $membreItem;

File diff suppressed because it is too large Load Diff

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);
}
}
}

View File

@@ -45,8 +45,41 @@ class Router {
$this->get('entites/postal/:code', ['EntiteController', 'getEntiteByPostalCode']);
$this->put('entites/:id', ['EntiteController', 'updateEntite']);
// Routes opérations
$this->get('operations', ['OperationController', 'getOperations']);
$this->get('operations/:id', ['OperationController', 'getOperationById']);
$this->post('operations', ['OperationController', 'createOperation']);
$this->put('operations/:id', ['OperationController', 'updateOperation']);
$this->delete('operations/:id', ['OperationController', 'deleteOperation']);
// Routes d'export d'opérations
$this->get('operations/:id/export/excel', ['OperationController', 'exportExcel']);
$this->get('operations/:id/export/json', ['OperationController', 'exportJson']);
$this->get('operations/:id/export/full', ['OperationController', 'exportFull']);
$this->get('operations/:id/backups', ['OperationController', 'getBackups']);
$this->get('operations/:id/backups/:backup_id', ['OperationController', 'downloadBackup']);
$this->delete('operations/:id/backups/:backup_id', ['OperationController', 'deleteBackup']);
// Routes passages
$this->get('passages', ['PassageController', 'getPassages']);
$this->get('passages/:id', ['PassageController', 'getPassageById']);
$this->get('passages/operation/:operation_id', ['PassageController', 'getPassagesByOperation']);
$this->post('passages', ['PassageController', 'createPassage']);
$this->put('passages/:id', ['PassageController', 'updatePassage']);
$this->delete('passages/:id', ['PassageController', 'deletePassage']);
// Routes villes
$this->get('villes', ['VilleController', 'searchVillesByPostalCode']);
// Routes fichiers
$this->get('files/browse', ['FileController', 'browse']);
$this->get('files/search', ['FileController', 'search']);
$this->get('files/stats', ['FileController', 'getStats']);
$this->get('files/metadata', ['FileController', 'getMetadata']);
$this->get('files/list/:support/:id', ['FileController', 'listBySupport']);
$this->get('files/info/:id', ['FileController', 'getFileInfo']);
$this->get('files/download/:id', ['FileController', 'download']);
$this->delete('files/:id', ['FileController', 'deleteFile']);
}
public function handle(): void {

View File

@@ -0,0 +1,304 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../Config/AppConfig.php';
require_once __DIR__ . '/LogService.php';
use Exception;
/**
* Service de chiffrement et compression des sauvegardes
*
* Ce service gère le processus complet de sécurisation des backups JSON :
* 1. Compression GZIP pour réduire la taille
* 2. Chiffrement AES-256-CBC pour la sécurité
* 3. Déchiffrement et décompression pour la restauration
*/
class BackupEncryptionService {
private AppConfig $appConfig;
private string $encryptionKey;
private array $backupConfig;
public function __construct() {
$this->appConfig = AppConfig::getInstance();
$this->encryptionKey = $this->appConfig->getBackupEncryptionKey();
$this->backupConfig = $this->appConfig->getBackupConfig();
}
/**
* Chiffre et compresse les données JSON d'un backup
*
* @param string $jsonData Données JSON à sauvegarder
* @return string Données chiffrées et compressées en base64
* @throws Exception En cas d'erreur de compression ou chiffrement
*/
public function encryptBackup(string $jsonData): string {
try {
// Étape 1: Compression GZIP si activée
$dataToEncrypt = $jsonData;
if ($this->backupConfig['compression']) {
$compressed = gzencode($jsonData, $this->backupConfig['compression_level']);
if ($compressed === false) {
throw new Exception('Erreur lors de la compression GZIP');
}
$dataToEncrypt = $compressed;
LogService::log('Compression backup réussie', [
'level' => 'debug',
'original_size' => strlen($jsonData),
'compressed_size' => strlen($compressed),
'compression_ratio' => round((1 - strlen($compressed) / strlen($jsonData)) * 100, 2) . '%'
]);
}
// Étape 2: Génération d'un IV aléatoire pour AES-256-CBC
$ivLength = openssl_cipher_iv_length($this->backupConfig['cipher']);
$iv = openssl_random_pseudo_bytes($ivLength);
if ($iv === false || strlen($iv) !== $ivLength) {
throw new Exception('Erreur lors de la génération de l\'IV');
}
// Étape 3: Chiffrement AES-256-CBC
$encrypted = openssl_encrypt(
$dataToEncrypt,
$this->backupConfig['cipher'],
$this->encryptionKey,
OPENSSL_RAW_DATA,
$iv
);
if ($encrypted === false) {
throw new Exception('Erreur lors du chiffrement AES-256');
}
// Étape 4: Concaténation IV + données chiffrées et encodage base64
$finalData = base64_encode($iv . $encrypted);
LogService::log('Chiffrement backup réussi', [
'level' => 'debug',
'data_size' => strlen($dataToEncrypt),
'encrypted_size' => strlen($finalData),
'cipher' => $this->backupConfig['cipher']
]);
return $finalData;
} catch (Exception $e) {
LogService::log('Erreur lors du chiffrement du backup', [
'level' => 'error',
'error' => $e->getMessage(),
'data_size' => strlen($jsonData)
]);
throw $e;
}
}
/**
* Déchiffre et décompresse les données d'un backup
*
* @param string $encryptedData Données chiffrées en base64
* @return string Données JSON originales
* @throws Exception En cas d'erreur de déchiffrement ou décompression
*/
public function decryptBackup(string $encryptedData): string {
try {
// Étape 1: Décodage base64
$rawData = base64_decode($encryptedData);
if ($rawData === false) {
throw new Exception('Erreur lors du décodage base64');
}
// Étape 2: Extraction de l'IV
$ivLength = openssl_cipher_iv_length($this->backupConfig['cipher']);
if (strlen($rawData) < $ivLength) {
throw new Exception('Données corrompues : taille insuffisante pour l\'IV');
}
$iv = substr($rawData, 0, $ivLength);
$encryptedContent = substr($rawData, $ivLength);
// Étape 3: Déchiffrement AES-256-CBC
$decrypted = openssl_decrypt(
$encryptedContent,
$this->backupConfig['cipher'],
$this->encryptionKey,
OPENSSL_RAW_DATA,
$iv
);
if ($decrypted === false) {
throw new Exception('Erreur lors du déchiffrement : clé invalide ou données corrompues');
}
LogService::log('Déchiffrement backup réussi', [
'level' => 'debug',
'encrypted_size' => strlen($encryptedData),
'decrypted_size' => strlen($decrypted)
]);
// Étape 4: Décompression GZIP si les données sont compressées
$finalData = $decrypted;
if ($this->backupConfig['compression']) {
// Vérifier si les données sont bien compressées (magic number GZIP)
if (substr($decrypted, 0, 2) === "\x1f\x8b") {
$decompressed = gzdecode($decrypted);
if ($decompressed === false) {
throw new Exception('Erreur lors de la décompression GZIP');
}
$finalData = $decompressed;
LogService::log('Décompression backup réussie', [
'level' => 'debug',
'compressed_size' => strlen($decrypted),
'decompressed_size' => strlen($decompressed)
]);
}
}
// Étape 5: Validation que le résultat est du JSON valide
$jsonTest = json_decode($finalData, true);
if ($jsonTest === null && json_last_error() !== JSON_ERROR_NONE) {
throw new Exception('Les données déchiffrées ne sont pas du JSON valide : ' . json_last_error_msg());
}
return $finalData;
} catch (Exception $e) {
LogService::log('Erreur lors du déchiffrement du backup', [
'level' => 'error',
'error' => $e->getMessage(),
'encrypted_size' => strlen($encryptedData)
]);
throw $e;
}
}
/**
* Vérifie si un fichier de backup est chiffré
*
* @param string $filePath Chemin vers le fichier
* @return bool True si le fichier est chiffré
*/
public function isEncryptedBackup(string $filePath): bool {
return str_ends_with($filePath, '.json.gz.enc') || str_ends_with($filePath, '.enc');
}
/**
* Vérifie si un fichier de backup est compressé (mais pas chiffré)
*
* @param string $filePath Chemin vers le fichier
* @return bool True si le fichier est compressé
*/
public function isCompressedBackup(string $filePath): bool {
return str_ends_with($filePath, '.json.gz') && !str_ends_with($filePath, '.enc');
}
/**
* Lit un fichier de backup en détectant automatiquement le format
*
* @param string $filePath Chemin vers le fichier de backup
* @return array Données JSON décodées
* @throws Exception En cas d'erreur de lecture ou format non supporté
*/
public function readBackupFile(string $filePath): array {
if (!file_exists($filePath)) {
throw new Exception("Fichier de backup non trouvé : {$filePath}");
}
$fileContent = file_get_contents($filePath);
if ($fileContent === false) {
throw new Exception("Impossible de lire le fichier : {$filePath}");
}
try {
// Fichier chiffré
if ($this->isEncryptedBackup($filePath)) {
$jsonContent = $this->decryptBackup($fileContent);
}
// Fichier compressé seulement
elseif ($this->isCompressedBackup($filePath)) {
$jsonContent = gzdecode($fileContent);
if ($jsonContent === false) {
throw new Exception('Erreur lors de la décompression du fichier');
}
}
// Fichier JSON brut
else {
$jsonContent = $fileContent;
}
// Décodage JSON
$data = json_decode($jsonContent, true);
if ($data === null && json_last_error() !== JSON_ERROR_NONE) {
throw new Exception('JSON invalide : ' . json_last_error_msg());
}
LogService::log('Lecture backup réussie', [
'level' => 'info',
'file_path' => $filePath,
'file_size' => filesize($filePath),
'is_encrypted' => $this->isEncryptedBackup($filePath),
'is_compressed' => $this->isCompressedBackup($filePath)
]);
return $data;
} catch (Exception $e) {
LogService::log('Erreur lors de la lecture du backup', [
'level' => 'error',
'file_path' => $filePath,
'error' => $e->getMessage()
]);
throw $e;
}
}
/**
* Génère le nom de fichier approprié selon la configuration
*
* @param int $operationId ID de l'opération
* @param string $timestamp Timestamp pour l'unicité
* @param string $type Type d'export (manual, auto, etc.)
* @return string Nom du fichier avec extension appropriée
*/
public function generateBackupFilename(int $operationId, string $timestamp, string $type = 'manual'): string {
$baseName = "backup_operation_{$operationId}_{$timestamp}";
if ($type !== 'manual') {
$baseName .= "_{$type}";
}
// Extension selon la configuration
$extension = '.json';
if ($this->backupConfig['compression']) {
$extension .= '.gz';
}
// Toujours chiffré
$extension .= '.enc';
return $baseName . $extension;
}
/**
* Retourne les statistiques de compression et chiffrement
*
* @param string $originalJson JSON original
* @param string $finalData Données finales chiffrées
* @return array Statistiques détaillées
*/
public function getCompressionStats(string $originalJson, string $finalData): array {
$originalSize = strlen($originalJson);
$finalSize = strlen($finalData);
return [
'original_size' => $originalSize,
'final_size' => $finalSize,
'size_reduction' => $originalSize - $finalSize,
'compression_ratio' => $originalSize > 0 ? round((1 - $finalSize / $originalSize) * 100, 2) : 0,
'is_compressed' => $this->backupConfig['compression'],
'is_encrypted' => true,
'cipher' => $this->backupConfig['cipher']
];
}
}

View File

@@ -0,0 +1,933 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xls;
use PhpOffice\PhpSpreadsheet\Writer\Csv;
require_once __DIR__ . '/../Services/FileService.php';
class ExportService {
private \PDO $db;
private FileService $fileService;
public function __construct() {
$this->db = Database::getInstance();
$this->fileService = new FileService();
}
/**
* Génère un export Excel complet d'une opération
*
* @param int $operationId ID de l'opération
* @param int $entiteId ID de l'entité
* @param int|null $userId Filtrer par utilisateur (optionnel)
* @return array Informations du fichier généré
*/
public function generateExcelExport(int $operationId, int $entiteId, ?int $userId = null): array {
try {
// Récupérer les données de l'opération
$operationData = $this->getOperationData($operationId, $entiteId);
if (!$operationData) {
throw new Exception('Opération non trouvée');
}
// Créer le dossier de destination
$exportDir = $this->fileService->createDirectory($entiteId, "/{$entiteId}/operations/{$operationId}/exports/excel");
LogService::log('exportDir', [
'level' => 'warning',
'exportDir' => $exportDir,
]);
// Générer le nom du fichier
$timestamp = date('Ymd-His');
$userSuffix = $userId ? "-user{$userId}" : '';
$filename = "geosector-export-{$operationId}{$userSuffix}-{$timestamp}.xlsx";
$filepath = $exportDir . '/' . $filename;
// Créer le spreadsheet
$spreadsheet = new PhpOffice\PhpSpreadsheet\Spreadsheet();
// Insérer les données
$this->createPassagesSheet($spreadsheet, $operationId, $userId);
$this->createUsersSheet($spreadsheet, $operationId);
$this->createSectorsSheet($spreadsheet, $operationId);
$this->createUserSectorsSheet($spreadsheet, $operationId);
// Supprimer la feuille par défaut (Worksheet) qui est créée automatiquement
$defaultSheet = $spreadsheet->getSheetByName('Worksheet');
if ($defaultSheet) {
$spreadsheet->removeSheetByIndex($spreadsheet->getIndex($defaultSheet));
}
// Essayer d'abord le writer XLSX, sinon utiliser CSV
try {
$writer = new Xls($spreadsheet);
$writer->save($filepath);
} catch (Exception $e) {
// Si XLSX échoue, utiliser CSV comme fallback
$csvPath = str_replace('.xlsx', '.csv', $filepath);
$csvWriter = new Csv($spreadsheet);
$csvWriter->setDelimiter(';');
$csvWriter->setEnclosure('"');
$csvWriter->save($csvPath);
// Mettre à jour les variables pour le CSV
$filepath = $csvPath;
$filename = str_replace('.xlsx', '.csv', $filename);
LogService::log('Fallback vers CSV car XLSX a échoué', [
'level' => 'warning',
'error' => $e->getMessage(),
'operationId' => $operationId
]);
}
// Appliquer les permissions sur le fichier
$this->fileService->setFilePermissions($filepath);
// Déterminer le type de fichier réellement généré
$fileType = str_ends_with($filename, '.csv') ? 'csv' : 'xlsx';
// Enregistrer en base de données
$mediaId = $this->fileService->saveToMediasTable($entiteId, $operationId, $filename, $filepath, $fileType, 'Export Excel opération - ' . $operationData['libelle']);
LogService::log('Export Excel généré', [
'level' => 'info',
'operationId' => $operationId,
'entiteId' => $entiteId,
'path' => $exportDir,
'filename' => $filename,
'mediaId' => $mediaId
]);
return [
'id' => $mediaId,
'filename' => $filename,
'path' => str_replace(getcwd() . '/', '', $filepath),
'size' => filesize($filepath),
'type' => 'excel'
];
} catch (Exception $e) {
LogService::log('Erreur lors de la génération de l\'export Excel', [
'level' => 'error',
'error' => $e->getMessage(),
'operationId' => $operationId,
'entiteId' => $entiteId
]);
throw $e;
}
}
/**
* Génère un export JSON complet d'une opération (chiffré et compressé)
*
* @param int $operationId ID de l'opération
* @param int $entiteId ID de l'entité
* @param string $type Type d'export (auto, manual)
* @return array Informations du fichier généré
*/
public function generateJsonExport(int $operationId, int $entiteId, string $type = 'manual'): array {
try {
// Récupérer toutes les données de l'opération
$exportData = $this->collectOperationData($operationId, $entiteId);
// Créer le dossier de destination
$exportDir = $this->fileService->createDirectory($entiteId, "/{$entiteId}/operations/{$operationId}/exports/json");
// Initialiser le service de chiffrement
$backupService = new BackupEncryptionService();
// Générer le JSON original
$jsonData = json_encode($exportData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
// Chiffrer et compresser les données
$encryptedData = $backupService->encryptBackup($jsonData);
// Générer le nom du fichier avec extension appropriée
$timestamp = date('Ymd-His');
$filename = $backupService->generateBackupFilename($operationId, $timestamp, $type);
$filepath = $exportDir . '/' . $filename;
// Sauvegarder le fichier chiffré
file_put_contents($filepath, $encryptedData);
// Appliquer les permissions sur le fichier
$this->fileService->setFilePermissions($filepath);
// Obtenir les statistiques de compression
$stats = $backupService->getCompressionStats($jsonData, $encryptedData);
// Enregistrer en base de données avec le bon type MIME
$mediaId = $this->fileService->saveToMediasTable(
$entiteId,
$operationId,
$filename,
$filepath,
'enc',
"Sauvegarde chiffrée opération - {$type} - " . $exportData['operation']['libelle'],
'backup'
);
LogService::log('Export JSON chiffré généré', [
'level' => 'info',
'operationId' => $operationId,
'entiteId' => $entiteId,
'filename' => $filename,
'type' => $type,
'mediaId' => $mediaId,
'original_size' => $stats['original_size'],
'final_size' => $stats['final_size'],
'compression_ratio' => $stats['compression_ratio'] . '%',
'is_compressed' => $stats['is_compressed'],
'cipher' => $stats['cipher']
]);
return [
'id' => $mediaId,
'filename' => $filename,
'path' => str_replace(getcwd() . '/', '', $filepath),
'size' => filesize($filepath),
'type' => 'encrypted_json',
'compression_stats' => $stats
];
} catch (Exception $e) {
LogService::log('Erreur lors de la génération de l\'export JSON chiffré', [
'level' => 'error',
'error' => $e->getMessage(),
'operationId' => $operationId,
'entiteId' => $entiteId
]);
throw $e;
}
}
/**
* Crée la feuille des passages
*/
private function createPassagesSheet(Spreadsheet $spreadsheet, int $operationId, ?int $userId = null): void {
$sheet = $spreadsheet->createSheet();
$sheet->setTitle('Passages');
// En-têtes
$headers = [
'ID_Passage',
'Date',
'Heure',
'Prénom',
'Nom',
'Tournée',
'Type',
'N°',
'Bis',
'Rue',
'Ville',
'Habitat',
'Donateur',
'Email',
'Tél',
'Montant',
'Règlement',
'Remarque',
'FK_User',
'FK_Sector',
'FK_Operation'
];
// Écrire les en-têtes
$sheet->fromArray([$headers], null, 'A1');
// Récupérer les données des passages
$sql = '
SELECT
p.id, p.passed_at, p.fk_type, p.numero, p.rue_bis, p.rue, p.ville,
p.fk_habitat, p.appt, p.niveau, p.encrypted_name, p.encrypted_email,
p.encrypted_phone, p.montant, p.fk_type_reglement, p.remarque,
p.fk_user, p.fk_sector, p.fk_operation,
u.encrypted_name as user_name, u.first_name as user_first_name, u.sect_name,
xtr.libelle as reglement_libelle
FROM ope_pass p
LEFT JOIN users u ON u.id = p.fk_user
LEFT JOIN x_types_reglements xtr ON xtr.id = p.fk_type_reglement
WHERE p.fk_operation = ? AND p.chk_active = 1
';
$params = [$operationId];
if ($userId) {
$sql .= ' AND p.fk_user = ?';
$params[] = $userId;
}
$sql .= ' ORDER BY p.passed_at DESC';
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
$passages = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Remplir les données
$row = 2;
foreach ($passages as $passage) {
$dateEve = $passage['passed_at'] ? date('d/m/Y', strtotime($passage['passed_at'])) : '';
$heureEve = $passage['passed_at'] ? date('H:i', strtotime($passage['passed_at'])) : '';
// Déchiffrer les données
$donateur = ApiService::decryptData($passage['encrypted_name']);
$email = !empty($passage['encrypted_email']) ? ApiService::decryptSearchableData($passage['encrypted_email']) : '';
$phone = !empty($passage['encrypted_phone']) ? ApiService::decryptData($passage['encrypted_phone']) : '';
$userName = ApiService::decryptData($passage['user_name']);
// Type de passage
$typeLabels = [
1 => 'Effectué',
2 => 'A finaliser',
3 => 'Refusé',
4 => 'Don',
9 => 'Habitat vide'
];
$typeLabel = $typeLabels[$passage['fk_type']] ?? $passage['fk_type'];
// Habitat
$habitat = $passage['fk_habitat'] == 1 ? 'Individuel' :
"Etage {$passage['niveau']} - Appt {$passage['appt']}";
$rowData = [
$passage['id'],
$dateEve,
$heureEve,
$passage['user_first_name'],
$userName,
$passage['sect_name'],
$typeLabel,
$passage['numero'],
$passage['rue_bis'],
$passage['rue'],
$passage['ville'],
$habitat,
$donateur,
$email,
$phone,
$passage['montant'],
$passage['reglement_libelle'],
$passage['remarque'],
$passage['fk_user'],
$passage['fk_sector'],
$passage['fk_operation']
];
$sheet->fromArray([$rowData], null, "A{$row}");
$row++;
}
// Auto-ajuster les colonnes
foreach (range('A', 'T') as $col) {
$sheet->getColumnDimension($col)->setAutoSize(true);
}
}
/**
* Crée la feuille des utilisateurs
*/
private function createUsersSheet(Spreadsheet $spreadsheet, int $operationId): void {
$sheet = $spreadsheet->createSheet();
$sheet->setTitle('Utilisateurs');
// En-têtes
$headers = [
'ID_User',
'Nom',
'Prénom',
'Email',
'Téléphone',
'Mobile',
'Rôle',
'Date_création',
'Actif',
'FK_Entite'
];
$sheet->fromArray([$headers], null, 'A1');
// Récupérer les utilisateurs de l'opération
$sql = '
SELECT DISTINCT
u.id, u.encrypted_name, u.first_name, u.encrypted_email,
u.encrypted_phone, u.encrypted_mobile, u.fk_role, u.created_at,
u.chk_active, u.fk_entite,
r.libelle as role_libelle
FROM users u
INNER JOIN ope_users ou ON ou.fk_user = u.id
LEFT JOIN x_users_roles r ON r.id = u.fk_role
WHERE ou.fk_operation = ? AND ou.chk_active = 1
ORDER BY u.encrypted_name
';
$stmt = $this->db->prepare($sql);
$stmt->execute([$operationId]);
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
$row = 2;
foreach ($users as $user) {
$rowData = [
$user['id'],
ApiService::decryptData($user['encrypted_name']),
$user['first_name'],
!empty($user['encrypted_email']) ? ApiService::decryptSearchableData($user['encrypted_email']) : '',
!empty($user['encrypted_phone']) ? ApiService::decryptData($user['encrypted_phone']) : '',
!empty($user['encrypted_mobile']) ? ApiService::decryptData($user['encrypted_mobile']) : '',
$user['role_libelle'],
date('d/m/Y H:i', strtotime($user['created_at'])),
$user['chk_active'] ? 'Oui' : 'Non',
$user['fk_entite']
];
$sheet->fromArray([$rowData], null, "A{$row}");
$row++;
}
foreach (range('A', 'J') as $col) {
$sheet->getColumnDimension($col)->setAutoSize(true);
}
}
/**
* Crée la feuille des secteurs
*/
private function createSectorsSheet(Spreadsheet $spreadsheet, int $operationId): void {
$sheet = $spreadsheet->createSheet();
$sheet->setTitle('Secteurs');
$headers = ['ID_Sector', 'Libellé', 'Couleur', 'Date_création', 'Actif', 'FK_Operation'];
$sheet->fromArray([$headers], null, 'A1');
$sql = '
SELECT id, libelle, color, created_at, chk_active, fk_operation
FROM ope_sectors
WHERE fk_operation = ? AND chk_active = 1
ORDER BY libelle
';
$stmt = $this->db->prepare($sql);
$stmt->execute([$operationId]);
$sectors = $stmt->fetchAll(PDO::FETCH_ASSOC);
$row = 2;
foreach ($sectors as $sector) {
$rowData = [
$sector['id'],
$sector['libelle'],
$sector['color'],
date('d/m/Y H:i', strtotime($sector['created_at'])),
$sector['chk_active'] ? 'Oui' : 'Non',
$sector['fk_operation']
];
$sheet->fromArray([$rowData], null, "A{$row}");
$row++;
}
foreach (range('A', 'F') as $col) {
$sheet->getColumnDimension($col)->setAutoSize(true);
}
}
/**
* Crée la feuille des relations secteurs-utilisateurs
*/
private function createUserSectorsSheet(Spreadsheet $spreadsheet, int $operationId): void {
$sheet = $spreadsheet->createSheet();
$sheet->setTitle('Secteurs-Utilisateurs');
$headers = [
'ID_Relation',
'FK_Sector',
'Nom_Secteur',
'FK_User',
'Nom_Utilisateur',
'Date_assignation',
'FK_Operation'
];
$sheet->fromArray([$headers], null, 'A1');
$sql = '
SELECT
ous.id, ous.fk_sector, ous.fk_user, ous.created_at, ous.fk_operation,
s.libelle as sector_name,
u.encrypted_name as user_name, u.first_name
FROM ope_users_sectors ous
INNER JOIN ope_sectors s ON s.id = ous.fk_sector
INNER JOIN users u ON u.id = ous.fk_user
WHERE ous.fk_operation = ? AND ous.chk_active = 1
ORDER BY s.libelle, u.encrypted_name
';
$stmt = $this->db->prepare($sql);
$stmt->execute([$operationId]);
$userSectors = $stmt->fetchAll(PDO::FETCH_ASSOC);
$row = 2;
foreach ($userSectors as $us) {
$userName = ApiService::decryptData($us['user_name']);
$fullUserName = $us['first_name'] ? $us['first_name'] . ' ' . $userName : $userName;
$rowData = [
$us['id'],
$us['fk_sector'],
$us['sector_name'],
$us['fk_user'],
$fullUserName,
date('d/m/Y H:i', strtotime($us['created_at'])),
$us['fk_operation']
];
$sheet->fromArray([$rowData], null, "A{$row}");
$row++;
}
foreach (range('A', 'G') as $col) {
$sheet->getColumnDimension($col)->setAutoSize(true);
}
}
/**
* Collecte toutes les données d'une opération pour l'export JSON
*/
private function collectOperationData(int $operationId, int $entiteId): array {
// Métadonnées de l'export
$exportData = [
'export_metadata' => [
'version' => '1.0',
'export_date' => date('c'),
'source_entite_id' => $entiteId,
'export_type' => 'full_operation'
]
];
// Données de l'opération
$exportData['operation'] = $this->getOperationData($operationId, $entiteId);
// Utilisateurs de l'opération
$exportData['users'] = $this->getOperationUsers($operationId);
// Secteurs de l'opération
$exportData['sectors'] = $this->getOperationSectors($operationId);
// Passages de l'opération
$exportData['passages'] = $this->getOperationPassages($operationId);
// Relations utilisateurs-secteurs
$exportData['user_sectors'] = $this->getOperationUserSectors($operationId);
return $exportData;
}
/**
* Récupère les données de l'opération
*/
private function getOperationData(int $operationId, int $entiteId): ?array {
$stmt = $this->db->prepare('
SELECT * FROM operations
WHERE id = ? AND fk_entite = ?
');
$stmt->execute([$operationId, $entiteId]);
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
}
/**
* Récupère les utilisateurs de l'opération
*/
private function getOperationUsers(int $operationId): array {
$stmt = $this->db->prepare('
SELECT DISTINCT u.*
FROM users u
INNER JOIN ope_users ou ON ou.fk_user = u.id
WHERE ou.fk_operation = ? AND ou.chk_active = 1
');
$stmt->execute([$operationId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Récupère les secteurs de l'opération
*/
private function getOperationSectors(int $operationId): array {
$stmt = $this->db->prepare('
SELECT * FROM ope_sectors
WHERE fk_operation = ? AND chk_active = 1
');
$stmt->execute([$operationId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Récupère les passages de l'opération
*/
private function getOperationPassages(int $operationId): array {
$stmt = $this->db->prepare('
SELECT * FROM ope_pass
WHERE fk_operation = ? AND chk_active = 1
');
$stmt->execute([$operationId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Récupère les relations utilisateurs-secteurs
*/
private function getOperationUserSectors(int $operationId): array {
$stmt = $this->db->prepare('
SELECT * FROM ope_users_sectors
WHERE fk_operation = ? AND chk_active = 1
');
$stmt->execute([$operationId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Récupère les données des passages dans un format simple pour l'export Excel
* (inspiré de l'ancienne version qui fonctionne)
*/
private function getSimplePassagesData(int $operationId, ?int $userId = null): array {
// En-têtes (comme dans l'ancienne version)
$aData = [];
$aData[] = [
'Date',
'Heure',
'Prenom',
'Nom',
'Tournee',
'Type',
'N°',
'Rue',
'Ville',
'Habitat',
'Donateur',
'Email',
'Tel',
'Montant',
'Reglement',
'Remarque'
];
// Récupérer les données des passages
$sql = '
SELECT
p.passed_at, p.fk_type, p.numero, p.rue_bis, p.rue, p.ville,
p.fk_habitat, p.appt, p.niveau, p.encrypted_name, p.encrypted_email,
p.encrypted_phone, p.montant, p.fk_type_reglement, p.remarque,
u.encrypted_name as user_name, u.first_name as user_first_name, u.sect_name,
xtr.libelle as reglement_libelle
FROM ope_pass p
LEFT JOIN users u ON u.id = p.fk_user
LEFT JOIN x_types_reglements xtr ON xtr.id = p.fk_type_reglement
WHERE p.fk_operation = ? AND p.chk_active = 1
';
$params = [$operationId];
if ($userId) {
$sql .= ' AND p.fk_user = ?';
$params[] = $userId;
}
$sql .= ' ORDER BY p.passed_at DESC';
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
$passages = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Traiter les données comme dans l'ancienne version
foreach ($passages as $p) {
// Type de passage
switch ($p["fk_type"]) {
case 1:
$ptype = "Effectué";
$preglement = $p["reglement_libelle"];
break;
case 2:
$ptype = "A finaliser";
$preglement = "";
break;
case 3:
$ptype = "Refusé";
$preglement = "";
break;
case 4:
$ptype = "Don";
$preglement = "";
break;
case 9:
$ptype = "Habitat vide";
$preglement = "";
break;
default:
$ptype = $p["fk_type"];
$preglement = "";
break;
}
// Habitat
if ($p["fk_habitat"] == 1) {
$phabitat = "Individuel";
} else {
$phabitat = "Etage " . $p["niveau"] . " - Appt " . $p["appt"];
}
// Dates
$dateEve = $p["passed_at"] ? date("d/m/Y", strtotime($p["passed_at"])) : "";
$heureEve = $p["passed_at"] ? date("H:i", strtotime($p["passed_at"])) : "";
// Déchiffrer les données
$donateur = ApiService::decryptData($p["encrypted_name"]);
$email = !empty($p["encrypted_email"]) ? ApiService::decryptSearchableData($p["encrypted_email"]) : "";
$phone = !empty($p["encrypted_phone"]) ? ApiService::decryptData($p["encrypted_phone"]) : "";
$userName = ApiService::decryptData($p["user_name"]);
// Nettoyer les données (comme dans l'ancienne version)
$nom = str_replace("/", "-", $userName);
$tournee = str_replace("/", "-", $p["sect_name"]);
$aData[] = [
$dateEve,
$heureEve,
$p["user_first_name"],
$nom,
$tournee,
$ptype,
$p["numero"] . $p["rue_bis"],
$p["rue"],
$p["ville"],
$phabitat,
$donateur,
$email,
$phone,
$p["montant"],
$preglement,
$p["remarque"]
];
}
return $aData;
}
/**
* Restaure une opération à partir d'un backup chiffré
*
* @param string $backupFilePath Chemin vers le fichier de backup
* @param int $targetEntiteId ID de l'entité cible (pour restauration cross-entité)
* @return array Résultat de la restauration
* @throws Exception En cas d'erreur de restauration
*/
public function restoreFromBackup(string $backupFilePath, int $targetEntiteId): array {
try {
// Initialiser le service de chiffrement
$backupService = new BackupEncryptionService();
// Lire et déchiffrer le backup
$backupData = $backupService->readBackupFile($backupFilePath);
// Valider la structure du backup
if (!isset($backupData['operation']) || !isset($backupData['export_metadata'])) {
throw new Exception('Structure de backup invalide');
}
$operationData = $backupData['operation'];
$originalEntiteId = $backupData['export_metadata']['source_entite_id'];
// Commencer la transaction
$this->db->beginTransaction();
// Créer la nouvelle opération
$newOperationId = $this->restoreOperation($operationData, $targetEntiteId);
// Restaurer les utilisateurs (si même entité)
if ($targetEntiteId === $originalEntiteId && isset($backupData['users'])) {
$this->restoreUsers($backupData['users'], $newOperationId);
}
// Restaurer les secteurs
if (isset($backupData['sectors'])) {
$this->restoreSectors($backupData['sectors'], $newOperationId);
}
// Restaurer les relations utilisateurs-secteurs
if (isset($backupData['user_sectors'])) {
$this->restoreUserSectors($backupData['user_sectors'], $newOperationId);
}
// Restaurer les passages
if (isset($backupData['passages'])) {
$this->restorePassages($backupData['passages'], $newOperationId);
}
$this->db->commit();
LogService::log('Restauration de backup réussie', [
'level' => 'info',
'backup_file' => $backupFilePath,
'original_operation_id' => $operationData['id'],
'new_operation_id' => $newOperationId,
'target_entite_id' => $targetEntiteId,
'original_entite_id' => $originalEntiteId
]);
return [
'success' => true,
'new_operation_id' => $newOperationId,
'original_operation_id' => $operationData['id'],
'restored_data' => [
'operation' => true,
'users' => isset($backupData['users']) && $targetEntiteId === $originalEntiteId,
'sectors' => isset($backupData['sectors']),
'user_sectors' => isset($backupData['user_sectors']),
'passages' => isset($backupData['passages'])
]
];
} catch (Exception $e) {
$this->db->rollBack();
LogService::log('Erreur lors de la restauration du backup', [
'level' => 'error',
'backup_file' => $backupFilePath,
'target_entite_id' => $targetEntiteId,
'error' => $e->getMessage()
]);
throw $e;
}
}
/**
* Restaure les données de l'opération
*/
private function restoreOperation(array $operationData, int $targetEntiteId): int {
$stmt = $this->db->prepare('
INSERT INTO operations (
fk_entite, libelle, date_deb, date_fin, chk_distinct_sectors,
fk_user_creat, chk_active, created_at
) VALUES (?, ?, ?, ?, ?, ?, 0, NOW())
');
$userId = Session::getUserId() ?? 1;
$stmt->execute([
$targetEntiteId,
$operationData['libelle'] . ' (Restaurée)',
$operationData['date_deb'],
$operationData['date_fin'],
$operationData['chk_distinct_sectors'] ?? 0,
$userId
]);
return (int)$this->db->lastInsertId();
}
/**
* Restaure les utilisateurs (uniquement si même entité)
*/
private function restoreUsers(array $users, int $newOperationId): void {
foreach ($users as $user) {
// Vérifier si l'utilisateur existe déjà
$stmt = $this->db->prepare('SELECT id FROM users WHERE id = ?');
$stmt->execute([$user['id']]);
if ($stmt->fetch()) {
// Associer l'utilisateur existant à la nouvelle opération
$stmt = $this->db->prepare('
INSERT IGNORE INTO ope_users (fk_operation, fk_user, chk_active, created_at)
VALUES (?, ?, 1, NOW())
');
$stmt->execute([$newOperationId, $user['id']]);
}
}
}
/**
* Restaure les secteurs
*/
private function restoreSectors(array $sectors, int $newOperationId): void {
foreach ($sectors as $sector) {
$stmt = $this->db->prepare('
INSERT INTO ope_sectors (
fk_operation, libelle, color, chk_active, created_at
) VALUES (?, ?, ?, 1, NOW())
');
$stmt->execute([
$newOperationId,
$sector['libelle'],
$sector['color']
]);
}
}
/**
* Restaure les relations utilisateurs-secteurs
*/
private function restoreUserSectors(array $userSectors, int $newOperationId): void {
foreach ($userSectors as $us) {
// Trouver le nouveau secteur par son libellé
$stmt = $this->db->prepare('
SELECT id FROM ope_sectors
WHERE fk_operation = ? AND libelle = ?
LIMIT 1
');
$stmt->execute([$newOperationId, $us['libelle'] ?? '']);
$newSector = $stmt->fetch();
if ($newSector) {
$stmt = $this->db->prepare('
INSERT IGNORE INTO ope_users_sectors (
fk_operation, fk_sector, fk_user, chk_active, created_at
) VALUES (?, ?, ?, 1, NOW())
');
$stmt->execute([
$newOperationId,
$newSector['id'],
$us['fk_user']
]);
}
}
}
/**
* Restaure les passages
*/
private function restorePassages(array $passages, int $newOperationId): void {
foreach ($passages as $passage) {
$stmt = $this->db->prepare('
INSERT INTO ope_pass (
fk_operation, fk_user, fk_sector, fk_type, passed_at,
numero, rue_bis, rue, ville, fk_habitat, appt, niveau,
encrypted_name, encrypted_email, encrypted_phone,
montant, fk_type_reglement, remarque, chk_active, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, NOW())
');
$stmt->execute([
$newOperationId,
$passage['fk_user'],
$passage['fk_sector'],
$passage['fk_type'],
$passage['passed_at'],
$passage['numero'],
$passage['rue_bis'],
$passage['rue'],
$passage['ville'],
$passage['fk_habitat'],
$passage['appt'],
$passage['niveau'],
$passage['encrypted_name'],
$passage['encrypted_email'],
$passage['encrypted_phone'],
$passage['montant'],
$passage['fk_type_reglement'],
$passage['remarque']
]);
}
}
}

View File

@@ -0,0 +1,439 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
class FileService {
private const BASE_UPLOADS_DIR = '/var/www/geosector/api/uploads';
// Permissions pour écriture web
private const FILE_PERMS = 0666;
private const DIR_PERMS = 0775;
private const OWNER_GROUP = 'nginx:nobody';
private \PDO $db;
public function __construct() {
$this->db = Database::getInstance();
}
/**
* Crée un dossier dans l'arborescence uploads
*
* @param int $entiteId ID de l'entité
* @param string $path Chemin relatif à partir de BASE_UPLOADS_DIR (ex: '/5/operations/2644/export')
* @return string Le chemin complet du dossier créé
*/
public function createDirectory(int $entiteId, string $path): string {
// Construire le chemin complet
$fullPath = self::BASE_UPLOADS_DIR . $path;
LogService::log('Création de dossier', [
'level' => 'info',
'entiteId' => $entiteId,
'path' => $path,
'fullPath' => $fullPath,
]);
// Créer le dossier avec tous les dossiers parents si nécessaire
if (!is_dir($fullPath)) {
if (!mkdir($fullPath, self::DIR_PERMS, true)) {
LogService::log('Erreur création dossier', [
'level' => 'error',
'fullPath' => $fullPath,
]);
throw new Exception("Impossible de créer le dossier: {$fullPath}");
}
// Appliquer les permissions et propriétaire
$this->setDirectoryPermissions($fullPath);
LogService::log('Dossier créé avec succès', [
'level' => 'info',
'fullPath' => $fullPath,
'permissions' => decoct(self::DIR_PERMS),
'owner' => self::OWNER_GROUP,
]);
}
return $fullPath;
}
/**
* Applique les permissions et propriétaire sur un dossier
*/
private function setDirectoryPermissions(string $path): void {
// Appliquer les permissions
chmod($path, self::DIR_PERMS);
// Changer le propriétaire et le groupe séparément pour plus de fiabilité
$chownUserCommand = "chown nginx " . escapeshellarg($path);
exec($chownUserCommand, $output, $returnCode);
if ($returnCode !== 0) {
LogService::log('Avertissement: Impossible de changer le propriétaire', [
'level' => 'warning',
'path' => $path,
'command' => $chownUserCommand,
'return_code' => $returnCode,
]);
}
$chgrpCommand = "chgrp nobody " . escapeshellarg($path);
exec($chgrpCommand, $output, $returnCode);
if ($returnCode !== 0) {
LogService::log('Avertissement: Impossible de changer le groupe', [
'level' => 'warning',
'path' => $path,
'command' => $chgrpCommand,
'return_code' => $returnCode,
]);
}
}
/**
* Applique les permissions sur un fichier
*/
public function setFilePermissions(string $filepath): void {
// Appliquer les permissions fichier
chmod($filepath, self::FILE_PERMS);
// Changer le propriétaire et le groupe séparément pour plus de fiabilité
$chownUserCommand = "chown nginx " . escapeshellarg($filepath);
exec($chownUserCommand, $output, $returnCode);
if ($returnCode !== 0) {
LogService::log('Avertissement: Impossible de changer le propriétaire du fichier', [
'level' => 'warning',
'filepath' => $filepath,
'command' => $chownUserCommand,
'return_code' => $returnCode,
]);
}
$chgrpCommand = "chgrp nobody " . escapeshellarg($filepath);
exec($chgrpCommand, $output, $returnCode);
if ($returnCode !== 0) {
LogService::log('Avertissement: Impossible de changer le groupe du fichier', [
'level' => 'warning',
'filepath' => $filepath,
'command' => $chgrpCommand,
'return_code' => $returnCode,
]);
}
}
/**
* Supprime un fichier
*
* @param string $filePath Chemin complet vers le fichier ou chemin relatif depuis BASE_UPLOADS_DIR
* @param string $fileName Nom du fichier (pour les logs)
* @return bool True si suppression réussie, false sinon
*/
public function deleteFile(string $filePath, string $fileName): bool {
// Si le chemin ne commence pas par /, on considère qu'il est relatif à BASE_UPLOADS_DIR
if (!str_starts_with($filePath, '/')) {
$fullPath = self::BASE_UPLOADS_DIR . '/' . $filePath;
} else {
$fullPath = $filePath;
}
LogService::log('Tentative de suppression de fichier', [
'level' => 'info',
'fileName' => $fileName,
'filePath' => $filePath,
'fullPath' => $fullPath,
]);
// Vérifier que le fichier existe
if (!file_exists($fullPath)) {
LogService::log('Fichier non trouvé pour suppression', [
'level' => 'warning',
'fileName' => $fileName,
'fullPath' => $fullPath,
]);
return false;
}
// Vérifier que c'est bien un fichier (pas un dossier)
if (!is_file($fullPath)) {
LogService::log('Le chemin ne pointe pas vers un fichier', [
'level' => 'error',
'fileName' => $fileName,
'fullPath' => $fullPath,
]);
return false;
}
// Supprimer d'abord les enregistrements dans la table medias
$deletedMediaRecords = $this->deleteMediaRecordsByFile($fullPath, $fileName);
// Tenter la suppression du fichier physique
if (unlink($fullPath)) {
LogService::log('Fichier supprimé avec succès', [
'level' => 'info',
'fileName' => $fileName,
'fullPath' => $fullPath,
'deletedMediaRecords' => $deletedMediaRecords,
]);
return true;
} else {
LogService::log('Erreur lors de la suppression du fichier', [
'level' => 'error',
'fileName' => $fileName,
'fullPath' => $fullPath,
]);
return false;
}
}
/**
* Supprime un dossier et tout son contenu
*
* @param string $filePath Chemin complet vers le dossier ou chemin relatif depuis BASE_UPLOADS_DIR
* @return bool True si suppression réussie, false sinon
*/
public function deleteDir(string $filePath): bool {
// Si le chemin ne commence pas par /, on considère qu'il est relatif à BASE_UPLOADS_DIR
if (!str_starts_with($filePath, '/')) {
$fullPath = self::BASE_UPLOADS_DIR . '/' . $filePath;
} else {
$fullPath = $filePath;
}
LogService::log('Tentative de suppression de dossier', [
'level' => 'info',
'filePath' => $filePath,
'fullPath' => $fullPath,
]);
// Vérifier que le dossier existe
if (!file_exists($fullPath)) {
LogService::log('Dossier non trouvé pour suppression', [
'level' => 'warning',
'fullPath' => $fullPath,
]);
return false;
}
// Vérifier que c'est bien un dossier
if (!is_dir($fullPath)) {
LogService::log('Le chemin ne pointe pas vers un dossier', [
'level' => 'error',
'fullPath' => $fullPath,
]);
return false;
}
// Supprimer d'abord les enregistrements dans la table medias pour ce dossier
$deletedMediaRecords = $this->deleteMediaRecordsByDirectory($fullPath);
// Supprimer récursivement le contenu du dossier
if ($this->deleteDirectoryRecursive($fullPath)) {
LogService::log('Dossier supprimé avec succès', [
'level' => 'info',
'fullPath' => $fullPath,
'deletedMediaRecords' => $deletedMediaRecords,
]);
return true;
} else {
LogService::log('Erreur lors de la suppression du dossier', [
'level' => 'error',
'fullPath' => $fullPath,
]);
return false;
}
}
/**
* Supprime les enregistrements medias correspondant à un fichier spécifique
*
* @param string $fullPath Chemin complet du fichier
* @param string $fileName Nom du fichier (pour les logs)
* @return int Nombre d'enregistrements supprimés
*/
private function deleteMediaRecordsByFile(string $fullPath, string $fileName): int {
// Convertir le chemin complet en chemin relatif pour la recherche en base
$relativePath = str_replace(getcwd() . '/', '', $fullPath);
// Rechercher les enregistrements correspondants
$stmt = $this->db->prepare('
SELECT id, fichier, file_path FROM medias
WHERE file_path = ? OR fichier = ?
');
$stmt->execute([$relativePath, $fileName]);
$mediaRecords = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($mediaRecords)) {
LogService::log('Aucun enregistrement media trouvé pour le fichier', [
'level' => 'info',
'fileName' => $fileName,
'relativePath' => $relativePath,
]);
return 0;
}
// Supprimer les enregistrements
$stmt = $this->db->prepare('DELETE FROM medias WHERE file_path = ? OR fichier = ?');
$stmt->execute([$relativePath, $fileName]);
$deletedCount = $stmt->rowCount();
LogService::log('Enregistrements medias supprimés pour fichier', [
'level' => 'info',
'fileName' => $fileName,
'deletedCount' => $deletedCount,
'mediaRecords' => array_column($mediaRecords, 'id'),
]);
return $deletedCount;
}
/**
* Supprime les enregistrements medias correspondant à un dossier et ses sous-dossiers
*
* @param string $fullPath Chemin complet du dossier
* @return int Nombre d'enregistrements supprimés
*/
private function deleteMediaRecordsByDirectory(string $fullPath): int {
// Convertir le chemin complet en chemin relatif pour la recherche en base
$relativePath = str_replace(getcwd() . '/', '', $fullPath);
// Rechercher tous les enregistrements dont le chemin commence par le dossier
$stmt = $this->db->prepare('
SELECT id, fichier, file_path FROM medias
WHERE file_path LIKE ?
');
$stmt->execute([$relativePath . '%']);
$mediaRecords = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($mediaRecords)) {
LogService::log('Aucun enregistrement media trouvé pour le dossier', [
'level' => 'info',
'relativePath' => $relativePath,
]);
return 0;
}
// Supprimer les enregistrements
$stmt = $this->db->prepare('DELETE FROM medias WHERE file_path LIKE ?');
$stmt->execute([$relativePath . '%']);
$deletedCount = $stmt->rowCount();
LogService::log('Enregistrements medias supprimés pour dossier', [
'level' => 'info',
'relativePath' => $relativePath,
'deletedCount' => $deletedCount,
'mediaRecords' => array_column($mediaRecords, 'id'),
]);
return $deletedCount;
}
/**
* Supprime récursivement un dossier et tout son contenu
*
* @param string $dir Chemin complet vers le dossier
* @return bool True si suppression réussie, false sinon
*/
private function deleteDirectoryRecursive(string $dir): bool {
if (!is_dir($dir)) {
return false;
}
$files = array_diff(scandir($dir), ['.', '..']);
$deletedFiles = 0;
$totalFiles = count($files);
foreach ($files as $file) {
$filePath = $dir . DIRECTORY_SEPARATOR . $file;
if (is_dir($filePath)) {
// Récursion pour les sous-dossiers
if ($this->deleteDirectoryRecursive($filePath)) {
$deletedFiles++;
LogService::log('Sous-dossier supprimé', [
'level' => 'debug',
'subDir' => $filePath,
]);
} else {
LogService::log('Erreur suppression sous-dossier', [
'level' => 'error',
'subDir' => $filePath,
]);
}
} else {
// Supprimer le fichier
if (unlink($filePath)) {
$deletedFiles++;
LogService::log('Fichier supprimé du dossier', [
'level' => 'debug',
'file' => $filePath,
]);
} else {
LogService::log('Erreur suppression fichier du dossier', [
'level' => 'error',
'file' => $filePath,
]);
}
}
}
// Supprimer le dossier lui-même s'il est vide
if ($deletedFiles === $totalFiles && rmdir($dir)) {
return true;
} else {
LogService::log('Impossible de supprimer le dossier principal', [
'level' => 'error',
'dir' => $dir,
'deletedFiles' => $deletedFiles,
'totalFiles' => $totalFiles,
]);
return false;
}
}
/**
* Enregistre le fichier dans la table medias
*/
public function saveToMediasTable(int $entiteId, int $operationId, string $filename, string $filepath, string $fileType, string $description, string $fileCategory = 'export'): int {
$stmt = $this->db->prepare('
INSERT INTO medias (
support, support_id, fichier, file_type, file_category, file_size, mime_type,
original_name, fk_entite, fk_operation, file_path, description,
created_at, fk_user_creat
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?)
');
// Déterminer le type MIME selon l'extension
$mimeTypes = [
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'json' => 'application/json',
'enc' => 'application/octet-stream'
];
$mimeType = $mimeTypes[$fileType] ?? 'application/octet-stream';
$relativePath = str_replace(getcwd() . '/', '', $filepath);
$userId = Session::getUserId() ?? 1; // Fallback si pas de session
$stmt->execute([
'operation',
$operationId,
$filename,
$fileType,
$fileCategory,
filesize($filepath),
$mimeType,
$filename,
$entiteId,
$operationId,
$relativePath,
$description,
$userId
]);
return (int)$this->db->lastInsertId();
}
}

View File

@@ -0,0 +1,282 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/ApiService.php';
require_once __DIR__ . '/LogService.php';
use PDO;
use ApiService;
use LogService;
class OperationDataService {
/**
* Prépare les données d'opération selon l'interface et le rôle utilisateur
*
* @param PDO $db Instance de la base de données
* @param int $entiteId ID de l'entité
* @param string $interface 'user' ou 'admin'
* @param int $userRole Rôle de l'utilisateur
* @param int $userId ID de l'utilisateur connecté
* @param int|null $specificOperationId ID d'opération spécifique (pour création d'opération)
* @return array Données formatées avec operations, sectors, users_sectors, passages
*/
public static function prepareOperationData(
PDO $db,
int $entiteId,
string $interface,
int $userRole,
int $userId,
?int $specificOperationId = null
): array {
$operationsData = [];
$sectorsData = [];
$passagesData = [];
$usersSectorsData = [];
// 1. Récupération des opérations selon les critères
$operationLimit = 0;
$activeOperationOnly = false;
if ($interface === 'user') {
// Interface utilisateur : seulement la dernière opération active
$operationLimit = 1;
$activeOperationOnly = true;
} elseif ($interface === 'admin' && $userRole == 2) {
// Interface admin avec rôle 2 : les 3 dernières opérations dont l'active
$operationLimit = 3;
} elseif ($interface === 'admin' && $userRole > 2) {
// Super admin : les 3 dernières opérations
$operationLimit = 3;
} else {
// Autres cas : pas d'opérations
$operationLimit = 0;
}
// Si une opération spécifique est demandée (création d'opération)
if ($specificOperationId) {
$operationQuery = "SELECT id, fk_entite, libelle, date_deb, date_fin, chk_active
FROM operations
WHERE fk_entite = ?
ORDER BY id DESC LIMIT 3";
$operationStmt = $db->prepare($operationQuery);
$operationStmt->execute([$entiteId]);
$operations = $operationStmt->fetchAll(PDO::FETCH_ASSOC);
$activeOperationId = $specificOperationId;
} elseif ($operationLimit > 0) {
$operationQuery = "SELECT id, fk_entite, libelle, date_deb, date_fin, chk_active
FROM operations
WHERE fk_entite = ?";
if ($activeOperationOnly) {
$operationQuery .= " AND chk_active = 1";
}
$operationQuery .= " ORDER BY id DESC LIMIT " . $operationLimit;
$operationStmt = $db->prepare($operationQuery);
$operationStmt->execute([$entiteId]);
$operations = $operationStmt->fetchAll(PDO::FETCH_ASSOC);
// Récupérer l'ID de l'opération active (première opération retournée ou celle avec chk_active=1)
$activeOperationId = null;
if (!empty($operations)) {
foreach ($operations as $operation) {
if ($operation['chk_active'] == 1) {
$activeOperationId = (int)$operation['id'];
break;
}
}
// Si aucune opération active trouvée, prendre la première
if (!$activeOperationId) {
$activeOperationId = (int)$operations[0]['id'];
}
}
} else {
$operations = [];
$activeOperationId = null;
}
if (!empty($operations)) {
// Formater les données des opérations
foreach ($operations as $operation) {
$operationsData[] = [
'id' => $operation['id'],
'fk_entite' => $operation['fk_entite'],
'libelle' => $operation['libelle'],
'date_deb' => $operation['date_deb'],
'date_fin' => $operation['date_fin'],
'chk_active' => $operation['chk_active']
];
}
// 2. Récupérer les secteurs selon l'interface et le rôle
if ($activeOperationId) {
if ($interface === 'user') {
// Interface utilisateur : seulement les secteurs affectés à l'utilisateur
$sectorsStmt = $db->prepare(
'SELECT s.id, s.libelle, s.color, s.sector
FROM ope_sectors s
JOIN ope_users_sectors us ON s.id = us.fk_sector
WHERE us.fk_operation = ? AND us.fk_user = ? AND us.chk_active = 1 AND s.chk_active = 1'
);
$sectorsStmt->execute([$activeOperationId, $userId]);
} elseif ($interface === 'admin' && ($userRole == 2 || $userRole > 2)) {
// Interface admin : tous les secteurs distincts de l'opération
$sectorsStmt = $db->prepare(
'SELECT DISTINCT s.id, s.libelle, s.color, s.sector
FROM ope_sectors s
WHERE s.fk_operation = ? AND s.chk_active = 1'
);
$sectorsStmt->execute([$activeOperationId]);
} else {
$sectors = [];
}
// Récupération des secteurs si une requête a été préparée
if (isset($sectorsStmt)) {
$sectors = $sectorsStmt->fetchAll(PDO::FETCH_ASSOC);
} else {
$sectors = [];
}
if (!empty($sectors)) {
$sectorsData = $sectors;
// 3. Récupérer les passages selon l'interface et le rôle
if ($interface === 'user' && !empty($sectors)) {
// Interface utilisateur : passages liés aux secteurs de l'utilisateur
$sectorIds = array_column($sectors, 'id');
$sectorIdsString = implode(',', $sectorIds);
if (!empty($sectorIdsString)) {
$passagesStmt = $db->prepare(
"SELECT id, fk_operation, fk_sector, fk_user, fk_type, fk_adresse, passed_at, numero, rue, rue_bis, ville, residence, fk_habitat, appt, niveau,
gps_lat, gps_lng, nom_recu, encrypted_name, remarque, encrypted_email, encrypted_phone, montant, fk_type_reglement, email_erreur, nb_passages,
chk_email_sent, docremis, date_repasser, chk_mobile, anomalie, created_at, updated_at, chk_active
FROM ope_pass
WHERE fk_operation = ? AND fk_sector IN ($sectorIdsString) AND chk_active = 1"
);
$passagesStmt->execute([$activeOperationId]);
}
} elseif ($interface === 'admin' && ($userRole == 2 || $userRole > 2)) {
// Interface admin : tous les passages de l'opération
$passagesStmt = $db->prepare(
"SELECT id, fk_operation, fk_sector, fk_user, fk_type, fk_adresse, passed_at, numero, rue, rue_bis, ville, residence, fk_habitat, appt, niveau,
gps_lat, gps_lng, nom_recu, encrypted_name, remarque, encrypted_email, encrypted_phone, montant, fk_type_reglement, email_erreur, nb_passages,
chk_email_sent, docremis, date_repasser, chk_mobile, anomalie, created_at, updated_at, chk_active
FROM ope_pass
WHERE fk_operation = ? AND chk_active = 1"
);
$passagesStmt->execute([$activeOperationId]);
} else {
$passages = [];
}
// Récupération des passages si une requête a été préparée
if (isset($passagesStmt)) {
$passages = $passagesStmt->fetchAll(PDO::FETCH_ASSOC);
} else {
$passages = [];
}
if (!empty($passages)) {
// Déchiffrer les données sensibles
foreach ($passages as &$passage) {
// Déchiffrement du nom
$passage['name'] = '';
if (!empty($passage['encrypted_name'])) {
$passage['name'] = ApiService::decryptData($passage['encrypted_name']);
}
unset($passage['encrypted_name']);
// Déchiffrement de l'email
$passage['email'] = '';
if (!empty($passage['encrypted_email'])) {
$decryptedEmail = ApiService::decryptSearchableData($passage['encrypted_email']);
if ($decryptedEmail) {
$passage['email'] = $decryptedEmail;
}
}
unset($passage['encrypted_email']);
// Déchiffrement du téléphone
$passage['phone'] = '';
if (!empty($passage['encrypted_phone'])) {
$passage['phone'] = ApiService::decryptData($passage['encrypted_phone']);
}
unset($passage['encrypted_phone']);
}
$passagesData = $passages;
}
// 4. Récupérer les utilisateurs des secteurs partagés
if (($interface === 'user' || ($interface === 'admin' && ($userRole == 2 || $userRole > 2))) && !empty($sectors)) {
$sectorIds = array_column($sectors, 'id');
$sectorIdsString = implode(',', $sectorIds);
if (!empty($sectorIdsString)) {
// Utiliser ope_users au lieu de users pour avoir les données historiques
$usersSectorsStmt = $db->prepare(
"SELECT DISTINCT ou.fk_user as id, ou.first_name, ou.encrypted_name, ou.sect_name, us.fk_sector
FROM ope_users ou
JOIN ope_users_sectors us ON ou.fk_user = us.fk_user AND ou.fk_operation = us.fk_operation
WHERE us.fk_sector IN ($sectorIdsString)
AND us.fk_operation = ?
AND us.chk_active = 1
AND ou.chk_active = 1
AND ou.fk_user != ?" // Exclure l'utilisateur connecté
);
$usersSectorsStmt->execute([$activeOperationId, $userId]);
$usersSectors = $usersSectorsStmt->fetchAll(PDO::FETCH_ASSOC);
if (!empty($usersSectors)) {
// Déchiffrer les noms des utilisateurs
foreach ($usersSectors as &$userSector) {
if (!empty($userSector['encrypted_name'])) {
$userSector['name'] = ApiService::decryptData($userSector['encrypted_name']);
unset($userSector['encrypted_name']);
}
}
$usersSectorsData = $usersSectors;
}
}
}
}
}
}
return [
'operations' => $operationsData,
'sectors' => $sectorsData,
'users_sectors' => $usersSectorsData,
'passages' => $passagesData
];
}
/**
* Prépare la réponse complète pour la création d'opération
*
* @param PDO $db Instance de la base de données
* @param int $newOpeId ID de la nouvelle opération
* @param int $entiteId ID de l'entité
* @return array Réponse formatée avec status, message, operation_id et données
*/
public static function prepareOperationResponse(PDO $db, int $newOpeId, int $entiteId): array {
// Utiliser le rôle admin pour récupérer toutes les données
$operationData = self::prepareOperationData($db, $entiteId, 'admin', 2, 0, $newOpeId);
return [
'status' => 'success',
'message' => 'Opération créée avec succès',
'operation_id' => $newOpeId,
'operations' => $operationData['operations'],
'sectors' => $operationData['sectors'],
'users_sectors' => $operationData['users_sectors'],
'passages' => $operationData['passages']
];
}
}