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:
2025-08-19 19:38:03 +02:00
parent 4f7247eb2d
commit 3443277d4a
185 changed files with 109354 additions and 102937 deletions

View File

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