Livraison d ela gestion des opérations v0.4.0
This commit is contained in:
@@ -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
|
||||
*
|
||||
|
||||
1066
api/src/Controllers/FileController.php
Normal file
1066
api/src/Controllers/FileController.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
1516
api/src/Controllers/OperationController.php
Normal file
1516
api/src/Controllers/OperationController.php
Normal file
File diff suppressed because it is too large
Load Diff
803
api/src/Controllers/PassageController.php
Normal file
803
api/src/Controllers/PassageController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
304
api/src/Services/BackupEncryptionService.php
Normal file
304
api/src/Services/BackupEncryptionService.php
Normal 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']
|
||||
];
|
||||
}
|
||||
}
|
||||
933
api/src/Services/ExportService.php
Normal file
933
api/src/Services/ExportService.php
Normal 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']
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
439
api/src/Services/FileService.php
Normal file
439
api/src/Services/FileService.php
Normal 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();
|
||||
}
|
||||
}
|
||||
282
api/src/Services/OperationDataService.php
Normal file
282
api/src/Services/OperationDataService.php
Normal 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']
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user