diff --git a/CHANGELOG-v3.1.6.md b/CHANGELOG-v3.1.6.md new file mode 100644 index 00000000..506daeb0 --- /dev/null +++ b/CHANGELOG-v3.1.6.md @@ -0,0 +1,40 @@ +# Changelog Version 3.1.6 + +## Date: 2025-08-21 + +### Améliorations des flux de passages + +#### Interfaces utilisateur +- Optimisation de l'affichage des listes de passages +- Amélioration de l'ergonomie de navigation +- Ajout de filtres avancés pour la recherche de passages +- Mise à jour de l'interface responsive mobile + +#### Flux de création +- Simplification du processus de création de passage +- Validation en temps réel des données saisies +- Ajout de modèles de passages prédéfinis +- Amélioration de la gestion des erreurs + +#### Flux de consultation +- Affichage optimisé des détails de passage +- Historique complet des modifications +- Export des données en plusieurs formats +- Amélioration des performances de chargement + +#### Flux de modification +- Interface de modification intuitive +- Suivi des changements avec comparaison avant/après +- Validation multi-niveaux des modifications +- Notifications automatiques des mises à jour + +### Corrections de bugs +- Correction de l'affichage sur écrans de petite taille +- Résolution des problèmes de synchronisation +- Amélioration de la stabilité générale + +### Améliorations techniques +- Optimisation des requêtes base de données +- Mise en cache des données fréquemment consultées +- Amélioration des temps de réponse API +- Refactoring du code pour une meilleure maintenabilité \ No newline at end of file diff --git a/api/TODO-API.md b/api/TODO-API.md new file mode 100644 index 00000000..9e8bf682 --- /dev/null +++ b/api/TODO-API.md @@ -0,0 +1,117 @@ +# TODO-API.md + +## 📋 Liste des tâches à implémenter + +### 🔴 PRIORITÉ HAUTE + +#### 1. Système de backup pour les suppressions (DELETE) +**Demandé le :** 20/08/2025 +**Objectif :** Sauvegarder toutes les données supprimées (soft delete) dans un fichier SQL pour pouvoir les restaurer en cas d'erreur humaine. + +**Détails techniques :** +- Créer un système de backup automatique lors de chaque DELETE +- Stocker les données dans un fichier SQL avec structure permettant la réintégration facile +- Format suggéré : `/backups/deleted/{année}/{mois}/deleted_{table}_{YYYYMMDD}.sql` + +**Tables concernées :** +- `ope_pass` (passages) - DELETE /passages/{id} +- `users` (utilisateurs) - DELETE /users/{id} +- `operations` (opérations) - DELETE /operations/{id} +- `ope_sectors` (secteurs) - DELETE /sectors/{id} + +**Structure du backup suggérée :** +```sql +-- Backup deletion: ope_pass +-- Date: 2025-08-20 14:30:45 +-- User: 9999985 (cv_mobile) +-- Entity: 5 +-- Original ID: 19500576 + +INSERT INTO ope_pass_backup ( + original_id, + deleted_at, + deleted_by_user_id, + deleted_by_entity_id, + -- tous les champs originaux + fk_operation, + fk_sector, + fk_user, + montant, + encrypted_name, + encrypted_email, + -- etc... +) VALUES ( + 19500576, + '2025-08-20 14:30:45', + 9999985, + 5, + -- valeurs originales + ... +); + +-- Pour restauration facile : +-- UPDATE ope_pass SET chk_active = 1 WHERE id = 19500576; +``` + +**Fonctionnalités à implémenter :** +1. **Service de backup** : `BackupService.php` + - Méthode `backupDeletedRecord($table, $id, $data)` + - Génération automatique du SQL de restauration + - Rotation des fichiers (garder 90 jours) + +2. **Intégration dans les controllers** + - Ajouter l'appel au BackupService avant chaque soft delete + - Logger l'emplacement du backup + +3. **Interface de restauration** (optionnel) + - Endpoint GET /api/backups/deleted pour lister les backups + - Endpoint POST /api/backups/restore/{backup_id} pour restaurer + +4. **Commande de restauration manuelle** + - Script PHP : `php scripts/restore_deleted.php --table=ope_pass --id=19500576` + +**Avantages :** +- Traçabilité complète des suppressions +- Restauration rapide en cas d'erreur +- Audit trail pour conformité +- Tranquillité d'esprit pour le client + +--- + +### 🟡 PRIORITÉ MOYENNE + +#### 2. Amélioration des logs +- Ajouter plus de contexte dans les logs +- Rotation automatique des logs +- Dashboard de monitoring + +#### 3. Optimisation des performances +- Cache des requêtes fréquentes +- Index sur les tables volumineuses +- Pagination optimisée + +--- + +### 🟢 PRIORITÉ BASSE + +#### 4. Documentation API +- Génération automatique OpenAPI/Swagger +- Documentation interactive +- Exemples de code pour chaque endpoint + +#### 5. Tests automatisés +- Tests unitaires pour les services critiques +- Tests d'intégration pour les endpoints +- Tests de charge + +--- + +## 📝 Notes + +- Les tâches marquées 🔴 doivent être traitées en priorité +- Chaque tâche implémentée doit être documentée +- Prévoir des tests pour chaque nouvelle fonctionnalité + +--- + +**Dernière mise à jour :** 20/08/2025 \ No newline at end of file diff --git a/api/docs/CHK_USER_DELETE_PASS_INFO.md b/api/docs/CHK_USER_DELETE_PASS_INFO.md new file mode 100644 index 00000000..62a889b5 --- /dev/null +++ b/api/docs/CHK_USER_DELETE_PASS_INFO.md @@ -0,0 +1,138 @@ +# Gestion du champ chk_user_delete_pass + +## 📋 Description +Le champ `chk_user_delete_pass` permet de contrôler si les membres d'une amicale peuvent supprimer des passages. + +## 🔄 Modifications API + +### 1. Base de données +- **Table** : `entites` +- **Champ** : `chk_user_delete_pass` TINYINT(1) DEFAULT 0 +- **Valeurs** : + - `0` : Les membres NE peuvent PAS supprimer de passages (par défaut) + - `1` : Les membres PEUVENT supprimer des passages + +### 2. Endpoints modifiés + +#### POST /api/entites (Création) +- Le champ est automatiquement initialisé à `0` (false) lors de la création +- Non modifiable à la création + +#### PUT /api/entites/{id} (Modification) +**Entrée JSON :** +```json +{ + "chk_user_delete_pass": 1 +} +``` +- **Type** : Boolean (0 ou 1) +- **Obligatoire** : Non +- **Accès** : Administrateurs uniquement (fk_role > 1) + +#### GET /api/entites/{id} (Récupération) +**Sortie JSON :** +```json +{ + "id": 5, + "name": "Amicale de Pompiers", + "code_postal": "75001", + "ville": "Paris", + "chk_active": 1, + "chk_user_delete_pass": 0 +} +``` + +#### GET /api/entites (Liste) +Retourne `chk_user_delete_pass` pour chaque entité dans la liste. + +### 3. Route /api/login +Le champ `chk_user_delete_pass` est maintenant inclus dans la réponse de login dans les objets `amicale` : + +**Réponse JSON :** +```json +{ + "user": { ... }, + "amicale": { + "id": 5, + "name": "Amicale de Pompiers", + "code_postal": "75001", + "ville": "Paris", + "chk_demo": 0, + "chk_mdp_manuel": 0, + "chk_username_manuel": 0, + "chk_copie_mail_recu": 0, + "chk_accept_sms": 0, + "chk_active": 1, + "chk_stripe": 0, + "chk_user_delete_pass": 0 // ← NOUVEAU CHAMP + } +} +``` + +## 🎯 Utilisation côté client + +### Flutter/Web +Le client doit : +1. **Récupérer** la valeur de `chk_user_delete_pass` depuis la réponse login +2. **Stocker** cette valeur dans l'état de l'application +3. **Conditionner** l'affichage du bouton de suppression selon cette valeur + +**Exemple Flutter :** +```dart +// Dans le modèle Amicale +class Amicale { + final int id; + final String name; + final bool chkUserDeletePass; // Nouveau champ + + bool get canUserDeletePassage => chkUserDeletePass; +} + +// Dans l'UI +if (amicale.canUserDeletePassage) { + // Afficher le bouton de suppression + IconButton( + icon: Icon(Icons.delete), + onPressed: () => deletePassage(passageId), + ) +} +``` + +## ⚠️ Points importants + +1. **Valeur par défaut** : Toujours `0` (false) pour la sécurité +2. **Modification** : Seuls les administrateurs (fk_role > 1) peuvent modifier ce champ +3. **Rétrocompatibilité** : Les entités existantes ont la valeur `0` par défaut +4. **Validation côté serveur** : L'API vérifiera également ce droit lors de la tentative de suppression + +## 📝 Script SQL +Le script de migration est disponible dans : +``` +/scripts/sql/add_chk_user_delete_pass.sql +``` + +## ✅ Checklist d'implémentation + +### Côté API (déjà fait) : +- [x] Ajout du champ en base de données +- [x] Modification EntiteController (create, update, get) +- [x] Modification LoginController (réponse login) +- [x] Script SQL de migration + +### Côté Client (à faire) : +- [ ] Ajouter le champ dans le modèle Amicale +- [ ] Parser le champ depuis la réponse login +- [ ] Stocker dans l'état de l'application +- [ ] Conditionner l'affichage du bouton suppression +- [ ] Tester avec des valeurs 0 et 1 + +## 🔒 Sécurité +Même si `chk_user_delete_pass = 1`, l'API devra vérifier : +- L'authentification de l'utilisateur +- L'appartenance à l'entité +- Le droit de suppression sur le passage spécifique +- Les règles métier (ex: pas de suppression après export) + +--- +**Date :** 20/08/2025 +**Version API :** 3.1.4 \ No newline at end of file diff --git a/api/docs/DELETE_PASSAGE_PERMISSIONS.md b/api/docs/DELETE_PASSAGE_PERMISSIONS.md new file mode 100644 index 00000000..0f9b0bc7 --- /dev/null +++ b/api/docs/DELETE_PASSAGE_PERMISSIONS.md @@ -0,0 +1,165 @@ +# API DELETE /passages/{id} - Documentation des permissions + +## 📋 Endpoint +``` +DELETE /api/passages/{id} +``` + +## 🔒 Authentification +- **Requise** : OUI (Bearer token) +- **Session** : Doit être valide + +## 📊 Logique de permissions + +### Règles par rôle : + +| fk_role | Description | Peut supprimer ? | Conditions | +|---------|------------|------------------|------------| +| 1 | Membre | ✅ Conditionnel | Si `entites.chk_user_delete_pass = 1` | +| 2 | Admin amicale | ✅ OUI | Toujours autorisé | +| 3+ | Super admin | ✅ OUI | Toujours autorisé | + +### Détail du contrôle pour les membres (fk_role = 1) : + +```sql +-- L'API vérifie : +SELECT chk_user_delete_pass +FROM entites +WHERE id = {user.fk_entite} + +-- Si chk_user_delete_pass = 0 → Erreur 403 +-- Si chk_user_delete_pass = 1 → Continue +``` + +## 🔄 Flux de vérification + +```mermaid +graph TD + A[DELETE /passages/{id}] --> B{Utilisateur authentifié ?} + B -->|Non| C[Erreur 401] + B -->|Oui| D{Récupérer fk_role} + D --> E{fk_role = 1 ?} + E -->|Non| F[Autorisé - Admin] + E -->|Oui| G{Vérifier chk_user_delete_pass} + G -->|= 0| H[Erreur 403 - Non autorisé] + G -->|= 1| F + F --> I{Passage existe ?} + I -->|Non| J[Erreur 404] + I -->|Oui| K{Passage appartient à l'entité ?} + K -->|Non| L[Erreur 404] + K -->|Oui| M[Soft delete : chk_active = 0] + M --> N[Succès 200] +``` + +## 📝 Réponses + +### ✅ Succès (200) +```json +{ + "status": "success", + "message": "Passage supprimé avec succès" +} +``` + +### ❌ Erreur 401 - Non authentifié +```json +{ + "status": "error", + "message": "Vous devez être connecté pour effectuer cette action" +} +``` + +### ❌ Erreur 403 - Permission refusée (membre sans autorisation) +```json +{ + "status": "error", + "message": "Vous n'avez pas l'autorisation de supprimer des passages" +} +``` + +### ❌ Erreur 404 - Passage non trouvé +```json +{ + "status": "error", + "message": "Passage non trouvé" +} +``` + +## 📊 Logging + +L'API enregistre : + +### En cas de tentative non autorisée : +```php +LogService::log('Tentative de suppression de passage non autorisée', [ + 'level' => 'warning', + 'userId' => $userId, + 'userRole' => $userRole, + 'entiteId' => $entiteId, + 'passageId' => $passageId, + 'chk_user_delete_pass' => 0 +]); +``` + +### En cas de succès : +```php +LogService::log('Suppression d\'un passage', [ + 'level' => 'info', + 'userId' => $userId, + 'passageId' => $passageId +]); +``` + +## 🎯 Exemple d'utilisation + +### Requête +```bash +curl -X DELETE https://api.geosector.fr/api/passages/19500576 \ + -H "Authorization: Bearer {session_token}" \ + -H "Content-Type: application/json" +``` + +### Scénarios + +#### Scénario 1 : Membre avec permission ✅ +- Utilisateur : fk_role = 1 +- Entité : chk_user_delete_pass = 1 +- **Résultat** : Suppression autorisée + +#### Scénario 2 : Membre sans permission ❌ +- Utilisateur : fk_role = 1 +- Entité : chk_user_delete_pass = 0 +- **Résultat** : Erreur 403 + +#### Scénario 3 : Admin amicale ✅ +- Utilisateur : fk_role = 2 +- **Résultat** : Suppression autorisée (pas de vérification chk_user_delete_pass) + +## ⚠️ Notes importantes + +1. **Soft delete** : Le passage n'est pas supprimé physiquement, seulement `chk_active = 0` +2. **Traçabilité** : `updated_at` et `fk_user_modif` sont mis à jour +3. **Contrôle entité** : Un utilisateur ne peut supprimer que les passages de son entité +4. **Log warning** : Toute tentative non autorisée est loggée en niveau WARNING + +## 🔧 Configuration côté amicale + +Pour autoriser les membres à supprimer des passages : + +```sql +UPDATE entites +SET chk_user_delete_pass = 1 +WHERE id = {entite_id}; +``` + +Cette modification ne peut être faite que par un administrateur (fk_role > 1) via l'endpoint : +``` +PUT /api/entites/{id} +{ + "chk_user_delete_pass": 1 +} +``` + +--- +**Version API** : 3.1.4 +**Date** : 20/08/2025 \ No newline at end of file diff --git a/api/docs/INSTALL_FPDF.md b/api/docs/INSTALL_FPDF.md new file mode 100644 index 00000000..3a88afac --- /dev/null +++ b/api/docs/INSTALL_FPDF.md @@ -0,0 +1,90 @@ +# Installation de FPDF pour la génération des reçus PDF avec logo + +## Installation via Composer (RECOMMANDÉ) + +Sur chaque serveur (DEV, REC, PROD), exécuter : + +```bash +cd /var/www/geosector/api +composer require setasign/fpdf +``` + +Ou si composer.json est déjà mis à jour : + +```bash +cd /var/www/geosector/api +composer update +``` + +## Fichiers à déployer + +1. **Nouveaux fichiers** : + - `/src/Services/ReceiptPDFGenerator.php` - Nouvelle classe de génération PDF avec FPDF + - `/docs/_logo_recu.png` - Logo par défaut (casque de pompier) + +2. **Fichiers modifiés** : + - `/src/Services/ReceiptService.php` - Utilise maintenant ReceiptPDFGenerator + - `/composer.json` - Ajout de la dépendance FPDF + +## Vérification + +Après installation, tester la génération d'un reçu : + +```bash +# Vérifier que FPDF est installé +ls -la vendor/setasign/fpdf/ + +# Tester la génération d'un PDF +php -r " +require 'vendor/autoload.php'; +\$pdf = new FPDF(); +\$pdf->AddPage(); +\$pdf->SetFont('Arial','B',16); +\$pdf->Cell(40,10,'Test FPDF OK'); +echo 'FPDF fonctionne' . PHP_EOL; +" +``` + +## Fonctionnalités du nouveau générateur + +✅ **Support des vrais logos PNG/JPG** +✅ **Logo par défaut** si l'entité n'a pas de logo +✅ **Taille du logo** : 40x40mm +✅ **Mise en page professionnelle** avec cadre pour le montant +✅ **Conversion automatique** des caractères UTF-8 +✅ **PDF léger** (~20-30KB avec logo) + +## Structure du reçu généré + +1. **En-tête** : + - Logo (40x40mm) à gauche + - Nom et ville de l'entité à droite du logo + +2. **Titre** : + - "REÇU FISCAL DE DON" + - Numéro du reçu + - Article 200 CGI + +3. **Corps** : + - Informations du donateur + - Montant en gros dans un cadre grisé + - Date du don + - Mode de règlement et campagne + +4. **Pied de page** : + - Mentions légales (réduction 66%) + - Date et signature + +## Résolution de problèmes + +Si erreur "Class 'FPDF' not found" : +```bash +composer dump-autoload +``` + +Si problème avec le logo : +- Vérifier que `/docs/_logo_recu.png` existe +- Vérifier les permissions : `chmod 644 docs/_logo_recu.png` + +Si caractères accentués mal affichés : +- FPDF utilise ISO-8859-1, la conversion est automatique dans ReceiptPDFGenerator \ No newline at end of file diff --git a/api/docs/PREPA_PROD.md b/api/docs/PREPA_PROD.md new file mode 100644 index 00000000..6189dca1 --- /dev/null +++ b/api/docs/PREPA_PROD.md @@ -0,0 +1,237 @@ +# PRÉPARATION PRODUCTION - Process Email Queue + Permissions Suppression Passages + +## 📅 Date de mise en production prévue : _____________ + +## 🎯 Objectif +1. Mettre en place le système de traitement automatique de la queue d'emails pour l'envoi des reçus fiscaux de dons. +2. Ajouter le champ de permission pour autoriser les membres à supprimer des passages. + +## ✅ Prérequis +- [ ] Backup de la base de données effectué +- [ ] Accès SSH au serveur PROD +- [ ] Accès à la base de données PROD +- [ ] Droits pour éditer le crontab + +## 📝 Fichiers à déployer +Les fichiers suivants doivent être présents sur le serveur PROD : +- `/scripts/cron/process_email_queue.php` +- `/scripts/cron/process_email_queue_with_daily_log.sh` +- `/scripts/cron/test_email_queue.php` +- `/src/Services/ReceiptPDFGenerator.php` (nouveau) +- `/src/Services/ReceiptService.php` (mis à jour) +- `/src/Core/MonitoredDatabase.php` (mis à jour) +- `/src/Controllers/EntiteController.php` (mis à jour) +- `/src/Controllers/LoginController.php` (mis à jour) +- `/scripts/sql/add_chk_user_delete_pass.sql` (nouveau) + +--- + +## 🔧 ÉTAPES DE MISE EN PRODUCTION + +### 1️⃣ Mise à jour de la base de données + +Se connecter à la base de données PROD et exécuter : + +```sql +-- Vérifier d'abord la structure actuelle de email_queue +DESCRIBE email_queue; + +-- Ajouter les champs manquants pour email_queue si nécessaire +ALTER TABLE `email_queue` +ADD COLUMN IF NOT EXISTS `sent_at` TIMESTAMP NULL DEFAULT NULL +COMMENT 'Date/heure d\'envoi effectif de l\'email' +AFTER `status`; + +ALTER TABLE `email_queue` +ADD COLUMN IF NOT EXISTS `error_message` TEXT NULL DEFAULT NULL +COMMENT 'Message d\'erreur en cas d\'échec' +AFTER `attempts`; + +-- Ajouter les index pour optimiser les performances +ALTER TABLE `email_queue` +ADD INDEX IF NOT EXISTS `idx_status_attempts` (`status`, `attempts`); + +ALTER TABLE `email_queue` +ADD INDEX IF NOT EXISTS `idx_sent_at` (`sent_at`); + +-- Vérifier les modifications email_queue +DESCRIBE email_queue; + +-- ⚠️ IMPORTANT : Ajouter le nouveau champ chk_user_delete_pass dans entites +source /var/www/geosector/api/scripts/sql/add_chk_user_delete_pass.sql; +``` + +### 2️⃣ Test du script avant mise en production + +```bash +# Se connecter au serveur PROD +ssh user@prod-server + +# Aller dans le répertoire de l'API +cd /var/www/geosector/api + +# Rendre les scripts exécutables +chmod +x scripts/cron/process_email_queue.php +chmod +x scripts/cron/test_email_queue.php + +# Tester l'état de la queue (lecture seule) +php scripts/cron/test_email_queue.php + +# Si tout est OK, faire un test d'envoi sur 1 email +# (modifier temporairement BATCH_SIZE à 1 dans le script si nécessaire) +php scripts/cron/process_email_queue.php +``` + +### 3️⃣ Configuration du CRON avec logs journaliers + +```bash +# Rendre le script wrapper exécutable +chmod +x /var/www/geosector/api/scripts/cron/process_email_queue_with_daily_log.sh + +# Éditer le crontab +crontab -e + +# Ajouter cette ligne pour exécution toutes les 5 minutes avec logs journaliers +*/5 * * * * /var/www/geosector/api/scripts/cron/process_email_queue_with_daily_log.sh + +# Sauvegarder et quitter (:wq sous vi/vim) + +# Vérifier que le cron est bien enregistré +crontab -l | grep email_queue + +# Vérifier que le service cron est actif +systemctl status cron +``` + +**Note** : Les logs seront créés automatiquement dans `/var/www/geosector/api/logs/` avec le format : `email_queue_20250820.log`, `email_queue_20250821.log`, etc. Les logs de plus de 30 jours sont supprimés automatiquement. + +### 4️⃣ Surveillance post-déploiement + +Pendant les premières heures après la mise en production : + +```bash +# Surveiller les logs en temps réel (fichier du jour) +tail -f /var/www/geosector/api/logs/email_queue_$(date +%Y%m%d).log + +# Vérifier le statut de la queue +php scripts/cron/test_email_queue.php + +# Compter les emails traités +mysql -u geo_app_user_prod -p geo_app -e " +SELECT status, COUNT(*) as count +FROM email_queue +WHERE DATE(created_at) = CURDATE() +GROUP BY status;" + +# Vérifier les erreurs éventuelles +mysql -u geo_app_user_prod -p geo_app -e " +SELECT id, to_email, subject, attempts, error_message +FROM email_queue +WHERE status='failed' +ORDER BY created_at DESC +LIMIT 10;" +``` + +--- + +## 🚨 ROLLBACK (si nécessaire) + +En cas de problème, voici comment revenir en arrière : + +```bash +# 1. Stopper le cron +crontab -e +# Commenter la ligne du process_email_queue + +# 2. Marquer les emails en attente pour traitement manuel +mysql -u geo_app_user_prod -p geo_app -e " +UPDATE email_queue +SET status='pending', attempts=0 +WHERE status='failed' AND DATE(created_at) = CURDATE();" + +# 3. Informer l'équipe pour traitement manuel si nécessaire +``` + +--- + +## 📊 VALIDATION POST-DÉPLOIEMENT + +### Critères de succès : +- [ ] Aucune erreur dans les logs +- [ ] Les emails sont envoyés dans les 5 minutes +- [ ] Les reçus PDF sont correctement attachés +- [ ] Le champ `date_sent_recu` est mis à jour dans `ope_pass` +- [ ] Pas d'accumulation d'emails en status 'pending' + +### Commandes de vérification : + +```bash +# Statistiques générales +mysql -u geo_app_user_prod -p geo_app -e " +SELECT + status, + COUNT(*) as count, + MIN(created_at) as oldest, + MAX(created_at) as newest +FROM email_queue +GROUP BY status;" + +# Vérifier les passages avec reçus envoyés aujourd'hui +mysql -u geo_app_user_prod -p geo_app -e " +SELECT COUNT(*) as recus_envoyes_aujourdhui +FROM ope_pass +WHERE DATE(date_sent_recu) = CURDATE();" + +# Performance du cron (dernières exécutions du jour) +tail -20 /var/www/geosector/api/logs/email_queue_$(date +%Y%m%d).log | grep "Traitement terminé" +``` + +--- + +## 📞 CONTACTS EN CAS DE PROBLÈME + +- **Responsable technique** : _____________ +- **DBA** : _____________ +- **Support O2Switch** : support@o2switch.fr + +--- + +## 📋 NOTES IMPORTANTES + +1. **Limite d'envoi** : 1500 emails/heure max (limite O2Switch) +2. **Batch size** : 50 emails par exécution (toutes les 5 min = 600/heure max) +3. **Lock file** : `/tmp/process_email_queue.lock` empêche l'exécution simultanée +4. **Nettoyage auto** : Les emails envoyés > 30 jours sont supprimés automatiquement + +## 🔒 SÉCURITÉ + +- Les mots de passe SMTP ne sont jamais loggués +- Les emails en erreur conservent le message d'erreur pour diagnostic +- Le PDF est envoyé en pièce jointe encodée en base64 + +--- + +## ✅ CHECKLIST FINALE + +### Email Queue : +- [ ] Table email_queue mise à jour (sent_at, error_message, index) +- [ ] Scripts cron testés avec succès +- [ ] Cron configuré et actif +- [ ] Logs accessibles et fonctionnels +- [ ] Premier batch d'emails envoyé avec succès + +### Permissions Suppression Passages : +- [ ] Champ chk_user_delete_pass ajouté dans la table entites +- [ ] EntiteController.php mis à jour pour gérer le nouveau champ +- [ ] LoginController.php mis à jour pour retourner le champ dans amicale +- [ ] Test de modification de permissions via l'interface admin + +### Général : +- [ ] Documentation mise à jour +- [ ] Équipe informée de la mise en production + +--- + +**Date de mise en production** : _______________ +**Validé par** : _______________ +**Signature** : _______________ \ No newline at end of file diff --git a/api/docs/SETUP_EMAIL_QUEUE_CRON.md b/api/docs/SETUP_EMAIL_QUEUE_CRON.md new file mode 100644 index 00000000..3dcf0896 --- /dev/null +++ b/api/docs/SETUP_EMAIL_QUEUE_CRON.md @@ -0,0 +1,149 @@ +# Instructions de mise en place du CRON pour la queue d'emails + +## Problème résolu +Les emails de reçus étaient insérés dans la table `email_queue` mais n'étaient jamais envoyés car il manquait le script de traitement. + +## Fichiers créés +1. `/scripts/cron/process_email_queue.php` - Script principal de traitement +2. `/scripts/cron/test_email_queue.php` - Script de test/diagnostic +3. `/scripts/sql/add_email_queue_fields.sql` - Migration SQL pour les champs manquants + +## Installation sur les serveurs (DVA, REC, PROD) + +### 1. Appliquer la migration SQL + +Se connecter à la base de données et exécuter : + +```bash +mysql -u [user] -p [database] < /path/to/api/scripts/sql/add_email_queue_fields.sql +``` + +Ou directement dans MySQL : +```sql +-- Ajouter les champs manquants +ALTER TABLE `email_queue` +ADD COLUMN IF NOT EXISTS `sent_at` TIMESTAMP NULL DEFAULT NULL AFTER `status`; + +ALTER TABLE `email_queue` +ADD COLUMN IF NOT EXISTS `error_message` TEXT NULL DEFAULT NULL AFTER `attempts`; + +-- Ajouter les index pour les performances +ALTER TABLE `email_queue` +ADD INDEX IF NOT EXISTS `idx_status_attempts` (`status`, `attempts`); + +ALTER TABLE `email_queue` +ADD INDEX IF NOT EXISTS `idx_sent_at` (`sent_at`); +``` + +### 2. Tester le script + +Avant de mettre en place le cron, tester que tout fonctionne : + +```bash +# Vérifier l'état de la queue +php /path/to/api/scripts/cron/test_email_queue.php + +# Tester l'envoi (traite jusqu'à 50 emails) +php /path/to/api/scripts/cron/process_email_queue.php +``` + +### 3. Configurer le CRON + +Ajouter la ligne suivante dans le crontab du serveur : + +```bash +# Éditer le crontab +crontab -e + +# Ajouter cette ligne (exécution toutes les 5 minutes) +*/5 * * * * /usr/bin/php /path/to/api/scripts/cron/process_email_queue.php >> /var/log/email_queue.log 2>&1 +``` + +**Options de fréquence :** +- `*/5 * * * *` - Toutes les 5 minutes (recommandé) +- `*/10 * * * *` - Toutes les 10 minutes +- `*/2 * * * *` - Toutes les 2 minutes (si volume important) + +### 4. Monitoring + +Le script génère des logs via `LogService`. Vérifier les logs dans : +- `/path/to/api/logs/` (selon la configuration) + +Points à surveiller : +- Nombre d'emails traités +- Emails en échec après 3 tentatives +- Erreurs de connexion SMTP + +### 5. Configuration SMTP + +Vérifier que la configuration SMTP est correcte dans `AppConfig` : +- Host SMTP +- Port (587 pour TLS, 465 pour SSL) +- Username/Password +- Encryption (tls ou ssl) +- From Email/Name + +## Fonctionnement du script + +### Caractéristiques +- **Batch size** : 50 emails par exécution +- **Max tentatives** : 3 essais par email +- **Lock file** : Empêche l'exécution simultanée +- **Nettoyage** : Supprime les emails envoyés > 30 jours +- **Pause** : 0.5s entre chaque email (anti-spam) + +### Workflow +1. Récupère les emails avec `status = 'pending'` et `attempts < 3` +2. Pour chaque email : + - Incrémente le compteur de tentatives + - Envoie via PHPMailer avec la config SMTP + - Si succès : `status = 'sent'` + mise à jour du passage + - Si échec : réessai à la prochaine exécution + - Après 3 échecs : `status = 'failed'` + +### Tables mises à jour +- `email_queue` : status, attempts, sent_at, error_message +- `ope_pass` : date_sent_recu, chk_email_sent + +## Commandes utiles + +```bash +# Voir les emails en attente +mysql -e "SELECT COUNT(*) FROM email_queue WHERE status='pending'" [database] + +# Voir les emails échoués +mysql -e "SELECT * FROM email_queue WHERE status='failed' ORDER BY created_at DESC LIMIT 10" [database] + +# Réinitialiser un email échoué pour réessai +mysql -e "UPDATE email_queue SET status='pending', attempts=0 WHERE id=[ID]" [database] + +# Voir les logs du cron +tail -f /var/log/email_queue.log + +# Vérifier que le cron est actif +crontab -l | grep process_email_queue +``` + +## Troubleshooting + +### Le cron ne s'exécute pas +- Vérifier les permissions : `chmod +x process_email_queue.php` +- Vérifier le chemin PHP : `which php` +- Vérifier les logs système : `/var/log/syslog` ou `/var/log/cron` + +### Emails en échec +- Vérifier la config SMTP avec `test_email_queue.php` +- Vérifier les logs pour les messages d'erreur +- Tester la connexion SMTP : `telnet [smtp_host] [port]` + +### Lock bloqué +Si le message "Le processus est déjà en cours" persiste : +```bash +rm /tmp/process_email_queue.lock +``` + +## Contact support +En cas de problème, vérifier : +1. Les logs de l'application +2. La table `email_queue` pour les messages d'erreur +3. La configuration SMTP dans AppConfig \ No newline at end of file diff --git a/api/docs/UPLOAD-MIGRATION-RECAP.md b/api/docs/UPLOAD-MIGRATION-RECAP.md new file mode 100644 index 00000000..e9586007 --- /dev/null +++ b/api/docs/UPLOAD-MIGRATION-RECAP.md @@ -0,0 +1,155 @@ +# 📋 RÉCAPITULATIF - Migration Arborescence Uploads + +## ✅ Modifications effectuées + +### 1. **EntiteController.php** (ligne 736) +```php +// Avant : "/entites/{$entiteId}/logo" +// Après : "/{$entiteId}/logo" +``` +✅ Les logos sont maintenant stockés dans : `uploads/{entite_id}/logo/` + +### 2. **ReceiptService.php** (ligne 95) +```php +// Avant : "/entites/{$entiteId}/recus/{$operationId}" +// Après : "/{$entiteId}/recus/{$operationId}" +``` +✅ Les reçus PDF sont maintenant stockés dans : `uploads/{entite_id}/recus/{operation_id}/` + +### 3. **ExportService.php** (lignes 40 et 141) +```php +// Avant Excel : "/{$entiteId}/operations/{$operationId}/exports/excel" +// Après Excel : "/{$entiteId}/operations/{$operationId}" + +// Avant JSON : "/{$entiteId}/operations/{$operationId}/exports/json" +// Après JSON : "/{$entiteId}/operations/{$operationId}" +``` +✅ Les exports sont maintenant stockés directement dans : `uploads/{entite_id}/operations/{operation_id}/` + +## 📂 Nouvelle structure complète + +``` +uploads/ +└── {entite_id}/ # Ex: 5, 1230, etc. + ├── logo/ # Logo de l'entité + │ └── logo_{entite_id}_{timestamp}.{jpg|png} + ├── operations/ # Exports d'opérations + │ └── {operation_id}/ # Ex: 1525, 3124 + │ ├── geosector-export-{operation_id}-{timestamp}.xlsx + │ └── backup-{operation_id}-{timestamp}.json.enc + └── recus/ # Reçus fiscaux + └── {operation_id}/ # Ex: 3124 + └── recu_{passage_id}.pdf +``` + +## 🔧 Script de migration + +Un script a été créé pour migrer les fichiers existants : + +**Fichier :** `/scripts/migrate_uploads_structure.php` + +**Usage :** +```bash +# Mode simulation (voir ce qui sera fait sans modifier) +php scripts/migrate_uploads_structure.php --dry-run + +# Mode réel (effectue la migration) +php scripts/migrate_uploads_structure.php +``` + +**Ce que fait le script :** +1. Déplace tout le contenu de `uploads/entites/*` vers `uploads/*` +2. Fusionne les dossiers si nécessaire +3. Simplifie la structure des exports (supprime `/documents/exports/excel/`) +4. Applique les bonnes permissions (nginx:nobody 775/664) +5. Crée un log détaillé dans `/logs/migration_uploads_YYYYMMDD_HHMMSS.log` + +## 🚀 Procédure de déploiement + +### Sur DEV (déjà fait) +✅ Code modifié +✅ Script de migration créé + +### Sur REC +```bash +# 1. Déployer le nouveau code +./livre-api.sh rec + +# 2. Faire un backup des uploads actuels +cd /var/www/geosector/api +tar -czf uploads_backup_$(date +%Y%m%d).tar.gz uploads/ + +# 3. Tester en mode dry-run +php scripts/migrate_uploads_structure.php --dry-run + +# 4. Si OK, lancer la migration +php scripts/migrate_uploads_structure.php + +# 5. Vérifier la nouvelle structure +ls -la uploads/ +ls -la uploads/*/ +``` + +### Sur PROD +Même procédure que REC après validation + +## ⚠️ Points d'attention + +1. **Backup obligatoire** avant migration +2. **Vérifier l'espace disque** disponible +3. **Tester d'abord en dry-run** +4. **Surveiller les logs** après migration +5. **Tester** upload logo, génération reçu, et export Excel + +## 📊 Gains obtenus + +| Aspect | Avant | Après | +|--------|-------|-------| +| **Profondeur max** | 8 niveaux | 4 niveaux | +| **Complexité** | 2 structures parallèles | 1 structure unique | +| **Clarté** | Confus (entites + racine) | Simple et logique | +| **Navigation** | Difficile | Intuitive | + +## 🔍 Vérification post-migration + +Après la migration, vérifier : + +```bash +# Structure attendue pour l'entité 5 +tree uploads/5/ +# Devrait afficher : +# uploads/5/ +# ├── logo/ +# │ └── logo_5_*.png +# ├── operations/ +# │ ├── 1525/ +# │ │ └── *.xlsx +# │ └── 3124/ +# │ └── *.xlsx +# └── recus/ +# └── 3124/ +# └── recu_*.pdf + +# Vérifier les permissions +ls -la uploads/*/ +# Devrait montrer : nginx:nobody avec 775 pour dossiers, 664 pour fichiers +``` + +## ✅ Checklist finale + +- [ ] Code modifié et testé en DEV +- [ ] Script de migration créé +- [ ] Documentation mise à jour +- [ ] Backup effectué sur REC +- [ ] Migration testée en dry-run sur REC +- [ ] Migration exécutée sur REC +- [ ] Tests fonctionnels sur REC +- [ ] Backup effectué sur PROD +- [ ] Migration exécutée sur PROD +- [ ] Tests fonctionnels sur PROD + +--- + +**Date de création :** 20/08/2025 +**Auteur :** Assistant Claude +**Status :** Prêt pour déploiement \ No newline at end of file diff --git a/api/docs/UPLOAD-REORGANIZATION.md b/api/docs/UPLOAD-REORGANIZATION.md new file mode 100644 index 00000000..125a63df --- /dev/null +++ b/api/docs/UPLOAD-REORGANIZATION.md @@ -0,0 +1,93 @@ +# Réorganisation de l'arborescence des uploads + +## 📅 Date : 20/08/2025 + +## 🎯 Objectif +Uniformiser et simplifier l'arborescence des fichiers uploads pour une meilleure organisation et maintenance. + +## 📂 Arborescence actuelle (PROBLÈME) +``` +uploads/ +├── entites/ +│ └── 5/ +│ ├── logo/ +│ ├── operations/ +│ │ └── 1525/ +│ │ └── documents/ +│ │ └── exports/ +│ │ └── excel/ +│ │ └── geosector-export-*.xlsx +│ └── recus/ +│ └── 3124/ +│ └── recu_*.pdf +└── 5/ + └── operations/ + ├── 1525/ + └── 2021/ +``` + +**Problèmes identifiés :** +- Duplication des structures (dossier `5` à la racine ET dans `entites/`) +- Chemins trop profonds pour les exports Excel (6 niveaux) +- Incohérence dans les chemins + +## ✅ Nouvelle arborescence (SOLUTION) +``` +uploads/ +└── {entite_id}/ # Un seul dossier par entité à la racine + ├── logo/ # Logo de l'entité + │ └── logo_*.{jpg,png} + ├── operations/ # Exports par opération + │ └── {operation_id}/ + │ └── *.xlsx # Exports Excel directement ici + └── recus/ # Reçus par opération + └── {operation_id}/ + └── recu_*.pdf +``` + +## 📝 Fichiers à modifier + +### 1. EntiteController.php (Upload logo) +**Actuel :** `/entites/{$entiteId}/logo` +**Nouveau :** `/{$entiteId}/logo` + +### 2. ReceiptService.php (Stockage reçus PDF) +**Actuel :** `/entites/{$entiteId}/recus/{$operationId}` +**Nouveau :** `/{$entiteId}/recus/{$operationId}` + +### 3. ExportService.php (Export Excel) +**Actuel :** `/{$entiteId}/operations/{$operationId}/exports/excel` +**Nouveau :** `/{$entiteId}/operations/{$operationId}` + +### 4. ExportService.php (Export JSON) +**Actuel :** `/{$entiteId}/operations/{$operationId}/exports/json` +**Nouveau :** `/{$entiteId}/operations/{$operationId}` (ou supprimer si non utilisé) + +## 🔄 Plan de migration + +### Étape 1 : Modifier le code +1. Mettre à jour tous les chemins dans les contrôleurs et services +2. Tester en environnement DEV + +### Étape 2 : Script de migration des fichiers existants +Créer un script PHP pour : +1. Lister tous les fichiers existants +2. Les déplacer vers la nouvelle structure +3. Supprimer les anciens dossiers vides + +### Étape 3 : Déploiement +1. Exécuter le script de migration sur REC +2. Vérifier le bon fonctionnement +3. Exécuter sur PROD + +## 🚀 Avantages de la nouvelle structure +- **Plus simple** : Chemins plus courts et plus logiques +- **Plus cohérent** : Une seule structure pour toutes les entités +- **Plus maintenable** : Facile de naviguer et comprendre +- **Performance** : Moins de niveaux de dossiers à parcourir + +## ⚠️ Points d'attention +- Vérifier les permissions (nginx:nobody 775/664) +- S'assurer que les anciens fichiers sont bien migrés +- Mettre à jour la documentation +- Informer l'équipe du changement \ No newline at end of file diff --git a/api/docs/logrotate_email_queue.conf b/api/docs/logrotate_email_queue.conf new file mode 100644 index 00000000..1e147848 --- /dev/null +++ b/api/docs/logrotate_email_queue.conf @@ -0,0 +1,19 @@ +# Configuration logrotate pour email_queue.log +# À placer dans /etc/logrotate.d/geosector-email-queue + +/var/www/geosector/api/logs/email_queue.log { + daily # Rotation journalière + rotate 30 # Garder 30 jours d'historique + compress # Compresser les anciens logs + delaycompress # Compresser le jour suivant + missingok # Pas d'erreur si le fichier n'existe pas + notifempty # Ne pas tourner si vide + create 664 www-data www-data # Créer nouveau fichier avec permissions + dateext # Ajouter la date au nom du fichier + dateformat -%Y%m%d # Format de date YYYYMMDD + maxsize 100M # Rotation si dépasse 100MB même avant la fin du jour + postrotate + # Optionnel : envoyer un signal au process si nécessaire + # /usr/bin/killall -SIGUSR1 php 2>/dev/null || true + endscript +} \ No newline at end of file diff --git a/api/docs/recu_19500582.pdf b/api/docs/recu_19500582.pdf new file mode 100644 index 00000000..ec40bc04 --- /dev/null +++ b/api/docs/recu_19500582.pdf @@ -0,0 +1,93 @@ +%PDF-1.4 +%âãÏÓ +1 0 obj +<< /Type /Catalog /Pages 2 0 R >> +endobj +2 0 obj +<< /Type /Pages /Kids [3 0 R] /Count 1 >> +endobj +3 0 obj +<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >> +endobj +4 0 obj +<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >> +endobj +5 0 obj +<< /Length 599 >> +stream +BT +/F1 14 Tf +217 792 Td +(AMICALE TEST DEV PIERRE) Tj +ET +BT +/F1 11 Tf +281 770 Td +(RENNES) Tj +ET +BT +/F1 14 Tf +213.5 726 Td +(RECU FISCAL N 19500582) Tj +ET +BT +/F1 9 Tf +263.75 704 Td +(Article 200 CGI) Tj +ET +BT +/F1 12 Tf +50 657 Td +(Dugues) Tj +ET +BT +/F1 11 Tf +50 637 Td +(8 le Petit Monthelon Acigne) Tj +ET +BT +/F1 16 Tf +257.5 598 Td +(8,00 euros) Tj +ET +BT +/F1 12 Tf +267.5 559 Td +(20/08/2025) Tj +ET +BT +/F1 10 Tf +277.5 529 Td +(OPE 2025) Tj +ET +BT +/F1 9 Tf +198.5 476 Td +(Don ouvrant droit a reduction d'impot de 66%) Tj +ET +BT +/F1 10 Tf +50 419 Td +(Le 20/08/2025) Tj +ET +BT +/F1 10 Tf +50 401 Td +(Le President) Tj +ET + +endstream +endobj +xref +0 6 +0000000000 65535 f +0000000019 00000 n +0000000068 00000 n +0000000125 00000 n +0000000251 00000 n +0000000353 00000 n +trailer +<< /Size 6 /Root 1 0 R >> +startxref +1003 +%%EOF diff --git a/api/docs/recu_19500586.pdf b/api/docs/recu_19500586.pdf new file mode 100644 index 00000000..dde38e68 --- /dev/null +++ b/api/docs/recu_19500586.pdf @@ -0,0 +1,75 @@ +%PDF-1.3 +1 0 obj +<< /Type /Catalog /Pages 2 0 R >> +endobj +2 0 obj +<< /Type /Pages /Kids [3 0 R] /Count 1 >> +endobj +3 0 obj +<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >> +endobj +4 0 obj +<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> +endobj +5 0 obj +<< /Length 767 >> +stream +BT +/F1 12 Tf +50 750 Td +(AMICALE TEST DEV PIERRE) Tj +0 -20 Td +(17 place hoche 35000 RENNES) Tj +/F1 16 Tf +0 -40 Td +(RECU DE DON N° 19500586) Tj +/F1 10 Tf +0 -15 Td +(Article 200 du Code General des Impots) Tj +/F1 12 Tf +0 -45 Td +(DONATEUR) Tj +/F1 11 Tf +0 -20 Td +(Nom : M. Hermann) Tj +0 -15 Td +(Adresse : 12 le Petit Monthelon Acigne) Tj +0 -15 Td +(Email : pierre.vaissaire@gmail.com) Tj +0 -30 Td +/F1 12 Tf +(DETAILS DU DON) Tj +/F1 11 Tf +0 -20 Td +(Date : 19/08/2025) Tj +0 -15 Td +(Montant : 12,00 EUR) Tj +0 -15 Td +(Mode de reglement : Espece) Tj +0 -15 Td +(Campagne : OPE 2025) Tj +/F1 9 Tf +0 -40 Td +(Reduction d'impot egale a 66% du montant verse dans la limite de 20% du revenu imposable) Tj +/F1 11 Tf +0 -30 Td +(Fait a RENNES, le 19/08/2025) Tj +0 -20 Td +(Le President) Tj +ET + +endstream +endobj +xref +0 6 +0000000000 65535 f +0000000009 00000 n +0000000058 00000 n +0000000115 00000 n +0000000241 00000 n +0000000311 00000 n +trailer +<< /Size 6 /Root 1 0 R >> +startxref +1129 +%%EOF diff --git a/api/scripts/cron/process_email_queue.php b/api/scripts/cron/process_email_queue.php new file mode 100755 index 00000000..e1945bb3 --- /dev/null +++ b/api/scripts/cron/process_email_queue.php @@ -0,0 +1,317 @@ +#!/usr/bin/env php + 1800) { + unlink(LOCK_FILE); + } else { + die("Le processus est déjà en cours d'exécution\n"); + } +} + +// Créer le fichier de lock +file_put_contents(LOCK_FILE, getmypid()); + +// Enregistrer un handler pour supprimer le lock en cas d'arrêt +register_shutdown_function(function() { + if (file_exists(LOCK_FILE)) { + unlink(LOCK_FILE); + } +}); + +// Simuler l'environnement web pour AppConfig en CLI +if (php_sapi_name() === 'cli') { + // Détecter l'environnement basé sur le hostname ou un paramètre + $hostname = gethostname(); + if (strpos($hostname, 'prod') !== false) { + $_SERVER['SERVER_NAME'] = 'app.geosector.fr'; + } elseif (strpos($hostname, 'rec') !== false || strpos($hostname, 'rapp') !== false) { + $_SERVER['SERVER_NAME'] = 'rapp.geosector.fr'; + } else { + $_SERVER['SERVER_NAME'] = 'dapp.geosector.fr'; // DVA par défaut + } + + $_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME']; + $_SERVER['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'; + + // Définir getallheaders si elle n'existe pas (CLI) + if (!function_exists('getallheaders')) { + function getallheaders() { + return []; + } + } +} + +// Chargement de l'environnement +require_once __DIR__ . '/../../vendor/autoload.php'; +require_once __DIR__ . '/../../src/Config/AppConfig.php'; +require_once __DIR__ . '/../../src/Core/Database.php'; +require_once __DIR__ . '/../../src/Services/LogService.php'; + +use PHPMailer\PHPMailer\PHPMailer; +use PHPMailer\PHPMailer\SMTP; +use PHPMailer\PHPMailer\Exception; + +try { + // Initialisation de la configuration + $appConfig = AppConfig::getInstance(); + $dbConfig = $appConfig->getDatabaseConfig(); + + // Initialiser la base de données avec la configuration + Database::init($dbConfig); + $db = Database::getInstance(); + + LogService::log('Démarrage du processeur de queue d\'emails', [ + 'level' => 'info', + 'script' => 'process_email_queue.php' + ]); + + // Récupérer les emails en attente + $stmt = $db->prepare(' + SELECT id, fk_pass, to_email, subject, body, headers, attempts + FROM email_queue + WHERE status = ? AND attempts < ? + ORDER BY created_at ASC + LIMIT ? + '); + + $stmt->execute(['pending', MAX_ATTEMPTS, BATCH_SIZE]); + $emails = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if (empty($emails)) { + LogService::log('Aucun email en attente dans la queue', [ + 'level' => 'debug' + ]); + exit(0); + } + + LogService::log('Emails à traiter', [ + 'level' => 'info', + 'count' => count($emails) + ]); + + // Configuration SMTP + $smtpConfig = $appConfig->getSmtpConfig(); + $emailConfig = $appConfig->getEmailConfig(); + + $successCount = 0; + $failureCount = 0; + + // Traiter chaque email + foreach ($emails as $emailData) { + $emailId = $emailData['id']; + $passageId = $emailData['fk_pass']; + + try { + // Incrémenter le compteur de tentatives + $stmt = $db->prepare('UPDATE email_queue SET attempts = attempts + 1 WHERE id = ?'); + $stmt->execute([$emailId]); + + // Créer l'instance PHPMailer + $mail = new PHPMailer(true); + + // Configuration du serveur SMTP + $mail->isSMTP(); + $mail->Host = $smtpConfig['host']; + $mail->SMTPAuth = $smtpConfig['auth'] ?? true; + $mail->Username = $smtpConfig['user']; + $mail->Password = $smtpConfig['pass']; + $mail->SMTPSecure = $smtpConfig['secure']; + $mail->Port = $smtpConfig['port']; + $mail->CharSet = 'UTF-8'; + + // Configuration de l'expéditeur + $fromName = 'Amicale Sapeurs-Pompiers'; // Nom par défaut + $mail->setFrom($emailConfig['from'], $fromName); + + // Destinataire + $mail->addAddress($emailData['to_email']); + + // Sujet + $mail->Subject = $emailData['subject']; + + // Headers personnalisés si présents + if (!empty($emailData['headers'])) { + // Les headers contiennent déjà les informations MIME pour la pièce jointe + // On doit extraire le boundary et reconstruire le message + if (preg_match('/boundary="([^"]+)"/', $emailData['headers'], $matches)) { + $boundary = $matches[1]; + + // Le body contient déjà le message complet avec pièce jointe + $mail->isHTML(false); + $mail->Body = $emailData['body']; + + // Extraire le contenu HTML et la pièce jointe + $parts = explode("--$boundary", $emailData['body']); + + foreach ($parts as $part) { + if (strpos($part, 'Content-Type: text/html') !== false) { + // Extraire le contenu HTML + $htmlContent = trim(substr($part, strpos($part, "\r\n\r\n") + 4)); + $mail->isHTML(true); + $mail->Body = $htmlContent; + } elseif (strpos($part, 'Content-Type: application/pdf') !== false) { + // Extraire le PDF encodé en base64 + if (preg_match('/filename="([^"]+)"/', $part, $fileMatches)) { + $filename = $fileMatches[1]; + $pdfContent = trim(substr($part, strpos($part, "\r\n\r\n") + 4)); + // Supprimer les retours à la ligne du base64 + $pdfContent = str_replace(["\r", "\n"], '', $pdfContent); + + // Ajouter la pièce jointe + $mail->addStringAttachment( + base64_decode($pdfContent), + $filename, + 'base64', + 'application/pdf' + ); + } + } + } + } + } else { + // Email simple sans pièce jointe + $mail->isHTML(true); + $mail->Body = $emailData['body']; + } + + // Ajouter une copie si configuré + if (!empty($emailConfig['contact'])) { + $mail->addBCC($emailConfig['contact']); + } + + // Envoyer l'email + if ($mail->send()) { + // Marquer comme envoyé + $stmt = $db->prepare(' + UPDATE email_queue + SET status = ?, sent_at = NOW() + WHERE id = ? + '); + $stmt->execute(['sent', $emailId]); + + // Mettre à jour le passage si nécessaire + if ($passageId > 0) { + $stmt = $db->prepare(' + UPDATE ope_pass + SET date_sent_recu = NOW(), chk_email_sent = 1 + WHERE id = ? + '); + $stmt->execute([$passageId]); + } + + $successCount++; + + LogService::log('Email envoyé avec succès', [ + 'level' => 'info', + 'emailId' => $emailId, + 'passageId' => $passageId, + 'to' => $emailData['to_email'] + ]); + } else { + throw new Exception('Échec de l\'envoi'); + } + + } catch (Exception $e) { + $failureCount++; + + LogService::log('Erreur lors de l\'envoi de l\'email', [ + 'level' => 'error', + 'emailId' => $emailId, + 'passageId' => $passageId, + 'error' => $e->getMessage(), + 'attempts' => $emailData['attempts'] + 1 + ]); + + // Si on a atteint le nombre max de tentatives, marquer comme échoué + if ($emailData['attempts'] + 1 >= MAX_ATTEMPTS) { + $stmt = $db->prepare(' + UPDATE email_queue + SET status = ?, error_message = ? + WHERE id = ? + '); + $stmt->execute(['failed', $e->getMessage(), $emailId]); + + LogService::log('Email marqué comme échoué après ' . MAX_ATTEMPTS . ' tentatives', [ + 'level' => 'warning', + 'emailId' => $emailId, + 'passageId' => $passageId + ]); + } + } + + // Pause courte entre chaque email pour éviter la surcharge + usleep(500000); // 0.5 seconde + } + + LogService::log('Traitement de la queue terminé', [ + 'level' => 'info', + 'success' => $successCount, + 'failures' => $failureCount, + 'total' => count($emails) + ]); + +} catch (Exception $e) { + LogService::log('Erreur fatale dans le processeur de queue', [ + 'level' => 'critical', + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + // Supprimer le lock en cas d'erreur + if (file_exists(LOCK_FILE)) { + unlink(LOCK_FILE); + } + + exit(1); +} + +// Nettoyer les vieux emails traités (optionnel) +try { + // Supprimer les emails envoyés de plus de 30 jours + $stmt = $db->prepare(' + DELETE FROM email_queue + WHERE status = ? AND sent_at < DATE_SUB(NOW(), INTERVAL 30 DAY) + '); + $stmt->execute(['sent']); + + $deleted = $stmt->rowCount(); + if ($deleted > 0) { + LogService::log('Nettoyage des anciens emails', [ + 'level' => 'info', + 'deleted' => $deleted + ]); + } + +} catch (Exception $e) { + LogService::log('Erreur lors du nettoyage des anciens emails', [ + 'level' => 'warning', + 'error' => $e->getMessage() + ]); +} + +// Supprimer le lock +if (file_exists(LOCK_FILE)) { + unlink(LOCK_FILE); +} + +echo "Traitement terminé : $successCount envoyés, $failureCount échecs\n"; +exit(0); \ No newline at end of file diff --git a/api/scripts/cron/process_email_queue_with_daily_log.sh b/api/scripts/cron/process_email_queue_with_daily_log.sh new file mode 100644 index 00000000..2530994b --- /dev/null +++ b/api/scripts/cron/process_email_queue_with_daily_log.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Script wrapper pour process_email_queue avec logs journaliers +# Crée automatiquement un nouveau fichier log chaque jour + +# Configuration +LOG_DIR="/var/www/geosector/api/logs" +LOG_FILE="$LOG_DIR/email_queue_$(date +%Y%m%d).log" +PHP_SCRIPT="/var/www/geosector/api/scripts/cron/process_email_queue.php" + +# Créer le répertoire de logs s'il n'existe pas +mkdir -p "$LOG_DIR" + +# Ajouter un timestamp au début de l'exécution +echo "[$(date '+%Y-%m-%d %H:%M:%S')] Démarrage du processeur de queue d'emails" >> "$LOG_FILE" + +# Exécuter le script PHP +/usr/bin/php "$PHP_SCRIPT" >> "$LOG_FILE" 2>&1 + +# Ajouter le statut de sortie +EXIT_CODE=$? +if [ $EXIT_CODE -eq 0 ]; then + echo "[$(date '+%Y-%m-%d %H:%M:%S')] Fin du traitement (succès)" >> "$LOG_FILE" +else + echo "[$(date '+%Y-%m-%d %H:%M:%S')] Fin du traitement (erreur: $EXIT_CODE)" >> "$LOG_FILE" +fi + +# Nettoyer les logs de plus de 30 jours +find "$LOG_DIR" -name "email_queue_*.log" -type f -mtime +30 -delete 2>/dev/null + +exit $EXIT_CODE \ No newline at end of file diff --git a/api/scripts/cron/test_email_queue.php b/api/scripts/cron/test_email_queue.php new file mode 100755 index 00000000..45dc3511 --- /dev/null +++ b/api/scripts/cron/test_email_queue.php @@ -0,0 +1,186 @@ +#!/usr/bin/env php +getDatabaseConfig(); + + // Initialiser la base de données avec la configuration + Database::init($dbConfig); + $db = Database::getInstance(); + + echo "=== TEST DE LA QUEUE D'EMAILS ===\n\n"; + + // Statistiques générales + $stmt = $db->query(' + SELECT + status, + COUNT(*) as count, + MIN(created_at) as oldest, + MAX(created_at) as newest + FROM email_queue + GROUP BY status + '); + + $stats = $stmt->fetchAll(PDO::FETCH_ASSOC); + + echo "STATISTIQUES:\n"; + echo "-------------\n"; + foreach ($stats as $stat) { + echo sprintf( + "Status: %s - Nombre: %d (Plus ancien: %s, Plus récent: %s)\n", + $stat['status'], + $stat['count'], + $stat['oldest'] ?? 'N/A', + $stat['newest'] ?? 'N/A' + ); + } + + echo "\n"; + + // Emails en attente + $stmt = $db->prepare(' + SELECT + eq.id, + eq.fk_pass, + eq.to_email, + eq.subject, + eq.created_at, + eq.attempts, + eq.status, + p.fk_type, + p.montant, + p.nom_recu + FROM email_queue eq + LEFT JOIN ope_pass p ON eq.fk_pass = p.id + WHERE eq.status = ? + ORDER BY eq.created_at DESC + LIMIT 10 + '); + + $stmt->execute(['pending']); + $pendingEmails = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if (empty($pendingEmails)) { + echo "Aucun email en attente.\n"; + } else { + echo "EMAILS EN ATTENTE (10 plus récents):\n"; + echo "------------------------------------\n"; + foreach ($pendingEmails as $email) { + echo sprintf( + "ID: %d | Passage: %d | Destinataire: %s\n", + $email['id'], + $email['fk_pass'], + $email['to_email'] + ); + echo sprintf( + " Sujet: %s\n", + $email['subject'] + ); + echo sprintf( + " Créé le: %s | Tentatives: %d\n", + $email['created_at'], + $email['attempts'] + ); + if ($email['fk_pass'] > 0) { + echo sprintf( + " Passage - Type: %s | Montant: %.2f€ | Reçu: %s\n", + $email['fk_type'] == 1 ? 'DON' : 'Autre', + $email['montant'] ?? 0, + $email['nom_recu'] ?? 'Non généré' + ); + } + echo "---\n"; + } + } + + // Emails échoués + $stmt = $db->prepare(' + SELECT + id, + fk_pass, + to_email, + subject, + created_at, + attempts, + error_message + FROM email_queue + WHERE status = ? + ORDER BY created_at DESC + LIMIT 5 + '); + + $stmt->execute(['failed']); + $failedEmails = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if (!empty($failedEmails)) { + echo "\nEMAILS ÉCHOUÉS (5 plus récents):\n"; + echo "--------------------------------\n"; + foreach ($failedEmails as $email) { + echo sprintf( + "ID: %d | Passage: %d | Destinataire: %s\n", + $email['id'], + $email['fk_pass'], + $email['to_email'] + ); + echo sprintf( + " Sujet: %s\n", + $email['subject'] + ); + echo sprintf( + " Tentatives: %d | Erreur: %s\n", + $email['attempts'], + $email['error_message'] ?? 'Non spécifiée' + ); + echo "---\n"; + } + } + + // Vérifier la configuration SMTP + echo "\nCONFIGURATION SMTP:\n"; + echo "-------------------\n"; + + $smtpConfig = $appConfig->getSmtpConfig(); + $emailConfig = $appConfig->getEmailConfig(); + + echo "Host: " . ($smtpConfig['host'] ?? 'Non configuré') . "\n"; + echo "Port: " . ($smtpConfig['port'] ?? 'Non configuré') . "\n"; + echo "Username: " . ($smtpConfig['user'] ?? 'Non configuré') . "\n"; + echo "Password: " . (isset($smtpConfig['pass']) ? '***' : 'Non configuré') . "\n"; + echo "Encryption: " . ($smtpConfig['secure'] ?? 'Non configuré') . "\n"; + echo "From Email: " . ($emailConfig['from'] ?? 'Non configuré') . "\n"; + echo "Contact Email: " . ($emailConfig['contact'] ?? 'Non configuré') . "\n"; + + echo "\n=== FIN DU TEST ===\n"; + +} catch (Exception $e) { + echo "ERREUR: " . $e->getMessage() . "\n"; + exit(1); +} + +exit(0); \ No newline at end of file diff --git a/api/scripts/migrate_uploads_structure.php b/api/scripts/migrate_uploads_structure.php new file mode 100644 index 00000000..4bb727fb --- /dev/null +++ b/api/scripts/migrate_uploads_structure.php @@ -0,0 +1,298 @@ +#!/usr/bin/env php + $destination"); + if (!$dryRun) { + if (is_dir($source)) { + // Pour un dossier, utiliser rename + return rename($source, $destination); + } else { + // Pour un fichier + return rename($source, $destination); + } + } + + return true; +} + +// Fonction pour copier récursivement un dossier +function copyDirectory(string $source, string $dest): bool { + global $dryRun; + + if (!is_dir($source)) { + return false; + } + + if (!$dryRun) { + if (!is_dir($dest)) { + mkdir($dest, 0775, true); + chown($dest, 'nginx'); + chgrp($dest, 'nobody'); + } + } + + $dir = opendir($source); + while (($file = readdir($dir)) !== false) { + if ($file === '.' || $file === '..') { + continue; + } + + $srcPath = "$source/$file"; + $destPath = "$dest/$file"; + + if (is_dir($srcPath)) { + copyDirectory($srcPath, $destPath); + } else { + logMessage("Copie: $srcPath -> $destPath"); + if (!$dryRun) { + copy($srcPath, $destPath); + chmod($destPath, 0664); + chown($destPath, 'nginx'); + chgrp($destPath, 'nobody'); + } + } + } + closedir($dir); + + return true; +} + +// Fonction principale de migration +function migrateUploads(): void { + global $dryRun; + + logMessage("=== Début de la migration des uploads ==="); + logMessage($dryRun ? "MODE DRY-RUN (simulation)" : "MODE RÉEL (modifications effectives)"); + + // 1. Migrer uploads/entites/* vers uploads/* + $entitesPath = BASE_PATH . '/entites'; + if (is_dir($entitesPath)) { + logMessage("Traitement du dossier entites/"); + + $entites = scandir($entitesPath); + foreach ($entites as $entiteId) { + if ($entiteId === '.' || $entiteId === '..') continue; + + $oldPath = "$entitesPath/$entiteId"; + $newPath = BASE_PATH . "/$entiteId"; + + if (!is_dir($oldPath)) continue; + + logMessage("Migration entité $entiteId"); + + // Si le dossier destination existe déjà, fusionner + if (is_dir($newPath)) { + logMessage("Le dossier $entiteId existe déjà à la racine, fusion nécessaire", 'INFO'); + + // Migrer les sous-dossiers + $subDirs = scandir($oldPath); + foreach ($subDirs as $subDir) { + if ($subDir === '.' || $subDir === '..') continue; + + $oldSubPath = "$oldPath/$subDir"; + $newSubPath = "$newPath/$subDir"; + + if ($subDir === 'operations') { + // Traiter spécialement le dossier operations + migrateOperations($oldSubPath, $newSubPath); + } else { + // Pour logo et recus, déplacer directement + if (!is_dir($newSubPath)) { + moveItem($oldSubPath, $newSubPath); + } else { + logMessage("Le dossier $newSubPath existe déjà, fusion du contenu"); + copyDirectory($oldSubPath, $newSubPath); + if (!$dryRun) { + // Supprimer l'ancien après copie + exec("rm -rf " . escapeshellarg($oldSubPath)); + } + } + } + } + } else { + // Déplacer simplement le dossier entier + moveItem($oldPath, $newPath); + } + } + + // Supprimer le dossier entites vide + if (!$dryRun) { + if (count(scandir($entitesPath)) === 2) { // Seulement . et .. + rmdir($entitesPath); + logMessage("Suppression du dossier entites/ vide"); + } + } + } + + // 2. Nettoyer la structure des dossiers operations + logMessage("Nettoyage de la structure des dossiers operations"); + cleanupOperationsStructure(); + + logMessage("=== Migration terminée ==="); + if (!$dryRun) { + logMessage("Logs sauvegardés dans: " . LOG_FILE); + } +} + +// Fonction pour migrer le dossier operations avec simplification +function migrateOperations(string $oldPath, string $newPath): void { + global $dryRun; + + if (!is_dir($oldPath)) return; + + logMessage("Migration du dossier operations: $oldPath"); + + if (!$dryRun && !is_dir($newPath)) { + mkdir($newPath, 0775, true); + chown($newPath, 'nginx'); + chgrp($newPath, 'nobody'); + } + + $operations = scandir($oldPath); + foreach ($operations as $opId) { + if ($opId === '.' || $opId === '..') continue; + + $oldOpPath = "$oldPath/$opId"; + $newOpPath = "$newPath/$opId"; + + // Simplifier la structure: déplacer les xlsx directement dans operations/{id}/ + if (is_dir("$oldOpPath/documents/exports/excel")) { + $excelPath = "$oldOpPath/documents/exports/excel"; + $files = scandir($excelPath); + + foreach ($files as $file) { + if ($file === '.' || $file === '..' || !str_ends_with($file, '.xlsx')) continue; + + $oldFilePath = "$excelPath/$file"; + $newFilePath = "$newOpPath/$file"; + + logMessage("Déplacement Excel: $oldFilePath -> $newFilePath"); + + if (!$dryRun) { + if (!is_dir($newOpPath)) { + mkdir($newOpPath, 0775, true); + chown($newOpPath, 'nginx'); + chgrp($newOpPath, 'nobody'); + } + rename($oldFilePath, $newFilePath); + chmod($newFilePath, 0664); + chown($newFilePath, 'nginx'); + chgrp($newFilePath, 'nobody'); + } + } + } + } +} + +// Fonction pour nettoyer la structure après migration +function cleanupOperationsStructure(): void { + global $dryRun; + + $uploadsDir = BASE_PATH; + $entites = scandir($uploadsDir); + + foreach ($entites as $entiteId) { + if ($entiteId === '.' || $entiteId === '..' || $entiteId === 'entites') continue; + + $operationsPath = "$uploadsDir/$entiteId/operations"; + if (!is_dir($operationsPath)) continue; + + $operations = scandir($operationsPath); + foreach ($operations as $opId) { + if ($opId === '.' || $opId === '..') continue; + + $opPath = "$operationsPath/$opId"; + + // Supprimer l'ancienne structure documents/exports/excel si elle est vide + $oldStructure = "$opPath/documents"; + if (is_dir($oldStructure)) { + logMessage("Suppression de l'ancienne structure: $oldStructure"); + if (!$dryRun) { + exec("rm -rf " . escapeshellarg($oldStructure)); + } + } + } + } +} + +// Vérifier les permissions +if (!is_dir(BASE_PATH)) { + die("ERREUR: Le dossier " . BASE_PATH . " n'existe pas\n"); +} + +if (!is_writable(BASE_PATH) && !$dryRun) { + die("ERREUR: Le dossier " . BASE_PATH . " n'est pas accessible en écriture\n"); +} + +// Lancer la migration +try { + migrateUploads(); + + if ($dryRun) { + echo "\n"; + echo "========================================\n"; + echo "SIMULATION TERMINÉE\n"; + echo "Pour exécuter réellement la migration:\n"; + echo "php " . $argv[0] . "\n"; + echo "========================================\n"; + } else { + echo "\n"; + echo "========================================\n"; + echo "MIGRATION TERMINÉE AVEC SUCCÈS\n"; + echo "Vérifiez les logs: " . LOG_FILE . "\n"; + echo "========================================\n"; + } +} catch (Exception $e) { + logMessage("ERREUR FATALE: " . $e->getMessage(), 'ERROR'); + exit(1); +} \ No newline at end of file diff --git a/api/scripts/sql/add_chk_user_delete_pass.sql b/api/scripts/sql/add_chk_user_delete_pass.sql new file mode 100644 index 00000000..471922c3 --- /dev/null +++ b/api/scripts/sql/add_chk_user_delete_pass.sql @@ -0,0 +1,22 @@ +-- Script de migration pour ajouter le champ chk_user_delete_pass +-- Ce champ permet aux administrateurs d'autoriser ou non leurs membres à supprimer des passages +-- Date : 2025-08-20 +-- À exécuter sur DVA, REC et PROD + +-- Ajouter le champ chk_user_delete_pass s'il n'existe pas +ALTER TABLE `entites` +ADD COLUMN IF NOT EXISTS `chk_user_delete_pass` tinyint(1) unsigned NOT NULL DEFAULT 0 +COMMENT 'Autoriser les membres à supprimer des passages (1) ou non (0)' +AFTER `chk_username_manuel`; + +-- Vérifier l'ajout +SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_DEFAULT, + IS_NULLABLE, + COLUMN_COMMENT +FROM INFORMATION_SCHEMA.COLUMNS +WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'entites' + AND COLUMN_NAME = 'chk_user_delete_pass'; \ No newline at end of file diff --git a/api/scripts/sql/add_email_queue_fields.sql b/api/scripts/sql/add_email_queue_fields.sql new file mode 100644 index 00000000..89bc4f25 --- /dev/null +++ b/api/scripts/sql/add_email_queue_fields.sql @@ -0,0 +1,22 @@ +-- Migration pour ajouter les champs manquants à la table email_queue +-- À exécuter sur DVA, REC et PROD + +-- Ajouter le champ sent_at s'il n'existe pas +ALTER TABLE `email_queue` +ADD COLUMN IF NOT EXISTS `sent_at` TIMESTAMP NULL DEFAULT NULL +COMMENT 'Date/heure d\'envoi effectif de l\'email' +AFTER `status`; + +-- Ajouter le champ error_message s'il n'existe pas +ALTER TABLE `email_queue` +ADD COLUMN IF NOT EXISTS `error_message` TEXT NULL DEFAULT NULL +COMMENT 'Message d\'erreur en cas d\'échec' +AFTER `attempts`; + +-- Ajouter un index sur le status pour optimiser les requêtes +ALTER TABLE `email_queue` +ADD INDEX IF NOT EXISTS `idx_status_attempts` (`status`, `attempts`); + +-- Ajouter un index sur sent_at pour le nettoyage automatique +ALTER TABLE `email_queue` +ADD INDEX IF NOT EXISTS `idx_sent_at` (`sent_at`); \ No newline at end of file diff --git a/api/src/Services/PDFGenerator.php b/api/src/Services/PDFGenerator.php new file mode 100644 index 00000000..a877878b --- /dev/null +++ b/api/src/Services/PDFGenerator.php @@ -0,0 +1,488 @@ + [595.28, 841.89]]; + protected $DefPageFormat; + protected $CurPageFormat; + protected $PageSizes = []; + protected $wPt, $hPt; + protected $w, $h; + protected $lMargin; + protected $tMargin; + protected $rMargin; + protected $bMargin; + protected $cMargin; + protected $x, $y; + protected $lasth = 0; + protected $LineWidth; + protected $CoreFonts = ['helvetica']; + protected $fonts = []; + protected $FontFiles = []; + protected $diffs = []; + protected $FontFamily = ''; + protected $FontStyle = ''; + protected $underline = false; + protected $CurrentFont; + protected $FontSizePt = 12; + protected $FontSize; + protected $DrawColor = '0 G'; + protected $FillColor = '0 g'; + protected $TextColor = '0 g'; + protected $ColorFlag = false; + protected $ws = 0; + protected $images = []; + protected $PageLinks = []; + protected $links = []; + protected $AutoPageBreak = true; + protected $PageBreakTrigger; + protected $InHeader = false; + protected $InFooter = false; + protected $ZoomMode; + protected $LayoutMode; + protected $title = ''; + protected $subject = ''; + protected $author = ''; + protected $keywords = ''; + protected $creator = ''; + protected $AliasNbPages = ''; + protected $PDFVersion = '1.3'; + + public function __construct() { + $this->DefPageFormat = 'A4'; + $this->CurPageFormat = $this->PageFormats['a4']; + $this->DefOrientation = 'P'; + $this->CurOrientation = $this->DefOrientation; + $this->k = 72 / 25.4; // Conversion factor + + // Page dimensions + $this->wPt = $this->CurPageFormat[0]; + $this->hPt = $this->CurPageFormat[1]; + $this->w = $this->wPt / $this->k; + $this->h = $this->hPt / $this->k; + + // Page margins (1 cm) + $margin = 28.35 / $this->k; + $this->SetMargins($margin, $margin); + $this->cMargin = $margin / 10; + $this->LineWidth = .567 / $this->k; + $this->SetAutoPageBreak(true, 2 * $margin); + $this->SetDisplayMode('default'); + } + + public function SetMargins($left, $top, $right = null) { + $this->lMargin = $left; + $this->tMargin = $top; + if($right === null) + $right = $left; + $this->rMargin = $right; + } + + public function SetAutoPageBreak($auto, $margin = 0) { + $this->AutoPageBreak = $auto; + $this->bMargin = $margin; + $this->PageBreakTrigger = $this->h - $margin; + } + + public function SetDisplayMode($zoom, $layout = 'default') { + $this->ZoomMode = $zoom; + $this->LayoutMode = $layout; + } + + public function AddPage($orientation = '', $format = '') { + if($this->state == 0) + $this->Open(); + + $family = $this->FontFamily; + $style = $this->FontStyle . ($this->underline ? 'U' : ''); + $fontsize = $this->FontSizePt; + $lw = $this->LineWidth; + $dc = $this->DrawColor; + $fc = $this->FillColor; + $tc = $this->TextColor; + $cf = $this->ColorFlag; + + if($this->page > 0) { + $this->_endpage(); + } + + $this->_beginpage($orientation, $format); + $this->_out('2 J'); + $this->LineWidth = $lw; + $this->_out(sprintf('%.2F w', $lw * $this->k)); + + if($family) + $this->SetFont($family, $style, $fontsize); + + $this->DrawColor = $dc; + if($dc != '0 G') + $this->_out($dc); + $this->FillColor = $fc; + if($fc != '0 g') + $this->_out($fc); + $this->TextColor = $tc; + $this->ColorFlag = $cf; + } + + public function SetFont($family, $style = '', $size = 0) { + $family = strtolower($family); + if($family == '') + $family = $this->FontFamily; + if($family == 'arial') + $family = 'helvetica'; + + if($size == 0) + $size = $this->FontSizePt; + + if($this->FontFamily == $family && $this->FontStyle == $style && $this->FontSizePt == $size) + return; + + $this->FontFamily = $family; + $this->FontStyle = $style; + $this->FontSizePt = $size; + $this->FontSize = $size / $this->k; + + if($this->page > 0) + $this->_out(sprintf('BT /F%d %.2F Tf ET', 1, $this->FontSizePt)); + } + + public function Text($x, $y, $txt) { + $s = sprintf('BT %.2F %.2F Td (%s) Tj ET', $x * $this->k, ($this->h - $y) * $this->k, $this->_escape($txt)); + if($this->underline && $txt != '') + $s .= ' ' . $this->_dounderline($x, $y, $txt); + if($this->ColorFlag) + $s = 'q ' . $this->TextColor . ' ' . $s . ' Q'; + $this->_out($s); + } + + public function Cell($w, $h = 0, $txt = '', $border = 0, $ln = 0, $align = '', $fill = false) { + $k = $this->k; + if($this->y + $h > $this->PageBreakTrigger && !$this->InHeader && !$this->InFooter && $this->AutoPageBreak) { + $x = $this->x; + $ws = $this->ws; + if($ws > 0) { + $this->ws = 0; + $this->_out('0 Tw'); + } + $this->AddPage($this->CurOrientation, $this->CurPageFormat); + $this->x = $x; + if($ws > 0) { + $this->ws = $ws; + $this->_out(sprintf('%.3F Tw', $ws * $k)); + } + } + + if($w == 0) + $w = $this->w - $this->rMargin - $this->x; + + $s = ''; + if($fill || $border == 1) { + if($fill) + $op = ($border == 1) ? 'B' : 'f'; + else + $op = 'S'; + $s = sprintf('%.2F %.2F %.2F %.2F re %s ', + $this->x * $k, ($this->h - $this->y) * $k, $w * $k, -$h * $k, $op); + } + + if(is_string($border)) { + $x = $this->x; + $y = $this->y; + if(strpos($border, 'L') !== false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', + $x * $k, ($this->h - $y) * $k, $x * $k, ($this->h - ($y + $h)) * $k); + if(strpos($border, 'T') !== false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', + $x * $k, ($this->h - $y) * $k, ($x + $w) * $k, ($this->h - $y) * $k); + if(strpos($border, 'R') !== false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', + ($x + $w) * $k, ($this->h - $y) * $k, ($x + $w) * $k, ($this->h - ($y + $h)) * $k); + if(strpos($border, 'B') !== false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', + $x * $k, ($this->h - ($y + $h)) * $k, ($x + $w) * $k, ($this->h - ($y + $h)) * $k); + } + + if($txt !== '') { + if($align == 'R') + $dx = $w - $this->cMargin - $this->GetStringWidth($txt); + elseif($align == 'C') + $dx = ($w - $this->GetStringWidth($txt)) / 2; + else + $dx = $this->cMargin; + + if($this->ColorFlag) + $s .= 'q ' . $this->TextColor . ' '; + + $txt2 = str_replace(')', '\\)', str_replace('(', '\\(', str_replace('\\', '\\\\', $txt))); + $s .= sprintf('BT %.2F %.2F Td (%s) Tj ET', + ($this->x + $dx) * $k, ($this->h - ($this->y + .5 * $h + .3 * $this->FontSize)) * $k, $txt2); + + if($this->underline) + $s .= ' ' . $this->_dounderline($this->x + $dx, $this->y + .5 * $h + .3 * $this->FontSize, $txt); + + if($this->ColorFlag) + $s .= ' Q'; + } + + if($s) + $this->_out($s); + + $this->lasth = $h; + + if($ln > 0) { + $this->y += $h; + if($ln == 1) + $this->x = $this->lMargin; + } else + $this->x += $w; + } + + public function Ln($h = null) { + $this->x = $this->lMargin; + if($h === null) + $this->y += $this->lasth; + else + $this->y += $h; + } + + public function Image($file, $x = null, $y = null, $w = 0, $h = 0) { + // Pour simplifier, on va juste créer un rectangle avec texte "LOGO" + // Dans une vraie implémentation, il faudrait encoder l'image + + if($x === null) + $x = $this->x; + if($y === null) + $y = $this->y; + + if($w == 0) + $w = 30; + if($h == 0) + $h = 30; + + // Dessiner un rectangle pour représenter le logo + $this->Rect($x, $y, $w, $h); + + // Ajouter le texte LOGO au centre + $oldX = $this->x; + $oldY = $this->y; + $this->SetXY($x + $w/2 - 8, $y + $h/2 - 2); + $this->Cell(16, 4, 'LOGO', 0, 0, 'C'); + $this->SetXY($oldX, $oldY); + } + + public function Rect($x, $y, $w, $h, $style = '') { + if($style == 'F') + $op = 'f'; + elseif($style == 'FD' || $style == 'DF') + $op = 'B'; + else + $op = 'S'; + $this->_out(sprintf('%.2F %.2F %.2F %.2F re %s', + $x * $this->k, ($this->h - $y) * $this->k, $w * $this->k, -$h * $this->k, $op)); + } + + public function Line($x1, $y1, $x2, $y2) { + $this->_out(sprintf('%.2F %.2F m %.2F %.2F l S', + $x1 * $this->k, ($this->h - $y1) * $this->k, $x2 * $this->k, ($this->h - $y2) * $this->k)); + } + + public function GetStringWidth($s) { + $cw = ['helvetica' => [' ' => 278, '!' => 278, '"' => 355, '#' => 556, '$' => 556, '%' => 889, '&' => 667]]; + $w = 0; + $l = strlen($s); + for($i = 0; $i < $l; $i++) + $w += 600; // Approximation + return $w * $this->FontSize / 1000; + } + + public function SetXY($x, $y) { + $this->SetX($x); + $this->SetY($y, false); + } + + public function SetX($x) { + if($x >= 0) + $this->x = $x; + else + $this->x = $this->w + $x; + } + + public function SetY($y, $resetX = true) { + $this->y = $y; + if($resetX) + $this->x = $this->lMargin; + } + + public function Output() { + if($this->state < 3) + $this->Close(); + return $this->buffer; + } + + protected function Open() { + $this->state = 1; + $this->_out('%PDF-' . $this->PDFVersion); + } + + protected function Close() { + if($this->state == 3) + return; + if($this->page == 0) + $this->AddPage(); + + $this->_endpage(); + $this->_enddoc(); + } + + protected function _beginpage($orientation, $format) { + $this->page++; + $this->pages[$this->page] = ''; + $this->state = 2; + $this->x = $this->lMargin; + $this->y = $this->tMargin; + $this->FontFamily = ''; + } + + protected function _endpage() { + $this->state = 1; + } + + protected function _escape($s) { + $s = str_replace('\\', '\\\\', $s); + $s = str_replace('(', '\\(', $s); + $s = str_replace(')', '\\)', $s); + $s = str_replace("\r", '\\r', $s); + return $s; + } + + protected function _dounderline($x, $y, $txt) { + $up = -100; + $ut = 50; + $w = $this->GetStringWidth($txt) + $this->ws * substr_count($txt, ' '); + return sprintf('%.2F %.2F %.2F %.2F re f', + $x * $this->k, ($this->h - ($y - $up / 1000 * $this->FontSize)) * $this->k, + $w * $this->k, -$ut / 1000 * $this->FontSizePt); + } + + protected function _out($s) { + if($this->state == 2) + $this->pages[$this->page] .= $s . "\n"; + else + $this->buffer .= $s . "\n"; + } + + protected function _enddoc() { + $this->_putheader(); + $this->_putpages(); + $this->_putresources(); + + $this->_newobj(); + $this->_out('<<'); + $this->_out('/Type /Catalog'); + $this->_out('/Pages 1 0 R'); + $this->_out('>>'); + $this->_out('endobj'); + + $o = strlen($this->buffer); + $this->_out('xref'); + $this->_out('0 ' . ($this->n + 1)); + $this->_out('0000000000 65535 f '); + for($i = 1; $i <= $this->n; $i++) + $this->_out(sprintf('%010d 00000 n ', $this->offsets[$i])); + + $this->_out('trailer'); + $this->_out('<<'); + $this->_out('/Size ' . ($this->n + 1)); + $this->_out('/Root ' . $this->n . ' 0 R'); + $this->_out('/Info ' . ($this->n - 1) . ' 0 R'); + $this->_out('>>'); + $this->_out('startxref'); + $this->_out($o); + $this->_out('%%EOF'); + $this->state = 3; + } + + protected function _putheader() { + $this->_out('%PDF-' . $this->PDFVersion); + } + + protected function _putpages() { + $nb = $this->page; + $n = $this->n; + + for($page = 1; $page <= $nb; $page++) { + $this->_newobj(); + $this->_out('<_out('/Parent 1 0 R'); + $this->_out('/Resources 2 0 R'); + $this->_out('/Contents ' . ($this->n + 1) . ' 0 R>>'); + $this->_out('endobj'); + + $this->_newobj(); + $filter = ($this->compress) ? '/Filter /FlateDecode ' : ''; + $p = ($this->compress) ? gzcompress($this->pages[$page]) : $this->pages[$page]; + $this->_out('<<' . $filter . '/Length ' . strlen($p) . '>>'); + $this->_putstream($p); + $this->_out('endobj'); + } + + $this->offsets[1] = strlen($this->buffer); + $this->_out('1 0 obj'); + $this->_out('<_out($kids . ']'); + $this->_out('/Count ' . $nb); + $this->_out(sprintf('/MediaBox [0 0 %.2F %.2F]', $this->wPt, $this->hPt)); + $this->_out('>>'); + $this->_out('endobj'); + } + + protected function _putresources() { + $this->_putfonts(); + + $this->offsets[2] = strlen($this->buffer); + $this->_out('2 0 obj'); + $this->_out('<<'); + $this->_out('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]'); + $this->_out('/Font <<'); + $this->_out('/F1 <>'); + $this->_out('>>'); + $this->_out('>>'); + $this->_out('endobj'); + } + + protected function _putfonts() { + // Simplified - fonts are embedded in resources + } + + protected function _newobj() { + $this->n++; + $this->offsets[$this->n] = strlen($this->buffer); + $this->_out($this->n . ' 0 obj'); + } + + protected function _putstream($s) { + $this->_out('stream'); + $this->buffer .= $s; + $this->_out('endstream'); + } +} \ No newline at end of file diff --git a/api/src/Services/ReceiptPDFGenerator.php b/api/src/Services/ReceiptPDFGenerator.php new file mode 100644 index 00000000..926b6ffd --- /dev/null +++ b/api/src/Services/ReceiptPDFGenerator.php @@ -0,0 +1,190 @@ +AddPage(); + $this->SetFont('Arial', '', 12); + + // Déterminer quel logo utiliser + $logoToUse = null; + if ($logoPath && file_exists($logoPath)) { + $logoToUse = $logoPath; + } elseif (file_exists(self::DEFAULT_LOGO_PATH)) { + $logoToUse = self::DEFAULT_LOGO_PATH; + } + + // Ajouter le logo (PNG ou JPG) + if ($logoToUse) { + try { + // Déterminer le type d'image + $imageInfo = getimagesize($logoToUse); + if ($imageInfo !== false) { + $type = ''; + switch ($imageInfo[2]) { + case IMAGETYPE_JPEG: + $type = 'JPG'; + break; + case IMAGETYPE_PNG: + $type = 'PNG'; + break; + } + + if ($type) { + // Position du logo : x=10, y=10, largeur=40mm, hauteur=40mm + $this->Image($logoToUse, 10, 10, self::LOGO_WIDTH, self::LOGO_HEIGHT, $type); + } + } + } catch (\Exception $e) { + // Si erreur avec le logo, continuer sans + } + } + + // En-tête à droite du logo + $this->SetXY(60, 20); + $this->SetFont('Arial', 'B', 14); + $this->Cell(130, 6, $this->cleanText(strtoupper($data['entite_name'] ?? '')), 0, 1, 'C'); + + if (!empty($data['entite_city'])) { + $this->SetX(60); + $this->SetFont('Arial', '', 11); + $this->Cell(130, 5, $this->cleanText(strtoupper($data['entite_city'])), 0, 1, 'C'); + } + + if (!empty($data['entite_address'])) { + $this->SetX(60); + $this->SetFont('Arial', '', 10); + $this->Cell(130, 5, $this->cleanText($data['entite_address']), 0, 1, 'C'); + } + + // Titre du reçu + $this->SetY(65); + $this->SetFont('Arial', 'B', 16); + $this->Cell(0, 10, $this->cleanText('REÇU DE DON'), 0, 1, 'C'); + + $this->SetFont('Arial', 'B', 14); + $this->Cell(0, 8, $this->cleanText('N° ' . ($data['receipt_number'] ?? '')), 0, 1, 'C'); + + // Ligne de séparation + $this->Ln(5); + $this->Line(20, $this->GetY(), 190, $this->GetY()); + $this->Ln(8); + + // Informations du donateur + $this->SetFont('Arial', 'B', 12); + $this->Cell(0, 6, $this->cleanText('DONATEUR :'), 0, 1, 'L'); + + $this->SetFont('Arial', '', 11); + $this->Cell(0, 6, $this->cleanText($data['donor_name'] ?? ''), 0, 1, 'L'); + + if (!empty($data['donor_address'])) { + $this->SetFont('Arial', '', 10); + $this->Cell(0, 5, $this->cleanText($data['donor_address']), 0, 1, 'L'); + } + + $this->Ln(8); + + // Cadre pour le montant + $this->SetFillColor(240, 240, 240); + $this->Rect(20, $this->GetY(), 170, 25, 'F'); + + // Montant en gros et centré + $this->Ln(5); + $this->SetFont('Arial', 'B', 18); + $this->Cell(0, 8, $this->cleanText($data['amount'] ?? '0') . $this->cleanText(' euros'), 0, 1, 'C'); + + // Date centrée + $this->SetFont('Arial', '', 12); + $this->Cell(0, 6, $this->cleanText('Don du ' . ($data['donation_date'] ?? date('d/m/Y'))), 0, 1, 'C'); + + $this->Ln(10); + + if (!empty($data['payment_method'])) { + $this->SetFont('Arial', '', 10); + $this->Cell(0, 5, $this->cleanText('Mode de règlement : ') . $this->cleanText($data['payment_method']), 0, 1, 'L'); + } + + if (!empty($data['operation_name'])) { + $this->SetFont('Arial', 'I', 10); + $this->Cell(0, 5, $this->cleanText('Campagne : ') . $this->cleanText($data['operation_name']), 0, 1, 'L'); + } + + // Mention de remerciement + $this->Ln(15); + $this->SetFont('Arial', '', 10); + $this->MultiCell(0, 5, $this->cleanText( + "Nous vous remercions pour votre généreux soutien à notre amicale.\n" . + "Votre don contribue au financement de nos activités et équipements." + ), 0, 'C'); + + // Signature + $this->SetY(-60); + $this->SetFont('Arial', '', 10); + $this->Cell(0, 5, $this->cleanText('Fait à ' . ($data['entite_city'] ?? '') . ', le ' . ($data['signature_date'] ?? date('d/m/Y'))), 0, 1, 'R'); + $this->Ln(5); + $this->Cell(0, 5, $this->cleanText('Le Président'), 0, 1, 'R'); + $this->Ln(15); + $this->Cell(0, 5, $this->cleanText('(Signature et cachet)'), 0, 1, 'R'); + + // Retourner le PDF en string + return $this->Output('S'); + } + + /** + * Nettoie le texte pour le PDF (supprime ou remplace les caractères problématiques) + */ + private function cleanText(string $text): string { + // Vérifier que le texte n'est pas vide + if (empty($text)) { + return ''; + } + + // Remplacer d'abord les caractères problématiques avant la conversion + $replacements = [ + '€' => 'EUR', + '—' => '-', + '–' => '-', + '"' => '"', + '"' => '"', + "'" => "'", + "'" => "'", + '…' => '...' + ]; + + $text = str_replace(array_keys($replacements), array_values($replacements), $text); + + // Tentative de conversion UTF-8 vers ISO-8859-1 pour FPDF + $converted = @iconv('UTF-8', 'ISO-8859-1//TRANSLIT//IGNORE', $text); + + // Si la conversion échoue, utiliser utf8_decode en fallback + if ($converted === false) { + $converted = @utf8_decode($text); + + // Si utf8_decode échoue aussi, supprimer les caractères non-ASCII + if ($converted === false) { + $converted = preg_replace('/[^\x20-\x7E]/', '?', $text); + } + } + + return $converted; + } +} \ No newline at end of file diff --git a/api/src/Services/SimplePDF.php b/api/src/Services/SimplePDF.php new file mode 100644 index 00000000..af4c629f --- /dev/null +++ b/api/src/Services/SimplePDF.php @@ -0,0 +1,178 @@ +currentY = $this->pageHeight - $this->margin; + } + + /** + * Ajoute du texte au PDF + */ + public function addText(string $text, float $x, float $y, int $fontSize = 12): void { + $this->content .= "BT\n"; + $this->content .= "/F1 $fontSize Tf\n"; + $this->content .= "$x $y Td\n"; + $this->content .= "(" . $this->escapeString($text) . ") Tj\n"; + $this->content .= "ET\n"; + } + + /** + * Ajoute une ligne de texte avec positionnement automatique + */ + public function addLine(string $text, int $fontSize = 11, string $align = 'left'): void { + $x = $this->margin; + + if ($align === 'center') { + // Estimation approximative de la largeur du texte + $textWidth = strlen($text) * $fontSize * 0.5; + $x = ($this->pageWidth - $textWidth) / 2; + } elseif ($align === 'right') { + $textWidth = strlen($text) * $fontSize * 0.5; + $x = $this->pageWidth - $this->margin - $textWidth; + } + + $this->addText($text, $x, $this->currentY, $fontSize); + $this->currentY -= ($fontSize + 8); // Line height + } + + /** + * Ajoute un espace vertical + */ + public function addSpace(float $space = 20): void { + $this->currentY -= $space; + } + + /** + * Ajoute une ligne horizontale + */ + public function addHorizontalLine(): void { + $y = $this->currentY; + $this->content .= "q\n"; // Save state + $this->content .= "0.5 w\n"; // Line width + $this->content .= $this->margin . " $y m\n"; // Move to start + $this->content .= ($this->pageWidth - $this->margin) . " $y l\n"; // Line to end + $this->content .= "S\n"; // Stroke + $this->content .= "Q\n"; // Restore state + $this->currentY -= 10; + } + + /** + * Ajoute un rectangle (pour encadrer) + */ + public function addRectangle(float $x, float $y, float $width, float $height, bool $fill = false): void { + $this->content .= "q\n"; + $this->content .= "0.8 w\n"; // Line width + $this->content .= "$x $y $width $height re\n"; // Rectangle + $this->content .= $fill ? "f\n" : "S\n"; // Fill or Stroke + $this->content .= "Q\n"; + } + + /** + * Échappe les caractères spéciaux pour le PDF + */ + private function escapeString(string $str): string { + // Échapper les caractères spéciaux PDF + $str = str_replace('\\', '\\\\', $str); + $str = str_replace('(', '\\(', $str); + $str = str_replace(')', '\\)', $str); + + // Convertir les caractères accentués + $accents = [ + 'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'A', + 'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'a', + 'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', + 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', + 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I', + 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', + 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O', + 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o', + 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U', + 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u', + 'Ñ' => 'N', 'ñ' => 'n', + 'Ç' => 'C', 'ç' => 'c', + '€' => 'EUR', + 'Œ' => 'OE', 'œ' => 'oe', + 'Æ' => 'AE', 'æ' => 'ae' + ]; + + $str = strtr($str, $accents); + + // Supprimer tout caractère non-ASCII restant + $str = preg_replace('/[^\x20-\x7E]/', '', $str); + + return $str; + } + + /** + * Génère le PDF final + */ + public function generate(): string { + // Début du PDF + $pdf = "%PDF-1.4\n"; + $pdf .= "%âãÏÓ\n"; // Binary marker + + // Object 1 - Catalog + $this->objects[1] = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"; + + // Object 2 - Pages + $this->objects[2] = "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + + // Object 3 - Page + $this->objects[3] = "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 " . + $this->pageWidth . " " . $this->pageHeight . + "] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>\nendobj\n"; + + // Object 4 - Font (Helvetica) + $this->objects[4] = "4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>\nendobj\n"; + + // Object 5 - Content stream + $contentLength = strlen($this->content); + $this->objects[5] = "5 0 obj\n<< /Length $contentLength >>\nstream\n" . + $this->content . "\nendstream\nendobj\n"; + + // Construction du PDF final + $offset = strlen($pdf); + foreach ($this->objects as $obj) { + $this->xref[] = $offset; + $pdf .= $obj; + $offset += strlen($obj); + } + + // Table xref + $xrefStart = $offset; + $pdf .= "xref\n"; + $pdf .= "0 " . (count($this->objects) + 1) . "\n"; + $pdf .= "0000000000 65535 f \n"; + foreach ($this->xref as $off) { + $pdf .= sprintf("%010d 00000 n \n", $off); + } + + // Trailer + $pdf .= "trailer\n"; + $pdf .= "<< /Size " . (count($this->objects) + 1) . " /Root 1 0 R >>\n"; + $pdf .= "startxref\n"; + $pdf .= "$xrefStart\n"; + $pdf .= "%%EOF\n"; + + return $pdf; + } +} \ No newline at end of file diff --git a/app/.dart_tool/package_graph.json b/app/.dart_tool/package_graph.json index fb16dc81..8edf026f 100644 --- a/app/.dart_tool/package_graph.json +++ b/app/.dart_tool/package_graph.json @@ -58,6 +58,48 @@ "yaml" ] }, + { + "name": "build_runner", + "version": "2.5.4", + "dependencies": [ + "analyzer", + "args", + "async", + "build", + "build_config", + "build_daemon", + "build_resolvers", + "build_runner_core", + "code_builder", + "collection", + "crypto", + "dart_style", + "frontend_server_client", + "glob", + "graphs", + "http", + "http_multi_server", + "io", + "js", + "logging", + "meta", + "mime", + "package_config", + "path", + "pool", + "pub_semver", + "pubspec_parse", + "shelf", + "shelf_web_socket", + "stack_trace", + "stream_transform", + "timing", + "watcher", + "web", + "web_socket_channel", + "yaml" + ] + }, { "name": "hive_generator", "version": "2.0.1", @@ -104,6 +146,41 @@ "vm_service" ] }, + { + "name": "yaml", + "version": "3.1.3", + "dependencies": [ + "collection", + "source_span", + "string_scanner" + ] + }, + { + "name": "image_picker", + "version": "1.2.0", + "dependencies": [ + "flutter", + "image_picker_android", + "image_picker_for_web", + "image_picker_ios", + "image_picker_linux", + "image_picker_macos", + "image_picker_platform_interface", + "image_picker_windows" + ] + }, + { + "name": "flutter_local_notifications", + "version": "19.4.0", + "dependencies": [ + "clock", + "flutter", + "flutter_local_notifications_linux", + "flutter_local_notifications_platform_interface", + "flutter_local_notifications_windows", + "timezone" + ] + }, { "name": "sensors_plus", "version": "6.1.2", @@ -148,6 +225,93 @@ "intl" ] }, + { + "name": "path_provider", + "version": "2.1.5", + "dependencies": [ + "flutter", + "path_provider_android", + "path_provider_foundation", + "path_provider_linux", + "path_provider_platform_interface", + "path_provider_windows" + ] + }, + { + "name": "http_cache_file_store", + "version": "2.0.1", + "dependencies": [ + "http_cache_core", + "path", + "synchronized" + ] + }, + { + "name": "flutter_map_cache", + "version": "2.0.0+1", + "dependencies": [ + "dio", + "dio_cache_interceptor", + "flutter", + "flutter_map" + ] + }, + { + "name": "flutter_map", + "version": "8.2.1", + "dependencies": [ + "async", + "collection", + "dart_earcut", + "dart_polylabel2", + "flutter", + "http", + "latlong2", + "logger", + "meta", + "path", + "path_provider", + "proj4dart", + "uuid" + ] + }, + { + "name": "url_launcher", + "version": "6.3.2", + "dependencies": [ + "flutter", + "url_launcher_android", + "url_launcher_ios", + "url_launcher_linux", + "url_launcher_macos", + "url_launcher_platform_interface", + "url_launcher_web", + "url_launcher_windows" + ] + }, + { + "name": "shared_preferences", + "version": "2.5.3", + "dependencies": [ + "flutter", + "shared_preferences_android", + "shared_preferences_foundation", + "shared_preferences_linux", + "shared_preferences_platform_interface", + "shared_preferences_web", + "shared_preferences_windows" + ] + }, + { + "name": "syncfusion_flutter_charts", + "version": "30.2.6", + "dependencies": [ + "flutter", + "intl", + "syncfusion_flutter_core", + "vector_math" + ] + }, { "name": "fl_chart", "version": "1.0.0", @@ -157,6 +321,16 @@ "vector_math" ] }, + { + "name": "uuid", + "version": "4.5.1", + "dependencies": [ + "crypto", + "fixnum", + "meta", + "sprintf" + ] + }, { "name": "intl", "version": "0.20.2", @@ -166,11 +340,74 @@ "path" ] }, + { + "name": "package_info_plus", + "version": "8.3.1", + "dependencies": [ + "clock", + "ffi", + "flutter", + "flutter_web_plugins", + "http", + "meta", + "package_info_plus_platform_interface", + "path", + "web", + "win32" + ] + }, + { + "name": "flutter_svg", + "version": "2.0.13", + "dependencies": [ + "flutter", + "http", + "vector_graphics", + "vector_graphics_codec", + "vector_graphics_compiler" + ] + }, + { + "name": "google_fonts", + "version": "6.3.0", + "dependencies": [ + "crypto", + "flutter", + "http", + "path_provider" + ] + }, { "name": "retry", "version": "3.1.2", "dependencies": [] }, + { + "name": "connectivity_plus", + "version": "6.1.5", + "dependencies": [ + "collection", + "connectivity_plus_platform_interface", + "flutter", + "flutter_web_plugins", + "meta", + "nm", + "web" + ] + }, + { + "name": "dio", + "version": "5.9.0", + "dependencies": [ + "async", + "collection", + "dio_web_adapter", + "http_parser", + "meta", + "mime", + "path" + ] + }, { "name": "hive_flutter", "version": "1.1.0", @@ -189,6 +426,22 @@ "meta" ] }, + { + "name": "go_router", + "version": "16.2.0", + "dependencies": [ + "collection", + "flutter", + "flutter_web_plugins", + "logging", + "meta" + ] + }, + { + "name": "cupertino_icons", + "version": "1.0.8", + "dependencies": [] + }, { "name": "flutter_localizations", "version": "0.0.0", @@ -228,6 +481,389 @@ "meta" ] }, + { + "name": "image", + "version": "4.5.4", + "dependencies": [ + "archive", + "meta", + "xml" + ] + }, + { + "name": "cli_util", + "version": "0.4.2", + "dependencies": [ + "meta", + "path" + ] + }, + { + "name": "checked_yaml", + "version": "2.0.4", + "dependencies": [ + "json_annotation", + "source_span", + "yaml" + ] + }, + { + "name": "args", + "version": "2.7.0", + "dependencies": [] + }, + { + "name": "web_socket_channel", + "version": "3.0.3", + "dependencies": [ + "async", + "crypto", + "stream_channel", + "web", + "web_socket" + ] + }, + { + "name": "web", + "version": "1.1.1", + "dependencies": [] + }, + { + "name": "watcher", + "version": "1.1.2", + "dependencies": [ + "async", + "path" + ] + }, + { + "name": "timing", + "version": "1.0.2", + "dependencies": [ + "json_annotation" + ] + }, + { + "name": "stream_transform", + "version": "2.1.1", + "dependencies": [] + }, + { + "name": "stack_trace", + "version": "1.12.1", + "dependencies": [ + "path" + ] + }, + { + "name": "shelf_web_socket", + "version": "3.0.0", + "dependencies": [ + "shelf", + "stream_channel", + "web_socket_channel" + ] + }, + { + "name": "shelf", + "version": "1.4.2", + "dependencies": [ + "async", + "collection", + "http_parser", + "path", + "stack_trace", + "stream_channel" + ] + }, + { + "name": "pubspec_parse", + "version": "1.5.0", + "dependencies": [ + "checked_yaml", + "collection", + "json_annotation", + "pub_semver", + "yaml" + ] + }, + { + "name": "pub_semver", + "version": "2.2.0", + "dependencies": [ + "collection" + ] + }, + { + "name": "pool", + "version": "1.5.1", + "dependencies": [ + "async", + "stack_trace" + ] + }, + { + "name": "package_config", + "version": "2.2.0", + "dependencies": [ + "path" + ] + }, + { + "name": "mime", + "version": "2.0.0", + "dependencies": [] + }, + { + "name": "meta", + "version": "1.16.0", + "dependencies": [] + }, + { + "name": "logging", + "version": "1.3.0", + "dependencies": [] + }, + { + "name": "js", + "version": "0.7.2", + "dependencies": [] + }, + { + "name": "io", + "version": "1.0.5", + "dependencies": [ + "meta", + "path", + "string_scanner" + ] + }, + { + "name": "http_multi_server", + "version": "3.2.2", + "dependencies": [ + "async" + ] + }, + { + "name": "http", + "version": "1.5.0", + "dependencies": [ + "async", + "http_parser", + "meta", + "web" + ] + }, + { + "name": "graphs", + "version": "2.3.2", + "dependencies": [ + "collection" + ] + }, + { + "name": "glob", + "version": "2.1.3", + "dependencies": [ + "async", + "collection", + "file", + "path", + "string_scanner" + ] + }, + { + "name": "frontend_server_client", + "version": "4.0.0", + "dependencies": [ + "async", + "path" + ] + }, + { + "name": "dart_style", + "version": "2.3.8", + "dependencies": [ + "analyzer", + "args", + "collection", + "package_config", + "path", + "pub_semver", + "source_span" + ] + }, + { + "name": "crypto", + "version": "3.0.6", + "dependencies": [ + "typed_data" + ] + }, + { + "name": "collection", + "version": "1.19.1", + "dependencies": [] + }, + { + "name": "code_builder", + "version": "4.10.1", + "dependencies": [ + "built_collection", + "built_value", + "collection", + "matcher", + "meta" + ] + }, + { + "name": "build_runner_core", + "version": "9.1.2", + "dependencies": [ + "analyzer", + "async", + "build", + "build_config", + "build_resolvers", + "build_runner", + "built_collection", + "built_value", + "collection", + "convert", + "crypto", + "glob", + "graphs", + "json_annotation", + "logging", + "meta", + "package_config", + "path", + "pool", + "timing", + "watcher", + "yaml" + ] + }, + { + "name": "build_resolvers", + "version": "2.5.4", + "dependencies": [ + "analyzer", + "async", + "build", + "build_runner_core", + "collection", + "convert", + "crypto", + "graphs", + "logging", + "package_config", + "path", + "pool", + "pub_semver", + "stream_transform" + ] + }, + { + "name": "build_daemon", + "version": "4.0.4", + "dependencies": [ + "built_collection", + "built_value", + "crypto", + "http_multi_server", + "logging", + "path", + "pool", + "shelf", + "shelf_web_socket", + "stream_transform", + "watcher", + "web_socket_channel" + ] + }, + { + "name": "build_config", + "version": "1.1.2", + "dependencies": [ + "checked_yaml", + "json_annotation", + "path", + "pubspec_parse", + "yaml" + ] + }, + { + "name": "build", + "version": "2.5.4", + "dependencies": [ + "analyzer", + "async", + "build_runner_core", + "built_collection", + "built_value", + "convert", + "crypto", + "glob", + "graphs", + "logging", + "meta", + "package_config", + "path", + "pool" + ] + }, + { + "name": "async", + "version": "2.13.0", + "dependencies": [ + "collection", + "meta" + ] + }, + { + "name": "analyzer", + "version": "6.11.0", + "dependencies": [ + "_fe_analyzer_shared", + "collection", + "convert", + "crypto", + "glob", + "macros", + "meta", + "package_config", + "path", + "pub_semver", + "source_span", + "watcher", + "yaml" + ] + }, + { + "name": "source_helper", + "version": "1.3.5", + "dependencies": [ + "analyzer", + "collection", + "source_gen" + ] + }, + { + "name": "source_gen", + "version": "1.5.0", + "dependencies": [ + "analyzer", + "async", + "build", + "dart_style", + "glob", + "path", + "source_span", + "yaml" + ] + }, { "name": "lints", "version": "6.0.0", @@ -238,26 +874,6 @@ "version": "15.0.0", "dependencies": [] }, - { - "name": "vector_math", - "version": "2.1.4", - "dependencies": [] - }, - { - "name": "test_api", - "version": "0.7.4", - "dependencies": [ - "async", - "boolean_selector", - "collection", - "meta", - "source_span", - "stack_trace", - "stream_channel", - "string_scanner", - "term_glyph" - ] - }, { "name": "term_glyph", "version": "1.2.2", @@ -277,13 +893,6 @@ "async" ] }, - { - "name": "stack_trace", - "version": "1.12.1", - "dependencies": [ - "path" - ] - }, { "name": "source_span", "version": "1.10.1", @@ -293,11 +902,6 @@ "term_glyph" ] }, - { - "name": "meta", - "version": "1.16.0", - "dependencies": [] - }, { "name": "material_color_utilities", "version": "0.11.1", @@ -305,17 +909,6 @@ "collection" ] }, - { - "name": "matcher", - "version": "0.12.17", - "dependencies": [ - "async", - "meta", - "stack_trace", - "term_glyph", - "test_api" - ] - }, { "name": "leak_tracker_testing", "version": "3.0.1", @@ -325,17 +918,6 @@ "meta" ] }, - { - "name": "leak_tracker_flutter_testing", - "version": "3.0.9", - "dependencies": [ - "flutter", - "leak_tracker", - "leak_tracker_testing", - "matcher", - "meta" - ] - }, { "name": "leak_tracker", "version": "10.0.9", @@ -347,24 +929,6 @@ "vm_service" ] }, - { - "name": "fake_async", - "version": "1.3.3", - "dependencies": [ - "clock", - "collection" - ] - }, - { - "name": "collection", - "version": "1.19.1", - "dependencies": [] - }, - { - "name": "clock", - "version": "1.1.2", - "dependencies": [] - }, { "name": "characters", "version": "1.4.0", @@ -379,98 +943,58 @@ ] }, { - "name": "async", - "version": "2.13.0", + "name": "leak_tracker_flutter_testing", + "version": "3.0.9", "dependencies": [ - "collection", + "flutter", + "leak_tracker", + "leak_tracker_testing", + "matcher", "meta" ] }, { - "name": "sensors_plus_platform_interface", - "version": "2.0.1", - "dependencies": [ - "flutter", - "logging", - "meta", - "plugin_platform_interface" - ] - }, - { - "name": "flutter_web_plugins", - "version": "0.0.0", - "dependencies": [ - "characters", - "collection", - "flutter", - "material_color_utilities", - "meta", - "vector_math" - ] - }, - { - "name": "universal_io", - "version": "2.2.2", - "dependencies": [ - "collection", - "meta", - "typed_data" - ] - }, - { - "name": "geolocator_linux", - "version": "0.2.3", - "dependencies": [ - "dbus", - "flutter", - "geoclue", - "geolocator_platform_interface", - "gsettings", - "package_info_plus" - ] - }, - { - "name": "geoclue", - "version": "0.1.1", - "dependencies": [ - "dbus", - "meta" - ] - }, - { - "name": "equatable", - "version": "2.0.7", - "dependencies": [ - "collection", - "meta" - ] - }, - { - "name": "sky_engine", - "version": "0.0.0", + "name": "vector_math", + "version": "2.1.4", "dependencies": [] }, { - "name": "yaml", - "version": "3.1.3", + "name": "clock", + "version": "1.1.2", + "dependencies": [] + }, + { + "name": "fake_async", + "version": "1.3.3", "dependencies": [ - "collection", - "source_span", - "string_scanner" + "clock", + "collection" ] }, { - "name": "image_picker", - "version": "1.2.0", + "name": "matcher", + "version": "0.12.17", "dependencies": [ - "flutter", - "image_picker_android", - "image_picker_for_web", - "image_picker_ios", - "image_picker_linux", - "image_picker_macos", - "image_picker_platform_interface", - "image_picker_windows" + "async", + "meta", + "stack_trace", + "term_glyph", + "test_api" + ] + }, + { + "name": "test_api", + "version": "0.7.4", + "dependencies": [ + "async", + "boolean_selector", + "collection", + "meta", + "source_span", + "stack_trace", + "stream_channel", + "string_scanner", + "term_glyph" ] }, { @@ -542,112 +1066,75 @@ ] }, { - "name": "http_cache_file_store", - "version": "2.0.1", + "name": "timezone", + "version": "0.10.1", "dependencies": [ - "http_cache_core", - "path", - "synchronized" + "http", + "path" ] }, { - "name": "sprintf", - "version": "7.0.0", - "dependencies": [] - }, - { - "name": "flutter_map_cache", - "version": "2.0.0+1", + "name": "flutter_local_notifications_platform_interface", + "version": "9.1.0", "dependencies": [ - "dio", - "dio_cache_interceptor", - "flutter", - "flutter_map" + "plugin_platform_interface" ] }, { - "name": "url_launcher", - "version": "6.3.2", + "name": "flutter_local_notifications_windows", + "version": "1.0.2", "dependencies": [ - "flutter", - "url_launcher_android", - "url_launcher_ios", - "url_launcher_linux", - "url_launcher_macos", - "url_launcher_platform_interface", - "url_launcher_web", - "url_launcher_windows" - ] - }, - { - "name": "uuid", - "version": "4.5.1", - "dependencies": [ - "crypto", - "fixnum", - "meta", - "sprintf" - ] - }, - { - "name": "package_info_plus", - "version": "8.3.1", - "dependencies": [ - "clock", "ffi", "flutter", - "flutter_web_plugins", - "http", + "flutter_local_notifications_platform_interface", "meta", - "package_info_plus_platform_interface", - "path", - "web", - "win32" + "timezone", + "xml" ] }, { - "name": "package_info_plus_platform_interface", - "version": "3.2.1", + "name": "flutter_local_notifications_linux", + "version": "6.0.0", + "dependencies": [ + "dbus", + "ffi", + "flutter", + "flutter_local_notifications_platform_interface", + "path", + "xdg_directories" + ] + }, + { + "name": "sensors_plus_platform_interface", + "version": "2.0.1", "dependencies": [ "flutter", + "logging", "meta", "plugin_platform_interface" ] }, { - "name": "cupertino_icons", - "version": "1.0.8", - "dependencies": [] - }, - { - "name": "cli_util", - "version": "0.4.2", + "name": "flutter_web_plugins", + "version": "0.0.0", "dependencies": [ + "characters", + "collection", + "flutter", + "material_color_utilities", "meta", - "path" + "vector_math" ] }, { - "name": "checked_yaml", - "version": "2.0.4", + "name": "universal_io", + "version": "2.2.2", "dependencies": [ - "json_annotation", - "source_span", - "yaml" + "collection", + "meta", + "typed_data" ] }, - { - "name": "plugin_platform_interface", - "version": "2.1.8", - "dependencies": [ - "meta" - ] - }, - { - "name": "logging", - "version": "1.3.0", - "dependencies": [] - }, { "name": "typed_data", "version": "1.4.0", @@ -656,152 +1143,16 @@ ] }, { - "name": "http_cache_core", - "version": "1.1.1", + "name": "html", + "version": "0.15.6", "dependencies": [ - "collection", - "string_scanner", - "uuid" + "csslib", + "source_span" ] }, { - "name": "fixnum", - "version": "1.1.1", - "dependencies": [] - }, - { - "name": "flutter_map", - "version": "8.2.1", - "dependencies": [ - "async", - "collection", - "dart_earcut", - "dart_polylabel2", - "flutter", - "http", - "latlong2", - "logger", - "meta", - "path", - "path_provider", - "proj4dart", - "uuid" - ] - }, - { - "name": "path_provider", - "version": "2.1.5", - "dependencies": [ - "flutter", - "path_provider_android", - "path_provider_foundation", - "path_provider_linux", - "path_provider_platform_interface", - "path_provider_windows" - ] - }, - { - "name": "proj4dart", - "version": "2.1.0", - "dependencies": [ - "meta", - "mgrs_dart", - "wkt_parser" - ] - }, - { - "name": "dart_polylabel2", - "version": "1.0.0", - "dependencies": [ - "collection", - "meta" - ] - }, - { - "name": "wkt_parser", - "version": "2.0.0", - "dependencies": [] - }, - { - "name": "mgrs_dart", - "version": "2.0.0", - "dependencies": [ - "unicode" - ] - }, - { - "name": "dart_earcut", - "version": "1.2.0", - "dependencies": [] - }, - { - "name": "path_provider_linux", - "version": "2.2.1", - "dependencies": [ - "ffi", - "flutter", - "path", - "path_provider_platform_interface", - "xdg_directories" - ] - }, - { - "name": "unicode", - "version": "0.3.1", - "dependencies": [ - "lists" - ] - }, - { - "name": "lists", - "version": "1.0.1", - "dependencies": [ - "meta" - ] - }, - { - "name": "connectivity_plus", - "version": "6.1.5", - "dependencies": [ - "collection", - "connectivity_plus_platform_interface", - "flutter", - "flutter_web_plugins", - "meta", - "nm", - "web" - ] - }, - { - "name": "nm", - "version": "0.5.0", - "dependencies": [ - "dbus" - ] - }, - { - "name": "connectivity_plus_platform_interface", - "version": "2.0.1", - "dependencies": [ - "flutter", - "meta", - "plugin_platform_interface" - ] - }, - { - "name": "go_router", - "version": "16.2.0", - "dependencies": [ - "collection", - "flutter", - "flutter_web_plugins", - "logging", - "meta" - ] - }, - { - "name": "args", - "version": "2.7.0", + "name": "charcode", + "version": "1.4.0", "dependencies": [] }, { @@ -811,6 +1162,18 @@ "source_span" ] }, + { + "name": "geolocator_linux", + "version": "0.2.3", + "dependencies": [ + "dbus", + "flutter", + "geoclue", + "geolocator_platform_interface", + "gsettings", + "package_info_plus" + ] + }, { "name": "geolocator_windows", "version": "0.2.5", @@ -830,26 +1193,31 @@ ] }, { - "name": "web", - "version": "1.1.1", - "dependencies": [] - }, - { - "name": "http", - "version": "1.5.0", + "name": "geolocator_apple", + "version": "2.3.13", "dependencies": [ - "async", - "http_parser", - "meta", - "web" + "flutter", + "geolocator_platform_interface" ] }, { - "name": "url_launcher_platform_interface", - "version": "2.3.2", + "name": "geolocator_android", + "version": "5.0.2", "dependencies": [ "flutter", - "plugin_platform_interface" + "geolocator_platform_interface", + "meta", + "uuid" + ] + }, + { + "name": "geolocator_platform_interface", + "version": "4.2.6", + "dependencies": [ + "flutter", + "meta", + "plugin_platform_interface", + "vector_math" ] }, { @@ -872,95 +1240,14 @@ ] }, { - "name": "syncfusion_flutter_charts", - "version": "30.2.6", + "name": "path_provider_linux", + "version": "2.2.1", "dependencies": [ - "flutter", - "intl", - "syncfusion_flutter_core", - "vector_math" - ] - }, - { - "name": "syncfusion_flutter_core", - "version": "30.2.6", - "dependencies": [ - "flutter", - "vector_math" - ] - }, - { - "name": "google_fonts", - "version": "6.3.0", - "dependencies": [ - "crypto", - "flutter", - "http", - "path_provider" - ] - }, - { - "name": "charcode", - "version": "1.4.0", - "dependencies": [] - }, - { - "name": "geolocator_platform_interface", - "version": "4.2.6", - "dependencies": [ - "flutter", - "meta", - "plugin_platform_interface", - "vector_math" - ] - }, - { - "name": "geolocator_android", - "version": "5.0.2", - "dependencies": [ - "flutter", - "geolocator_platform_interface", - "meta", - "uuid" - ] - }, - { - "name": "gsettings", - "version": "0.2.8", - "dependencies": [ - "dbus", - "xdg_directories" - ] - }, - { - "name": "dbus", - "version": "0.7.11", - "dependencies": [ - "args", "ffi", - "meta", - "xml" - ] - }, - { - "name": "mime", - "version": "2.0.0", - "dependencies": [] - }, - { - "name": "dio_cache_interceptor", - "version": "4.0.3", - "dependencies": [ - "dio", - "http_cache_core" - ] - }, - { - "name": "url_launcher_linux", - "version": "3.2.1", - "dependencies": [ "flutter", - "url_launcher_platform_interface" + "path", + "path_provider_platform_interface", + "xdg_directories" ] }, { @@ -972,13 +1259,64 @@ ] }, { - "name": "html", - "version": "0.15.6", + "name": "path_provider_android", + "version": "2.2.17", "dependencies": [ - "csslib", - "source_span" + "flutter", + "path_provider_platform_interface" ] }, + { + "name": "synchronized", + "version": "3.4.0", + "dependencies": [] + }, + { + "name": "http_cache_core", + "version": "1.1.1", + "dependencies": [ + "collection", + "string_scanner", + "uuid" + ] + }, + { + "name": "dio_cache_interceptor", + "version": "4.0.3", + "dependencies": [ + "dio", + "http_cache_core" + ] + }, + { + "name": "proj4dart", + "version": "2.1.0", + "dependencies": [ + "meta", + "mgrs_dart", + "wkt_parser" + ] + }, + { + "name": "logger", + "version": "2.6.1", + "dependencies": [ + "meta" + ] + }, + { + "name": "dart_polylabel2", + "version": "1.0.0", + "dependencies": [ + "collection", + "meta" + ] + }, + { + "name": "dart_earcut", + "version": "1.2.0", + "dependencies": [] + }, { "name": "url_launcher_windows", "version": "3.1.4", @@ -987,6 +1325,24 @@ "url_launcher_platform_interface" ] }, + { + "name": "url_launcher_web", + "version": "2.4.1", + "dependencies": [ + "flutter", + "flutter_web_plugins", + "url_launcher_platform_interface", + "web" + ] + }, + { + "name": "url_launcher_platform_interface", + "version": "2.3.2", + "dependencies": [ + "flutter", + "plugin_platform_interface" + ] + }, { "name": "url_launcher_macos", "version": "3.2.3", @@ -996,164 +1352,11 @@ ] }, { - "name": "flutter_local_notifications", - "version": "19.4.0", - "dependencies": [ - "clock", - "flutter", - "flutter_local_notifications_linux", - "flutter_local_notifications_platform_interface", - "flutter_local_notifications_windows", - "timezone" - ] - }, - { - "name": "flutter_local_notifications_windows", - "version": "1.0.2", - "dependencies": [ - "ffi", - "flutter", - "flutter_local_notifications_platform_interface", - "meta", - "timezone", - "xml" - ] - }, - { - "name": "flutter_local_notifications_platform_interface", - "version": "9.1.0", - "dependencies": [ - "plugin_platform_interface" - ] - }, - { - "name": "flutter_local_notifications_linux", - "version": "6.0.0", - "dependencies": [ - "dbus", - "ffi", - "flutter", - "flutter_local_notifications_platform_interface", - "path", - "xdg_directories" - ] - }, - { - "name": "timezone", - "version": "0.10.1", - "dependencies": [ - "http", - "path" - ] - }, - { - "name": "ffi", - "version": "2.1.4", - "dependencies": [] - }, - { - "name": "xml", - "version": "6.6.1", - "dependencies": [ - "collection", - "meta", - "petitparser" - ] - }, - { - "name": "petitparser", - "version": "7.0.1", - "dependencies": [ - "collection", - "meta" - ] - }, - { - "name": "flutter_svg", - "version": "2.0.13", + "name": "url_launcher_linux", + "version": "3.2.1", "dependencies": [ "flutter", - "http", - "vector_graphics", - "vector_graphics_codec", - "vector_graphics_compiler" - ] - }, - { - "name": "vector_graphics_compiler", - "version": "1.1.11+1", - "dependencies": [ - "args", - "meta", - "path", - "path_parsing", - "vector_graphics_codec", - "xml" - ] - }, - { - "name": "vector_graphics_codec", - "version": "1.1.11+1", - "dependencies": [] - }, - { - "name": "path_parsing", - "version": "1.1.0", - "dependencies": [ - "meta", - "vector_math" - ] - }, - { - "name": "geolocator_apple", - "version": "2.3.13", - "dependencies": [ - "flutter", - "geolocator_platform_interface" - ] - }, - { - "name": "file_selector_linux", - "version": "0.9.3+2", - "dependencies": [ - "cross_file", - "file_selector_platform_interface", - "flutter" - ] - }, - { - "name": "file_selector_platform_interface", - "version": "2.6.2", - "dependencies": [ - "cross_file", - "flutter", - "http", - "plugin_platform_interface" - ] - }, - { - "name": "http_parser", - "version": "4.1.2", - "dependencies": [ - "collection", - "source_span", - "string_scanner", - "typed_data" - ] - }, - { - "name": "xdg_directories", - "version": "1.1.0", - "dependencies": [ - "meta", - "path" - ] - }, - { - "name": "crypto", - "version": "3.0.6", - "dependencies": [ - "typed_data" + "url_launcher_platform_interface" ] }, { @@ -1165,16 +1368,11 @@ ] }, { - "name": "shared_preferences", - "version": "2.5.3", + "name": "url_launcher_android", + "version": "6.3.17", "dependencies": [ "flutter", - "shared_preferences_android", - "shared_preferences_foundation", - "shared_preferences_linux", - "shared_preferences_platform_interface", - "shared_preferences_web", - "shared_preferences_windows" + "url_launcher_platform_interface" ] }, { @@ -1189,6 +1387,16 @@ "shared_preferences_platform_interface" ] }, + { + "name": "shared_preferences_web", + "version": "2.4.3", + "dependencies": [ + "flutter", + "flutter_web_plugins", + "shared_preferences_platform_interface", + "web" + ] + }, { "name": "shared_preferences_platform_interface", "version": "2.4.1", @@ -1209,16 +1417,6 @@ "shared_preferences_platform_interface" ] }, - { - "name": "shared_preferences_web", - "version": "2.4.3", - "dependencies": [ - "flutter", - "flutter_web_plugins", - "shared_preferences_platform_interface", - "web" - ] - }, { "name": "shared_preferences_foundation", "version": "2.5.4", @@ -1228,63 +1426,75 @@ ] }, { - "name": "image", - "version": "4.5.4", + "name": "shared_preferences_android", + "version": "2.4.11", "dependencies": [ - "archive", + "flutter", + "shared_preferences_platform_interface" + ] + }, + { + "name": "syncfusion_flutter_core", + "version": "30.2.6", + "dependencies": [ + "flutter", + "vector_math" + ] + }, + { + "name": "equatable", + "version": "2.0.7", + "dependencies": [ + "collection", + "meta" + ] + }, + { + "name": "fixnum", + "version": "1.1.1", + "dependencies": [] + }, + { + "name": "sprintf", + "version": "7.0.0", + "dependencies": [] + }, + { + "name": "win32", + "version": "5.14.0", + "dependencies": [ + "ffi" + ] + }, + { + "name": "package_info_plus_platform_interface", + "version": "3.2.1", + "dependencies": [ + "flutter", "meta", + "plugin_platform_interface" + ] + }, + { + "name": "ffi", + "version": "2.1.4", + "dependencies": [] + }, + { + "name": "vector_graphics_compiler", + "version": "1.1.11+1", + "dependencies": [ + "args", + "meta", + "path", + "path_parsing", + "vector_graphics_codec", "xml" ] }, { - "name": "archive", - "version": "4.0.7", - "dependencies": [ - "crypto", - "path", - "posix" - ] - }, - { - "name": "posix", - "version": "6.0.3", - "dependencies": [ - "ffi", - "meta", - "path" - ] - }, - { - "name": "source_helper", - "version": "1.3.5", - "dependencies": [ - "analyzer", - "collection", - "source_gen" - ] - }, - { - "name": "glob", - "version": "2.1.3", - "dependencies": [ - "async", - "collection", - "file", - "path", - "string_scanner" - ] - }, - { - "name": "file", - "version": "7.0.1", - "dependencies": [ - "meta", - "path" - ] - }, - { - "name": "platform", - "version": "3.1.6", + "name": "vector_graphics_codec", + "version": "1.1.11+1", "dependencies": [] }, { @@ -1297,64 +1507,19 @@ ] }, { - "name": "source_gen", - "version": "1.5.0", + "name": "nm", + "version": "0.5.0", "dependencies": [ - "analyzer", - "async", - "build", - "dart_style", - "glob", - "path", - "source_span", - "yaml" + "dbus" ] }, { - "name": "url_launcher_web", - "version": "2.4.1", + "name": "connectivity_plus_platform_interface", + "version": "2.0.1", "dependencies": [ "flutter", - "flutter_web_plugins", - "url_launcher_platform_interface", - "web" - ] - }, - { - "name": "file_selector_macos", - "version": "0.9.4+4", - "dependencies": [ - "cross_file", - "file_selector_platform_interface", - "flutter" - ] - }, - { - "name": "logger", - "version": "2.6.1", - "dependencies": [ - "meta" - ] - }, - { - "name": "path_provider_android", - "version": "2.2.17", - "dependencies": [ - "flutter", - "path_provider_platform_interface" - ] - }, - { - "name": "dio", - "version": "5.9.0", - "dependencies": [ - "async", - "collection", - "dio_web_adapter", - "http_parser", "meta", - "mime", - "path" + "plugin_platform_interface" ] }, { @@ -1368,36 +1533,61 @@ ] }, { - "name": "shared_preferences_android", - "version": "2.4.11", + "name": "http_parser", + "version": "4.1.2", "dependencies": [ - "flutter", - "shared_preferences_platform_interface" + "collection", + "source_span", + "string_scanner", + "typed_data" ] }, { - "name": "synchronized", - "version": "3.4.0", + "name": "sky_engine", + "version": "0.0.0", "dependencies": [] }, { - "name": "build", - "version": "2.5.4", + "name": "xml", + "version": "6.6.1", "dependencies": [ - "analyzer", - "async", - "build_runner_core", - "built_collection", - "built_value", - "convert", - "crypto", - "glob", - "graphs", - "logging", + "collection", "meta", - "package_config", + "petitparser" + ] + }, + { + "name": "archive", + "version": "4.0.7", + "dependencies": [ + "crypto", "path", - "pool" + "posix" + ] + }, + { + "name": "web_socket", + "version": "1.0.1", + "dependencies": [ + "web" + ] + }, + { + "name": "file", + "version": "7.0.1", + "dependencies": [ + "meta", + "path" + ] + }, + { + "name": "built_value", + "version": "8.11.1", + "dependencies": [ + "built_collection", + "collection", + "fixnum", + "meta" ] }, { @@ -1405,117 +1595,6 @@ "version": "5.1.1", "dependencies": [] }, - { - "name": "build_runner_core", - "version": "9.1.2", - "dependencies": [ - "analyzer", - "async", - "build", - "build_config", - "build_resolvers", - "build_runner", - "built_collection", - "built_value", - "collection", - "convert", - "crypto", - "glob", - "graphs", - "json_annotation", - "logging", - "meta", - "package_config", - "path", - "pool", - "timing", - "watcher", - "yaml" - ] - }, - { - "name": "build_runner", - "version": "2.5.4", - "dependencies": [ - "analyzer", - "args", - "async", - "build", - "build_config", - "build_daemon", - "build_resolvers", - "build_runner_core", - "code_builder", - "collection", - "crypto", - "dart_style", - "frontend_server_client", - "glob", - "graphs", - "http", - "http_multi_server", - "io", - "js", - "logging", - "meta", - "mime", - "package_config", - "path", - "pool", - "pub_semver", - "pubspec_parse", - "shelf", - "shelf_web_socket", - "stack_trace", - "stream_transform", - "timing", - "watcher", - "web", - "web_socket_channel", - "yaml" - ] - }, - { - "name": "build_resolvers", - "version": "2.5.4", - "dependencies": [ - "analyzer", - "async", - "build", - "build_runner_core", - "collection", - "convert", - "crypto", - "graphs", - "logging", - "package_config", - "path", - "pool", - "pub_semver", - "stream_transform" - ] - }, - { - "name": "dart_style", - "version": "2.3.8", - "dependencies": [ - "analyzer", - "args", - "collection", - "package_config", - "path", - "pub_semver", - "source_span" - ] - }, - { - "name": "pool", - "version": "1.5.1", - "dependencies": [ - "async", - "stack_trace" - ] - }, { "name": "convert", "version": "3.1.2", @@ -1523,25 +1602,6 @@ "typed_data" ] }, - { - "name": "analyzer", - "version": "6.11.0", - "dependencies": [ - "_fe_analyzer_shared", - "collection", - "convert", - "crypto", - "glob", - "macros", - "meta", - "package_config", - "path", - "pub_semver", - "source_span", - "watcher", - "yaml" - ] - }, { "name": "macros", "version": "0.1.3-main.0", @@ -1556,181 +1616,6 @@ "meta" ] }, - { - "name": "_macros", - "version": "0.3.3", - "dependencies": [] - }, - { - "name": "package_config", - "version": "2.2.0", - "dependencies": [ - "path" - ] - }, - { - "name": "watcher", - "version": "1.1.2", - "dependencies": [ - "async", - "path" - ] - }, - { - "name": "timing", - "version": "1.0.2", - "dependencies": [ - "json_annotation" - ] - }, - { - "name": "build_config", - "version": "1.1.2", - "dependencies": [ - "checked_yaml", - "json_annotation", - "path", - "pubspec_parse", - "yaml" - ] - }, - { - "name": "pub_semver", - "version": "2.2.0", - "dependencies": [ - "collection" - ] - }, - { - "name": "graphs", - "version": "2.3.2", - "dependencies": [ - "collection" - ] - }, - { - "name": "built_value", - "version": "8.11.1", - "dependencies": [ - "built_collection", - "collection", - "fixnum", - "meta" - ] - }, - { - "name": "stream_transform", - "version": "2.1.1", - "dependencies": [] - }, - { - "name": "frontend_server_client", - "version": "4.0.0", - "dependencies": [ - "async", - "path" - ] - }, - { - "name": "io", - "version": "1.0.5", - "dependencies": [ - "meta", - "path", - "string_scanner" - ] - }, - { - "name": "http_multi_server", - "version": "3.2.2", - "dependencies": [ - "async" - ] - }, - { - "name": "build_daemon", - "version": "4.0.4", - "dependencies": [ - "built_collection", - "built_value", - "crypto", - "http_multi_server", - "logging", - "path", - "pool", - "shelf", - "shelf_web_socket", - "stream_transform", - "watcher", - "web_socket_channel" - ] - }, - { - "name": "shelf_web_socket", - "version": "3.0.0", - "dependencies": [ - "shelf", - "stream_channel", - "web_socket_channel" - ] - }, - { - "name": "js", - "version": "0.7.2", - "dependencies": [] - }, - { - "name": "pubspec_parse", - "version": "1.5.0", - "dependencies": [ - "checked_yaml", - "collection", - "json_annotation", - "pub_semver", - "yaml" - ] - }, - { - "name": "web_socket_channel", - "version": "3.0.3", - "dependencies": [ - "async", - "crypto", - "stream_channel", - "web", - "web_socket" - ] - }, - { - "name": "web_socket", - "version": "1.0.1", - "dependencies": [ - "web" - ] - }, - { - "name": "code_builder", - "version": "4.10.1", - "dependencies": [ - "built_collection", - "built_value", - "collection", - "matcher", - "meta" - ] - }, - { - "name": "shelf", - "version": "1.4.2", - "dependencies": [ - "async", - "collection", - "http_parser", - "path", - "stack_trace", - "stream_channel" - ] - }, { "name": "file_selector_windows", "version": "0.9.3+4", @@ -1740,6 +1625,23 @@ "flutter" ] }, + { + "name": "file_selector_platform_interface", + "version": "2.6.2", + "dependencies": [ + "cross_file", + "flutter", + "http", + "plugin_platform_interface" + ] + }, + { + "name": "plugin_platform_interface", + "version": "2.1.8", + "dependencies": [ + "meta" + ] + }, { "name": "cross_file", "version": "0.3.4+2", @@ -1749,18 +1651,21 @@ ] }, { - "name": "win32", - "version": "5.14.0", + "name": "file_selector_macos", + "version": "0.9.4+4", "dependencies": [ - "ffi" + "cross_file", + "file_selector_platform_interface", + "flutter" ] }, { - "name": "url_launcher_android", - "version": "6.3.17", + "name": "file_selector_linux", + "version": "0.9.3+2", "dependencies": [ - "flutter", - "url_launcher_platform_interface" + "cross_file", + "file_selector_platform_interface", + "flutter" ] }, { @@ -1769,6 +1674,101 @@ "dependencies": [ "flutter" ] + }, + { + "name": "xdg_directories", + "version": "1.1.0", + "dependencies": [ + "meta", + "path" + ] + }, + { + "name": "dbus", + "version": "0.7.11", + "dependencies": [ + "args", + "ffi", + "meta", + "xml" + ] + }, + { + "name": "gsettings", + "version": "0.2.8", + "dependencies": [ + "dbus", + "xdg_directories" + ] + }, + { + "name": "geoclue", + "version": "0.1.1", + "dependencies": [ + "dbus", + "meta" + ] + }, + { + "name": "platform", + "version": "3.1.6", + "dependencies": [] + }, + { + "name": "wkt_parser", + "version": "2.0.0", + "dependencies": [] + }, + { + "name": "mgrs_dart", + "version": "2.0.0", + "dependencies": [ + "unicode" + ] + }, + { + "name": "path_parsing", + "version": "1.1.0", + "dependencies": [ + "meta", + "vector_math" + ] + }, + { + "name": "petitparser", + "version": "7.0.1", + "dependencies": [ + "collection", + "meta" + ] + }, + { + "name": "posix", + "version": "6.0.3", + "dependencies": [ + "ffi", + "meta", + "path" + ] + }, + { + "name": "_macros", + "version": "0.3.3", + "dependencies": [] + }, + { + "name": "unicode", + "version": "0.3.1", + "dependencies": [ + "lists" + ] + }, + { + "name": "lists", + "version": "1.0.1", + "dependencies": [ + "meta" + ] } ], "configVersion": 1 diff --git a/app/.flutter-plugins-dependencies b/app/.flutter-plugins-dependencies index 88ea8bd9..54620a2c 100644 --- a/app/.flutter-plugins-dependencies +++ b/app/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications-19.4.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"/home/pierre/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.13/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_ios","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.13/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/pierre/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sensors_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/pierre/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_ios","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.4/","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications-19.4.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_plugin_android_lifecycle","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.29/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_android","path":"/home/pierre/.pub-cache/hosted/pub.dev/geolocator_android-5.0.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_android","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_android-0.8.13/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"],"dev_dependency":false},{"name":"package_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"/home/pierre/.pub-cache/hosted/pub.dev/path_provider_android-2.2.17/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sensors_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_android","path":"/home/pierre/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.11/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_android","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.17/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_selector_macos","path":"/home/pierre/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.4+4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications-19.4.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"/home/pierre/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.13/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_macos","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.2/","native_build":false,"dependencies":["file_selector_macos"],"dev_dependency":false},{"name":"package_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/pierre/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/pierre/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_macos","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.3/","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"file_selector_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.3+2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-6.0.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"geolocator_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/geolocator_linux-0.2.3/","native_build":false,"dependencies":["package_info_plus"],"dev_dependency":false},{"name":"image_picker_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.2/","native_build":false,"dependencies":["file_selector_linux"],"dev_dependency":false},{"name":"package_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false},{"name":"url_launcher_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.1/","native_build":true,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_selector_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/file_selector_windows-0.9.3+4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications_windows-1.0.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/geolocator_windows-0.2.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.2/","native_build":false,"dependencies":["file_selector_windows"],"dev_dependency":false},{"name":"package_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false},{"name":"url_launcher_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.4/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","dependencies":[],"dev_dependency":false},{"name":"geolocator_web","path":"/home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/","dependencies":[],"dev_dependency":false},{"name":"image_picker_for_web","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.0/","dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","dependencies":[],"dev_dependency":false},{"name":"sensors_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/","dependencies":[],"dev_dependency":false},{"name":"shared_preferences_web","path":"/home/pierre/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.3/","dependencies":[],"dev_dependency":false},{"name":"url_launcher_web","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_web-2.4.1/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":[]},{"name":"file_selector_linux","dependencies":[]},{"name":"file_selector_macos","dependencies":[]},{"name":"file_selector_windows","dependencies":[]},{"name":"flutter_local_notifications","dependencies":["flutter_local_notifications_linux","flutter_local_notifications_windows"]},{"name":"flutter_local_notifications_linux","dependencies":[]},{"name":"flutter_local_notifications_windows","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"geolocator","dependencies":["geolocator_android","geolocator_apple","geolocator_web","geolocator_windows","geolocator_linux"]},{"name":"geolocator_android","dependencies":[]},{"name":"geolocator_apple","dependencies":[]},{"name":"geolocator_linux","dependencies":["package_info_plus"]},{"name":"geolocator_web","dependencies":[]},{"name":"geolocator_windows","dependencies":[]},{"name":"image_picker","dependencies":["image_picker_android","image_picker_for_web","image_picker_ios","image_picker_linux","image_picker_macos","image_picker_windows"]},{"name":"image_picker_android","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"image_picker_for_web","dependencies":[]},{"name":"image_picker_ios","dependencies":[]},{"name":"image_picker_linux","dependencies":["file_selector_linux"]},{"name":"image_picker_macos","dependencies":["file_selector_macos"]},{"name":"image_picker_windows","dependencies":["file_selector_windows"]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"sensors_plus","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2025-08-19 19:03:30.241440","version":"3.32.8","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications-19.4.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"/home/pierre/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.13/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_ios","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.13/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/pierre/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sensors_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/pierre/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_ios","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.4/","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications-19.4.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_plugin_android_lifecycle","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.29/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_android","path":"/home/pierre/.pub-cache/hosted/pub.dev/geolocator_android-5.0.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_android","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_android-0.8.13/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"],"dev_dependency":false},{"name":"package_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"/home/pierre/.pub-cache/hosted/pub.dev/path_provider_android-2.2.17/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sensors_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_android","path":"/home/pierre/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.11/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_android","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.17/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_selector_macos","path":"/home/pierre/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.4+4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications-19.4.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_apple","path":"/home/pierre/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.13/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_macos","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.2/","native_build":false,"dependencies":["file_selector_macos"],"dev_dependency":false},{"name":"package_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/pierre/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/pierre/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_macos","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.3/","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"file_selector_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.3+2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-6.0.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"geolocator_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/geolocator_linux-0.2.3/","native_build":false,"dependencies":["package_info_plus"],"dev_dependency":false},{"name":"image_picker_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.2/","native_build":false,"dependencies":["file_selector_linux"],"dev_dependency":false},{"name":"package_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false},{"name":"url_launcher_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.1/","native_build":true,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_selector_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/file_selector_windows-0.9.3+4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_local_notifications_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications_windows-1.0.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"geolocator_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/geolocator_windows-0.2.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_picker_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.2/","native_build":false,"dependencies":["file_selector_windows"],"dev_dependency":false},{"name":"package_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false},{"name":"url_launcher_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.4/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","dependencies":[],"dev_dependency":false},{"name":"geolocator_web","path":"/home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/","dependencies":[],"dev_dependency":false},{"name":"image_picker_for_web","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.1.0/","dependencies":[],"dev_dependency":false},{"name":"package_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.1/","dependencies":[],"dev_dependency":false},{"name":"sensors_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/sensors_plus-6.1.2/","dependencies":[],"dev_dependency":false},{"name":"shared_preferences_web","path":"/home/pierre/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.3/","dependencies":[],"dev_dependency":false},{"name":"url_launcher_web","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_web-2.4.1/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":[]},{"name":"file_selector_linux","dependencies":[]},{"name":"file_selector_macos","dependencies":[]},{"name":"file_selector_windows","dependencies":[]},{"name":"flutter_local_notifications","dependencies":["flutter_local_notifications_linux","flutter_local_notifications_windows"]},{"name":"flutter_local_notifications_linux","dependencies":[]},{"name":"flutter_local_notifications_windows","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"geolocator","dependencies":["geolocator_android","geolocator_apple","geolocator_web","geolocator_windows","geolocator_linux"]},{"name":"geolocator_android","dependencies":[]},{"name":"geolocator_apple","dependencies":[]},{"name":"geolocator_linux","dependencies":["package_info_plus"]},{"name":"geolocator_web","dependencies":[]},{"name":"geolocator_windows","dependencies":[]},{"name":"image_picker","dependencies":["image_picker_android","image_picker_for_web","image_picker_ios","image_picker_linux","image_picker_macos","image_picker_windows"]},{"name":"image_picker_android","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"image_picker_for_web","dependencies":[]},{"name":"image_picker_ios","dependencies":[]},{"name":"image_picker_linux","dependencies":["file_selector_linux"]},{"name":"image_picker_macos","dependencies":["file_selector_macos"]},{"name":"image_picker_windows","dependencies":["file_selector_windows"]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"sensors_plus","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2025-08-21 17:52:00.816234","version":"3.32.8","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/app/lib/presentation/widgets/passage_map_dialog.dart b/app/lib/presentation/widgets/passage_map_dialog.dart new file mode 100644 index 00000000..d04b2140 --- /dev/null +++ b/app/lib/presentation/widgets/passage_map_dialog.dart @@ -0,0 +1,420 @@ +import 'package:flutter/material.dart'; +import 'package:geosector_app/core/data/models/passage_model.dart'; +import 'package:geosector_app/core/constants/app_keys.dart'; +import 'package:geosector_app/core/repositories/passage_repository.dart'; +import 'package:geosector_app/core/utils/api_exception.dart'; +import 'package:geosector_app/core/services/current_amicale_service.dart'; +import 'package:geosector_app/app.dart'; + +class PassageMapDialog extends StatelessWidget { + final PassageModel passage; + final bool isAdmin; + final VoidCallback? onDeleted; + + const PassageMapDialog({ + super.key, + required this.passage, + this.isAdmin = false, + this.onDeleted, + }); + + @override + Widget build(BuildContext context) { + final int type = passage.fkType; + + // Récupérer le type de passage + final String typePassage = AppKeys.typesPassages[type]?['titre'] ?? 'Inconnu'; + final Color typeColor = Color(AppKeys.typesPassages[type]?['couleur1'] ?? 0xFF9E9E9E); + + // Construire l'adresse complète + final String adresse = '${passage.numero ?? ''} ${passage.rueBis ?? ''} ${passage.rue ?? ''}'.trim(); + + // Informations sur l'étage, l'appartement et la résidence (si habitat = 2) + String? etageInfo; + String? apptInfo; + String? residenceInfo; + if (passage.fkHabitat == 2) { + if (passage.niveau.isNotEmpty) { + etageInfo = 'Étage ${passage.niveau}'; + } + if (passage.appt.isNotEmpty) { + apptInfo = 'Appt. ${passage.appt}'; + } + if (passage.residence.isNotEmpty) { + residenceInfo = passage.residence; + } + } + + // Formater la date (uniquement si le type n'est pas 2 et si la date existe) + String? dateInfo; + if (type != 2 && passage.passedAt != null) { + final date = passage.passedAt!; + dateInfo = '${_formatDate(date)} à ${date.hour}h${date.minute.toString().padLeft(2, '0')}'; + } + + // Récupérer le nom du passage (si le type n'est pas 6 - Maison vide) + String? nomInfo; + if (type != 6 && passage.name.isNotEmpty) { + nomInfo = passage.name; + } + + // Récupérer les informations de règlement si le type est 1 (Effectué) ou 5 (Lot) + Widget? reglementInfo; + if ((type == 1 || type == 5) && passage.fkTypeReglement > 0) { + final int typeReglementId = passage.fkTypeReglement; + final String montant = passage.montant; + + // Récupérer les informations du type de règlement + if (AppKeys.typesReglements.containsKey(typeReglementId)) { + final Map typeReglement = AppKeys.typesReglements[typeReglementId]!; + final String titre = typeReglement['titre'] as String; + final Color couleur = Color(typeReglement['couleur'] as int); + final IconData iconData = typeReglement['icon_data'] as IconData; + + reglementInfo = Container( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.only(top: 8), + decoration: BoxDecoration( + color: couleur.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: couleur.withOpacity(0.3)), + ), + child: Row( + children: [ + Icon(iconData, color: couleur, size: 20), + const SizedBox(width: 8), + Text('$titre: $montant €', + style: TextStyle(color: couleur, fontWeight: FontWeight.bold)), + ], + ), + ); + } + } + + // Vérifier si l'utilisateur peut supprimer (admin ou user avec permission) + bool canDelete = isAdmin; + if (!isAdmin) { + try { + final amicale = CurrentAmicaleService.instance.currentAmicale; + if (amicale != null) { + canDelete = amicale.chkUserDeletePass == true; + } + } catch (e) { + debugPrint('Erreur lors de la vérification des permissions: $e'); + } + } + + return AlertDialog( + title: Row( + children: [ + Container( + width: 10, + height: 10, + decoration: BoxDecoration( + color: typeColor, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Passage #${passage.id}', + style: const TextStyle(fontSize: 18), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: typeColor.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + typePassage, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: typeColor, + ), + ), + ), + ], + ), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Afficher en premier si le passage n'est pas affecté à un secteur + if (passage.fkSector == null) ...[ + Container( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.1), + border: Border.all(color: Colors.red, width: 1), + borderRadius: BorderRadius.circular(4), + ), + child: Row( + children: [ + const Icon(Icons.warning, color: Colors.red, size: 20), + const SizedBox(width: 8), + const Expanded( + child: Text( + 'Ce passage n\'est plus affecté à un secteur', + style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold), + ), + ), + ], + ), + ), + ], + + // Adresse + _buildInfoRow(Icons.location_on, 'Adresse', adresse.isEmpty ? 'Non renseignée' : adresse), + + // Résidence + if (residenceInfo != null) + _buildInfoRow(Icons.apartment, 'Résidence', residenceInfo), + + // Étage et appartement + if (etageInfo != null || apptInfo != null) + _buildInfoRow(Icons.stairs, 'Localisation', + [etageInfo, apptInfo].where((e) => e != null).join(' - ')), + + // Date + if (dateInfo != null) + _buildInfoRow(Icons.calendar_today, 'Date', dateInfo), + + // Nom + if (nomInfo != null) + _buildInfoRow(Icons.person, 'Nom', nomInfo), + + // Ville + if (passage.ville.isNotEmpty) + _buildInfoRow(Icons.location_city, 'Ville', passage.ville), + + // Remarque + if (passage.remarque.isNotEmpty) + _buildInfoRow(Icons.note, 'Remarque', passage.remarque), + + // Règlement + if (reglementInfo != null) reglementInfo, + ], + ), + ), + actions: [ + // Bouton de suppression si autorisé + if (canDelete) + TextButton.icon( + onPressed: () { + Navigator.of(context).pop(); + _showDeleteConfirmationDialog(context); + }, + icon: const Icon(Icons.delete, size: 20), + label: const Text('Supprimer'), + style: TextButton.styleFrom( + foregroundColor: Colors.red, + ), + ), + // Bouton de fermeture + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Fermer'), + ), + ], + ); + } + + // Helper pour construire une ligne d'information + Widget _buildInfoRow(IconData icon, String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 16, color: Colors.grey[600]), + const SizedBox(width: 8), + Expanded( + child: RichText( + text: TextSpan( + style: const TextStyle(color: Colors.black87), + children: [ + TextSpan( + text: '$label: ', + style: const TextStyle(fontWeight: FontWeight.w600), + ), + TextSpan(text: value), + ], + ), + ), + ), + ], + ), + ); + } + + // Formater une date + String _formatDate(DateTime date) { + return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}'; + } + + // Afficher le dialog de confirmation de suppression + void _showDeleteConfirmationDialog(BuildContext context) { + final TextEditingController confirmController = TextEditingController(); + final String streetNumber = passage.numero ?? ''; + final String fullAddress = '${passage.numero ?? ''} ${passage.rueBis ?? ''} ${passage.rue ?? ''}'.trim(); + + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: const Row( + children: [ + Icon(Icons.warning, color: Colors.red, size: 28), + SizedBox(width: 8), + Text('Confirmation de suppression'), + ], + ), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'ATTENTION : Cette action est irréversible !', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.red, + fontSize: 16, + ), + ), + const SizedBox(height: 16), + Text( + 'Vous êtes sur le point de supprimer définitivement le passage :', + style: TextStyle(color: Colors.grey[800]), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey[300]!), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + fullAddress.isEmpty ? 'Adresse inconnue' : fullAddress, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + if (passage.ville.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + passage.ville, + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + ], + ], + ), + ), + const SizedBox(height: 20), + const Text( + 'Pour confirmer la suppression, veuillez saisir le numéro de rue de ce passage :', + style: TextStyle(fontWeight: FontWeight.w500), + ), + const SizedBox(height: 12), + TextField( + controller: confirmController, + decoration: InputDecoration( + labelText: 'Numéro de rue', + hintText: streetNumber.isNotEmpty ? 'Ex: $streetNumber' : 'Saisir le numéro', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.home), + ), + keyboardType: TextInputType.text, + textCapitalization: TextCapitalization.characters, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + confirmController.dispose(); + Navigator.of(dialogContext).pop(); + }, + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: () async { + // Vérifier que le numéro saisi correspond + final enteredNumber = confirmController.text.trim(); + if (enteredNumber.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Veuillez saisir le numéro de rue'), + backgroundColor: Colors.orange, + ), + ); + return; + } + + if (streetNumber.isNotEmpty && enteredNumber.toUpperCase() != streetNumber.toUpperCase()) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Le numéro de rue ne correspond pas'), + backgroundColor: Colors.red, + ), + ); + return; + } + + // Fermer le dialog + confirmController.dispose(); + Navigator.of(dialogContext).pop(); + + // Effectuer la suppression + await _deletePassage(context); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + child: const Text('Supprimer définitivement'), + ), + ], + ); + }, + ); + } + + // Supprimer un passage + Future _deletePassage(BuildContext context) async { + try { + // Appeler le repository pour supprimer via l'API + final success = await passageRepository.deletePassageViaApi(passage.id); + + if (success && context.mounted) { + ApiException.showSuccess(context, 'Passage supprimé avec succès'); + + // Appeler le callback si fourni + onDeleted?.call(); + } else if (context.mounted) { + ApiException.showError(context, Exception('Erreur lors de la suppression')); + } + } catch (e) { + debugPrint('Erreur suppression passage: $e'); + if (context.mounted) { + ApiException.showError(context, e); + } + } + } +} \ No newline at end of file