feat: Release version 3.1.4 - Mode terrain et génération PDF
✨ Nouvelles fonctionnalités: - Ajout du mode terrain pour utilisation mobile hors connexion - Génération automatique de reçus PDF avec template personnalisé - Révision complète du système de cartes avec amélioration des performances 🔧 Améliorations techniques: - Refactoring du module chat avec architecture simplifiée - Optimisation du système de sécurité NIST SP 800-63B - Amélioration de la gestion des secteurs géographiques - Support UTF-8 étendu pour les noms d'utilisateurs 📱 Application mobile: - Nouveau mode terrain dans user_field_mode_page - Interface utilisateur adaptative pour conditions difficiles - Synchronisation offline améliorée 🗺️ Cartographie: - Optimisation des performances MapBox - Meilleure gestion des tuiles hors ligne - Amélioration de l'affichage des secteurs 📄 Documentation: - Ajout guide Android (ANDROID-GUIDE.md) - Documentation sécurité API (API-SECURITY.md) - Guide module chat (CHAT_MODULE.md) 🐛 Corrections: - Résolution des erreurs 400 lors de la création d'utilisateurs - Correction de la validation des noms d'utilisateurs - Fix des problèmes de synchronisation chat 🤖 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ namespace App\Controllers;
|
||||
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
require_once __DIR__ . '/../Services/ReceiptService.php';
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
@@ -551,10 +552,44 @@ class PassageController {
|
||||
'operationId' => $operationId
|
||||
]);
|
||||
|
||||
// Générer automatiquement un reçu si c'est un don (fk_type = 1) avec email valide
|
||||
$receiptGenerated = false;
|
||||
if (isset($data['fk_type']) && (int)$data['fk_type'] === 1) {
|
||||
// Vérifier si un email a été fourni
|
||||
$hasEmail = false;
|
||||
if (!empty($data['email'])) {
|
||||
$hasEmail = filter_var($data['email'], FILTER_VALIDATE_EMAIL) !== false;
|
||||
} elseif (!empty($encryptedEmail)) {
|
||||
// L'email a déjà été validé lors du chiffrement
|
||||
$hasEmail = true;
|
||||
}
|
||||
|
||||
if ($hasEmail) {
|
||||
try {
|
||||
$receiptService = new \App\Services\ReceiptService();
|
||||
$receiptGenerated = $receiptService->generateReceiptForPassage($passageId);
|
||||
|
||||
if ($receiptGenerated) {
|
||||
LogService::log('Reçu généré automatiquement pour le passage', [
|
||||
'level' => 'info',
|
||||
'passageId' => $passageId
|
||||
]);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la génération automatique du reçu', [
|
||||
'level' => 'warning',
|
||||
'error' => $e->getMessage(),
|
||||
'passageId' => $passageId
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'message' => 'Passage créé avec succès',
|
||||
'passage_id' => $passageId
|
||||
'passage_id' => $passageId,
|
||||
'receipt_generated' => $receiptGenerated
|
||||
], 201);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la création du passage', [
|
||||
@@ -705,9 +740,52 @@ class PassageController {
|
||||
'passageId' => $passageId
|
||||
]);
|
||||
|
||||
// Vérifier si un reçu doit être généré après la mise à jour
|
||||
$receiptGenerated = false;
|
||||
|
||||
// Récupérer les données actualisées du passage
|
||||
$stmt = $this->db->prepare('SELECT fk_type, encrypted_email, nom_recu FROM ope_pass WHERE id = ?');
|
||||
$stmt->execute([$passageId]);
|
||||
$updatedPassage = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($updatedPassage) {
|
||||
// Générer un reçu si :
|
||||
// - C'est un don (fk_type = 1)
|
||||
// - Il y a un email valide
|
||||
// - Il n'y a pas encore de reçu (nom_recu est vide ou null)
|
||||
if ((int)$updatedPassage['fk_type'] === 1 &&
|
||||
!empty($updatedPassage['encrypted_email']) &&
|
||||
empty($updatedPassage['nom_recu'])) {
|
||||
|
||||
// Vérifier que l'email est valide en le déchiffrant
|
||||
try {
|
||||
$email = ApiService::decryptSearchableData($updatedPassage['encrypted_email']);
|
||||
|
||||
if (!empty($email) && filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$receiptService = new \App\Services\ReceiptService();
|
||||
$receiptGenerated = $receiptService->generateReceiptForPassage($passageId);
|
||||
|
||||
if ($receiptGenerated) {
|
||||
LogService::log('Reçu généré automatiquement après mise à jour du passage', [
|
||||
'level' => 'info',
|
||||
'passageId' => $passageId
|
||||
]);
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la génération automatique du reçu après mise à jour', [
|
||||
'level' => 'warning',
|
||||
'error' => $e->getMessage(),
|
||||
'passageId' => $passageId
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'message' => 'Passage mis à jour avec succès'
|
||||
'message' => 'Passage mis à jour avec succès',
|
||||
'receipt_generated' => $receiptGenerated
|
||||
], 200);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la mise à jour du passage', [
|
||||
@@ -800,4 +878,150 @@ class PassageController {
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le reçu PDF d'un passage
|
||||
*
|
||||
* @param string $id ID du passage
|
||||
* @return void
|
||||
*/
|
||||
public function getReceipt(string $id): void {
|
||||
try {
|
||||
$userId = Session::getUserId();
|
||||
if (!$userId) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Vous devez être connecté pour effectuer cette action'
|
||||
], 401);
|
||||
return;
|
||||
}
|
||||
|
||||
$passageId = (int)$id;
|
||||
|
||||
// Vérifier que le passage existe et que l'utilisateur y a accès
|
||||
$entiteId = $this->getUserEntiteId($userId);
|
||||
if (!$entiteId) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Entité non trouvée pour cet utilisateur'
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer les informations du passage et du reçu
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT p.id, p.nom_recu, p.date_creat_recu, p.fk_operation, o.fk_entite
|
||||
FROM ope_pass p
|
||||
INNER JOIN operations o ON p.fk_operation = o.id
|
||||
WHERE p.id = ? AND o.fk_entite = ? AND p.chk_active = 1
|
||||
');
|
||||
$stmt->execute([$passageId, $entiteId]);
|
||||
$passage = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$passage) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Passage non trouvé ou accès non autorisé'
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($passage['nom_recu'])) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Aucun reçu disponible pour ce passage'
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer le fichier depuis la table medias
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT file_path, mime_type, file_size, fichier
|
||||
FROM medias
|
||||
WHERE support = ? AND support_id = ? AND file_category = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
');
|
||||
$stmt->execute(['passage', $passageId, 'recu']);
|
||||
$media = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$media) {
|
||||
// Si pas trouvé dans medias, essayer de construire le chemin
|
||||
$filePath = __DIR__ . '/../../uploads/entites/' . $passage['fk_entite'] .
|
||||
'/recus/' . $passage['fk_operation'] . '/' . $passage['nom_recu'];
|
||||
|
||||
if (!file_exists($filePath)) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Fichier reçu introuvable'
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
$media = [
|
||||
'file_path' => $filePath,
|
||||
'mime_type' => 'application/pdf',
|
||||
'fichier' => $passage['nom_recu'],
|
||||
'file_size' => filesize($filePath)
|
||||
];
|
||||
}
|
||||
|
||||
// Vérifier que le fichier existe
|
||||
if (!file_exists($media['file_path'])) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Fichier reçu introuvable sur le serveur'
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Lire le contenu du fichier
|
||||
$pdfContent = file_get_contents($media['file_path']);
|
||||
if ($pdfContent === false) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Impossible de lire le fichier reçu'
|
||||
], 500);
|
||||
return;
|
||||
}
|
||||
|
||||
// Option 1: Retourner le PDF directement (pour téléchargement)
|
||||
if (isset($_GET['download']) && $_GET['download'] === 'true') {
|
||||
header('Content-Type: ' . $media['mime_type']);
|
||||
header('Content-Disposition: attachment; filename="' . $media['fichier'] . '"');
|
||||
header('Content-Length: ' . $media['file_size']);
|
||||
header('Cache-Control: no-cache, must-revalidate');
|
||||
echo $pdfContent;
|
||||
exit;
|
||||
}
|
||||
|
||||
// Option 2: Retourner le PDF en base64 dans JSON (pour Flutter)
|
||||
$base64 = base64_encode($pdfContent);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'receipt' => [
|
||||
'passage_id' => $passageId,
|
||||
'file_name' => $media['fichier'],
|
||||
'mime_type' => $media['mime_type'],
|
||||
'file_size' => $media['file_size'],
|
||||
'created_at' => $passage['date_creat_recu'],
|
||||
'data_base64' => $base64
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la récupération du reçu', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'passageId' => $id,
|
||||
'userId' => $userId ?? null
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur lors de la récupération du reçu'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user