From 12d514d8661d445495eff146f595518de6a74a9a Mon Sep 17 00:00:00 2001 From: DahoudG <41957584+DahoudG@users.noreply.github.com> Date: Mon, 15 Sep 2025 20:15:34 +0000 Subject: [PATCH] =?UTF-8?q?Versione=20OK=20Pour=20l'onglet=20=C3=A9v=C3=A9?= =?UTF-8?q?nements.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- unionflow-mobile-apps/ANIMATIONS_FEATURES.md | 150 +++ .../main/res/xml/network_security_config.xml | 2 +- unionflow-mobile-apps/coverage/lcov.info | 1181 +++++++++++++++++ unionflow-mobile-apps/flutter_01.png | Bin 0 -> 350261 bytes .../lib/core/animations/animated_button.dart | 320 +++++ .../animations/animated_notifications.dart | 352 +++++ .../core/animations/micro_interactions.dart | 368 +++++ .../lib/core/animations/page_transitions.dart | 76 ++ .../keycloak_webview_auth_service.dart | 4 +- .../lib/core/constants/app_constants.dart | 2 +- .../lib/core/di/injection.config.dart | 48 +- .../lib/core/di/injection.dart | 13 + .../core/models/cotisation_filter_model.dart | 326 +++++ .../models/cotisation_filter_model.g.dart | 72 + .../lib/core/models/cotisation_model.dart | 6 + .../models/cotisation_statistics_model.dart | 295 ++++ .../models/cotisation_statistics_model.g.dart | 105 ++ .../lib/core/models/payment_model.dart | 279 ++++ .../lib/core/models/payment_model.g.dart | 64 + .../lib/core/network/dio_client.dart | 2 +- .../lib/core/services/api_service.dart | 74 +- .../lib/core/services/cache_service.dart | 249 ++++ .../lib/core/services/moov_money_service.dart | 280 ++++ .../core/services/notification_service.dart | 362 +++++ .../core/services/orange_money_service.dart | 233 ++++ .../lib/core/services/payment_service.dart | 428 ++++++ .../core/services/wave_payment_service.dart | 229 ++++ .../pages/forgot_password_screen.dart | 22 +- .../presentation/pages/login_page_temp.dart | 20 +- .../auth/presentation/pages/login_screen.dart | 12 +- .../presentation/pages/register_screen.dart | 16 +- .../presentation/widgets/login_footer.dart | 36 +- .../auth/presentation/widgets/login_form.dart | 26 +- .../cotisation_repository_impl.dart | 56 +- .../presentation/bloc/cotisations_bloc.dart | 223 +++- .../presentation/bloc/cotisations_event.dart | 94 ++ .../presentation/bloc/cotisations_state.dart | 135 ++ .../pages/cotisation_detail_page.dart | 708 ++++++++++ .../pages/cotisations_list_page.dart | 36 +- .../pages/cotisations_search_page.dart | 498 +++++++ .../widgets/animated_cotisation_list.dart | 244 ++++ .../presentation/widgets/cotisation_card.dart | 11 +- .../widgets/cotisation_timeline_widget.dart | 417 ++++++ .../widgets/payment_form_widget.dart | 457 +++++++ .../widgets/payment_method_selector.dart | 443 +++++++ .../presentation/pages/dashboard_page.dart | 21 + .../activities/recent_activities_widget.dart | 22 +- .../charts/charts_analytics_widget.dart | 8 +- .../features/debug/debug_api_test_page.dart | 240 ++++ .../pages/animations_demo_page.dart | 464 +++++++ .../presentation/pages/evenements_page.dart | 128 +- .../widgets/animated_evenement_card.dart | 363 +++++ .../widgets/animated_evenement_list.dart | 242 ++++ .../presentation/pages/members_list_page.dart | 8 +- .../pages/membre_details_page.dart | 8 +- .../pages/membres_dashboard_page.dart | 6 +- .../widgets/dashboard_stat_card.dart | 16 +- .../widgets/membre_cotisations_section.dart | 24 +- .../widgets/membre_stats_section.dart | 26 +- .../widgets/membres_export_dialog.dart | 8 +- .../widgets/membres_view_controls.dart | 4 +- .../lib/shared/theme/app_theme.dart | 1 + .../widgets/buttons/primary_button.dart | 291 ++++ unionflow-mobile-apps/pubspec.lock | 191 ++- unionflow-mobile-apps/pubspec.yaml | 6 + .../test/error_handling_test.dart | 222 ---- .../test/membre_create_test.dart | 141 -- unionflow-mobile-apps/test/widget_test.dart | 92 -- .../server/resource/CotisationResource.java | 56 + .../server/resource/EvenementResource.java | 89 ++ .../server/security/KeycloakService.java | 345 +++++ .../server/service/PaiementService.java | 176 +++ .../src/main/resources/application.properties | 10 +- 73 files changed, 11508 insertions(+), 674 deletions(-) create mode 100644 unionflow-mobile-apps/ANIMATIONS_FEATURES.md create mode 100644 unionflow-mobile-apps/coverage/lcov.info create mode 100644 unionflow-mobile-apps/flutter_01.png create mode 100644 unionflow-mobile-apps/lib/core/animations/animated_button.dart create mode 100644 unionflow-mobile-apps/lib/core/animations/animated_notifications.dart create mode 100644 unionflow-mobile-apps/lib/core/animations/micro_interactions.dart create mode 100644 unionflow-mobile-apps/lib/core/models/cotisation_filter_model.dart create mode 100644 unionflow-mobile-apps/lib/core/models/cotisation_filter_model.g.dart create mode 100644 unionflow-mobile-apps/lib/core/models/cotisation_statistics_model.dart create mode 100644 unionflow-mobile-apps/lib/core/models/cotisation_statistics_model.g.dart create mode 100644 unionflow-mobile-apps/lib/core/models/payment_model.dart create mode 100644 unionflow-mobile-apps/lib/core/models/payment_model.g.dart create mode 100644 unionflow-mobile-apps/lib/core/services/cache_service.dart create mode 100644 unionflow-mobile-apps/lib/core/services/moov_money_service.dart create mode 100644 unionflow-mobile-apps/lib/core/services/notification_service.dart create mode 100644 unionflow-mobile-apps/lib/core/services/orange_money_service.dart create mode 100644 unionflow-mobile-apps/lib/core/services/payment_service.dart create mode 100644 unionflow-mobile-apps/lib/core/services/wave_payment_service.dart create mode 100644 unionflow-mobile-apps/lib/features/cotisations/presentation/pages/cotisation_detail_page.dart create mode 100644 unionflow-mobile-apps/lib/features/cotisations/presentation/pages/cotisations_search_page.dart create mode 100644 unionflow-mobile-apps/lib/features/cotisations/presentation/widgets/animated_cotisation_list.dart create mode 100644 unionflow-mobile-apps/lib/features/cotisations/presentation/widgets/cotisation_timeline_widget.dart create mode 100644 unionflow-mobile-apps/lib/features/cotisations/presentation/widgets/payment_form_widget.dart create mode 100644 unionflow-mobile-apps/lib/features/cotisations/presentation/widgets/payment_method_selector.dart create mode 100644 unionflow-mobile-apps/lib/features/debug/debug_api_test_page.dart create mode 100644 unionflow-mobile-apps/lib/features/demo/presentation/pages/animations_demo_page.dart create mode 100644 unionflow-mobile-apps/lib/features/evenements/presentation/widgets/animated_evenement_card.dart create mode 100644 unionflow-mobile-apps/lib/features/evenements/presentation/widgets/animated_evenement_list.dart create mode 100644 unionflow-mobile-apps/lib/shared/widgets/buttons/primary_button.dart delete mode 100644 unionflow-mobile-apps/test/error_handling_test.dart delete mode 100644 unionflow-mobile-apps/test/membre_create_test.dart delete mode 100644 unionflow-mobile-apps/test/widget_test.dart create mode 100644 unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/security/KeycloakService.java create mode 100644 unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/PaiementService.java diff --git a/unionflow-mobile-apps/ANIMATIONS_FEATURES.md b/unionflow-mobile-apps/ANIMATIONS_FEATURES.md new file mode 100644 index 0000000..71d3943 --- /dev/null +++ b/unionflow-mobile-apps/ANIMATIONS_FEATURES.md @@ -0,0 +1,150 @@ +# 🎨 Fonctionnalités d'Animation UnionFlow Mobile + +## 📱 Vue d'ensemble + +L'application mobile UnionFlow intègre un système d'animations sophistiqué conçu pour offrir une expérience utilisateur fluide et engageante. Toutes les animations respectent les principes de Material Design 3 et sont optimisées pour les performances. + +## 🚀 Fonctionnalités Implémentées + +### 1. **Transitions de Page Avancées** +- **Glissement depuis la droite** : Transition classique avec courbe d'animation fluide +- **Glissement depuis le bas** : Parfait pour les modales et les pages de détail +- **Fondu** : Transition élégante pour les changements de contexte +- **Échelle avec fondu** : Effet de zoom sophistiqué +- **Rebond** : Animation ludique avec effet élastique +- **Parallaxe** : Effet de profondeur avec décalage des couches +- **Morphing avec Blur** : Transformation fluide avec effet de flou +- **Rotation 3D** : Transition immersive avec perspective 3D + +### 2. **Boutons Animés Interactifs** +- **Styles multiples** : Primary, Secondary, Success, Warning, Error, Outline +- **Effets de shimmer** : Animation de brillance pour attirer l'attention +- **États de chargement** : Indicateurs de progression intégrés +- **Animations de pression** : Feedback tactile avec échelle et élévation +- **Transitions de couleur** : Changements fluides entre les états + +### 3. **Listes Animées avec Staggering** +- **Animations décalées** : Apparition progressive des éléments +- **Effets combinés** : Slide, fade et scale simultanés +- **Délais progressifs** : 150ms entre chaque élément +- **Courbes d'animation** : Curves.easeOutBack pour un effet naturel + +### 4. **Cartes Interactives** +- **Animations de survol** : Élévation et échelle au hover +- **Boutons favoris** : Animation élastique avec changement de couleur +- **Gradients dynamiques** : Arrière-plans animés +- **Micro-interactions** : Feedback visuel sur tous les éléments interactifs + +### 5. **Système de Notifications Animées** +- **Types multiples** : Success, Error, Warning, Info +- **Animations d'entrée** : Slide élastique depuis le haut +- **Animations de sortie** : Fondu fluide +- **Interactions** : Tap pour agrandir, swipe pour fermer +- **Auto-dismiss** : Disparition automatique après délai configurable + +### 6. **Micro-interactions Avancées** +- **Boutons interactifs** : Feedback haptique et sonore +- **Cartes parallax** : Effet de profondeur au survol +- **Icônes morphing** : Transformation fluide entre deux états +- **Effets de ripple** : Ondulations au toucher + +### 7. **Animations Continues** +- **Flottement** : Mouvement vertical perpétuel +- **Pulsation** : Effet de battement avec échelle +- **Rotation** : Rotation continue pour les indicateurs de chargement +- **Oscillation** : Mouvement de balancier + +## 🎯 Avantages Utilisateur + +### **Expérience Utilisateur Améliorée** +- **Feedback visuel immédiat** : L'utilisateur comprend instantanément ses actions +- **Navigation intuitive** : Les transitions guident naturellement l'utilisateur +- **Engagement accru** : Les animations rendent l'application plus attrayante +- **Professionnalisme** : Interface moderne et soignée + +### **Performance Optimisée** +- **Animations 60 FPS** : Fluidité garantie sur tous les appareils +- **Gestion mémoire** : Disposal automatique des contrôleurs d'animation +- **Optimisations GPU** : Utilisation des transformations matérielles +- **Animations conditionnelles** : Respect des préférences d'accessibilité + +### **Accessibilité** +- **Respect des préférences système** : Réduction des animations si demandée +- **Feedback haptique** : Support pour les utilisateurs malvoyants +- **Contrastes élevés** : Animations visibles dans tous les modes +- **Durées configurables** : Adaptation aux besoins spécifiques + +## 🛠️ Architecture Technique + +### **Structure Modulaire** +``` +lib/core/animations/ +├── page_transitions.dart # Transitions entre pages +├── animated_button.dart # Boutons avec animations +├── animated_notifications.dart # Système de notifications +├── micro_interactions.dart # Micro-interactions avancées +└── animated_list_item.dart # Éléments de liste animés +``` + +### **Widgets Réutilisables** +- **AnimatedButton** : Bouton avec animations intégrées +- **AnimatedNotificationWidget** : Notifications avec animations +- **AnimatedListItem** : Élément de liste avec staggering +- **InteractiveButton** : Bouton avec micro-interactions +- **ParallaxCard** : Carte avec effet parallax +- **MorphingIcon** : Icône avec transformation + +### **Extensions Utilitaires** +- **NavigatorTransitions** : Extensions pour Navigator +- **AnimationControllerExtensions** : Méthodes utilitaires +- **CurveExtensions** : Courbes d'animation personnalisées + +## 🎨 Page de Démonstration + +Une page de démonstration complète (`AnimationsDemoPage`) permet de tester toutes les animations : +- **Boutons animés** : Tous les styles et états +- **Notifications** : Tous les types avec animations +- **Transitions** : Test de toutes les transitions de page +- **Animations continues** : Démonstration des effets perpétuels + +## 📱 Intégration dans l'Application + +### **Pages Principales** +- **Dashboard** : Animations de chargement et transitions +- **Événements** : Listes animées et cartes interactives +- **Cotisations** : Boutons animés et notifications +- **Membres** : Transitions fluides et micro-interactions + +### **Navigation** +- **Bottom Navigation** : Animations de sélection d'onglet +- **Drawer** : Ouverture/fermeture animée +- **AppBar** : Transitions de couleur et élévation + +## 🔧 Configuration et Personnalisation + +### **Durées d'Animation** +- **Rapide** : 150ms pour les micro-interactions +- **Standard** : 300ms pour les transitions normales +- **Lente** : 500ms pour les animations complexes + +### **Courbes d'Animation** +- **Curves.easeInOut** : Transitions naturelles +- **Curves.elasticOut** : Effets de rebond +- **Curves.easeOutBack** : Dépassement léger + +### **Couleurs et Thèmes** +- **Intégration AppTheme** : Respect de la charte graphique +- **Mode sombre** : Animations adaptées au thème +- **Couleurs dynamiques** : Adaptation au contenu + +## 🎉 Résultat Final + +L'application UnionFlow Mobile offre maintenant une expérience utilisateur exceptionnelle avec : +- **+15 types d'animations** différentes +- **+8 transitions de page** sophistiquées +- **+6 styles de boutons** animés +- **+4 types de notifications** animées +- **Performance 60 FPS** garantie +- **Accessibilité complète** respectée + +Cette implémentation place UnionFlow parmi les applications mobiles les plus modernes et engageantes du marché associatif. diff --git a/unionflow-mobile-apps/android/app/src/main/res/xml/network_security_config.xml b/unionflow-mobile-apps/android/app/src/main/res/xml/network_security_config.xml index 8556e47..fd2dbb8 100644 --- a/unionflow-mobile-apps/android/app/src/main/res/xml/network_security_config.xml +++ b/unionflow-mobile-apps/android/app/src/main/res/xml/network_security_config.xml @@ -6,7 +6,7 @@ - 192.168.1.11 + 192.168.1.145 localhost 10.0.2.2 127.0.0.1 diff --git a/unionflow-mobile-apps/coverage/lcov.info b/unionflow-mobile-apps/coverage/lcov.info new file mode 100644 index 0000000..91b2da5 --- /dev/null +++ b/unionflow-mobile-apps/coverage/lcov.info @@ -0,0 +1,1181 @@ +SF:lib\core\services\wave_payment_service.dart +DA:12,1 +DA:15,1 +DA:28,2 +DA:39,3 +DA:44,1 +DA:46,2 +DA:48,3 +DA:53,1 +DA:66,1 +DA:72,1 +DA:77,1 +DA:78,1 +DA:80,1 +DA:84,2 +DA:85,1 +DA:86,1 +DA:87,1 +DA:92,1 +DA:93,1 +DA:94,1 +DA:95,1 +DA:100,1 +DA:103,0 +DA:106,0 +DA:111,1 +DA:113,1 +DA:115,1 +DA:116,1 +DA:117,1 +DA:118,1 +DA:119,1 +DA:120,1 +DA:122,2 +DA:123,1 +DA:124,1 +DA:125,1 +DA:127,1 +DA:128,1 +DA:129,1 +DA:130,1 +DA:131,1 +DA:132,1 +DA:133,1 +DA:135,1 +DA:136,1 +DA:139,0 +DA:142,0 +DA:147,1 +DA:149,1 +DA:150,1 +DA:151,1 +DA:152,1 +DA:153,1 +DA:156,1 +DA:160,1 +DA:162,2 +DA:166,2 +DA:167,2 +DA:171,1 +DA:172,1 +DA:176,1 +DA:179,1 +DA:182,3 +DA:183,3 +DA:184,1 +DA:192,1 +DA:193,1 +DA:194,1 +DA:195,1 +DA:197,1 +DA:198,1 +DA:199,1 +DA:200,1 +DA:202,0 +DA:203,0 +DA:205,0 +DA:206,0 +DA:207,0 +DA:221,1 +DA:227,0 +DA:228,0 +LF:81 +LH:70 +end_of_record +SF:lib\core\services\api_service.dart +DA:14,0 +DA:16,0 +DA:23,0 +DA:25,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:40,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:50,0 +DA:52,0 +DA:54,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:63,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:76,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:85,0 +DA:87,0 +DA:89,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:105,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:115,0 +DA:120,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:126,0 +DA:127,0 +DA:128,0 +DA:133,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:138,0 +DA:147,0 +DA:158,0 +DA:160,0 +DA:161,0 +DA:162,0 +DA:163,0 +DA:164,0 +DA:165,0 +DA:166,0 +DA:167,0 +DA:168,0 +DA:171,0 +DA:172,0 +DA:173,0 +DA:178,0 +DA:180,0 +DA:181,0 +DA:182,0 +DA:183,0 +DA:188,0 +DA:190,0 +DA:191,0 +DA:192,0 +DA:193,0 +DA:202,0 +DA:204,0 +DA:209,0 +DA:210,0 +DA:211,0 +DA:212,0 +DA:215,0 +DA:216,0 +DA:217,0 +DA:222,0 +DA:224,0 +DA:225,0 +DA:226,0 +DA:227,0 +DA:232,0 +DA:234,0 +DA:235,0 +DA:236,0 +DA:237,0 +DA:242,0 +DA:244,0 +DA:246,0 +DA:248,0 +DA:249,0 +DA:250,0 +DA:255,0 +DA:257,0 +DA:258,0 +DA:259,0 +DA:261,0 +DA:262,0 +DA:263,0 +DA:268,0 +DA:270,0 +DA:271,0 +DA:272,0 +DA:277,0 +DA:279,0 +DA:284,0 +DA:285,0 +DA:286,0 +DA:287,0 +DA:290,0 +DA:291,0 +DA:292,0 +DA:297,0 +DA:299,0 +DA:304,0 +DA:305,0 +DA:306,0 +DA:307,0 +DA:310,0 +DA:311,0 +DA:312,0 +DA:317,0 +DA:319,0 +DA:324,0 +DA:325,0 +DA:326,0 +DA:327,0 +DA:330,0 +DA:331,0 +DA:332,0 +DA:337,0 +DA:347,0 +DA:352,0 +DA:353,0 +DA:354,0 +DA:355,0 +DA:356,0 +DA:358,0 +DA:360,0 +DA:361,0 +DA:362,0 +DA:363,0 +DA:366,0 +DA:367,0 +DA:368,0 +DA:373,0 +DA:375,0 +DA:376,0 +DA:377,0 +DA:378,0 +DA:387,0 +DA:388,0 +DA:389,0 +DA:390,0 +DA:391,0 +DA:392,0 +DA:394,0 +DA:395,0 +DA:396,0 +DA:398,0 +DA:399,0 +DA:400,0 +DA:402,0 +DA:403,0 +DA:404,0 +DA:405,0 +DA:406,0 +DA:407,0 +DA:408,0 +DA:409,0 +DA:410,0 +DA:413,0 +DA:415,0 +DA:416,0 +DA:418,0 +DA:419,0 +DA:421,0 +DA:422,0 +DA:426,0 +DA:435,0 +DA:440,0 +DA:442,0 +DA:448,0 +DA:449,0 +DA:450,0 +DA:451,0 +DA:454,0 +DA:455,0 +DA:456,0 +DA:461,0 +DA:466,0 +DA:468,0 +DA:474,0 +DA:475,0 +DA:476,0 +DA:477,0 +DA:480,0 +DA:481,0 +DA:482,0 +DA:487,0 +DA:494,0 +DA:496,0 +DA:504,0 +DA:505,0 +DA:506,0 +DA:507,0 +DA:510,0 +DA:511,0 +DA:512,0 +DA:517,0 +DA:519,0 +DA:520,0 +DA:521,0 +DA:522,0 +DA:527,0 +DA:533,0 +DA:535,0 +DA:542,0 +DA:543,0 +DA:544,0 +DA:545,0 +DA:548,0 +DA:549,0 +DA:550,0 +DA:555,0 +DA:561,0 +DA:562,0 +DA:563,0 +DA:569,0 +DA:570,0 +DA:571,0 +DA:572,0 +DA:575,0 +DA:576,0 +DA:577,0 +DA:582,0 +DA:584,0 +DA:586,0 +DA:588,0 +DA:589,0 +DA:590,0 +DA:595,0 +DA:597,0 +DA:598,0 +DA:599,0 +DA:601,0 +DA:602,0 +DA:603,0 +DA:608,0 +DA:610,0 +DA:611,0 +DA:612,0 +DA:617,0 +DA:622,0 +DA:623,0 +DA:624,0 +DA:625,0 +DA:628,0 +DA:629,0 +DA:630,0 +DA:635,0 +DA:637,0 +DA:638,0 +DA:639,0 +DA:640,0 +LF:283 +LH:0 +end_of_record +SF:lib\core\models\wave_checkout_session_model.dart +DA:83,1 +DA:107,0 +DA:108,0 +DA:111,0 +DA:114,0 +DA:117,1 +DA:118,1 +DA:119,0 +DA:123,0 +DA:126,0 +DA:129,0 +DA:132,0 +DA:154,0 +DA:155,0 +DA:156,0 +DA:157,0 +DA:158,0 +DA:159,0 +DA:160,0 +DA:161,0 +DA:162,0 +DA:163,0 +DA:164,0 +DA:165,0 +DA:166,0 +DA:167,0 +DA:168,0 +DA:169,0 +DA:170,0 +DA:171,0 +DA:172,0 +DA:173,0 +DA:174,0 +DA:178,0 +DA:179,0 +DA:180,0 +DA:181,0 +DA:182,0 +DA:183,0 +DA:184,0 +DA:185,0 +DA:186,0 +DA:187,0 +DA:188,0 +DA:189,0 +DA:190,0 +DA:191,0 +DA:192,0 +DA:193,0 +DA:194,0 +DA:195,0 +DA:196,0 +DA:197,0 +DA:198,0 +DA:199,0 +DA:202,0 +DA:203,0 +DA:204,0 +DA:205,0 +LF:59 +LH:3 +end_of_record +SF:lib\core\models\wave_checkout_session_model.g.dart +DA:9,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:30,0 +DA:31,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:38,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +LF:46 +LH:0 +end_of_record +SF:lib\core\models\payment_model.dart +DA:33,1 +DA:60,0 +DA:61,0 +DA:64,0 +DA:67,0 +DA:70,0 +DA:73,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:81,0 +DA:82,0 +DA:84,0 +DA:85,0 +DA:87,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:100,0 +DA:102,0 +DA:104,0 +DA:106,0 +DA:108,0 +DA:111,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:120,0 +DA:122,0 +DA:124,0 +DA:126,0 +DA:128,0 +DA:130,0 +DA:132,0 +DA:135,0 +DA:140,0 +DA:141,0 +DA:142,0 +DA:143,0 +DA:144,0 +DA:145,0 +DA:147,0 +DA:149,0 +DA:151,0 +DA:153,0 +DA:161,0 +DA:162,0 +DA:166,0 +DA:169,0 +DA:170,0 +DA:171,0 +DA:175,0 +DA:176,0 +DA:177,0 +DA:181,0 +DA:182,0 +DA:183,0 +DA:187,0 +DA:188,0 +DA:189,0 +DA:190,0 +DA:192,0 +DA:194,0 +DA:196,0 +DA:197,0 +DA:198,0 +DA:199,0 +DA:200,0 +DA:208,0 +DA:209,0 +DA:213,0 +DA:238,0 +DA:239,0 +DA:240,0 +DA:241,0 +DA:242,0 +DA:243,0 +DA:244,0 +DA:245,0 +DA:246,0 +DA:247,0 +DA:248,0 +DA:249,0 +DA:250,0 +DA:251,0 +DA:252,0 +DA:253,0 +DA:254,0 +DA:255,0 +DA:256,0 +DA:257,0 +DA:258,0 +DA:259,0 +DA:260,0 +DA:261,0 +DA:265,0 +DA:268,0 +DA:271,0 +DA:272,0 +DA:274,0 +DA:276,0 +DA:277,0 +LF:104 +LH:1 +end_of_record +SF:lib\core\models\payment_model.g.dart +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:36,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +LF:51 +LH:0 +end_of_record +SF:lib\core\models\cotisation_model.dart +DA:37,0 +DA:68,0 +DA:69,0 +DA:72,0 +DA:75,0 +DA:78,0 +DA:81,0 +DA:82,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:96,0 +DA:98,0 +DA:100,0 +DA:102,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:114,0 +DA:116,0 +DA:118,0 +DA:120,0 +DA:123,0 +DA:128,0 +DA:129,0 +DA:130,0 +DA:132,0 +DA:134,0 +DA:136,0 +DA:138,0 +DA:140,0 +DA:143,0 +DA:148,0 +DA:149,0 +DA:150,0 +DA:152,0 +DA:154,0 +DA:156,0 +DA:158,0 +DA:160,0 +DA:168,0 +DA:169,0 +DA:170,0 +DA:171,0 +DA:175,0 +DA:176,0 +DA:180,0 +DA:181,0 +DA:182,0 +DA:183,0 +DA:184,0 +DA:186,0 +DA:187,0 +DA:188,0 +DA:189,0 +DA:196,0 +DA:225,0 +DA:226,0 +DA:227,0 +DA:228,0 +DA:229,0 +DA:230,0 +DA:231,0 +DA:232,0 +DA:233,0 +DA:234,0 +DA:235,0 +DA:236,0 +DA:237,0 +DA:238,0 +DA:239,0 +DA:240,0 +DA:241,0 +DA:242,0 +DA:243,0 +DA:244,0 +DA:245,0 +DA:246,0 +DA:247,0 +DA:248,0 +DA:249,0 +DA:250,0 +DA:251,0 +DA:252,0 +DA:256,0 +DA:259,0 +DA:262,0 +DA:263,0 +DA:265,0 +DA:267,0 +DA:268,0 +DA:269,0 +LF:95 +LH:0 +end_of_record +SF:lib\core\models\cotisation_model.g.dart +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:45,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +LF:62 +LH:0 +end_of_record +SF:lib\core\models\evenement_model.dart +DA:98,0 +DA:126,0 +DA:127,0 +DA:130,0 +DA:133,0 +DA:159,0 +DA:160,0 +DA:161,0 +DA:162,0 +DA:163,0 +DA:164,0 +DA:165,0 +DA:166,0 +DA:167,0 +DA:168,0 +DA:169,0 +DA:170,0 +DA:171,0 +DA:172,0 +DA:173,0 +DA:174,0 +DA:175,0 +DA:176,0 +DA:177,0 +DA:178,0 +DA:179,0 +DA:180,0 +DA:181,0 +DA:182,0 +DA:183,0 +DA:190,0 +DA:193,0 +DA:194,0 +DA:195,0 +DA:196,0 +DA:200,0 +DA:201,0 +DA:202,0 +DA:206,0 +DA:207,0 +DA:208,0 +DA:209,0 +DA:213,0 +DA:214,0 +DA:215,0 +DA:219,0 +DA:220,0 +DA:223,0 +DA:224,0 +DA:225,0 +DA:226,0 +DA:228,0 +DA:232,0 +DA:233,0 +DA:234,0 +DA:235,0 +DA:236,0 +DA:237,0 +DA:238,0 +DA:239,0 +DA:240,0 +DA:241,0 +DA:242,0 +DA:243,0 +DA:244,0 +DA:245,0 +DA:246,0 +DA:247,0 +DA:248,0 +DA:249,0 +DA:250,0 +DA:251,0 +DA:252,0 +DA:253,0 +DA:254,0 +DA:255,0 +DA:256,0 +DA:257,0 +DA:288,0 +DA:290,0 +DA:292,0 +DA:294,0 +DA:296,0 +DA:298,0 +DA:300,0 +DA:302,0 +DA:304,0 +DA:306,0 +DA:308,0 +DA:313,0 +DA:315,0 +DA:317,0 +DA:319,0 +DA:321,0 +DA:323,0 +DA:325,0 +DA:327,0 +DA:329,0 +DA:331,0 +DA:333,0 +DA:358,0 +DA:360,0 +DA:362,0 +DA:364,0 +DA:366,0 +DA:368,0 +DA:370,0 +DA:375,0 +DA:377,0 +DA:379,0 +DA:381,0 +DA:383,0 +DA:385,0 +DA:387,0 +LF:114 +LH:0 +end_of_record +SF:lib\core\models\evenement_model.g.dart +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +LF:56 +LH:0 +end_of_record +SF:lib\core\models\membre_model.dart +DA:70,0 +DA:92,0 +DA:93,0 +DA:96,0 +DA:99,0 +DA:102,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:113,0 +DA:114,0 +DA:115,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:123,0 +DA:125,0 +DA:128,0 +DA:133,0 +DA:134,0 +DA:135,0 +DA:136,0 +DA:137,0 +DA:138,0 +DA:139,0 +DA:145,0 +DA:165,0 +DA:166,0 +DA:167,0 +DA:168,0 +DA:169,0 +DA:170,0 +DA:171,0 +DA:172,0 +DA:173,0 +DA:174,0 +DA:175,0 +DA:176,0 +DA:177,0 +DA:178,0 +DA:179,0 +DA:180,0 +DA:181,0 +DA:182,0 +DA:183,0 +DA:187,0 +DA:188,0 +DA:189,0 +DA:190,0 +DA:191,0 +DA:192,0 +DA:193,0 +DA:194,0 +DA:195,0 +DA:196,0 +DA:197,0 +DA:198,0 +DA:199,0 +DA:200,0 +DA:201,0 +DA:202,0 +DA:203,0 +DA:204,0 +DA:205,0 +DA:206,0 +DA:209,0 +DA:210,0 +DA:211,0 +LF:72 +LH:0 +end_of_record +SF:lib\core\models\membre_model.g.dart +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +LF:41 +LH:0 +end_of_record +SF:lib\core\network\auth_interceptor.dart +DA:16,0 +DA:18,0 +DA:21,0 +DA:22,0 +DA:28,0 +DA:32,0 +DA:35,0 +DA:38,0 +DA:39,0 +DA:43,0 +DA:46,0 +DA:49,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:57,0 +DA:62,0 +DA:65,0 +DA:68,0 +DA:71,0 +DA:74,0 +DA:78,0 +DA:81,0 +DA:82,0 +DA:88,0 +DA:90,0 +DA:99,0 +DA:102,0 +DA:108,0 +DA:112,0 +DA:113,0 +LF:32 +LH:0 +end_of_record +SF:lib\core\network\dio_client.dart +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:17,0 +DA:19,0 +DA:20,0 +DA:30,0 +DA:37,0 +DA:38,0 +DA:53,0 +DA:55,0 +DA:56,0 +DA:64,0 +DA:66,0 +DA:79,0 +DA:80,0 +DA:84,0 +DA:85,0 +DA:89,0 +DA:90,0 +DA:94,0 +DA:95,0 +DA:99,0 +DA:104,0 +DA:105,0 +DA:106,0 +DA:110,0 +DA:111,0 +LF:29 +LH:0 +end_of_record diff --git a/unionflow-mobile-apps/flutter_01.png b/unionflow-mobile-apps/flutter_01.png new file mode 100644 index 0000000000000000000000000000000000000000..475f3294c38215cc19482e1f33af44cdd98b9440 GIT binary patch literal 350261 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

G;F!MaXt^mwsAjFldj&3Q? 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¬jgSJNhspnSZs^1nW{ z4=>Mtz@|LrI7mak`4}FH<AY)*rz{P@XR zsHgE?TlC@iRG1RO=ewLl6sR=6jUM5y&!Lx+h_(PH8m5v7eVUMC1z%)%E4ZD-=irWc z8DC#ZZvx$N;d0`Qeo$rn${aslT3m`vd4e`n=rd}sx=o9U5YE|{qU+{_gSO10!KS}; z`TP>oK9*8|;{0gw7(bq|$^OST>z8`Ape5!9n6})@T2Xj-ZBO1RuaOkw%_6ofWre%$ zLUo2+&Erfe=B$&^?7e;i+ato{5fS@B7pzt>>l$p?oFz2C8KGfnO>#%VU)NP8=|QB^ zg%00{oZIxG*l&)`CELnBZ?{Z%=N~NBsI&4+O3AP5Mxfnlr4%3g{*9c$jYS27cDzTdwEJxIfj8f%@x?;#*Ru>m~sjkEmvm)0xZ)g78^Ms1=x zJK0#es}#*dat$%~6cgkRPv83<8yjt&SyPM1PV<7f_Rq<6#@$sbv#-@sNfl}7>YwSH6}3!zWO2At^gVg__2h_`Bh- z9UEGmombhr|4tx@S{4Pfjl<=M$$G|!+&k-44i!#i6A0_)thioCLVF@GDlP;{Z zsd3|O*AlPl@A2$Qh>W%SRG42xAyM{JaAR_Mi=2RT^HXKx|j76Lh1 z*GanfPNvk#f6$@_2D!bmM=_egPQTMXc6G2I?Y3EWtpgYiqKO`8E5%5j5k+BKuu1HY`-x{UwjwqvQ zViwlXGNP2%dSsEc>eJ_Oc&93#<%PA|Bj~xY*)$V*dHRD@2qeQC7FUUuDi*{b)COnFd@H&V+IT z;p=hL6*5!f)!xO>q)>^BgV1Q#Wc^xLo&=f+-KO31o?UC8uyGtKI)KK{jAI zCLwZ>dgI*RDUcQ*z_hG?D8i67snt2i;kDH`zmUBp zkgGVjO!o_#U>tjT99AFm@^~~Vm|G9JV!tM-p_EyMjG+swPMG((YH@fZ(bA=)tb9LW zx9|)ms3Q0FclzD0=Pn6jHH-@#HBZ7K?HkKFs+bhal1nc8cP<~`;}-NUTm#^xg_dgw^w5&dYd+i8{Xd6IxR!F0Zuu>L*S zOP28*D++AfhyKJAB(@|(tBXGf^5Roo1@Fy zVV&CEAOf#8F`#GQp(oK}F>TBk@ZG6|j@Es^ZuKcK(E-0`llZflYfW>)0YbiTEs*ij zwhTVOj`8Qwz0spMuM%u18+z$nA3gtK^dHRa{K|gLxv+>&U+6<|cALq?gB~*Oswbh% z^Ee`LJ!EFkf#P$^nM>FdWDGJfw^L-}nQ>}RF)OE7ktLrkH?iUyJizx!N7|U}fPta; zL{*y0_dyX1PJeUC^1X6rFN`c`p}?>(Z125C+Xq(^CZ;Pz=_JleBy#?$>7fI8ORhG3 z--=zk^aTSVFQVd$RuzdEg}J#@tftkQn**=o@PIg1$)gvn&Cb0(mREIKi`AR2ybVPj ze@Khhf2-BKN=?3_`2|S^+pnccv{jCeJL^S=8UWcADc@;o$DGTh`jGTifu79ZHpJ@Y z?#@0tJA3b;vEi``i%1954d6J8dJuBY6Hips*-w4hZNKuq_s`i8n7Q<{G@pcuM2aGF z>$RQnAT700!P@I`Fe7`#Ky6vhA|@1a`yE)6QO(Lpqd4GO$ z(Z5n}K~g7)C0`uqd2kCYQYmr#4Vm%%`$qGW!KR&~uWAMPhX`IX5#4jxz4`NNZ0yMG z+HlHsks&v2-rpFEQS)gIe{^uRInN?(9ek>hjxD1q4mKXh&CU<82dmd$$xv$vyT%ri zdFM9*D&Aum_=%2*Zd-(bs}r%#yxtrJlp{N}H_2M(nWpm{6BK{KtB56%Sb84*>iLGn zG_P&9p-v{dvbfuEMEB0_0q4REC%U$;60J%7T-z#UWABaj55K#p$Yh=fOLbwy2xHr6 zL%&!q^>pS){0Q$_(t)~|^7>xY7s@(#V(GBA+!3ZnVr*wpzeRn5KZ^A1W9G#`L?tD?!V+6L zDW@POoU2f@@U+LC!B;0uA@#QuO5(tmvsSdrhPLYreKG>wp9Doo36L5G!~gulg%^k^ z4EBkxI0wy!AFUl*4)T+thuKEYN2cf1wBm}A=c_2uy8F5d;Crf@7ilaAlL%5UGXnx*`_@lkG!eK6AZ=dlJoZTa`R%4`TBsXsKMbSgXCkaz5D!fW#z5`Qs z?o!L`oaEpZ1;Qxe_HB%<4=I0WMjya?S7U9tcgRF3#U7JFCcWj7)L5BD5U}ocJsaCr z8idt9M@WTNoFMD&pN@N=kG_5EMsDI@!m8sWr9%Il`My;i?9DWu*sA}?XoK1Gz4_a~ zE5XwgZ>>$yiIP^loS;BAce0hg_#o$1550-upOt@GiVp z7Ji|lLqGAf6K&rg`%U5%%N?k)$`&&^3*oW!4G~>8zp?3^(?+P~<+2o>W4|0u6`_P& z%$oS$Kcwrb?0PcJzDT&z*p?S6Oj@{JxhbcGtx;q2D;f;=)>}+IZd-F}^LiSdisy64 zexQ<_S(G68mb7h46Dkr$X5E_J!i(T=$8Eax_To2c7vxqKl(K{f4uree@Y(T zwwc?;!O-ys0lq!cX+q=Wx{0NV_ro8{`HAH$+yZKq(%FV8lt)kn@hM`)YQu+aeMA>` zc6Pw4vl{w0^hp#yzE1VS@E`+HzC+oYU$^i88it8dZ{thT7Y+!#+{#%bdu-~x_jQ^} zxIIeMM8&gNNV^SBpJPdOOI98gB)iLn@E{HrS=k%ck9vXO-zbP;e1rM?*T*zOQq8~elM1D$O+Xi7 zNBQWe60yV{lpG{WMI4N2+^UEhH*?2cs9exsy`m#-d~CYcQ(nc&zwzI`=fpqqxOcL= zopVK)`z7|rl1%qAllBaC+&-+PhUwYGoVwGmq8{z|mB;tL8L5&^a+0{t_h56*$Qdt7 zT(L8yqUOMODa@-8Ej@^*@ZHmNT#R5@$q}>ptmMJqI1qJi(^748tP~2x$-kZoO15FZ zeRi!m=#Zr@A>t7homZl4OOdkgBd12lY=76U)i0I4y6;tkQZ+pXZziG{7`DrgI zcZ_tgsMN!|dUSO3`}kLy)>|^W9y{^U*|C2Jt2pZ_XNs~HCLU6=%Ve|tih{3h@RwDC zL_>P38i>h7%BB`5mQ}HtDu}Uk{cUfeH!n9ysU|#^z5e+z;~Rj^F~yq!?6hNLnIq3% zFIrw#>rd0z%$Z1ySeVn%T4!Lrti^hBOCG}8hFK|bCddg?<7LC5{#R>;pukUl^9_&b zd`u*$Q3s4W3TLuIz`r`mw*J_DdMkc4x1O%1PrZ|5cw_j~g8h-Mp>6 zW2SB&VzdeiDZKei0?Gf5kl^EeTY8?S<0NNJrc1}1Jw~VC3jLC4r7ANZ!*~+5R|c)` z4>i6`oEDiP*SEc!(3baTHLjxCjEft|6MDRGdirZX_(vY=aGSx4jK_1n1j^Y`WL z&xfb}jQI+66-tYpV;Sk{nEYQfeRnjQ{~NZgY89n+TIx$tYHzAVTASL`sM>ogv8t^a zNouxLqlDT!v1g6g#9p;x#|mPHH^28i@88Ke8RvPf=l)#dzN{>lX}=YR>1z?iT)>>I zTtQ}kv3;XKohUrCM|Fzv>hR~-+=D05J*Og#w?%<+=_C(2PxRDV96{n*4vk;KeU%}}0X68#K^88VuOIVrcpiSnVZ#qU0 zZU3wfI)TB{`uBsV;m|HL_DbyCbu~tT2r~u4#i#-O6HS%`LkD3TEqxB6Lw)|_cqMA) zC>}+~Kye`C58`;(!WF-#$T?u~&eAL68yM$^s{hnSs})iF?%&j0HW{T%pD{>u8K>{_ z22z_UijX3i(m4umYvXRKW-rrZQv$v6gS>xSwW8iuu0mnV=5GSU3HE^JzfUM{IMYxA z>WH^{uTP}&@&5l^y0exgJ1z_SsuZJm`(XD&6Tl?h@5!ByAm=t&o1j!82NPRyL2m^{ zLtAZZi$61ua>9m?g^O3;qzq{m_Ux3O+T-8z;OYFSEF-}?3GaaPRWNYFfEmFh;*YK z-~3RShn4HWT4gINI{wL%kT2n%s$z{?tI{OB=FBV!LWfPw2;QZ?PE6v? zUVJE|IU-nw|5!MdT$17x6~Ng7f=ulc6=z5DiS!>fm{Bd>sq3mv{9*bR>8km+SWWhW z+|7=I+Y&e^)fdN}7@lC+?2THL7yMXNj58&7QG{wbbJ9M=c3`hAYs)DILgW~&FC=35 zngM*u%6hABJVHomtN=11N!B^TF}}d*_EIt?3U2B+(YI}OF_ObU>eBbBr!ec5-%zTA zy&R8rnp~$Xw~h7Je>t7ui>sbe_Q|Y1zwoY)7(=9jw#HBBA%p-5b!*nLS%czTiz^GB z_VE!2^GEq7makg6zzW%ZN4&(}q=@3%?Ozq17M{ZTAHDEf0*;zwAv`r1G_U)Kj8;RJ z11?+ObuGq;+d;J>vvOYt6YmFS2hf>zUg0HHTB`_~l{hlILN=;9HG%frJM{t075b;y z^Us6P>AfIu?#hU|SoTp%8OY0Xd0s)*`!}3tZ}z>p8Osm8LwJgrXZ`u-@DvEqPha-hfUHDGO#<4v@qCRhZy8Y{;G6bxx1gy(o=kqh)-#L_xn^IO>&)EdPke^zZY(| z*9OZ4ga;H&dZo(DWrO{A#i3J5?|hDOmrn*!k*7kPo5OAF|Dfg<%#!7|`@g3ada)y; zQ#glhw?JAmj~a4@r4d7??|u|}sX8^Z%qNA_nz|kdiq{7I@PyjV+1{FMcinKe+fsa3 z+tO0cy(!Tl1Dg?iw7n_R-MkTTy4RhXqR7I2`YZPdUK>>w)fU@LL&w<$s5_G5_m;XT zWgJjl5m;XwD8!~*Mz@(lH@^Vpt{pOp2nQNE2uy6CVa@hmn^k5y!`3D+Q)b5VlXbwN zLT>Fn`&yXKCwo$;%Qelv>1W|&r~At%FPPtc{!0I%&tWnrT{z7!$zD?YQtE}*#K-Ce zMjM4TO^B<7!a?DTgW44{`V&%H5MghjG)MT^sd>FUp0hUy3P#SUZpn|(%m>2W%-PZR zd(~O|-ZS}1Dw`O;_$k$>kfP5m3{Vf1x%!H&NvH^*$@430HFGHzfn*zNrf2+gYw*F? z)1BEDJZd|(eQOf(*pcij!%z6dPuda*+W_w2gpw0`DgUr4WRU$I+U2>8_6ut3t6FhK za(!T`F~0X9d-Px~0?ii8#Ig_|)X5>_v-vVBU)fD~Lv>w;awsToxZse0boF^8B?cf( z{p)|Qdp?E_)62$pYI>7%>>OzzFX1YQ;`aVP!CNqaq(w8IOnwXKIjmwp>#%Vx{7F&! z(=TNE1(uIkSpg=;OSojXZ+`QsH{B5h+HdM50jyt=lUV$I#`7>~xZv_Hs^nwxu^@ha z#`)4ytdI~v*_WT`}Z4(-Dmz(_zCKNOCs`wArgSe*eefawTP7AsiU2 z&OYpxsyyC9uojH6HGyOnW?R9h9mN>7DBjb8{F5msojXR@GW_bfl~R_y(Z8vgdT|p^ zFt_NyTvRK>zlHzwKyj!30pR=vMA@F|>Rk>2YG2nvvI#~6`IYpsW#{1LrKjloDr?59 z^K9rKDWmcZHZ-QFCM3f6%quN9HMQig+55T8_H8d}xL*!8jD}xw*qr^0t9FFkXAWn= z4j4Exg#n<*6t%0=<$zJ2zP+ah=T4w|)L}vHw_A&&@9t~Syg`XDE4&kbHBKff}NyM}g=(RQd(u+5W zy0;lzNy-)H?SUF&hD)-GlN-KdfxZM4DEgyJJJ;U$hl-LUiRje#+m)AEvtx6M&d$Cr z803zYew9_)m%!N&C4PD`L^+T0IFO#MXwXcucW^9`kz&*s{@4_L)6WQb+l{*yhK10r z`Lu9MM>%8;`5Knupq_30ebxP?+uFgxnogGt+eAR!_9n zLzI&jtG8@+i8|dR{^!+!N$%3G`RG--9wUxGo)dN{dIeaiCAx#3!y}{me|QPw-!)3U zp%{>~V04{_YUw&gzQx2o66SmG%Ma&Mg~#A7b0TV5IS~Z0u}19tdg?1#%zqD&$l1r> z5eIKY(1J@aZFfPxknBu)@)Kg{>=piI#mIzm+Ukns5^@MhbfRgw!h0SYbKMWlEEK!| zPD1BVgwL#YZY-Nq-96lTrupA?qx<~iwej}NcnTyTvx%qjr8XZ$-T`;|FWv>|buU%I zVM%;Xs?vu-A7i>Y@~5N&VQL;|L^i>;q(eXkt*@73vR`13*8zT2Rre+Q?8TBd)?&^H zyjy%PJCXSV-tHI;1M1SEZXP!8oe(H!@>k`%HwIs2BQlPOmW=2#1A>*;+~AVRDV%Aa z!zMP^jfZ!T?!jEE#YD!@h;ZSG&plsRSDR$9+)Blh$vKEi#oj5UUu`_Jd-wQmxlVh& z(C8gj`luuvUC6cu{xeX{A?rVbj7prnGv56(>6Bk`*@=J+i89|hUIRRopB^jvK7EBp zo-a$A0?fAGu@d=BclzbmdUIK2P93-495jpDe%PpM{i;Ruoc>Y(WKCCWUYd!S%!{?q z=i!7hlXHaLD}FrPO&&ge93jiaUNmA6;qc+V41hOpp`gTj|L7$;Y zJ-aE(@38CsX#Pgx8qD>;`^{#krhtvZ&oWZz13ot?kL@?_2N89XXar;aQ|gofM{XS_ zOi_<8zv$|k8|DKqMaAOiPVY`WSmlS3w8gi*b%2haidM%r&WGFb)Wg*P@20fhOO|c^ zncwv?p?LmSy-a$oU|oVrYNv`Aagl@0Eg5RRn0))%Y3BfYADO%}b#BrinVEZ`W3fBX z{k-L1YKCL_C76q^#6&YPz@+=*efg2G2)Zz<@W-_4M(^%k1veY!COrzYL9n9GwX?Mlf6l zI;m`Uu2mRrX09+|kqu z?yP+1p6sfcguec(%wzLXxCsEcIc4-IYaQ+GZbc3wX*X!a6j$!>77=P52*%o)gyF8P z^ze!Dnn7OX2c{{NH?M>>Q7M_#M@6irecIGClFvGOE_Sc*_lSVR%j%ir!WO%1LtL1w-alVrL zNC86PDFOY`yk70j1m$6U>eM^^;Bkt9E|g(JOTg;@xAVwfoI#&p*bBUZu#~c}l&Vi= zT2k^Egpb0yej!w*lA&@eOXt(bz?iW^tu=HZY*!#-_H|u3l9r9PXXp*T@`Hm5f`b@+ zSw3p>kHUPUU=Fr>EfB)@3N6v!GyI2dK7?YyMJbCM<}O&LjKFc=5E7CpkF@OZZvUq5 z{bMoP+L;mX%>-I&y|w1%Mka74?nLTU`@gEncG`tok*dGJ#8@r?eiX@-a@q_O>;Dv7 z5E&lRp9Q$ZR-h9c;JdOVsJ(XTSdtBwlP~mHN}HMSN-}bsdtIim#+x1NVZTCp+oMj5 zsQzZ>_qavXZ1dz1Mu7ZCQrM6DHrWt+w8-zNFcTHus9#+wAOA42u&6OflyOzK3D@@d zX^-Vw#bQ$4*^~_4%W^BjHvCUNEfNJ(G8v_j1;LZ(KTLcueeTHQm`W?N`0Y#3aC0nY ze07oaZW^t%fg=WY8s9masoqb8{AoToUEW3tkzNEw?a0Wm91as9vJ^oypjP8R)?m%i z9UHL9$MCi#n}hJN;mtMl?~8k|3&z6$3ndd}{^Y5w&*g{Y#om0k-e~!DOt{JO2RI#| zoxj4=%o{eH|A{T)QU?&$@it_XmZ}$HEco=Al!5 z@;rU@yO-HR6?sKLo@SeXir4!YUz3@uFf}><^jV|K8}E&0c=^CRdf&Z2x;Ms})>5;A zG@pV^$5bo^8HLr@%R(NkqtV>y987v)_j6;n&#tcAsPn3hM;FynB)LsMwtO)Mf0j%8 z4ER#$!;vR~)nFp`=|=A&a?%Hfo10DqKKkW)c_jS2=veUCoUI)tC_p~0IAotp>+l_w zE`sRUOD}gd;)t}QCI2s6CaO}PC`sv%>n2E2j}zXYT2LVcno!;1u(lQo|3Qf0lTR@d^?#QgPDp3Zu#@>%;9e(5AD zn+kuBz9_5Ej1g+GZf2F6GEGMcm+!1WhysGPG~0DEQ_J0(NYX(p;rV-Or)K`ma!Vou zb??&l{#^6KW@E=q{}u+T#lJBjMl52(cOsSHe2KiMpR5TbfPn+->M<}3e);6XGJ%%ALZT)jL$`N6M>7l4@T>&=ScvSe z7{^y!{wCdFgEIGY<3%68Nk?dEC@Jc^#TpUn)K)vgu_@VN#g9#}@rvv#|HqG(SAz79 zdB9Nt%{=y`T7suFr4o?Z9XsJVgy>j-)CLvAKy`DlTC|vk78z~Xd=8=xBRlM}2U3f& z53@DBA&OXU9g3{=Dvv}_iED8fIOBYy27jKaOB1`S+jzW%K6kKnp5>oBxuZ4>I5n^L z148=}8<(|1oYkE#t;TOn*wMxEexK`|Qj_9y9?~Lp;<|bAr|5vd%O`L35KggA<&@ke zuNd01^MiYIlAtgG#anT9az#onc)$HuR8;==3Y1xS(|2#*M>=dj2^VZK$5?K3z^PsY z5NHD!b6ne+<>QA^2Jace4^l;WZ&BY)Z@zF%qQ6XM*M+^x4)8F6i3hjw{H84~|DMYi z%9&%>9T?T|+rTzxz=iNRf5EL~Zk0vXXJ2pR@Fd*7YbMn!gv}aPv z*2aHpmEa~CnD0ABM~CMQ#M!00)o58m7;o6ulquF-7vZsR2=7ACMNF z^WSYT@^8WOC;5|)9I6D!sUHBoQzc8^UDS8tr~rcn92Ia*f6dnyD|1}KS4IqrrxP4+ zi(QJtDx&+yC0H4=(FUity^qzUWS4yG?bY=|JJ4;w5nRS5>VqLqz*P9!+PdbRJOt9e zk~%&|7r^!4u+y!GjA-MH3P#eEBC$7d!-I-!k~RC$Uzx^n_%%+s4^(@ zE-!?VhoM%3JoRU#iaqv&D;`W&;9QLQ5HQ?8oM1z#(sR8 z?y&@WM&{IRo#Tko>7uWadY#Ex3N;XY+I%7~-&!R`G8t3?b8xHu)y3W-&x0ntdd)|a zdwoag9SyuH3iMQi)7)1~R^B6?+hxt3D?dAqG_m}D=HtFNwPNI{asUK z5L1lv{iIyd_47=}B*H0RNKo=ij}5a>m%7uF9ML;yCT+`iZLj(eT7BCqZkt-wYK(g3 z5j1m72iK~Ek-|Y)YA%{SSZsf{x0KFPuZcqUknqj2Vfn|)T@EaJSNP4oyT!WYg!}uy z>e`t3F9*Ake3LRlPy2K-O`~452l+p*XN2Rc?OqyUs6RI5yLdb3T z;ZipDNV5;)t=hI5ANq7DPr6%9nHWmq@VkdQUf>96c66y92M#BGMY8YJ58SUjs)6T! z^6fk@D`(aeF3@np=Qt~Jqp-0LFQJYt2eLsAQK|U>nfTz3q-hJ-IL5m9UX^4gU{n!;_~ksAY$;BVZge*dk76!ECi|% z-KZT}ww~7pRikb>Fm#9@R>;F;bH--ridCThqD-MfRm*2iIAXl!JKAB+N;6t z!SiUe7^ZPz7&@Aw^74W=4Me24NlH0ix%>5fZ#A2w#}PvP{oA}ToONqzIfYc!>|-Pb zSA4IG5PoAQ5t%=}xR!>N*cmqrljt^h&3Jd!?>&y859Mq{7k(P)!9_9AWVsPyVsO*e z#NUikj5aW*P9Xr$^pUFqFBY}K2f3ASu@o9({7+g>n+$|X&7ik@JhP9B!`m-l~?K;E6NC*kG* z@-OzU5b@ia$86!mU}iG|f40*rd1>h>q;Yhi0+B-V+=sM~!et4!TN8AJ2HM_qG1;oJ zwu)t6US(lX!4RqTfgyxNR?abvO=+mxmh-V(Gx043Cy#H155I9MQ?k6;&#(pN(|_~V zO@od^As?2qiG#qO;`+YCK0IfOCA40kX>p0PkDsjfYCr^4XR>cv>GYPGH9O-98M~$l z)IJ^zHlJ}jy8NBQFxxo1FPlj#ah;Ibaq%`4uwcShhTAKxFA-VZFRTbW3pH`syA^n~ z_jipbuV{@p^;sm&T(ewxMp;eL^V?L~S&JF4%u+AZDk~>CjFOVMa;P@}XGrZ?)cxw| z-^B-n@S4|Cr(C^<;f*}$X5aMjB6QS;@76gX1W#~s6^SfwwH@iyn=+}?p3!r|dA6H> zqe#-Z%sXKk`lrtJFm~V!)P30wJCNHbUE)@}J*hvbB2@UJ@8wcamyg4L((sCluO1S!R8f(gcgEV+ZSRa@>JBZNcm^mogV-evl6X+=l?H&f!DaZ9q5VfJ(YGZOx@$N<{%;;DS2!3aBZm z-}<5M7eTklXK}OGPSq5IOqOGNO70hX`1|WQTz$k_dSr$vbr939cF!NI0jr&v9byRP zj;EFH!5Y6sPqtnl-e1UukduCqkxyk_n5n0hEg&D~=6!{DgyKLFD|}&J(z5(k1?1qc zUPjC%=8KhrnoSEiJ_eCYM|dj28lU8r5Hu{6iKZ-xs>lNCa(J)D>*t!t!h=-4@})5+ zFtYoP;XkAHCDb>wv%^G*e1y52@^?x%w>Cx*{NcY@p>a4)>!atQqpUKbsAQt&u zG-G? zyKl_HO!N|BkPJ5ll+(js>$_`ou~{1+ZCCIs%oc_aGh;htys05CplRs%{-3F!58i6C z#JMSs}1FzVoB92o#CNP{ELB>>j@`8w~b@qudi5@j(B|wUf?e z!I3cOj33YqX=G%kRl0s7C@&=9%Gq_9G+^lKYwZ^&ak9m?D7kEN*FLFjizf{CV!4#y zlqjFgVdx9jf*q=XSA;jBF_-d{M^&Qku^&_l`##g2XcB1EX)Rj)gQ3oNC0t<=BHKl9jgvjOClL1ZU6RyZ-R3_s-zteHC56)x-_lI z^YmLV^K*j`7V%ab13`|Fn)Bk?Lk{oqyT|7$ z;a-W=1u8u_Tw2+LrQvXddF;Pe19x41ig|4$^c$_57OT>JnpiUM^$u>a&SUy%{OrD> zW`VfsGwNpsCl?p9))n8~L~4fPzHiL6@f6dZO@13HHtm3k9^4(U$j^8<7z2oPTG0Zy z{kxZBQz-^8osPJ-FvMPO4IG=esoZ4~aQjqy{=g7Wy=S>SbuRlNUbOUDK#3Z--IIDy z3l$Ku{pZD*ofOm50AXVNHkHI~!afj%QR3PETK@9j!cb`=$os_J+xOuJA^wH#ZhzwD zDfU`&-PV^nUBOGzdt=D$yc8ctR324B>=|-C#?>c>f@J$}I=-0y&LJ+)l8?HlA7j$X zg9`dGN%z-JPT9C(YM-WE4NK@?ulnXsqYP9~=9#S$qo3@n(SF^{WeDnf%bz+mV<>1h zpQMv~F3X+kJP>R=VHRT$ORW?Wg@S5kT+2ao7TcMGm5LX zzq?3o)S5uzIM~SU5NiiMUysYy&mmEBEmJ2QE$M91t2yZjUihH&@;O)1+k(V^44=VL zkie3zr#mNYMHyl&1!~z5v-ukNIE)xalb0i^+cz1(0=XK(y-W(4x8s0R$EWM0O*u%a zKIEf=A3tuWM@Z$ucO|IzVl5kFO~#4GaP@jwKI_ZtmfLYjmYDkogxl7$DsXwX_v?_| zP_225wGoSpy2Ew&cm(OZ17hsHhQL2#q!>wQy~VHFiQ%3v6hm^~PI>EEd1pf3DOQGW zo@a3XIiK;gJkMP01A}=9aXww_;sae7SNY_od2=1DAo)kRp?LS&4vdZ83F@$16a>=v ziM5fP&36+P>)#4p(bT4aCT@-fYqtoYsLaI!CT+6{j1AcA14bNy>b>N}CUwbY>Q%>uiV&B*#0 zMSUT%TzwnF9u21Gc1d-E?qF?N(qyjpR3$&a(79M777*K);W2e;j(ApfX7s{g}1$jw|)D{!D)4Y z#yVF_mkbKDH@3hhu)NiN?3XV-sYJB*M8QluvJI%s{M+|2zP`II-hjf{;JQ7qXPv=e zI9I}-fWO3H=9xIl)_W#@f!L@jZnbdy*K~B}6G(K2ii;}N$8rMq1L_b-MMMpT`WO2#f*V)**EH@=Zjal{=+yI`& ztXVnZn;11@#eq5e9tY37I54Zam*}0!^TG4$#FH;vs5O@6a5WNeJWf1cl7iqsYZQq8 z`~STFXP)tdjx~0dU-_n-Mqos~%`9uWAU0qu*x;ycgWxxw44xt~CtDj{D>_JaLL74W>y{dM3& zCm!~#&;j>Q0=1G5vQ-*Lu)x%-MfrzOs=s_6md-r&0QZTQ`@z?&_AR}0`#HvFG;1F3 zuk*hBO--};OTyJP2W*SPWaOO}S36pnX!LKG$$-^Hp4)iWBrG#%o|eXi-LL!@6_t+7UBaE>_FKpU_ z8BEiRelbSeuT@=*TAIWXN_q-D6z|8CTD{P1TDpI8)|x+1ByR0N&fB1-HC;@%0Dzmu`{yOvh*@RHNa zvT3`El`Dx$${va=X<>X6hN&laBAGjFOhJT`>$8m9DUOVik0xKBY*-wLj2pP@0=8)= z3}?#JxbIzYi4Dk_kP89HB%sJnoCsb0g(4itP2Ic)beZ9{;>TY(^M(`pw%)1Dn}kn- z?A4}4`79^eLvS&VZ@2J16t+J{TD_XkezgsewSg36h=cVdHq$A3i$gmqUFmR=tMjtS zQhG02X@c8|G`ID$KldVszLzT@>(p(_xD@jC{=f3tQw0`~B6o2nuDr~;!e2QcAvx#o z_YAx5O81sJQ0X^UrQzByO&BuN6WiT?N{gWSPE#bRKE4S`3mBU_+Z7xv%qpU1EC%=d z^PW&%{6@T~=EqWe&bw9fpNvHby>|1kLt2|1DV!}swEse}#>=zp(^g1bicy5qfvAw< zPVMB>b1Ar)KD8PYE-^8rK@V2N(W^X)y0xpaSTbrJXXxQdsj&U&CDAF8 z;iUhPcP&BOycq(?BHr(#rQ@YVxf6ngE24&zmAN%J#DXfa5#N+~n>FmCWBltn;w37I zn4h!x4>T}e3tkQqg04q&Zc`mYM>6g+==#Kt+JJz{8I~u1I^p?UIPVkpb}|Y@jXtBy z>zXVyamugw9K|Y^K+ochtJgYr_l3LTB^?6<^9+-9R-s)4t@cuhc{t8^T!>JD$^G=N z%R`5#6|ZCOzq{^y(@Unr9h+WwsdP#Ok%m zuV2}0*bsMcp~~hr@O+h#!frw?tCY*w0uv1q@T(0g`b3A$jBZ?vaMdx}5>%VtbS}Fw z*uvbE7ZVy$p=<^?!KW?oXgu(De-7GTKryfupQCCS#GpKdb(+}Bc&R%(XzZ-Hh+>`W zNeC0O+%zG9T%q(nh2)g6J(ZP%g{d%A9(c+vy|iJo&W>&>Um`O-MJoT z-2d}@3OEjuzMEz?ZdkB^(NyNn-$dxEaOu7+y^WmDFxq^ z?tocW3&)+FK*_>a3gN8+@}wdHeWR?#M;7amnk<4~Pl4hqOGJcZLV5uTEk-(esDJq# zR@N?`g*JCC+UHG7ePdyhZvTwh;_AmzK%a1?UH-wR-MNO#nW3HRKMK5!GCWQGv~^y& zuU&NaHG`3p1ISHBXD>YCA*&77MELLFoL09g-ajo>nLS87%XU_}w)^*cOfs$~V_BZo z)+k4rZ@=+VEUAbaIqN0Wg6`jKi;f^Mf?y0 zcr-(a^6z=um|nYla|8|FZN8BV?_t}kl%+S!O;%{Nqm)tsLi4ATtw~F6B-bHS-x5ZC zZ*yPc6VOH5vtTev)}pO~Mo>EOo%rDNa15N~*auX;I2HaMRjHoyD}1)GMOdFfWQ2>` z>-YKu5p-#ii}`iO-3OxyB0g8fqe&d+v<-N6aJhhK&lO&md_mT7EC1fxJnzy_HS`h*rks{>xqhbd;*C9{as6D#K)H?9;R_T8+8`$s(%w%%W_Gia zOq?HOa8fi2`!UaHzk~{d#ZF9*O=4|=P;1(%r4}vfvigKZ%|5v}Sj4`m5r0KU$h3vy zd6x2{m>$=ljrAg~u7hr)2^!?2WP(@zj~^JliAA%Di*NFy{2aQC@D=QZ;p|F|5)LY0 z>9)jePiQ}VVX67rrxUyU8=H@@7+CUAenC~YVR~Z`VInDg&*B?mb)4(o+_?)~e9UQW zm}Ks-OKK+zYFfW`>%#27@jUf10@30{tnHb=nmo1Z_eOn*YvSB!+Us*rtI|#P?8dcw zYv&jGjPND`E|Q0De$j9W`>rfX7zha5n3sy&_WJR~`#tsqguqfw|7P6I7x&$57g1kY z-TGyFi?_^yZPU2Awp;wcEv7;Lt`*SpoSCNM3V)St`1?^UEAAE5e9G3O*!-Og7kBYW z+0W0KTn(&Y@0)Ah+#}BzVK`P}V~PXhW5q@;On{CrDYZRIKQRo?WcsWHFNcYsCZt#{ z?>ILGIoJ%Y{xaOO2lxlDp8?UpZCU_?Rpne#;^uiaYZ})AHe8r}AmIHw)nZPM5!e+u zve>BaqV`!w7ow93cKCnO60vc}sow=+M_M5~}k0?7a;JuZ0j^LlKb527`6Q1=( z^HI=ZVaY&iAx*RFZUM*($!BZT!6xY?Z*ool5|9)N6*R^<64!iXX#}9FAZ%vH`8QPjZ+aG5=Jnk zBJ8m0&HiiYC^*@W)9~doL-tqFfVZg`63cI)b04y*5eF5Ngo$5eYxf%70~IpPM!fYZ z0vEu;ALCceRbW#OAXvLyJCwRL$b0sYSGT^ZsW#8F2JYRM=J~3I?6wJ|G5fTpx(eg0 zZT0P98E=ix;ScZrSFTQzJ_IHAj?}s#`G`DVnh&#S9s49D-ITS5`JX0K}Bc*C<5S3^_&pSBJ1!P>{8q4Kq5|B)Xn1uYX%2 zIstVEBiWL9qwJnKqR|JrvX!$b`JUW{$PVXJ+`!U4xs8^uP{9Kb#BaaQYgUE}H!@T$ zTg8q{lhsaH8l2JxGUg24-wXIr1~(dUe$NFaFgfT=C0+;Aa8;?wHml6pVTOPbQ@x6Q zl|lbdV*~=vIC~rPGEm982WmO(V*XA9LOXuR1-*coVzGlP|AoeUSJcFBs#C+hG%n18Hc93*EoZ;SKWr2 z*X|qdTqT5=G5>pQ^>)8T{*WMDa4>kcD#b|bJTKAt99VoMnf|Z3M}rekYxC3g_ga?+ zgaAP*JuL6mE@Lm_voHcS(>o<+$I21(%x%nGGSJb z#L8wr`-z!^z{mMa5nte+U9%Tn?N(?`bWI+bK@eySRDJ) zNoZC2%Pn)gM7d8M!99ZFy7gQ_VJ6G(fucD_J(U8t#vNt65S4c~Ba+s@b~k$Q^_}B( zxkQ-OOl#ND%;D;;TQ9m(U)A8xZQ7?t?%$7HuH;xO6s+HdtfRMRO2Z2^?46HQcqo#b zX=iTWr^j6EHL623($=UTKU?-X{yrJ|7A?kKKq(nL(sR7kq&w6$ z%ajFs1ss*ar@4doy3HB2|Am(cW87n|JPT>&OxkBR6`Ha}7H`3dCUq*K8~RibnCfyIvz&|0O8cUNt*5l=6LrTN6$CmGKXyOl9qgT|SB3VFEb>b0 z_ssqx8j|lb;kWSpuNaJVE`a)xJaNW&OD$RSFlf8ws15bC@IxG ze?*cxK9ytpZU1BF@->s5qukLqVWrPQ=1mtZjNo!%e&|OErV- z-v;lg!mSuazXYTUu3WIRPFR0wtMYl^O>Fy~{&g$$hNZK>lE7SCaiG*vm`Ow~E3<$W zhU)lm;5mT)f(k6L6DlC#MBgxd-}_*hbw%h^RQTlO)ZAI`gMS567~ZY0AE$J5Gv06s zYraU7jOb#$MN;ncP@iOS>)l*@<4&*@^Qi4Ajl5#ueTe5036!Zsu7icY0dOqamof-L zn)rH_WOOk{+z@l25T;U_>MakbC3AL~|;rcN8 zFtP1F<*CCw#clBST8M@J`D4E`_Xd(E%o*^GSzMip8w}aS#McRi1~R|je|UmEuLx`Q z01mgfz1Y5^@3*M+V_`YGpQNI%ybg@vbps6FH%id;c6WF066>^9598Q77tSu)*;q2* z<2U6i6^l*%5=c5W85J&l@h(d0RQ?T(2ADb}6QhR%CH!f)c`YgO6yvnT#Hs(IxH0z^ z!|mhG$s;d;<(Wl~1ygzjf?2lBQ^vn=&GeprqSOY#YnhD7)&q1urOk1oJdN?QWRgKK z9kBQ+PxK}G3YuRt&nIJL=EIe~x)%t^j9X-@+MDcgHe-DDuVVFPd(Ayc<+HP`_Qj7h zv2MQa&WF9FxacD{0l?E2TAOweUyi&Eaj>xQJK@dIun;Vei*eWvJVAB)qIC%~0tRa?ONC3TdHn?oC(C=4IFB34fK!ap`F=3+Se#Hh2Dt8 z7Ifj%w%ICYoW0tx6~xbU5KQLC!$vlY>9LpNzQpSa7ujnn$ldjaPfd2bZ0&yO6PslSiLyP&-3W)U_c+79y z7%15+p3(70naPpT9JU-_Yx+{!yI4@BoZ7kWH-1N&B&!C?WgI0}I`T49bG zbWKQ&_7`zcqy{3}RaGbhF3F0uhpy881b6vU=Tmyh7x$Oa$^JN-Rir|!m=N9EZBJj0 z2m*;IB1Y--Bjzmv#$m-L{T--qJ=)r7@fGG7|e zU2uIp{2KzAPhS)*^Z72iD`=_{mWw|IX}ry=ztCjb#ai6&$Dw{ z!;EfvquE(qtbD|2BFZT$YNY00LkPad9!%Fu061%*vbQDqd*uaaKR(ILfnf(rK9B~jNM|P3d=Dn z)9$lCqoU?&+NgmUxfAmK-+d1yWIR3V9k}%VnFQ=Pp4OciE)NU3lh1D;4u8C~+w?u< zr7P8M0F?5|)D%WyU$h|0U-^a@{8}&A-TjrUX2d?fjI?n54!c}vo8y&$`BG1R-Pr$z z_JXDVO$tqn7BwvjInlXxZ&_N^Uj77*T4*ypEm`YAR{NeFn6M86vLf7e?f%sE2WAkB@OP6Q{=2pDf704t`Z|3Ek0;|k~4@w!dc%8NucU%qe zOD@ZCVA@Lna)4LGH5Qcix1U|(g|2Ys>B;;rEcJgX; z9{1aIv%Q-YXtfr;x~bVYO!G;2mQHA5%LnU6BTRG;7rGXh7qxk3xGK z)2xKhFZ+{D`Y^~tuc?~&>Cgh4NXJx<*KU({Kw*E+>gA;mq;^7TJ}P;1t*~%AG-s0R;2%jUo4$ ztyajW1MsiZWGIY7R4R2v-=Zk3&UTLo4Dg6{m=k4qvAxQyPq-P*bb_Jku*qX(D!CTw zcntm1(mM!F&1PEyXV+^+en=T%#QBthgj#q)XvB`FXlYYheF~%ivn87*`9hkkoyOxv zvkY%dflDc0-}!4V7afp_5NQesMw5qs07L39(8JYA)f?V)$s<=hXYhp7aMti#So<(E0a&3fkD+MJGSD&cywjbO1uNQJbD zF??g*EbOt6634SBV(1DPlHuER&8|)cN|H|n=q5H)OvpFfa6w`MK(MN~2WF^b((voHq6;DKDH7t(;wLD#_+)x zXI++U+~MMf?5aa@Wxm!W+mp{a+4hIe2=+IrX1W3Am@Q{Y_tOQ#tAr$8!S6E98zpk( zL+%gt1(~$Z{6?t6f-|2j9ugf8mySqs*To$AfRvFj{@pilIB#Em$Bt>}qsdK#gkCHg zmE!Wjgp_Kbux&->@x^foOeM;Rw$XsxV8mtW`K0 zcJQ&hsd2Fpqf!+dFc(+(c$??iGvlg}wJfGW+g{DXg>Nrarc;Haq_0M>IP9KK1al+% zXzl7qg-u)9jgz_yz&^s21?*7xyT$En>*xZMLSFa#$yQ13S@L`Zi*o8hA@_mst#CVfDAP~sm)+-D?i!q7fVG2%`pVwz zKcv=+=~TQzxT`%cq>!GVF%h43@U$AAhG@lN06IYXMHJ?F@^mh>_Q7(m0CVuu?+)F1 zo~4bd%|B9oYTn-gg11YNPeZ9981EGwywayT6Rh?~AolP79d+`bqVxvo{^Z?%w6}Dp zkk}!bW;7TIuI}dkAb271dsbI8!eQPj3WaAgjzEmCxU?ZOTJPT<<4}1t%P=tHIv68q z96?z1eKcz5&b0n=Y0#4XW*F?1=U$em_rcLeCTxM7kOwQQXCirU=ca+!|K^a$#%EOK zovfce>rj$wq4;wNq?Bvkv&?OKX%tJ*CSh*^-_~KwOX=R3L%S4-A-q*lam!9*h}2ws z6Q%krHp4^3w$g_=*yC=&Knc@5;5^OK9Ac=)g#8tDn`(DDcNQ-8j@RsbNmc<$&&s(l zPDim(_5aXx)?rP*Z@d>!5k*iDlo%+8G)ngnK}IMg-O@0+Mu$Pel>CBpC_TCxVf4rW zBL<;KcCxu3RVcXeaY5*z@lexY$!#0l_UWE<#;8B1C-8^ zblor13S+ApN)PH}U)LLJKx9zkR#ZG1!qa%okzYM%SR0#$P%&oM#nfvu~a_Fy0p8sOPZ~iMw9DPn7 z@*$R%Jr5avA7$i#TShk+tVLQ(rlpG9FmBwY(k_k%=ulM8o^YD~4x7WmOFiq|o}4ZY zS$dn>Dyxm$ny%Ahb_%w>rd1Y=irWsSU9Rn6lHF$0`QKXLqM%_)UmWUU>?@6$y#eHl zTz4GuSUiah)~3MRjjUs?N6|PK^I}&IcK;^rO=+HQj#Sks3DkT*kZ>y(0Y_4?@Ppc< z{|V)0#4Po^{=XKWP!8wwnF^^W!=1FxYYQ81{5~S_=TPL_??0xnOF!7ObVDoFi*=a^ zkW|qq$&0Cn>%%v14Sg*+|G3x&qG4ugkFR?zOjH_q@LiRZMhb-uk{h)3K zegu3OLet57?Wfz+6&V)5-&sz?Tin0R>fsog{Pyq&p@O-w+KP}KAEtVcu?55VQP+rU zuX%V*TTe3AjPsc5$WD>+=aAT|o3LFl2EYAG@IQH0F01NM`6Adh$JK3lPfn_Z}!NQ+AUib4CiNv4(ox z>9tx%RA4h}cE<5${H`Bz`}hb!I{4fr}{{J>YbyK zolSMJkErT??Dt(J)A{tW;8%WG?S9mbK>#$Y3DTag*89gBVvg_vU`K0Wbyc_V>&Z>O zB2K(KjQ+B+mXNhnL8KQzvo>RjCxj|zTT3RGc7vwjPgB^wJV;C_KByfdkz z{q_gcfQDbEDz@puN7o0pvq#D( zQCzR60wajzb&L@{a3-#{{^ZuVf>L)m3Vy2T6f`d zs#wkgq@8Elb1k0jG#G6)udo##6vi?JZiETbr^#>I4>obK{#M+9_v*Kx+s)3a$A5Q- z1?8fC;UxE_oItT^$S>szF2}?R485M6mEJ>OapxT)ZNlM#)=t2_l%q&R44N5(t2~P4 zhmt0BWM__!K2mnIUbn#3gDuu>=RXs&5Z>`wKDLo&a%I;)nXONWaSv?{yIEDVTGp85 zR&OSTmRvA6$+(EN9GQIJ^1Veyk=}A_^F>Vbl@IP8{a3SdZSs$P~o#DfL3Q6yCdI=$%}dBfyK(nfSCv=I7dXLZ>;QE`IOi;H;CxH z#R%>KXAu$L6Q?trhS7o20i_`D~^_eD@pN z%(E&^||SWcBVO&2KJ|8VBqSU_5RslKFQCCF)M zsuP&*V;X`t*vr*G9o#5Dg$yjtF#F^`$HI=?4(X5EIKJQ;m7QB$PNymSR(9;zGuvyY zqb-%oG(3CvO%@om^K9MzKM{USBIdpKZyu_%+`UiY`j+g|+&Zh|GKt@ODDk_l85&^4 zmCr3>)~m*E_V~EaT}=e&X;O(@4EOc%OFotDI0r&XRE*mnR57WJSrW~c0=D1f#Q8`& z&4@ebYM{a4*2V%v^YSBR8mqhVS3+0K+lMLrv2tKZ?EE=}15|_Ls87mO2dMGX)=bv6 zW_X^4p{(qfDmE5>Yp0X);dFV*$Sbk(yCZR~Yna0sLbk>C>Gj6N&d1k``VW&YOBw09 zo^Zx^K3Wpj<;>O;B!&f%4n8E~S8sGHBp=|JL*$D<@b=JHzlWn)~QW+GPq&2{WwjL$y;L`BdlWUFd*F zW?6IsqAJcdQDa@xn&4dUrGKxT+pM#?9acr2Rlzn1C>>!*%BWK_eN8qKJKpzUZ)>nu zdRL<8+w>R$DRzI%yCj1`-1KRnw`VKfueI&Qkd~YRG)OLY$9ys)$)UOO@i);p|8%jI z5b)jY(&?QkVmc$9IP;%AqxGDL>bR77Utf2IHR$NEp{0m-@Hs0zeHyjKMO#)g@RJXA z3!A1%l#_7?T#8lD`EW@30mPPt=x&LWKlx;W?OceJXF+6UM_c)ewaL$kY4*tcu4Bm6 zbf16hO6?w*F?)0pk2-r>ggBHyH8Iy^7KQ%lF1zB*D6d))JcqYpH4bs_dwErd?yXFGZ0voX*xU8(*c#Cs zw#tD7o=It(RQ*pEKC#~J_*aH9p+vd-=(7%r*lVg-TqO;2kdmK9R0^-Kjvf(h4gQ+U zEzwQvEOO$`_T&wD)OWV$e&0MPzw=~?y7=mGJ@s_uMp+&uK8Re;>JrMjuDDc`DQ z)L^I_F}QJ3zHvqQkcNo)mGU~~w`62T87jg`hyx1~{Vf+G&o@(xy$~ggStGpAn>(;B z264};CZ(KTVy*|5H?U}@-@h?+t{dS0H7{ino6mEXnkO$Wy=zhI{74G>`s+{83(dg! z^Ka$a`_8vY65|Up+1Bb#cuImRCJGK)Pkv?h7RRrn^5OwAFAGUI5~lZ^uG`#h*U3zf zU}M_Qm_;@S#IEk?5gUbNiA<;9Q09))gSKvl6U09iJbL@vh?;TSyQJ2mfeQ?PsoInj zvdW6>`^cM85t#_kM)ET}S}>W_!aRT)ZSiz6*g4eHu5NFP93!*%TCdkD;>V<=4wy6} zU_B$VB5aHS9@sD*VqOkBHLVjCb+`D?PA9O;&=5{u<3bJyJxh+fdAU6dipPC;IE zj1O|gi~&CKzB_An+|fEk=`HuWR9t4Vhns6#grq4xeC#%_$!&XFgLH98bvom%5EuJt zri{V;Xa@^^hWoerj40yd;%&R*9L%z#Txz+-L`rZQyux9kZ8M;?#hk~rdu&d@le#*y zxvAD5-De{eIca=SWQzL}Lz#8^d8s>s@1fkyh%XB)rvC9$@aMMkYzQQ3I@s?smFd$= zN@L_>Vk5udSIyVYPAOP*!5q~H1eyoIa?tZMJFzy9Vf#8sT_~;yhi+7C(u1WjHJk$Y&EgrlqevbVNaFbrf0#sHs7U~MT*jPS>-cmzWIbSp+_#SNG3iwWpZ_bzQfv;;-;!t&kX zLvxFWISa2Ep%G?9qB3szf6E>doowe>4O7~TGY}-x3m-3R6iT=*uQOs z(CcZ(=!MlR9w0~x-S6llvlAPZ?VPjRu9WPHpgT~ci!<*lLn+yTjZ=t~vw2`25d|6jY^r*f! zPy5&PD~(|rBm0=bj9fbhCtIYX81v6CC)a>^>gem5-WNQZISyuj@#^F_ix(WuDBoBXLqsCiT6nnCR~3ZQR;EH0}B;wd8v)8C91F zp;Z#C7aWzOAtu+$uMqLArRWunkr3o-wSW#U9j+1x)GtB2tY5C2{cXzTLzWhdp2VX6 zqL23LC*0QYo2jEWIOT6TFa>z?dl#KKc5K>P+0ITmqGxNvK8^-*&FgAVSNJQp{2NZb zNs2@_E#X@2V5ED%ay4mqyU;!kqEj?A*l)6zt>~IKQPWiYTjZ-yhCh=AQK!fPZ3mq_ zN3uMXc|seP*E3dmx)CAc_o^J3bx+#A8P83eNbLY5i0(A>VoEprePQyedl?3;#AvAr zuz!jDfLYQeXQ|)6?KwM~{>9V6=gT9b7HxK0a>(#BINT5zeb2$d#7@P5>8lQdbC;^k(WO-9)mPGv(A z&(jnvaa|!Pjro$(=#gJJ+Do5HSa07s>VwZ^X%<$1wJ7F_YCdbrj#MC9pD+APLgt3; z%W7i%yN|P!wH1(c;fpgr-wvFOl+PIm>lj`!J@|CUYpuf~!0HW^gnu2YYqQub1%9Sm zyv)4hYS&BxmxKa2<9^Rc>ahk7HrSFf`mJ_{u4%a#pn);`T zJwa33-RM?HfoR9DU}S*d`u(7deFcp-{Kod}um}gKKU3{)%=)Kdp6&(5Xlz#d=lboe z(!it9M5pQ_wwe#3gU^Ta8!N}`9ImRYb=r9=MuPJfg+`O`l;ehsproY0iIdLlz>@&p zzTIaLu-oBv5i$nNO3I%&^BZ@^KiWYUi(gx72SP$1YL{$PjK{7ua>&&pQk^${H!9rd zBk#jWkk-q3W7=lCOE*c&hv(PzRYud!#uU|OT#9E&`N0A!<)Kb7GbxMBFjR&zk-v(~ z5gQ?K3Y>p9rT!!)u`^TMC+bGdlXr~p2_YvhcFe3Uo(i6l#(_kA1<9CG3)?@*+m`D_AF3&LRVXt&rqlOISsL7CC^j_um+2GE>D8qRfiH0X zuAUS^(wpt^f;`f9QCzHe1MLSqjTYB;((`F>%m6^Cw)+8N19%g>EG?0Ui8|k*$wz?jq``dP` zDuRzBQ;>XKzz>!_UIV?osfR(w>P{LJyVNS}4%1ddxtRG@_}UCFY6t8;w-P`;AQrlz zccGCdFpwORiB__gsTT=aY-}4jspM;W!%^bB7~uv@ObgI$l0Ryd_RbdRmJ*kqrKir4 zBHo<91kMxQrvED`B9b~=}D9s(knar-11t)ylfW> zS79)^B0+$fb-~ejVk5DeSN_O(1M^q_mt+(5VXd}QNhbph)S*6#Ih5#4AhxXT{}lPT zcq)E*&ox!zKeuh+b$LtU)(%C!cj;H-o2ea^`=KH{GjoTdncx=XPVTn9Zx{+Y#y?Zn zLbx}JwMy#W@84gj%WY$dJ;VD~(5C7Yb|3gQm=Xbrc~3shwy_B4GFoO#0&O$@Dbh^> z*Dl|>%lHPuW;?%5gBZVn1)RnT^q={!ZP>wFo*s4l29b&tj!9=EVhGGsc(&8)@n zSxJ~F22uNY%UF9n`M3})xM8$i(i=s=OgTUT59sTbRSos>LDG8a+QMWJk^|coaZotE z4+wi_X?0-`vkkK?BT_RnQTFArPmY*Hbvu6yq2#vKV1Do9jyild< znrhLDvCh$;H#2$yS+)26Y$X{g%k!0noEv zMZP8A;BMCL%{M&L+&yBBUF(nXFx~d`A%Ca5A91EMt)@qN__GM;k1hH&N1uSl6tg0h zv6a#VxcNo(=oliDBP%#jU9G=(7#-urqUmtt(YIT~+#Sd+-p~uxpo$-_;Kb3sz^Udf zlN!_pgGgNhTps0(#S5Yuo)yQmD}qNdY;H>ca<-t{d9)%925jUP*IN&F^bJV=+jlH0 zrsh4yF2hjjlul~8nbq{80nx|+h|L#329gQ#sg2uHqQ$H0rXx^o%s@$!JPyB^KhUf1 z51Y+{R_hj6o9(MO;vX|G0jW?UV~f#%6OeOfe4wRFYU!hAU~-oQMA z1jYmL{K_$zaS~}(b0o18m`q;)jNOi_%`~9MRI$7KR|0+UKbDOA7PU{D4m$JUc`Ku- ziE6A+J>gK_z3-^?V84-NuEFVfiY5)z$BW#@6{*!chQ|a<5-ey_?VIiU^G}c&2!hB);i&I zC5b@Za34Xxd&&9Jofo?0SVi3~O@FfQWpplNA7K~bOi@^J)@|o^r<8L-Go(LO6JI#} zp)594IYvd}wBJAQ4;FMR=6NEp3;8!9oS80@w>xHlv!6`1hgahaFLZz{5TKj1<`=e+ zrmz>%my;MF!*zdcY2}Mt>=^>$>a%61%x=_Bn=n;B7AVzpG62Utgi?QWJ^{-vDf^IK z3OwMmwim#EZi1cY@=s7_g+BcpK znEdlo)D-vn+kjfy#+WDFuS@CskH*i%y?`Q6hEGWq`%`xIr7d0>mQqcttElrxq3v+y zfZX{3sj!j@BRxs@w0cAmyjt`nLJNzNZd`C0f z{my%@=-l%Cnu7VQ3j*8OV02`2o~X_G7l<#%Y&c zW9^YY;Awe=3Y-X$qYCkd=5iSbUH&&2nZm~; z9f*mRY@mtf20?V1X8+!MfKgTL(oXt_l+}Y<`_~R=`|l6u#GjQ*+Xf=uHy2(Q=(MR6 zAcO|aVoa&w50hytF4Ibqd>q-$uCIgYr+0edI^&5T?Oymm^=NhWr~A@>aMWtjUc=iF zw^J(6S_l0m)QJ8#S{z==I5_`bAmEb+N%;o~L;LbY&A#n#LVJ0!= zC@y|7?9Xvajc$92%MYFKvJqJJ;k*XCMOb8!$n#4K{&J_Kj^klbs~gd8aPj$(v1|$6 zWwx0atysNBC+YsFclzvrJyj+kz&XjqOGIO5VntqS+RobZp|6VFvj&LcQoNMxXZIy! zu<){g13Hes?5O_FocghpxS+xFPZXa4|K$h^4t(^e5^r*eUxb^Uexkbg56E-Cn$IM^ z#o+YqbAU6SFWJ4=`j>g!9g_DA!rh)`c(Ec#d9gdVCpb_t5*5DbesbonUk*!t^T0XS z;(7*ZFI=7%5B;?U66h8?92jP2lfHzktG6lEV8$ZpSggXzrGEn#^&jX^=%_LtG_Ov8 zP@Oo#|F@Xw)D|_byS@A^H#F-?pJeLGM#C{L1!{V;MY%>SRhUhG=A6RrZS}F*6yIT9 z74zZ9!w1E9DVoW}!QFl(LBG50O^Xf;@{Ga!IbVu?7e`MF$Mb^l)N~iTH+nF#Y4F2$ zxdpn^`)q4@3cY=T{Etymz~mbv6ucopeL1#XG46qBR+4bodY%VbFGS4S^ivR3^>mtb zAW`!4WtcyfyNK!?yNpFet{Mfu1hC%TEpn$X#^{4=YsDlr;2suN{l)F={yoIaPbTMB zH0rKm)#k658L73j_5I8A??pVJ`1*pmhEMgzeOky5_hSnWtH#aSbI_X&Wtq|MKn4T3mztVYzlhRg9ImOoJLQEYFu>OM$Go{g zNe;(VCok6Sr-K~+>5RH7jezWHf|dE-JY$liL3ehlx93k(IB(;E#j=mbnGCdDGXB$% zUUcKTc=@|#>k)7}pVe}xNpFSiT_$e7Xh3Riecx_EuXB#vaFiu$_rDUKe!ARKnP!=t z=G;VCbieA+%j3fUv6VSA#YrHeUI!yYM}Tv-<6Pr+#JcC~JVcabxUpJ#svsXeTf-ep z?j66z?ek;bEx@GNpzb{!MC=RWoT<1UWL4*rMXLB^L>9L!;N5Talf$c7PHebR6McaC z94$1}_iXOu-vhAh0Ph!Li;o8-LI3W)S`?C=Y~#M-^%+_3{HCVxO+mD8h!p2P-q2#w zH8atiy1#cjC;ZqF<_4W-Y9GaXyz5LxG4_c8D7Y+)jS1V{bpn4F-P>PH@-ROL< zYS#=u7HIuv{~Arg_i{z{BL3tByD@{XZN#=r*YPS}x6yIU>J9&7KlA)KrfGG>jR@p83l%!!9j`BVP!#(*d;xC|PdoNto23SC8#2hc zCS>C6O^Z)F9QprR0Nkp+2%?;zCZKu)39C2Dlm1$=!n!ax?v;oLL1}AKLC5iE_Klx! zkK}I4i~aB6RcOV#FZ4spE!jS-qvXm+ALlU5Zj>nh`2$zUf`lgjTW|}S@>)N2T7aMV zh&9E#!y@`#fwWlYgKVnk3=)W-7Z_0oyHkeC*)zz@D`}^~SEFpd^fLJS7tA50d>$Z8 zu_qv1{*XS2Yye&P41zBA+I}x-{xgQ$E^dHOZIYrsvb0^h)3JlYyF|A>$h`r?s$)a= zgH4KxY&JwEK_0=ZhlTSzZ{-Q}1nEs88&v5O2-c%+^cZY67iQ?zW>60`qzS$jA(gwS#ue`WA_PCU31h* zf?`pXvpYi7!B%e+;buP^<(|iG&;z_+_8;1u4O%OS(}dAgKQV%^d?r0G_3{rCN?l2C zph~yTs?@VV)%1K;354^a*xP^XpC38@TpyfX#AvEKusH!R5hDi>x$u|p_nujLj@gDO zFRpe2FZ^1U(DO@(H0;unbgSrtUs)=i;^R~fvEk_`gDbb8X^HTLGwCznk%55rmDNONvDA^$) zHK1dF%ZFehosz`jDedD5u{u6>eE8x}h%3oxr4^rd?bW9rXj(G2AL7>VSZ3+D2O&Fw z1ua2LaE=_AC@J~Ee#oibZc9VkRGvhQjXq;p-nx>`96?LHimU@u(T#0=Q~(sRw@(43 zE^5PHcZ&u$;e?Io#NF_ijq%b2Cy~Iep$Fi#tK-q5_5p{a-=KD$M zB)-gBES1-Gz!0Xok=w)clGJajz5Q`F+bekz`gQ2po3)X?^;02WX8M?S*`HmY&zaSc z-AT~S#gwnLn4;SAp8t4GsN#Or=qKK{I_&%IL(n!4atVV<&cx&?;OKIg`uvpKStFhb zl+a9?WS0$29h|1uf@<>x@2iWY=6iz_uIGI2F58Zz;<*)=W9u&!vA)yYv4Cr`uly{` z{SR{+2~e`R6^0{3&f$#*o%K0q;9h91V#i=CPU zC14b%_kt}^aQu|f6gGWGxIq)}*8nD=B68K_m!2HB%i*}etmrr*_~p^mJ}B!i1r5P2 z&>G&vmP`JL7!(B@U7G!H>Iv8*s2Dqmb$b_3J8hihT{ZOP0KT{2mwP*!@iFAnOHz9| z0J-CITj=!Rrc3Y(FBaHVRm)^@P7Qj~xC(P9kQTNfgf4apZ0_g;hdw7|{ukDUr)?~mR4g{xk$C9+wNg)6x@?2sS z762^T0Ueog;!$AcpF&!qiq%qM*YupPu%-97!#Rn~!oYtiON&w|5%x$oUJ(A3Pvw@gtE#Z`cK;XypM&h##Ki<@~o z43~K%J-W;;l9CGzDICuzIMmbG>Ak~VbGUcSP&By}-3YQcIVWy#BtC&k(nX&!C407R z^9oi_ZRVQX@dRXvDb`a$;$oQ_ z5dMF!O-;ng-nKF55xt#cMR;ymr6S~8LzC(>_dwsv=uOD!dHkYH2+M6f0V^GJryTh@ zx`abn3$4Zaacif5V7N|69$jN}%oxd2aB_Lhy!=>6GGi}9=yILncMtlGUSAB8puM#0 zqvLaOnRjP{ycuji-v4R%Y0={rfLlN;_sTAXkDZ|SBQ8_OuNzieF>w*=w0f)bdb_?( znT0^_(YbH9Q460i;1f#YNI0w~Nwx*`6zp zp5rz$y#AYlJErx(;cyp!_;!2K&D4#}49k5)zwWo=Oss z|7z?q4Y)-o;Rrtpd_2o@u)R6u)%=ebKk*{%nYU%%rNCWGKXLtN^PKz|M$jDaFe$ES zSpBuKyshDvrj)>;+GdxVEfLQj?sjK*2{%BHnEL2r=_RG@!c;>j3}AruFUMks!D|fV zic_ZW#XsSUB>|T@|7y(SQm@tlPr$aL3u9}88bHaQJDv|EpR5^dCi5^-myfFd zy*9W||0Ao=Mt=|hj}t_6nOqhAm=XY$u(9nixRk-m1%!>hdH0*F$q;|QyJ5Vtf@&Nz z5v&P+6PQQZwS;w}=VI6tFaLPRUqmonJ?c~~Z4F|*k`h1X6=YnHw8{S^wAOXDz1L_?3Nt+Qq!Hkk&2UYpko9hR$r4okluXp%ltMQg>4V3F8? zl0(fGhxY5uGrd0Ti=A>AF+CvryTq&zz}uZfUhZ#+R$y{~(9aB)(;5L(+$lB`jX?M0 zENsMnu1!fj^9Wn9z1VGW?KAZwhll*rOR~Jv%}?I+n`GGik+fU2o0nsEb!)CCJ3??+mJnW_a-O3C#pk&)=%!$FnatyaD zquw6DnQ`{z{{GjJvktdXyi-T&(;}z*>*7PLSa56(ZaTDdNC&k*a0^)2;R)IlxeP~l zzSQJzIZgM>`avsKas2aN+Hukn$0_j)(Z)qPcX4Me#MsHAJf4ic_vbL$ytOjPhBvR< zvw|-G0uZ@Z16KDbEJ~tlwsWZ7v$0Um2OA-5sdI%*Wc11WUigT~PX2J7i@pS5FQ~MS zn77GuuNAQ+ONTE7?CvaamqvtFbHQ4L+j7yR0jUC|;F6=%k($!#Mn}?S85a@#gj4rA{+eK*=2B_b`&LRrj5ATO-fi_A&KGL&c^xgLx?(+QX7pWe#AgHKpI2{5uQDn(ildwFz){%O%22eH@ zByiM4TNlVgkXkC_{6bg!*I8(pmN4Er>&B``XY#j_LKXT-Z}v*Xp)8f-jSC}z z1D|0I@IJ%~`zM{o=$ac@`n9mPzy9kD{dasQp209o}3 zY}X8>cXHX#>tV`8zL=o}FlY%!AP{d5!}DlM%3}Z%*}@MgQosA_ISiwtKX6|WZ`2Z(ziu@RX8G^v%-!d5_@LRc7F!aBfI(Pp=Qj2hTb-*Y{ScX|?F9iB zs~7d-TgEQ-Nb`dtHm&L}_HLzRm7~CRSIrNS)Zu=p18!evj$Jxmng2r+M%lhA0Z5E$#DIUoToxmtlVHhOPT7O zXMu|tkTYk$?V2FHD*@m~PQD=(rCMZG9JBn9Z=>i?yHoGo0|%k#mqmb9=+_;oE-a

n@(Hgr9zD}gWK{p?!QW4YWt&zUduh;IkC#m=8All-As$fJpQ#m z9G4(aHE=mL%yk{z#TzWL*8U+BIDKcB#Q4kHlsNdE@-aI~ZRkh?@VuGmCTR-;eZy$3N86MdX4+_RA-cGuf?ZqEa zQ59iQ{*q;*@ygUcG36wyCqfMK2*pU|6g*0v$zm)Zx?#)S z*gQjJfz}GZ2z+vY1=yiyTc*3GE*K=+An2*%J`0XUDT_~fEek(X1l_HOg^G4B?4;Hq z@NU!M9_MU@?S!QBW$(@L!c?|WNw zT(daq5A|p2p{NboND^-ONz=zx4_1aQW5GgiufFL~?kP3~&;pyqikC3@XCF|t`l8(~ zcE^Y7lVd=#0O+`dSKYpb|IEXTaM|&e>NX@88q^S@#q=vGe>1dyyIWY2uHJVK7#U2y zTZcVIDU8v+f#lE?d#TXUr;Y(-g2FNppM$W9+Bc=ly&mmEI^F|iw;tu+Iz9*Hp$GBu z$fa5%gmRTxZRF?$b>?R-W&fY@$UlM&f{}eQDyxr5y3AsUe6<^Bom-a6A%;OF14R82 zl4sn;`-UWvkM^Jq-k!H#1K?sd>+#T@vklpJrMq7h|99F38%tRez!jzBe6~>Wo<*y) z@We~Zb3|5f-EH&md%cnOi?y5P36k$ACshw#$;PbPNNHWFiNg3{n}c8ijw+5)P%(_V z$ldQ-AKzhG^^B(M*f4fDd}lC$;)=e(l9Tl{z*k$OO~8 z%jICk&`v7kc(3PPOHD|#S7&ay|natq&{fsr17Tnr@wS@AaJ3mGF;LCo^jJWkdSlN)s zBb8~n=-NDW$&o3)>CN-u*r7`W^F8#~8Fb)9z^z26M#&3?(HK<>2p#V>M%)?y^x2Ne z-+51rZ-g)JAGxSwBSw}ak^NPBj`2W7?OvW!dD?MFl}^wKn=%ut{A%WXS;tS8pF#_% zlh0FYEN|{1&r6}azhi(lYZ;ck;(y&-<4K|sT{V*=-&1b7{R6ta)a_{t~^i)EMilfvrba9) zUwP<-<{XY)*atlGuED1sPLF4;gIh)Drv+OQh1?gbLpfPmVT^|5KGvo39{yW5Bdzqc zf!g~jPJiSEb!4_>5OX4#gZ&;To<}Bcn8|FXna7W>P$^__yR4AUd#Hsco4c?3j~36B zUPe5PLN{`5y`L0?Oh}~Vt8SjqFvuSN>{hL6I`u-L?K>;oUByf}xrYPmSOz=h%zm93 z{FsQ6$pBcwE!^}*yB@cqi48Kgy^OYd%R-i(vkWWvsw*;eG$+iD*?3S+ENbo9f+3wtOv&&$NaLnfJ5KX2JE~&?9Qc(WQcml__P) zaW&tP1=6$MNmdi%d%VAQdBtsds`wWZ#7I45YGtPw3Q?;0K9&1t>-A(dYknww+8h3# ztBUK%Q`9SZ1-7-uUN$7sWCl`|Cq|W1#Tg!%_!JXPZbCzypq||M^aK1GeG|NU*iC^? z%{MbXfU9eG=n?Z&CQBa!ahAVaaAP-KVfn7q*>n+c9+bIpcz9U3mJSI^vQj*w9VaC1 zRPSUSEL}XV0W7FLF?;BMgxt1-`uB`)zk1E6Q0=X>krnIzq!{0YLeB$#ZPeZrd+bln zI=-k<#=oC9qiteTuC`@)Z2T+RW^|2FcZhl+7}8;axoRdOvZ>SWL~@O}mAHRch4!X8 zNRgUa?vr{3_R0m<)(!PRnCh@sI5X|0*rNC)YW@C8=fpuVGEcS_+VLg+LBeARnURvU z{Ms37_hVjpo<7&>TTv?wOdcMd~|zmvO-9od~kH7q0z`sEZptigAIPk4qu1w^N)Br zG}k1m)Ra7K>9l<{AlEh?DG^&+I3V_gQzmra5^*x|3jg+NzdIDPq@|C&;9P*dj$OPd znYZot&pZlN{k4yv^IQNoa(`(N(g4T=T|xByzoPY)*^Y+Zd#M|_D@KogtbAJzI;Zd3 ze9%eBVl)N6PgFmp(I|O6FeKBles@%`kBWR>oYVx4i%a`35gd*vKX9(n_v; z(c|l1lZ{KmJ|#p)!4ZEfdBI0DpHPmo{$*~t4JyI$Lm0znNhfS~Ci+mGx!|Cr%3lx9 z{aS;rgD+|I3xaE~n>To8ib>k%p*(&E$;wgrJJjnh65j6p{Li8Cp%d(=J8a8E$l%g2 z)Zr+)V}Pv`Z2Eq?2d;EA52!xQ8o3dYFk8J*A}GzjvyQ%mMwzWl*GhPiPN{pF2uV-( zQ+Wnt(Wh`D>;;Xkk4}gDthvrMlDvM6F3$luyJ_?U$+;ufOQkH7c&{DQvTaWqZ1ruw-% z+Fkq~OC+r1ZG7+J37Z!VW;*_R-)+?O_KF-rPnSqQwdJFk%`}3wlTod-CXB18{04#u z<)9}3+uA^{o7Z{uw%m;!HN+OmOFq-Do(!*R_Y`>EJ?z9tBv}ou=o== z67p)UAxO_vs%H17)6fpQbZr}fDjyFuomfrj zMLH@u{6!eA}clb8J-50b} zwf<)KLWKoylEZ#r=r6pyu`>@SDXw%9?F|}TjreB@D;wAI;!TA9OBq73HZoYy2zjLO zgluQA`S)oj;T!_J*y2BHFe-Y`EbiCZ2b^?(R(#XXj&6Zv9mtj9=SvD-A%>V9-(iIm zpgFN?F+}m|!L^A5_nHhERC!HoIGXD(>yb>^CW6F{-LCdWbv2_#UPQgG4jqv8G1L4T z=7{3n zsd_hTps?r5|H{wV@GIcuZ$_T!hs*4a#CuMfqg}!~YZG?7^XTf)i7?p-;Z-}C&9Jps zmn2PUqc-4x7aloOAoX8q_9VjLjfTQH@RaQOppM^^5iiHIiQ0dsXFf9?MR35yRh-JM zuxYU5lHe*AuNescTax?#Taw(s)+%^z6Daer_a}<&S~Vd2Q)xw(UAaifYv>Y2e4QQf z1R8KR-zq?_i4+hv3izj)m+&Mdb!IDs9H1)M*h-Txb7ckMToYI9D`iwT)Ce?iYuO zcNGobxw{(v4fkfQvh=g~pffeEVrfuK#vrfL(FoaH7aAWwqcQ~MTq-p^uNPw+V}jkk$3kg$!-TGYuG+Pfx#K20M-pzf*umhVw6S0jDa#n(UbNFGv+}c>D<~Jkq}VW`AFi3)p*G_j8)Ql5~My|MQ%u8KEpcUL(ro znhcHY-+2xv7=GVC7+py%c`!mvTPMA!M3inA6NQHfP^`e)#?eZo54_UXR0d-LE6N z!-~0SnU=F_awtwTIK1&Kr15}!&J$r3>CzCHg;c}rpTd_hol>G|4u*ePHqJMmp-_u?tZK8?gUHv@b7n*nmH8&7tMDH8#v>0wAMS0($p45 z;eh;c99rSvK-m+rY^Wht?TH}Ij#4V8X10(u4*V+J)?>tLAxI`-pGXnS#CrdN6T z_ZzP_{Au?R204S9Lo1I74aF}q+`cZC<_L*fU#Yd=n_-i~@vQPJDNeh$ziJuKlR3DD z@3{AE?DkfaR9{NwrQl!ua%;UUJHyX;sTLI|he#ugCrB8NL1>h5|KZ-Nd6(H){jld# z{^ekE6X%kjqfj3N^x#Xv_vO(yn+2tPu`8;-y$SfIW_Vtw+Wwi}PF*Q%UMCZ_Ha^HF zi+U5bY6W`_W_>BW%Dm3GQ_<|eQz+bY6YL#w*Vp+S=Ph;&ug<x3kfwWU3_--rvZgFtr|FkiSY@UZd(93w|G4|efwO421|Fr;T$PJaoXx>N&kAzLF z4yR>F2y;QHesu{`*eW`H38XteyW)tjxDqVrt8-||gGig#;%Hfaz&F0@C!>oX7ez$) zMLChcG{KPXN*r>2qtpb5{-msgZqR$I3qyTqako-j7#tME3-FwJ@n(6cd~m|P(i^Up z(=z3jxyQBZ>r|lVo9_~3dLzBx%%;bmqOgOzS+S!pLqI&PyfGaYvwm3?@)@7kI z1(GsJWDz1Sgje(~?+FS4Tq>s<8YiyT@&x(m&7dA+W(UP5%1{75ByC;eQ-Ww9Qa5_@${^^Nc;}v0dH<%ck%vf31o^}J4Ni6$ zphqaXJ~#IC#q2WIjF7E!S&QWR4>~|@KJhoPf&w1naDu8n++M~B`5=obUzgQ+g+IMfT7YhPgDveISAiZr zn(+!}XV(}S+wG0Jp*v+b{nDP5%)X?k5_0+NInkE18t+D3a+|y}5)aE;+QAYc`FLOD z>^XXbe;~T*?m{|3>8;~>>jwb(#Kez`-KxcixS54$k8bA6s-nWu`*G|3=#( zj@KHTKWSC-{bzZxCbRZoQ*v?YK4rguGC=^6nA7_&>WHn!%gv1b0(|&O7b; zwC-?MR%R>9I}i6?x@_tC)ep`hreek9-u-|krpYC$V&I_3bwne%Sq&8X)H)3U4*$oR zavbI#oZcM?**W%rfkpVQwH+((VB7wBp}Zrwo!<8X-c)5Y%7TS+$8{0f&&#*?is(W( zAsYZB*yqC^6Y#0M`MkT{U8Z{Nv5EE9u0%$LviCgF64vsF$Ud%iVB8(tju|csqsCBY zBeO&$Gu(n_uDL{$+1karA+s+i0;BZ!S$EMuIF*#RF+UT}ql>FPHFFt!mn-YYjNsU< zF40UUPcW}j{`v08A(&+$kaBX5tT+hqC-~chwXu1AO>vjEqWRJjW}reeSldY#^+3iH zvbqeaW%#-UKoz0}i+U`e?D}q3T`whYZ0W?q7^Xg{kKz5aUe9@W`n&Qdo8CEYKdYYf zu?Zt$!qY#P!unKm4RArWTbO*lUE;0FullsFx8+Tkda{|2?N9Ty)A8i_> z4^=W*f`1pg`aNC_bGzF#20}J4VPlXz7pC1?P(Z)azgk)eoiYB{r4UW6$9Y z71cJ_Dj)Q1-GGN)(Ime4Yj@V^pzN9PDzlGY@&M^B>r_$?vobLn0sD8<27(jJeX8JF zVSTW1pVd>NWS}dghy1iEbm{j&_?FUe;1J@=hNbx|0Mz&6ngYHp;Lw_W{6sxp@z2wx zm@$+%jQyXMGjcgj{reg4gszP#qB_*3_mM zEM0%8a{GI_mo5;V(gP+UcNAG7hWU`(6@r-CcN%9;8%*3YxNmS6|0MW(ku&f!bR;IL zYqtKQItUO-S$LD|#Q7j3gXlFFUyEzyX(=WdSm+;&#H*{{fcYx99epYzwrJ*^{D|sm z9l6)mTo^qS5b^cJwpYFv@Zp`NQo3y0tzh%askx&sNOz_B#rRXa#XA6jVsmmq!-|kq z3nGO~sbAH+d&?eUJ?VNkRp%G*+c)TC4cPHv2xLp&!H_6L~N6N z`tiv2=vKISf1HiEx^vHGkx=^J(I0=0I}>_W@lCQl6b$Ewmi^*@7oq3vPE5#;|JMPX zPnyaHXzr&%Js0XthlRyl&LR6M3-9NJUH*D2?uTSCFF^nb{&U>)IqNT=L5 zbq@Nm$KOQu=Y9BGIv|b=NcT&a0ta<-f#5#Fs8|qM652Q~ihhU{QJI-T=Oq=Oes1uU zUh`19AFbzQUUCFj83O6^HjfSIM8n|KWg;L2Iw$WHzE&2#`e6#hBF|Byu8nAE-*76MXYi&SQpzL zot|}4*4rsqW$j@0U;i5q2+Erlf1s?NtiN3jAE=^4AN;y#(@PtJT>m%uLKHKh?taI& z{=aoicSVear%=034{AK!peEaz;4+lrou%BMowi@VmfPYVKC)}TF@*GHzSd=>TSFPM z1d1gB5CPG!33oec@vfEK`t*m1Rq!Aw1n8-A{&6X$kJu5c4|{&d9dac1e&vA;FRU_; zVA!KxwR*F?l2E5O-Z~G}o%CtDxsW?5KYW2B`_;Y44UiO#i@Z%GiV0ibLVE$ddG1Jv zF<-lsB*hz?@2q`Qyc#a%2Kiaj4&`np;-BZWSeluxGZRW=&IyA6zX^)GsPJ9d=UJJ_ zpecE7yfv2rQ0v&tF>LLF{+Yb5@xp5ub})p zWvhRs?6a@RW1+8COY#Rn<{G0jY&0ioK`env#iNek(29JxOS( zEnIaU`E1+pV(Cgf3z7HW>+3)IOPOQMDUF^4=+R~}MDw5Y9=u#tJ%RZ<3q;n~2I{h^1{B;gEdNO|% zlOR)n6wl4L(q5^zsskS8X3l4ARm};L@rV1D;(On1Y5O*_SBCMch~WJWqjd0q1F;rF zJu~yj!XFC{(%d&)od^g^NOuxHcI@2K-`9Pc6^xuhbn2f8Vs(uc>Y=RT1USPAs%ei4 z{`Ri8|GjtFhS1D&sfeJP;i8`W!1<3^EAYkLFTm4=Hsy$RDOG|EcfKE894pt>e(+Dx zIb7ZHGiaCJ0+>;rO?JrCD5J<1klxs#*x=bb88SH{4EKY+EgL7z85EziWu-2Mt*~nR z;7r}>hC1IpRQQS8UX8NCCS|SLg()vhHSI8OPoNHY!wZ5_WQOEuj!F%;q*hB@6Dya0 z?@6j9Bl%A?oyg7|JYV`@Z+UTkqJu*8EmbXxieq1PV=IIWW8G)R%FW)3RLe zCuvD}LF4Ja_u0!I8elwDJ_j7TzTVAJb>U6k|8QSL<;Rk*Ur#$wbf$6JxwKx$1ytK( zyjtj&(yt8`w8E0ef8gDmR#e@~5u9P+mKpDbPK^rP9CR$>Zu+u#EmiS=q>pXL@W1eG zi_j7*vp=6u7Fqt!@w|ng5A9Z8w6w5p$h9KU*wL7L?y%0TJm-lw}Jrs?Z>-oGmyTH+n{(AW&7p*2! zQI+Zt*_H;m7qt4HJz9iLeX#HxtHC>}Xbz(|GL>SX&LXqwqS(%E?8ht`K5RBBgsS>v zRV47~_g@k%HfgBDtyU6;_sh0d{_OZ?r_=m;IJ5n3oZx;EhdDQkniJLPOq^nq+|lm#gFH6zJr6$^qj8>t-{xU&?y2cT);GG3(gsiH zv3K>|I1GS8ycPgAw{^`fja$3-ZfqDCb-{tzhKlxy}CbZP@Gdl4Bav}i7h5u>`VCPF` zW~LKI=*5lfHq6P&NkPi{y3-M9CAxL-2=L#)W{sdwTFfTwSfEXI?|jL}Zvh-}+1-dG z9^_v$N4$-URW+&Ei`^LU@v`2=WH`BdH#r$F54Q9@L{S zq(*Y&n#^KJg0C}q)~Sa{?e| zB@Gea$Jf*LyuF!gCu;hJO*s4JhAYV6{B5<49C@K~816aPxr_YtFg6Y4G2( zH(+2msPealMt?o(LGh*i+)zooV6&DvutB*Ye4agkv!Avefe${rV%mT3@vWJgQ>!9% z|2wt?YVrlb1hiiQ7R1~(}q;|J4?>0daYM?>^B zFgEFXPutnDgtDpbr|K~G#4lofH4SLMS)7yFN%*4Gv5C?%km6%k0bVVr$f**Tf#S z`B%oCZ2&$|jw3)*-d!>LCGCLR0&Q)^bGQQxm!+db+gg<~7isB16|nwrQvS*KGXG8j z`L)+uYaD;RtUJl4e>`|8WVx?HOkAQjvFMynCRpN4AJry97r7>U}`QnKh58UCy7&+oGL^}^7 zIh!Hn8dx9Pc)=qzPpgf>28w#09MW;lD1FFSE2|Mg$vNUdC1j0UZM=Zo14kjJqIh2I zv1iS4X?GBfnLDtB>$zu>kADC{+jY`mH!#m1B8Xd9JTD*+k@a|k&CvI&2V7TCrH-<= z)+%NB)2O_+rfYDtJJb^t1i!!jX&x0G(q||GHwcQTYaY3^bi+dfl&^=_Ss?cBXwhAQ z+F<4{#1o`A=S#0$uLKk_)xT(&*gxnS*NZ{y3nS8EyJ1JirkC5#%>tEMk4s;7FMW?M zYPpM(_ve8_wIDHWg<*b;y|%1=jtW?14|F^h z;Hq zt^O-u#?ceC5Tzq|chn!X8`tS#w%mKuPK~)~@-Duw7kJgG1|ac$f;&Jc2F@Nt^uR^1 zP9Ig1n#4;9j$PsEm6G9NMd1&_P2Xl+6A2jV9{}Z)`g7M^z_ujd=en9JThmM zu7t`8i;;x@#zhUbX%`f|^To{$wi1VD4(%6=X2AUPiLLZ#z?16AKa*L|f}oGHAJmc~ z3n}T+g2*JrXrVeIx!58QLr4&>E2GgFe0{!h&btivoAIqH9E16p6{Tx0BIO&LB0;)n zrkRHpchDal--X;l2YBTI#5I^jHm$j>mYOEBhcDdX-w=`vQdO~i2 z2EOGA&W{AY{8lLx1oMu_cJi(ZIYp_rPf}LT!dgayIlcFAu~WPNhaW^G{z3aDa%z15SzBG; znE$3i%}8}+5&V`-!nmeaz-IrY>WfZU;cs+7)XAtX^JY(L2Q{goUzOtL0@I(|0X3v> zKqI}q=O}=>S3z9^6_K`q$m=4pK927zhMq^5E5xj(Ptf8n5j| z_G~)6R*bg3v!ek|^5$~pb61obO87N~ztI1j=FItIC}$nFz-18NbpY#JTT@?}s&N*2 z7Z7Q8)*mAO&daz108<-eBQE@|{Yv0YL>vnbNV>688Y?MJU7 z4cA|;j*X#h5{ytXMLeosCghH21E;r7y{3-+L+!3kd=%Hn z;T^a~K%q}gyA1r+w;-!7U$)J@w95kj3O7#qC11CBtp#;+G*)l;gf;1rFW}B*v2(`G zU}vW`YoSAAb=#EK3+ys8%)iTFv0;k)!L=8B!APm`Op32p|6T%q5+d zQc~zCmqwVIz~+qJVWZnt#2w+l2EbBfY_fZMG(kD)WL5DOWc0SQT;>(kcK3YJIY;iJ z(NOTEPATl_52=g-AfaZz$U9PiqMhu2d0q`LDy$_LRnmsSEUqXE>Ts%bbZOPjd;dL=ZbrG=A zm4obU;lArR=q%$xjQ#@lC9nefHp}(JuB1oOH<7yIW|sQ3TM1tH>Vx}@q9jO4S)|84 z603nt*{NdB7a3i*Cnb&%lyzXDU7NJSgZFrBWJK~uj+!sIi7WfX+xk6*IO&SWsZDKD zNNcBLfX1^1UL!Q*rLfdZYZmSzn$*bC@EhgM^&QtyW%0Cz{iWuIq1Sr@XVomGLuxfV z54o!DH&1N@ej!glhy21q?%*`|arA2_q_yhYc`g@3CF70QZw_kVR>b-bb%zfCIw!jO z2+;On3&dFcIJMxuH_I_C>UJUQ8YPoH zQxx`afA8?{I--nfO_k1lY)Av(i`8Mlm&Twa4=B4E?YjkiSIm-yK_@yh>}Fr-PR(+^ z1Kn6MCJ>oh4un@Mwc=>Z+o>KFfsZERbX);zBYSAUKFDkjABN*fW65Qw{dYL{zpVp8 z1D1O-9L!_TPE}s~WlMmZabht-{Vh;qKBc{!)P?g`Ik|G=16R~Fp<8NmOVc+d4tebp z2EU|#cE+JDP6TDM-uE*x0|ch;FXK^rDau!qH!ME=#p#kIUy-koXi$pz&V_~ z?mS15+(d0XO@9Uz-mvT3|N)Nw>{)>{& zz}(griDG!!XL&zieghN9V}0o%SWz4quC80CqLtzmn6>;akq3as>h#*b%8@*@9$Dl@w7N>Jj(SUVK>%Uh)&^I|A%{&2IsSrxq@ zKk-+1)Q-|sHBt2a>-&<~JD;Ch$tSS+1UhGl%VJ4*XZ>fQ=8`5u1Su`FOKXsg714G-{DsbTkUQ zBE@f30_}ILfSmZxu`2lK@A*PQ9DUI8F9ZXiO|MD|#$GOYve!5j>lB~)*(Cc(0I z&w<>_abl-0Q#q+2%POh;`*yK+Uq@*8>(7LqqC1fiu+i5YyVWvI^r9{y3H%(xgG4?jC?yTBx2=}ilv+U<+#jT=FYZmOP~py}-Iux>07>)p zdpNEgd2s;#4lrN0&A+uNiFv!LQoo~q5vySj8-AyVr~Ixhzw~hv)MgZ@N8(rl6WDpe zc>0l-c~SI@n{X0~eZeZ5z2;*((7vo@##wKg4QbVMzhpl>i03`=Z%N6d7uq--STo9V zf!cThgS_ocz&%d$gz%?e&9WHXLQ~Q%N3y9K_%jR*@|DUWdLd;y%31T0!Ib&Q|D+7Y zY@)Eb2!XYf27%YKT&Kd2nB-!L-QXHT3>uQF71UPx&m5>(J}D~ib+eMw`9L?+kSz{# zy*urmd0WA{3QK<5IUG*WN(C9Iwlas%B3R2DVj!FeHH3UeUw=TO})TBS|5x1BVJ zit5@*d>DjUvOV?JjO1_@GUYBZ7@?%JxOXnqy*j+8?l~iz)$s%EaG(6-4)5xZbAElK zRk`~xsnzjaTv~161>~ExJ;3qdS>0}%SqYoax)-D*vbPkw-5nQVbI>lYRfq|An%*+9 z0i>f#wG zOpC2*DwRvXJo$2RoO+epz6!$Jy!PWRFE09-A8XC~aFE(f2Ul8}1y|l=24ApeCG`OB zEHaqEd-v6HM3VIaL$2F}NAl=w7#2H1tjO|59*YFEgv8Pz1`wOXDzT@{(Ngo)4Sjgv zfuUOB({WG|ryKNMm+=$#zLGw$;U~{LH}{s&o2&c(uLanw&(WlH8k~;ZZn>|4y48g} zCmH%hN0Ar!y5D|2Gy?f2m|1F*-u3{Um0qcC){B#n@zxrrw5D?Kz+EF5Q3|f$zTXK` zs&DHc5#7mYkN~WKoVu)2F1yOs*j?4{qWB~zT4y=0a0M#$*+p!Z5ABQ$>8T)HUO&E_ z*9E!t%*^JDDVY>TWDCO>*-{5(4cc}X8_z!+ZBbB>XnBxbWr#Eh-V2RQiGmc+{%qDy zt}xy0#tu9G83UyUwgejvfX|NM;)w5GV#sHGv#(1{{;o5LqtZ?D2f6}hOa&!^+<*4t zM02Z8zvxr%R>9Ez2b(APRhz}>lVoRHly^&2k6y#5b6uX;^I;8G8#RUBwKNWqy+4SKi5Vq}x97)3GGjE*v7fh^L%{MVC zL0@mKcHx<c~JOlRG;*HDp9eI9d=lI+|n~nF>;%#fBSUK^7g`X>0S4- ze`D%vo!>CAL;7TK6WNNx&X3<}Xn?sk}Vg=>%B=b&<6D=GT8OBH95sSQ@5d+KOO93z3P*5L? zUGGYKQJf1T1}7f0tJ}YfA%@DU6>s0 zd~2V+pu)(oIT^M6^BirTPh7e*8R;A~uZDiBxAU3`dDEEx4;H)%=2x=M8%*50s*&NP zI#y#nBSIY-L?si*j=%X~5sV0n_vj0D{IR_p=xEn1`L7=;3iS(ixO0qqBr+%Ut+I1! z-3MZ~jhS=A>hGSkNmu& zXb2mFzw%g@KYP9-8gd$M=KMKo>xq6MNIdtVE-j8bcJTquYQR4=!h=nB`_1fQ)}6HN zTz73Q{`&pol>_BPA!n${(@DCrnm?$S!So+?2hF*HJ-2I8mW=O%$R`ylBDHg}sT}eRFj9-XtSmut zmLKsw97F~6B$h0aFP&wyKcry6ip1>w z8JU#gl`)3~{5*RP`dukQH~6z|kkd+}L300S*0lnXRq}K4+{&8Buavl=3#%7Y?P2mk z@x4AnDZbL)Lsb;j_ybcT;(JR&p^qG@h1}6NZ(3xtO33RLGuG zCY}{2);pWK@ot(fOc`T_(^y1BR2`H;MNokT?R#(wIFe_kW^Ru$ypz~{7(+kIS%6@( zEoa?^@CiVt+IH3C&hb#YR-GO-Yv9Ja%qeez*Iu-~C;sX%ZTW)}U2^Kp&}0yXZd zCLA!bC&QHRJnIxho+jzrM@YK!NOphcoBW%eZ=)>ZN%I_;%y8C*RwoTRq>mCTDw5tO zM^l-Qm1b~FBX0PX$F#)du&hNh0$C5j>QvT2POM~@l>l%DAN>-x{!NB7w($1u`&LIP z9V>6Mgv<)Y=>o?*)P<)bm6nbm6!IDGd;$<A)sxT5Zo^sJ&T^0rY0 zEbHU4KyAJh@F|L#O)`W@GLXzzdws*8^14d!f+3W_=i$nHsEXdWRps=kAF>K6_|wGD zq43je3SzP9EoX4+zNFjC=&Y4|6utEULL527?t@H*aDR~9L%=sA<+_{}x3^?z&{r}a z07(piQMDlkodfnPOSY_B_R^tcr9rYzfn!13Y6WRfQfGD1OwPJkAAfCS zInF|a*wKX@+-7P(HOWXex!m4rou${Vpp?cTsvLlBvV))JU-{g^NED4OCG+d%@Y|w_O=B<(D>|beI#kO+ypGaP|DY(;Pc~>f z`S{GjubmH{WOsy|1-J0rv#?tJ$*7-F&qDdL?lF`%5UYJzB7&R$9d2viD0v zzj~$V3nLVdx7st({ArsBmeqJ^g#$gJ#geH(!VDY_|4^Eik$)OaRnf&X~J!U4M_tmKi&hHEZ>z>8kW%ncAb0Elv>XpEf zruIMXraiW!36Y!=@#mdXT)?p2Ft@;R?#z_$Mq;yyW}7vi9_$yN+lqW^$eoNi;O$;4((LLFOd82_gRRb?u z!oRVrzBHtf5A)|8)HO68e7+3M z`Ry;T;CHInRiAEm2duZh?S*0Pp&8puC;sluXnl!bTYDhfk#+KX)}1^t*Wlqv{q%`E zk$|?b@8C|`^|B$rlFE>8vd5KPwxqQ{`P%!_{ddJCkKo~BMYB-*7RGQA#i<@LD~o%K z({`jwYp|5dCTp8I+6Z{)xP9(IUra?+{9WBR=9=Y~!ydz*c4YuK?(fZ4Z2x-vbwrgT z66ekxKYFa_{3}t-Drw#ZUumAzmAC$g*$)Gk-!1!FIMn^OEZ4Q>0s9TvijsDw-AU8V zlXauFWD%h2al-c*OduW-b`+z2J-CXy;KEHU(PvjI!`X|<9!Yg4gJ`BBlPzoKz-%sk zh4!S}Hd+WNsy&6x&qDTMJn-%w_?Vd6QsC+o`t+OjQyTo%9(yHj;f~P!4u-716#0$A zzUosK+S$eBzlttVB(69gG}*EE!%v`-Bmc1+v+4y8u6wC2$ITK~pDU^^r)(pW^lNhw zr-RT#C21;}1LzAqH4_}hYJ^d(|U&Yr+O2WXg;ot!#jz>vIUO13vrCrEX=jdHm%CRv0 zUl%^^g%+Z;V+%-Fv$ddT$Zx{Fan{v}4y1Vh>j|$_GA<6V#LMjY8}*p^ra5LVCl=$^ z{_Xy-c3yW_7=;AAY8UCY8hLK!d@>82JxU875%6GMYP}C1Nv=xe%@nMI0`i6<|23gY ziIw*aBDn1|Opqta$%n`u&oafFI4HZIP2Tq5S|=j8awviN=d1VO`VPiSK{c_=8_t|e zb_pZIlT++d>yH3N;e&PLX?6$86ANJG1>Zg!sUss$z-5z1;+U^f&V}O5u3f-+K*VfC zbj9@XV$12iCL8iZR%7N;F*P*G@9#Dfmy*55c|`mDR_e`WM^%;m1{P2s@wjC9+iuPX zz5hb|Katgo@QY{KT~f4R?X1^DTJ?(A!r`QG|2ra0;F!E;b z%joX8r4FN9W475qn?kW2dGEH0j>q+7zWk8Pxn(O}S-ll6=K~JhNkHUu*4FFUR!pJ%<>b?}?(uv;?f|h<{yFV77 zWg>W%7<#-vuJBdEdqWy^XcBXxwNpuHWx^u77=x`*ZPD5R`A()rsrylXT6Z*~C-MBMO_0v}TC$cx!ZE)AH$JBJOFgRZSU~GS$z&tayik1bz zuFoDb6&%i%gw@C|_>bE*CKp29XKRL_Ae$Ht=Zwd{fYprXgN}jAtp^HB_KRrQw`Tz* zLe)?{h5hH?F_|eunw{DBOhK&!m5q_3e@BH71;YyP;MuXslHsD z+BLb@57vYQYL8*&WMHqiRzRhpflUg6fQyB#Xh|Vn20{4{(N0?M5Esqald#Jy%bd+f zGg;?REA8JBD8^;nDH^D978>js0q*mk&(V~*qnj(8UzJ9eG(%dbd7jWQ`?juLT1EZ( z8M(Y#o`WW1yfG6gnTwlj7zU&P52#GI;@qj$>-rJjd-r=jK2ei8eh!gz+MuNyG6>Gq z03@8u%*>-vyD@aRq%sQ`jYX2>3Fk{UYWV72PF?8XC9jFwIMtg6=U1hQGCTy+;MowN z1&pTDS?PVppSw|ieV+1OBjwEP?Jc2PG}8cjnAbv8!VFk*&!-}y4?gMqsf>4&{y-x zb3G_o#A~X+1bupoKADyJGDfh!xQQ?r3JqJ6e3p7F*>r({Z#5ZLvA24~tEiqZts)|% zPjK&>xz`SM9X#vR!xc_`sKW_TN~%I^&t8suv53Jw?ZUftEa~o1BF#55B*P8~E+_j_ zunU3vXQXOT*j1YSnMW{q@BfDXS26I?R*kd0_4c<43FJ?mw?@(zFYIi{YPL2#}=QLr>TtK;R?JTN(41X|-T8#{WC^Z0T{|`MDH3 zImeAIOL~eb2DP=5m^e({5*>jhkp}IM&GO=on1PI}%Q^L5fa}F-2_x+5x2hf3tAt?S9+*}gyZev!!4PVGx3-CoFcf76{tVIHs!AC@W?FD2#tZRZck zI^uBc2ey5DGUa5M zt+YWXfr&*xrJV^Yb?FqEeTv7OcYd_#TL!}SSpNIm5g00ichy=a!&7ky;NI1vFy*zf z(1?uPN4{J>Q`@VnVA{l0bp2)Hu$x^^9(2EWX83_hDf(+7D9c|ee{V5q^YrmyVuXQf&VCtz8=JXM&vM6+>UL2 z^>gQEt$$P=Z@%`b6O0=L{vErd6VcXH%r4V2ntk9FD4R5;8F@ZwqBv=m@kFqhS)ERJ zH3JFdK5*jRy3!l=7Xs2&-V@U(txi}W)gfHnp5D3>hGO_?jo!A$1qUm{ct@}RNr}{BO2Xb zhR~q@-x^2Vta|0X35L^wbTw%|G(~@#Ye}U7TAa|>kbq8BLF4)&`JFcZGh5VV}w!OUK z<9tkxZhiQeZ`>%o`amDJf9ORN89I5x=@&T~vN>0uy1zB4h|RCfpn=d#LR%eLvMB1m z0psC3T2M9W$@TLb=gg4Sob1vtenWZ%)h*Oy8M5tX2MD4ZVmB1&%W^1;thsbSty475 z<2u4Crk*K>-=lq?UrmHoK=pP7NA^PA-T2+`FKDa5WF10pPy8b@7Rcm#IH?>Ces&&z zY=Z}B(DTbqu6~>uPvkSCrsSd*7!EYO(970n%#Lp8S0^m_u)wgc#RVxpEZS%Z%q`Gg z*@aG3r%ZuA2salI?83%(Ic!zu?;DX~KaYm$0J0dLCpEBsVs*`N*c>I<06EB(ok3lZ zDuW)=;I8($>}YHd_%QFct#-AtC^HU$D4j4v4-w~&{y!xIN+A9grq@8&d6m11(CZC&qO&XRO7(WIpe<8$9>$C54XDx1( zyCaz8H(J!tTBWq;2i6RQG!jzi`+D|al=1?FV4tafZxc0ZWJa#ET!TrR6mV8X1oNok zpEJeX<##8eG3^mlK!n9z8-v3{M{gU`4@H4KsOZc2LIxUKn;)OM+cp`#fX%Y5>>o4G zCni&M@{1egKJGP-^#%LA-M@bP0&?PHFd0>VgeC66Ni|+)AnB{B?^&#TeuOxZdu_YD zrf5QY>DBFrZIl;%iu?uz-zKxE-2FX+p#K@rBP60g8bwJitaAN|IvFc=T9?k}UM1N= z{s>CaG>H;Pgz93*$~)?qV-@q8LA{47U-M|{bNjI2n%)&oO^ulD&kBeOqM8f6OpG79g-M#Ez_ z0}US-a@KEL+tI$N%Pe&SC6nKQ@_$K&lpH9`W}pY+ zbchU1w&r#K-9I0Oe*IN@S!y!p*h}&mLQpkBD}fm9X6|!&^a)oRI{gpy^cB?OH8hxd zRnTRJ)8qDk=~oQPr$!%j0}|kN(xDxU#^S-4dq% za0a|XPP&sMqeXLw7?hq^#GaqvD{#0G_kCL`^7~3?KKiJUGwE@9TR{77SkOMd-?9C{ zBXb~STWUHZ=xFM+zQQZ3YaZ_ch+MozHlfhgsq=D63X}hnvTKB4c=?ahvOInv z+BivNm5~Lx(ZkpS%fw}Z=e@jsb$PL>#Selu%T~^tU!Ag-#rjCo!bcg8hT1DK8BFUg ze@WmrbjY`X*h+sS+eW+s!<1J}MB=5lu`W#J_XE<(zOn z|K%*r9{9^4G8_|5fb2C+-#dYuQ_$eo-M+GU+59weW#I2ibt=OB4*Q9{FPvy@|LXxv z@=dX<5)UAMv;KhJmo%$z8E=;QLo1kA0mF_Loi#cL#XQ78G{oo9%p;a5AnG&Su z#lynYsyr`3D79c~qaz(@|Bne=IU?zW*2SnZm;e`rJ!K4@+52lx=BwUTfm}>e8Jb0; zOG_PE-uU-H5smF|I4I?h7@Q_W_yRiycxv93$@e{Q-{KAG0#yr1dW%j9w~Dg-(htn2 zF|Zr8_0) zN1h||FIB3HBvZo0JZL&QD$z#m+6$;7DGIMqAj>NLAGP(VdtU#hP!c&+H$yw)D?wxY zHsU2dBJmO?_}1#v3H#6^k2-H37BmMFN0D|}-MK|)&oF6cG(XPVZ_^HHNS3zMOLr2R z70jBK?3qz9ooufQu|;Lm{=ir*y&03h139pJ^vO(rxs&^q>5G;D=`zBqUy1rMLLc@U zfIN)K*PY67f<04_d7g~@?8T8q#245uzV|5?vCU1p8PQ)xX+UsWQH`YzB}_I) z=)G9u2&)eq|GLnx9A>$sCf84M$ki^ zN*`3iNIAZ?dh31JX)wLQc4N))mk%>|+RVaan>j4;&iA@=WnnD!USqg)Mp z=qG{!*1OUk0k?NlY7F;#cRMO2vXL`#qfu%8K&Z(F?m2obzZsI02<-j)e!?4J*jtP# ztnKsGkL0+@`F}K>XI#?l`~L5yja$vkOcO0f<-T((DwvA5wW&6;D$mvcJdD~KN86tA$U;DEx_NeM@Hq-4wnqr4buy3bSiCf6VbP{(~ zXULizgO|_Z$H?kpNVFOnG{*?}cSuaZr&1%tJ#XkFI>-s*rBzMQDt+Rdye?EqvYaIs z1#kU#sNBVm)bU=*;Rc-z+!3sOln7;LsLCP z=Z?d#&09&K@+{tLcD7`LbJM(3^-;M({a2Ts9g2mu#(~mh{%=l#2#hx<%h@}~kTCly z9$Ke7UUEcM|qwKDXBI1>GBo_fubOJF)PjnZ)?K8T*EtXHd@FS2)UNOGDo77184m-# zB!+vM_a0TaIW&rq^vE6G0Qeo&``c@0k-v!08l5!zL}|7{qL_`$G$f=DcG#6bgR9Sc zM5{-6SV4n`41MFJ+oK;@<)z$RC)DRnni#v_nO6^81dZ@o-qc_~m_W9QtvJymR=gxB zG2g0_WLnoM7Oni{@j*9lTBR(QF`~1q?l@pCR#ZX56&AT4oC+`s@6(QG4~1nItpHOd zPxSeQD}?T_`wn;7qE8a4NRa*PmChXuVe5@F!D^>2O}-&IshMBl=&l$-0`=~>c?sq1 zw$CR#jDW*N{-E z{%AiUrJk`sLYYg^?j{o&|DOBFSG#Z5dx$vzyxarbg>9h{GH)56_IUX>awv<`D7T%k1nfgjnc3}CjHS3uXxN#Do$giR zh(|=gg&^-RIbT)eJln4*Y`;YS=*`5T@ z%zm)PW+N!0I1XJUln(xRb4;{|rWDLB^(q?`QmaQIddXor)Q%6#+RlfA+=?`JNQy&o zWq`p}^7^7$ihS!>I5zGez)unHqk;X-HUh(AkBn-)EvU-bBfj)E-BW zPiLp{mxC1|T=<6m_rlR|DR5Zrp5~txN3R=wH`M*gM1FV46!i082);9cEYSB$vWI0y z6764c!0>hJ<`@c>$~uYlJjyG@3u!h+2U6PsDUo5TH1lM{vY_2b_5Tw8Vx!ELsx0Cs zJvdhI!L$0l8C(T$#EueZU>9>&8T384aOhdi^toLt%aG>*TB5rqyS9~B6vX29R*B9L z^&okbgLSyaZU9&ZR?nS?{q03OoQrt)#?uNlL97#5W>)^)gPvI(-fg*~ z$9^EY@4Ft(XR>X+82h=@Zl$bvzw%x&7j!Ky(40eNZU>pMb$-g~92yr|{24|;8}^#n zJxjDuB`SPbBMmmPoXPfSgRVqlsQ&t?b#F`Pbh$40YJk|dJ2ZK^=A`U1BL|kTtL=-WAHmHg(c%J4^uh)g%l52QBf^t2eOpEMdy03B z{ftHC0{p{O^m_1ra#pQnLP#7#h|l&BJ<9$@O{Kpd6W}2CmpmGN+PPIE6NC2}sr0w) z6$7w|U^0yExt4&cg#J(71_<4?duk}FbYtgAjyM@#ua;#shJKUUdtB4*i8O#k(-V&qTcaOfH;Z^m| z$5&e&y-mDhDR<-tQ5q=(7!UJ8BHii@{Jkz=P946pNM>G(SPtjzg|Uw2zp7!LrV9qF zN!0+|2Qgs}%)u`9?oP2kAu)uTYe+!rKdjD>T)C_3C0lT9f{xa1vyr|tp zwQS#Ntcjo1R+-}N^D@&on9$iZy1GIwKHl1XR>fbczS01nM^D!&6TjC|avQdIqV#wsy(~+2;-0TNBLHj;h~ig!@OI0QSt*`ixge~ z)m$@Ud?@CxX33WzNquu1I{Y2=Zn^{hq1KL20veHo-M_kKkp^Oq{&YZi<_?pyE2RLc zrQPUXnQuqb>0wW1sDBwZ>+lFx$xjO(TkE!O1-$igB?o!ffc=;M04hdlW(vj`EnZJ4 zgyiumcrEnd?evv3P0NDyL^3*1q^*E0Xc`eFm?sGPdq-xx4mCXBs(`=$5z&<*T$l;; zh4eCa4o3YS`^*bCX?4A8uiJzCxeo&kP$9jmJ2~@zx2V;&P{r8UnC^2KT5KkaG4qWIY(yq>hs)J?r5Ev?-!c34N_Nrx@9 zrdzAW9P zw%FuD!f)&wrhF*wGlf()Js!ok(n)CVPM|Y5sPB;cbzzb0e(gAk`#_fxV`}+V7;My@ z)Gt0O&u7+NbQ%32EOIsnVmJGGAyFuD_&$k06`*Ug#ZE(`*K=yri~s7C6@#^1&Me%u zs^dXE@^MK;>iLwrKH%WfC$Wr{U+937@_t5SP%Uo%rd<#XP8g}2nMx+BFWUd;%XJOO zA7&2I{%Z4Srp!||cH;kLj0DEXc`pX6nT(u*$w_h({RHE7z4Qlo;xcG(hf?Eb>eEK#lKTI(&wAXlGPiDsN#u>OBd?~rtA{h+?M zDkxTd;YcI`*wW)RKPMrwIJMQ>K9H0@k}Y zp8q6%H>cNVy6IbubFF@y1tsW$zcuD{QmLR=)0qWR)6nciv4)3{<{L&M+`Gq*=xcA3 z15SIVO#ZNLW#6J1D_&#qy=|kd#-MZfm=x$kfwOJkYXKdBby4%fw2RrKr}ztq_kdrREQO;oEcJmR_PHpgtm%@*y0Y$M-?#W617M z%vqEiGJ+}=Z3GKpA72;ILrPu2sKIXkgX!eiW%8E+4y{TYbeunIXTPF2lc}b$R}`<= zK2n=EP75sDk;~Y4yBCkh6NpW{mb~2*LX8|`aK97tdjDHS8+JjmMWhhKJjDOcVGP4Y zV5D10A~zQ-okL5lfF4`Gl<#aECj6koph*Yt@Ql^XTRXTs#TfezW$k-*H%glPHMD~yAUO7h|CEIXf~d)|1LWg!q4_+9ZDOLLWck8pc=^EWL^uW=}TLJO-o-?;g#q(+C1A*^IxF$J&wK(>`&u$q6|2A81Kilw&=A(3kLe7KV zzbdPN8LhvovQl?rlWopz9~5Fahqwi*-mC1G3%2ii%{84b;W|<9?%xoABpL;8(1@^5 zp#w~!X^Y+eU{jR?lCkc1?4gjIwaw}}v+qi4?;hliXqeH2h%t1?mvGb%u3-2w4Scf; zu$$sImlYAGqsw95;^hW4oQd0(DYj1}^gmW1>wFx}s>)=ZHwzx?p|*`tE>e!=So6LCfn$;|N1 zRYv|5l*`-CF9HI^3qS};uCDU6kdhJAYHa3@im0L9a4Q&2E5q~v}{oWL|e5?i+QW1cupdmEMV>o*fh$oVAAEfe*`t9K_|F_E^oyybDZG z)>eI2dtVq7>l;Rx63Yi7<(k%0@^m0G?X6Rs0O zbqCe1lxT}Ts*euX_B>->a=8lsxA-?tg!4^3PEBs|uHC_v-mQ9F`PbIPW93O6mdn)h z=H(V?o9ozgw_9x_?ULC0+W)IgAWa7I4X=7{t^5ZFUTJMmwMfPuWbW%8vBh?7-kNn1 zk|6Dl+0rzo@|5)94~w9)fO}CLhCKQf(*KLBz+=pdY)ANpBERP%LR|N+WP|t@;dl|f zWSqh22}#*!2)2-N1Q7xYb*YT8TR8JNB~Y9o^9sT9tdJ)s#=I%kzkTG8gWL0;*=+8K zAJa#e1L<&1Oy18ZsB%X`4XLS^QD!qRt|{CZ_cQTN{&o=luag+w7k`uCd130`YIP9# zVzE%iHmM#{+v*s7h42Z+jd_=dTOTq-WEIG-mn0mJJ?qh;BzW&!V-*jb1}w-MIOr_gZ<4`}ec0OXNk6 zdCG;LM%y$k$BzBzH<4%)w~+1j1@-VMd^K|X*~z$-D1{CzOXhJzZ>;)<<7?Elb7)CF zk;<#ycUW>6^>KWJjw9)yqQ~X@edZEf*5+04e?6ClJ~7mBhcHRZ=Bg%3`(HL9(?cLs zR2QZ=m^}UxQy@mWTEcgSlThEB+w%v_+#NMub-+yd>03s^D_NJC|bKBQeJb;cip9rSAI2ijDwQv zMx5-d0;1&*`WUCht&EK-hiP_~@C|l#c}iB^wEU>mzq)AG?npnlovmYJR`ZK%+4}0$ zX|21ekwzeVX@H^wm{0=b#bcYxm9IA?Ez8MV>wSadHK$WxzmkttV&;~BaBBTawl71d zN3t2BkbtFnZKHUds+w+^t+wJnE7 zhSvT1Ke-A;!*e9L}Q?5&_2i*FAaAiNtoP#QDYa4<8$E-C4UlE}}s`2beF1gxDNm-O07OhYpUBOCNj# z>3P}nMaHPuQ&Z8xWhR~~K$JPnI2Vf;7b(wba^}>Yuk32iJI)Nw!qsj}w6X1Utef*b zd5|xoPL7>zMq*Y2W~Gr}uC(2J=-(*q-1mXdu*+|^=4%*Jz+yc(=Gv{Du?qJB*mgnh zve79(gXDlwVdqN<4Iz+4y-CH&2NPn!Le}G~e zQm*e{A`B?%+w7!p_WIwu^5KGf{$_0vuIiCd;3X|mvUbnP5D|7Q1u6VU-{P62R`G!W zMNF%31sP$w8|3v)jUfGWs^=fhuzcvNb^8$z|T1@!yzwW`ViM0UF@4;PC!^daK=hUW{8dw_eGd8`7t#QEVNxPL3zU6`dUob} ze2fPhl`S0DBv3H}st<5AE*Ian)!N|?bRU)%61JG~)rsxZpVcYf$FbGEP&R^R`2 zcZ34RZ(ID=UX;I7)ZB!S8ehJK4ZnGPg`FV!Kr;W$%?y5wV(9B3p4OIovNyiI$7PN~ z*}VFI?~!bwl7vwIA1&5$Xh#t z-#CJ&-)oN#mwnsus;$s|xHPvx_#EVOjB3bgf#SkSNmF-eD~WxpO6uJ<@Vy`nOTPh_ z)IW%nAeYA6)U3O$)GDgccxgwX{h`Sa+hR=_qhWFUuyjM|KLse^ya=c~QB%6l$2q&p z?y&b*gEF7oe=IIVL6g{%qe8E-t#H>IxAL)^fYT$<8h70V-HMLC86vzGx?=$^)N8@b ze#lOvhgu;zo9^AZ94*|-1&@v)Z?DwQ3ZsNM3WU747Nj~~dC1M#zSPGVq@KE6&y(GCvU|L`9-Fju0ytdq%)&}L5k6OIY3?}XVg5z(IkcQtO3jpOR)vC@`sbdggZpdj$4kib zk^8!d8Ua2s?&*f%k;zo}9h-FU)n=M;vm>kK(kFb;|DJ#7l8lW_wJzqncQzftx#Xe= zYRz4Dfj!}4HXPN4XBsrh(Qhbbu7WxVs z$Rcj{01J%p+ZM#WjI!;1$uu@vrTjXy`|E zrXA65fqlO{47pmFbGc+8#_$gJ_leHv_r1|P5tTxKj&YVLAvaUitOtS~Y7Q+)`2fqN zC)8V@s(tY9a!gRRT(W1*JppsR2^Cmjq%!M3DC@o~&ZU@L3*B>f*j z_nPPmRK|Wo?5zHGzvn9CWY8(QdJ}gSjM{aIJ)~)nLm^D^oTfXe1qcx;B9|@D#D0n? zm}-)0(j#~6)%OmE_7u6D@7j!rwVj=f$9ZIM0rps*S5}@k`D*)VX$qG&!{}UGh}90k zPYg6+a8KABikWF#=Rh*qlq{3t2h#PvE%QiXc}l#P#f0!RrqHi0-9p zKLXrv3ndCTbC`f~=GItAs~M>CI7hO;!QH$ub91GWU2`J=sE6wmjDx8y&8Mx+BRhcKPvPRQ9q;bMweu*Z1wi)=6R%#(U5zPAAgSqcdyvHTGG5@;9y(e z1#+wR`+MO^??fY0>H3y^ik+c)hp&K#m{bmUUU)gT{@w;xbzN$A6xy?UbGbE-*g=G} ztxkkz{d>o=UiKaw4`UmnKGcf29{VUQkYq1;!`H-prI{NOTP82(m5CNVn>yMi)3!`m zeX4FKtrk|tkrQt6VY7s{bp9?RncN-GU%{G)240~2+svqbdZ1nRUJr|v&eA%f9wUF7 zF<#8v#N=|0O<))`Y`(6{8CTKUwGvb7o4s7s>`1l4>WzRz27JjcGjsnI%{F?4leGt1 zB3i-C&Z9}?x1~{R&+@35dq$g!n)9lH2--D^fF_cPU<*&ql&azZ4@XP{QK~37as8AJ z{A=jcOBHjr=Y>qC`)2$qDdavHX3TiZhHLqpM+l{8<_?*?RY1k|5B^y?-TY+b43CzA z@$CPx0K#_~Z8ku!S?IYI52N}GQ>jj^cx^oK{2c9oT89X#0v$Z%d;e2xyCfs5+rg#x zlEmfHx|hoQ%J*w+KqRbl8aucxi4RuLk3T%hNJUxyGMj;z%)!04!_V4Suw+5b2P3QO zQ1cWGsU)s`Wmx1Qcp)*+`R-_Li{@%%sJ5cZUJX~5Pmb|HAt&zDIl{P*-hcoQ*RW3R zuj|Z<66B@N{eaQb>LAY_w;o)XKY=E9dWl6<=SS7{yC~zxl`h`V1N(X#+)^wmjKzO+ zk!uL3`ZD^IUb&LbVRvRWI1hU}5D`}4f*P+t#|U=OR4V#3SeTyR%d8;7P@^U|Cq^{tRd2mxlec z<&&4GbAB;kqy?_!u>~{&v=mn$j z1`Bn!ifkV;QE}#1yY609Z(hRRQed(4XCI0=t^_Esx4fS8yUJBx?Q(2*#M{w>DnNv~ z?7$0VY!X z(E&nZ`~-eh1P~lD_;->+;1r2js~>X$6X@k1UO=cgZvAz&gXtVkfBcnO^LG5|E$b05 z$0v|2O1*+Cxf~@|)p_CbrpXResMT7f+TCNs(|pJ_*I}tjHu5sUxSp5Hi@iv4eFMgR zP&f@ZANRQ@$k!J{8tkr|H()5{5Pd>r7{IIl`K3RlOUf3 zF6ON*Q;p%FOaloKFTa^~RhwCg*p9bq{qPUR+Y&xMl1>${9dp~>WtHZMu9sqhxGkqe zHQc{tUo>D)-d&+?rxdiI5hEvsF{ldONoNN=Tuctu zCl~%+ckMB&CPsfgzVJe%PEN1@)v>J{NDXui=C@be`h0YeYS3L+;#5sGZ?!nKJOk@_{X&3V|E_aMt>;sdc%t}Ied`NUpY#{jnm81PpX7z#Iv#6c zdssE?Z;WM6NCsAc%=}|GIPCg0Qg^@x_J5|r{DWG*Zmax@CKde4Mo@SQLLJzoqH>@dJBrE425j*9?9l7okLjpsiE!kcIN zu#1Lc*{JI!_&varKPP>h$#dsI{BKk@bn@CV+w1LH*k#R?GT_{%i}A zR=-ESBy;0Ai@vixsYw#Y@+xIvN&pQtl;#pn|#>E?(_wAA$(M*bRb;g1Bl>%2QJcfig{5as4CS&{Vmn}LQ)&05v5 zwgoH4Mopm{-?nV0*%)ucN=_YdPmtxmz58yhqvVwHUC~k7xB^Sz)TzfjeNE!#h~ntW z)~y*W?|fIRI=unBmjRCQ@O$Czb9GMdyX2p?_`#9W#Hij!)Tj}r5$>wyO=h%(=nNse1v?O5}&7e z4E{R*LMsdkqsw{cPHi5qgo5$kZUj06WCRPy!hH?qEA~uwEg#UJoe;R z#3f^t=*rcCDUmxLoPH)o*aEB^oxxUi&nG1;MY4j7`jImu4s17o_qBJGwj%PTBNENx zm+#G{wN&h!j}U^vkJw|=+a`WXJGESw3*THtlm!6_6Vh6}W6Q6JE$^)pow5 z@&kIF_8_)sze5FLIE<8gW$@cv?&iT|qksg6-nzv**AspX<)GuYhWo9(qCY*pc5VGh zHl0KNixSghuzO(pD*p8Q?!Th!yrK29KNqq09Lo4z@Zz!EoBSai(FE6`u;H%Rzcm4` z?Jmcd%mdMUvPMfQBMiHK{P&MTSE5`_a)1{ z=o!!ZHd5i0){GjHza(rRZoj4@Zfu6u7OQ<+^an8;FPAp{M~#<^c&%h;rrIA7@fIZA zp<4wyvV!N#|poQ;e%$&JoCy3s~eXmpQ)a zk6qWN{DF+9Rz>twxe&C)iEYPATbsSab|G?3-IYtxZj!E8B<(`{?{5Z(eytY88G4zA z3$cX5_O87qn!3I0kvMYjC~Vd-H)gBZUm~OKP@yu6vRxVKOO2+_HJ|ki1Lgt-{V%ty zFO$)a$-_mkDjrhd+x3qIJ;NtmUnt+-cryVX5_lj|Rye@jCD`#eOo(PsDQ!8tPkbz+ zm=O1-o>udR`*Z;svNKdT+gqDH+Yo`X8%Fk3-){UGx#Fyyws^7k>}xad97KE-sA791 z%N4`)JeIJM+erz3n2?`J@7#dIi{YF;td`l%NgyA8S7$A@wv6?7eQ? zj4|2y&5=6VaxN+*e*JEYRjb%yzh(gK;<`l+%bs$hE#k|`Kg-WTBuZ{?Z{%DRp?&j- zyYgsGq7@)6M_aTxZd_ts?9}Pj--8X+h1(=2*#%cw2-!xA!)Y_{EJ)A4lJADY7g|C!|~`_DP3;AeB&mZ zSJIYO1PkAMEk00babRwmAOhwh`Te+4U1AEj08zYL%SCL3CM3jrFF`x@uu}!)z+RD@ zTphv}e0Yq^14UCnY|^sQI+WN9{a~%4WDJqNkWYysg^i(}gK3CAbw%r$j+HX{V;0`d zE>;Yg@GwTFBQNNKI(qHmaN<euX%SH;K?uG4#_EGGcB;}3N z#L~Omez()iB3k`aJ0^1^39Tj>B0ZZxHMQ*h#Xv&8GWViDYd2YqYsdLgYQ&EoveeLG zrt7C)#s|x^dIJCc)H$Q)qbCNa;##*h%{R2s)_F&1wUg;ksB5xUnU@gMxn`gphWr&f zM_*jAsW?}A;~8hc=@3hUeM7p0Ixk9_N9F_5o@jh;_@M2z=@_D#0uGLYex+X>r?SnA zXxCk}`9;w_cW<2%L@DI0uOM|Jnw{>2K*6E<{fayPSrxo$zU2|l=Ms}Me}irxHs_pD z8<856WTrwC3`mBErB!-;8}~xjWG)Lat37bqc?_EjwOn!S6+E*C-f`FAq*ti^_yJoekNj#9X3+f9;@L^UGLmth<= z`$OrvQ=0_&`g)7;!;Sk2VW$9OqT22T06#IP1&hz{I)wZhKiFL#J+_M~II@ITJzBH^ zNv)i##z(Cje{whyfrD5DqHC~6{;5cYTotm05<8?DNnXP3bvQ){uULB117pH!0l;9z znyzv3-tqB(Sl^a&|-n?9aS}00(Bwn~sBReF~hq!q?24P+{xEXBADjB*#(MtbD;yP$X36rxrVEJ@2 z{Oj#%kT0_eng1iAz=BmL84oB^kADwf%m7^5)Qp;=X-94Kfn{6#^+tA1-_oM!`U6_g z5Y$1?8Cj0L<(Ct|S=wgY`t#oRTx7}b#j6EZ=NEzEXjQD&n!HNb#ztnK zOTI-kG?GCHH`+=Xireg1+dg_*bvQx_aOk{4d^)eO`C+|mPqM_3?r0+npLBg(A}1s%0z#2VBu?g; zLJvZ1^Ye!5qkC`3$`incmkJG5rmgbK+NKtB{pj|QhV55r4eQw=CkValxsfwqv})Lr!M0h?=9-nRthF1lK*DYFr&Ky3Me|%r z^QN1Bs!z-m?-@3pL0GhtwQ`0%`32f`mYvcF359J27D;8JrBdM_J4RZJ&os_X&w$_0 zMD+F(%z?&Y{$vTsu7`K_xw%Olpw1%1UuI}PR&KoIz^eYSQl9*?SUWkz9pi-(Syp(ngeh=`Efh1%d}O5=Y!<#hixkMq+)fB zen_cw)=XXD*t{fRct33y?(dIm;l<2+2xyjyEDcLCt>>q_mB|*D$A;go<^0bzTV}4j zA`=x6s30A|5Ol?AV7)mpT77}XG-@V=-^KPQhjXs;1aYp{L@3qalUqhE={Z^wW;@7W`aA!aExdc|u_wn7sn#cL%B&xBt8AD&+>2EwPQ@ zM~Jq^`|KUPg&GIXNnxM8u}#qwIUyKTkPJTWH12YzBrN|VJlmyxw$;sOi#D!%l=xg< zJZ06tmoXl=@s9>tI*4(9`2e^N|5_e+K~~#*4NiQQGWh$NEq=mAXxfMM-XGqhcf87H zksUe@l(2n~YMw-l$uKIn6G3G|UeArLT`?{}OE6&VeEXGM7=yA4eTKlYiLTGC6JEsE zDa2QYpOzsLdsgb>N$~bR5}*X9me-{!DC|i9l6Cs?2EosMM^uqjsA~m`$~%~CWB&Yi z_^_|_K$f3}DL4&#I<*L#y0d=WT3tT_0e0C!0#$Q$vgcqYS(0bJ*Bk->jiPhb7&&OKCW6(#J4jh&$OhdVfdSzPD8 z+ns8>N?H;c54z~^oK_t#zV2{euK>O(Y*}=7lx;O2#MADBHuEb!__#aZjjE1}WOPd1 zPSpv9F3YP_s~bEjiQI0?k4h#AivyWsDj_ioLJg^5aMiAT4R}p~ivUnAqz*C5tQiL{ zYO-SNZZNQX4+1^b#X!ZBR{_8<->;l^*_@AC2{tpb>&Ap_>v&^8q>$L zkq{eDl~Dfis1;a5H`K}d#_;93lrZtfp<6lJ8^P4@-LS*ZfSGm@NFA0+Z^yefGx&pa zjvp;q0i2yi?>cTRwjbKM3w2O_Mt0>yZT@|5={^nN$-Pgjgn~IY8e3XU81$19_x4#* zQH_4u{1S74U%u~^-9Z~OqgB15a8TBrtwpH9AENXYOtL%$<-|5g}| zW_ju&tR>Y?cg_ViiphTN=)k>$pCV%IUU>LvJJlyv&dOxL^fkZ$0GK`L@r?tf*; zE6Q2cg~q!p($6$7NF4FNH96S3Asyj^3$`=$ZTC-^^ye5>el& zINJ^&&v6QLANI7$gR^(pvzbfYXpdf#X%=Wt0{ti*Cm4!aZAQD1m^_|Dp`MI3KOkJnFFJK9&NE4XYTGT|?T9gG5`o zZRqafE!}dyrxG9>@&Uq+sr6kL)Lf1bOZr%3ar}>mX%M4w&V`mjllw!cSy&ss3y~&) zq`Q1kTkmF>*LMQsTeb_@O?@@OaCELcG2p~382Z(DV^%06=F{U}O2(~M8##0chNp2S zhU;ha8V56HMvbOpBfH#_z0TkcO=7m6*^l)gzP~~FexOGc6wYG;*o%n3i?j^R@pkVDPg@Y@Q3BhoNi z=I#!m@k9yOV`v6*)iOVfS`3yoYK6>>>}jIsk6yBK$UaXGTt|wKKAD(MvNnUfeAc?^ z#l9x*t13c?;ih2A7Px*W^4sSYcWZRFaP85=4BPLAdB*7Kh~&jbHX3|o*z7WvUa5%# zM@igSe`Drtx&v_HoxU%str&wVWK{<;Ip;v#0Qbwqo=ZjBONrYP`^=vU6HKMJPjgC8=60eV}rgH4gnhdRQ`hw)>FkwYDJu_t7)Y>|v-qQ4-q^LV59@O6K(TmF3vfgj6ModOyp3KiVH z{ZE$vi2k5ITcbl73yPeka_%Gaw z)e)}zQagUd((I#qK3C+CbA(fPg}%b&&iXT|Wlq*aO_tEh?R0BF_p&z4in36%$Pn3_ zj-ftrkv|E0fBO*j~o zhSwVapHO(5H%irY`CE2N+1-|1`IvA16Wx|0d|H=wUkcxNU|KUTT<@aSyt6#g|K~PE zm-<(la8Dh^l)6tZJY9tjm|1Z8Ay>s1{_0&+O#WWvfdgt0j}#}oy41h?i#d-|owIub zF|Hn%Et(P;f3%zXytO+5;AFZ2Ihzl9H2g9L>7k1L%Pp&YBmP#bbKkQoyj@j)a`-(L z*#|^{Zb6nE&Y?*9@YtSk^w6R?~Jz74)y$+Ond79wZ-Y4nK+ zy!UK3usms9Y{y*Ryrp~xU63$DMfZKfX z%}fuhyL&SGIe2WB;r!*`-spz|mwfe|b&<@CV8Yz3>n-Jse+ag&C6OVz13zl^O52zZ z4tf3B#h7tiYHj8oWheAB9*{H56TTH#%B{s+`xKBRbxLe)Lg0;3#pcMPC1R+F$e_q2 zm(-2mx8Rt~f96BHJ(5OI@A}_Ej6fYXR2B)wPN|pm+`GIla=%rbf4H@#$RqL%Wt@k* z-+2C6DfxZ<;dkusdJQCcLTF@Zq<+^Yn&0*7#K-4acd`o*t19`e791sE9}2Mb&YfR9 zyY-qZjM{r{qTJjBjSD1aL3P}Oe5;MpKc&~5q^X(xw2t`IcuR%`MqQ+xLO!@%Hz)I( z&9=NYxw2tzQ$Q%B`WD5$(WqJZ_o6KfNcs`cGKSkYjl|=NS%ueHaw&O#s`R%S)-(41 zZa-=vRCwoOcAWh7zQfl?NFw9VFgi+^%ySf$Qq$8K0FDeZ_FkxDfXOw|A!n7r$Gt{CXm3*_x+s@5g)16wr15HRtHe zybEtv?Xot1rks+QLsQh#uj`ZYB+>M6L)4wJSaqcdgTpsl0-I`IBJiA^61&!Zcr_-J zXKh{R&+mnZ+mCPzYpc->|Dts1NL&wj@7ppw53Fsnu^(>O!*)u(z3;q*D^Z%|MlhYj zCZ3(A&zNrtr+aTkozSablwG6T19g=qx2w7Uhg0<)Q^DBgUEnHrCQ>zBG4z+l!-sE6fvgotE#TYw_`9&k8k5&CkS_>RPb$%jZL$O`?$i>ZBO+1T{pmD z%7shR`=XH-=F43VblgtbtzAOOYQS8-olQ@Gd>!~)6TCE}?EY1ahkK3PW z{Kdua$<6Z$*?-xMg=i0VTz06>-`+NiXuTR=sR!uU;U|P{r$45@wyfC`CclRCyKCvS zDq$`SNyri?;F(d*)P3l(i&;SPpmbR|!b8s=cF{`R^)ylg`%I(`brE;mb#H_zx-zDc zN=yF~pwvxjj=M6iH6+n?qw_{7nEBwZ8`$sQS>Yo&TKXfXGGTIM&+2$_B}>k^!8Dhg zWJKy2_@F$B&qV&S(#|6wBA;!jp5th4HmCg)Nqs--+l{7p9o z-;=ZH=T#EdPv~G@-Q%yolK*j!u2Vj}!wLP;a6OF)Xnl%(r<6NwPK=^N6C!WyN}(Kj z-(&BoKsSHC0m*3!!}0(B&&a6CZEIfrdXb?Bfk!TB-mUv-H~Ng7btt2G;%?Su%$j_w zpBmmv@hJ_|yx+>MqtVup-Me<2fm%D+BDR+rL zy+l{}j^S75Q6(FgM?1>xTSXC5{&I_^`3mZpXThwSoo;T~V|1kARdAHNJHjw@)~^)c zXVw<34KCK>_LC9C6DX!Gk_P^v&kMd*|&EjT@>Cf zprw%ZYr`UgUApM6wp_ze;tSnz2GcG5$b+qy<-kIVbGF~VtlmUvpFwm=QE^e8?iMRz zd$$GAijk-rrN8Wag0_EZyRZIHd=>}u30qbfT0NJwzuqn_9#bBr>Azx{+BSU9q2c|h z3>#4Z19;yj>_c)0<4EW|4qc#)yKg3~t27MF3*>a+&VC5JsaZ&E#z z3B;TkQVUUY?E^;LI};q$!k#%cLVm;9kjl8Sd}aQvXHQyK;o%^zBb)z#t?Z20zWn>A zusl<}CT+ek+fSw}z%`DBp+n!nN$dKgm%9}ErTtGOOlr{ou>cgk>AJ}W)oAHyS>^2@ zL1NjiBs5d2k>P|DO)3jPU4dMJrM#FccvSGGx+jTccPq-36aA#;ZwGs-|4#yZZZF+w z+IaY}A>yW{sDxH+P=Z;YreJJ2Wv{Jq0!{(`FJq#sgx@M#s&51nZm&NQO#{8_}DmTwC8 zvaxZ~DDsw7x;3R_)47$3IyM?&spj!(b+&4booTVTc)W;!4*bW;C)|nsZWVk?$4FO% z^_IaTG>SuLz(V}~;o)JmaHs2b(Ts;#K&^16PAy_`8l$B@eXXHMY7!5|rYJgM<7olZ zOdUazWIVZJ4-V^Ou7URR(tg6W!T@snDkhrb!_R)B4Jv}3R z+h*DDHa2Z|pfLtTR1?5}d_whv8i4tX^4)dL=JUxQUu0EG()FOJ``-$RJsE2$BwX!= zp%Kserdg^)c}pqP^Yreuu}L0HzfH4+?B6L<^g=|501-aa6tlLbjT}Xw9`f5bpM@pvows5b{awhO<-{uwo7ZTQk6(l-V_Kl7gg#8kUy#D#k)i>J})%=sT zi}0<0o>M=4&u;tjtF=+<{RJ2xH6O0jah8ufRZ!T<+ovk*ZgI(z-l<)<|NO7FsE(F` zH63y<4ECnFq=q!8n}{POf-0{;whT|H`}_Za0TL$Y4I{d{htwO|99u!ND?+Sw4U0=I zvKtT0QqK_*k0l+JLb`ZJ=;oD3U9~mEP9ku@$L>WI_Q=U^0vG@qZy7mJGcn>ZVDn5X z30pnQ+cD><7gLD7|W zHV{B;|> zn!9b=D?-h*d+mp1nUrs&8Am-7ohzs$M)Q`fMpFDM?ZTI__t1%uOOtok_Ak#S>5)!u z*O=R}kMd};-O!AxTJ!+vATHjCT_-9_iTc3vZSnWnMM3QwS{p`dK2F{-+%Jsz(|*e-(Sq2jq~Kn;X@tbx|Ag`9Ae_2c!-7CX2_L{fsAmQEQdk6^6w^K7;2zz-;?yD6FGL~r6gL9j|v zgre(XZE{+eHZ^D7{buiz!dW;wCU`j_yklo>BcyNo>*hCmtdw) zV6OrJ!_=MkH?gq06Ab!CQ)1R)O2rv4*uv{}(sMs&M@V>7!4&QktwT=>>VLt=hg5c~ zwE3E0KDJip)fU5e(uj+{{SPwjo03q9iPPuC?K2QNaK23h7J71JM2YA{oOt_6PX#2! z@Do7n6^nL&gEJM^V72dSh>!Kg2GI{!I^Hx7vp*^6ECIBP$>H}#inzUFOAf;w#trZt z%JIND>s!0uKfc^dUGXu1~Z$U#_8GDX< zyC;8&FH*FVEBC@X|0ytrV_DYwe3(idG@O2r!L4OPF{D5?zGQdFFW1^Me(0|c%``8& z7zz})IK%5*oCr4n-*J?Q{5MSPl&bC}yay=#ManN%V$8YD^vMrqd5>qWRa}BuDL*M; zX+~LAX6{75w803Tc_z=v%0ui*Zf^#Qe}Ljo3v-{dN_-_#$Xe5HEaW6Y%KQAJSJzb| z!2Out!$%<`njfGNX3R;^`l+Qq8vYm~!3a#D)Zf_*p3Sd^3s=!om)N9>);YAX3-2}s zVAljI$3Z{o-diX?pduRbt4WXs*vAf~VCnlm&c(AVAIr$%FeML)aQW^zWzF+>Z&2)G zLRA~o5JDa7!=J>zO=v%(LrV48gOr7OxC!d$ zg7dlq({6{YjEbV6O23=Ztq?PT;jf~cOxr^RQ`=JSssv^3x;1%up4BD{&QUe)={0v$ zuAM9f{zF<#mC5id&&LEaE=RkpnA>IkBuvc)J$0e$N*i{!nImV$Kfyxl%tFS5!*3`f zSE$e(B@i8E^hzwtNw<$aMn=@R|CIwFGr2@6tScrIg-~%HxPw<^}fZt?mtYe+w6s~{|YKc%#kE`wLXKxBy$t#uZT*!^`AW9G5|LgA;P z1I-R~h5+q$_xXj~5hW=*HZ%m!rxgebsE?Gv*~B)33I&-xADz%CLP_QtrTr%rVl?fJ zkIwn1a@>YndaG?c%UZ^SYQs%iT5;ruN4wm^%K(@3GX3rBHdT|-7}1_wG9&H0FE>sw z*mT{x;D#Q^L(L{pyUx$F=>Us06wKf0U2LfGz4W7FoOpYmI=}6e)p;r{j*iS7`fl0ZyRva~F4%v^yw!IX<61*yKZ=zBe6H;A(Fd2MDZ(ZTd@T#w-&{ zX(1b5_*(zvW`^NAo*8;V02_e;kUd^kwfhB&YJUOqO7@8qVIuQo~dA^ zO0)k6j7yM}a_;@_s;2jIOjEi7cjETrw=)j$(+7c7hr^K=vcyO!X(XZBQ0x?(%yyA_ z`RY?R+c>c+4T=A+?{hwDr7`_~tQR2*@PzO(o{_iROjOmil zjJSIm1*%qNP$k*{^5t@N{zo+I`!;DiprUa1gCv zY~q24^Eh?#n8oADeE;zWjk>WxmZO1?biG8a&e&3}d00AGwru7+@c7)`FJPFlwHdpk zd;~Jn9zq92%N~{HufN<~lO14Xg`awloib89!ChmPXUF?kq?Y;wqa#6sII~U>a<1^B zbUygYU?Zm1qxtW7iZk_jh+$|2ngjkqjpi0T5fz_JzIXmN4f2^Wp;m~uGCdgT1r)UY zh5jvDFT93|B5kC13&1JE{V>@;pqtqFM{T`ou}`E_BVDCkU8!#VPS`j+;6IvmN`LtW+ z+B&Q;@o&YjVE(ZyM}f=#S530S(zwh&u}QXRCG=vR2qfu)! z^Cs{Nl#c*4O2bRn?WZ05Q@!F09$v$hbm}Cz*o@F6Voapxoz}Tu$7YSB;w#H4D%B8W zx~5_7KVL&~$|_L`GO)w6a~p;E*-uh%3Uv?NTJ?BbZ<;mM{$V^n*Q(z%p{mFbiQmP8N)x~LD4>w>ub;c8gm$68*Lwpd>w=8)|Jl;2Kswo*HhDVvq@!?U%@ppZ zcF8Zuqy0E^t09$|lJ#2T=)nTK*8{=d`9OB0i>5TO{v}% zo|}Z?(Rz`9{M=PKWYzU$DxevMg-x9K?}!#Bju>5<`{D9rM&TW_2#hB z^NISD@==<=nk`LeD6sQeWih9RK|%})GU-*$^-Lj6xR$lnlF*L*Vs0nX!~6A#jYk=Q z+r@q-WwUBudg%fC0w%n$o}aHdCF)VTc!hrhz4umaXa#HA|ZXD^jW!E}5qCXC@2?)#{e)=kdfzTWNa z=$hVK7xl=in>wm4+ zD%x8TS|Gkh-=-olzU;H{SSJcm$m-9ujvD&;_{6Io>>mi|y%xnM-%(7%=L>vt%PRvK z%F(GyWuJcLj)qb}st&-p`BB{C$OvB$b5#LjBBoOjOXu!Oo6e)1swM42h2h1@=uaEh zQsTSutGGMp?fXOwlg~5X{e%nqZv$%&Apj~5lYq#>S+IuA=E%p%x zq*h&6K*CJiVvo7rKPsA}99@VqA8kxOx=rrvpS)ME%{L>*2!q*j&iC%(Yo*ESpqF*= zvb<>{;ptBUeTAI!Q<4S|N)u&&<2W0G|Hg524#wOnj1GU4pnGwjRnWK09;K60s__@n zMW%}3P9Rw3jEUJe=lMj>o;_~b>S$RXgnA&rt*vTzm{+IZ2-{diy;TlX+?A5xoS6O;WZNh>k2%Qp~lLB>2hV;jFtfDq^*?#`RCC$6v zgmdApZ`~UBhD*KA#c(&$4q&{RNWFOelp(C`cprfr+1SSZA!bLBgZD(*aXOVbUx+@K^XMUBSuG#ivj=`Rh#Y$aooFGwB~ zyK3K{`sr6bX~dAH79M4KoXT1jX7z4H7Z4Gt`5zGq&X}%m$amx4z#aGUM`r^VU%pn? zwQD&0E#-m%An4p`*p+B09iugN{js{@#n zl#~@_fzEt9C2gtA_TW%OVc4hGS5T%2>BQNv7`PF1^V9ND@5Zs!FU1}` zE}&$Jz>W*K7tvxY2~nB%iTnxjAhHX%pJveZk1MjQ6$V;N!<%)Cj($elU%+04nH1;E z$W*S9M`c=%6_({avQUyq)A))u&&XQs-kb|tPrT&1tOx6IdlEoV(W9u{k|T!4JE8!O zkFzRdkuPqyQy%*CTC+&o5?X(5AH=K3<4z0@^s|}Lby=z{Z4$sO=$Hz$dfvhu+X^ZR z!KiFT(Zi9J9j#YTQJ-mQ>%MXT!WWp~b)01Dl#(p2{ zUw0_tIQ6(Isl!{Q|Ncle%UnM4hz<4*z# z_ZXgj#{D>o_M)8e%W5=NW|L>UtvEK9{jlaQ;61H}{@80Gs-Nd9YXFg!v~TeNfa6sA z@l%bhzVu|7!<0jQ*pQaaU6|!vjmqzqYb7L%zLlVu4S}7$UH3N#(S&?{>$!W;Y&Mgw zySL!iY4_S?h27=WE1T8SDfadhG@Q1PHGMDBMKwR2S4Yg1{8pjsM8zVdrik?Zx9{dw zgo<$#v$yK^SJ6x|{3at^bBDiz)M|(i$$3MC4>jZotJA)ZL&wrbO3ClB+E)8K@3qdx zT>5rztTY9By*&5b|3OakfxwdYEFJTltCvQqYxIe)pGWPl$Kgq7F{C{%PvJ%GoD?jA zv|7vx98ClU$QAcInntoXP`a-z7YPbpDha;Z03kgJla}jF=NaYMZC;~4Onp_ATsr9U zPIztT+r^7sY+9rW_@R(q*TCTi^~XhX$trrCASJhR``1J5h8NdYKScgFM)yF(RAb-! zM|C=OYpLBo7A(g3=VtC29*H>U2)d5^cc&)5RI37bViYe7&5v#^G7fITem`;r+~!lP z!f^cGO9HxISBQ4edz;^`I$!eG_PgqAryfW3>`FCMH9;`wzlE=p`kTW$7xC`5IS`FI z4TEM{OT{q;B>Wj0meezie;BQ@XgmDOG0VjdNcrZgE4m*zq#o{Tiavhrh+FRd0W~As z_wo=UG&Q5l8(!4pFZRolR^h*629FAvvX;ulE*1W>;&1A^9Dc4b=ZX+8`d!)IL>k|W zkz&pw#0S-AjRk}qTZt?9L~mzKslP-+exK!TAeXTmZx1_v1Do|6G-6|T?sbbtqoN@E zn9c*r{WD}=Wh-ZW@qG;jNCsjMz|nA}gFX^)QSL#;}(!?S>UZilGVEWIl-*nTw&6kXt_WHO3ljW_Z5GF>2i z=%by91JaUrHmP*|6xf)2vrhuN6lt&~un?|C7UNtQ%{!vuo=I?<#f6ImuIsVX=p+M@| zxRQS3WNsg@QMls;BjMwbe$+1l545z=KV7V-zp)tvb)Oc0Iu{jg;2cYm&F^YZ^V5D{#L!Jz}iHMJOH+^fO&ddoHH z1KDajm4@aU?zWss@{L;OP+Rxnl_6JA*zY_^nf#tZqe z@i7)=q48l*OEnQAT01XZB87L>gN0QR5h00lL7FFHB3eQW;<~y0+nAJY_g^aoMh-(J z=AO+5a+QZ`xY>3OY2=Sj5?oYYVkme_{2n=!Zy~q1zTKYR>)AClR$*zH-R<=8_}jX! zTz6D3ujeaXUKglN^{@1Ks-byj?cEk%)|rv5XhzB31@(~w;w8emtK`V+e6Aqa^YE|2 zvb$npC;7jc+m)E>#&g(ARq^O8m6|ILLh{pq8zFPaM?a%_$0dl8eVU_55^^cp; z0)txWcT%4O70$ucnmc4|rt~z`7v3kRe?81+XiS)=yG++9F=O5HVLTHLI9cC0>4@sb znJBlBSJ!6=TU9+x5>I^t3L}ngN4yvj(^gB#jqJEE~#`bi(0Py8*RCR{a- z-hCYL?&6Aw^v(4`))t?jcO6v<7LCx&Rg`Tf$HVhuMl0ZlW&I&ZO}1R&iB3Gk*Vzjq zwM?2d9Bp@}kY`=fb9W2&RQuCljr_C6Zy(COutT?Aiw^Iyo`|-2fW+SEjEj>E7NB3C zc`a0HXox-D>^L6-H|zo#6TLvsI8fi2?j4YC|K3J*I|Rrd0?qDKJQ)l1+b_nNj+O{a zzSYf1eg+ukp*5U|&@PU-j<*}uw6L{^SgfTwl04m7;u*woGs-r1{&>l3O4qG@7`2+r zo~~pZY+0>qQDmublJr3q&F<&gW~qGy+2QCs$@_|&jCpY=>3$(aMW-gFq{ryRoT*~~ z-y~sA#F$FQfdP*PB+fxImbXu++s3vLVL=>DRoj9@o{qiZAiS2Ea%{pet8@2YbTmp* ztm}ded%C=Oq9$1RS4vAIEY?K&Wb0;USmz06^tnt(yBlp6XZK`3(>lVh zP)XHs^NVW;$FQ~7hP32%2Q5^@dcBJ5(R$P}Pqc{%MI)&+&%Z>?`v0*2(z)i=-J>at zkHZHcUl=YqZCn-=!GIE*c1}TObTHd_U6|g>q`xP0K^^2aFwK!0)45K3L17>P@@8>N zdeUhdGI->}&9L=ZN!-#Kts4cNg@t^08+Z}n`PO`axY1<6dq>wM+nE4S+*GIjP!6Fh zcz>5KwpOvg_1*7_vvBi^E%7IOvnSQFW)jTzRKFfh9PFOtzxBGJD;dI?^%TXOKXeyD z0{nK^nNY817QA3v8U2+q&gjhhWf~Bjm}#zk-BZ4dMmlHV9eV-5{aF~3TL}CkH_s== zWj|8kMBu@~%&{O&+(FCcbA{?gzIlCAbOkx~sWvE9vwa3xSLhze*BLId^R{O%5LRFDLT(RXRS;PL|9_u8t_WR>*qnc;E}ir&K65R=5@X3moxAlpY$5 z!f&5^2_@M#VAcP1+5IA0=&HI*2eT?ljUF;*Icrs|I1*6yRsFC{`XudEWl;343>sv7 zfS39aJus4?b^9dX=xlxexm#_WUP3J3v=mnGq2 zgCgq@xYpZ@Ve%srXMqob;FPfgay~saLi|hUfmfW0YRLZTc_QleegYfE?tq$^@wx<` zUXgV7vfvU4g%sS|3BF+BMbge$71K<1TicF09l{D@F%4-Sny=com0gSrd}__{i`4<7 zHDBB!={Jw>EC@Sc(**vQl+rA!2u2DLGE|Y-u29d-IbYEJo!VIH$xnBm>rSWt|NTKA zUQn)X)js^_QUZ?dH;QqzaUIY3KIim6*R7_|G6vcyBegcJ>z$& zXBzN8yz6K|^Qk;IvwrTBA=+=h^}o@9{tF6qYov{{nLiANp%oKtwS0~&1$XS&-^n~H zWf&1hpYQgcs1TkM-@9mzI1a5rHvTsR@{Wd<)LJ&PFHb!EKC#Yb@avYTu6|=R;YG-SGB2=%K@?bhi??*Y@dR~xkX*(+PA`T z^FJRNChz}cqPByf^WjrNPN)R*|Kh*S&Q8eCm=|TU4va(vIZ($VLjIQ6Wm5aNM)Ki* z_nm|J3o@oP9m-xzUvKUR16$!i#k#3$TrCE#p3cA9-q;Ek7{Vh~9Y5TO)fS5FiYz6} ze9>JAA9*O@XBZSkjM>tyM^p)FpFRjb{FB3p(sbT?dmg4`X0TAOSNj>^lPcNhg%yD8{*+jeq$9$LQGZE0sKB8ub$0f5PLP?;KY3T3pf?|D>#QWb(@m zSYTOaZ+S!eJqI_PBkjHJdc>SQin7VK^0un0uJc#=0@?dCLHGd)F#?zfG{JhjC(~5U zLH&rpdm|Ew+sqfXmK^(kb84ZM99h6Wto2--Etufx!NY>FJTpcubGAR31*F zki|<7A0PdgSwL_p;|okV%fq@>yUiKqSF?weOSmhuA2zoCoVwBh3Iv=$0C3eS+y91cicG_P(GVey<3wpzmtp_ksFdp{{f|gb<-u+;6XE8^ z><8_3Ks!ae;%90&Xz5wY${8LMJK;zt0g%pz>1O;-975sF#`;*o32yfwRS}t6P}z` zfjE$9bT>;G3@4*-Vp1N}dj!!p8hR+KMIa(9;fIK4eHop?9E`Ef=XYmebhOG&BA|&3Uz+b=&nNU_C<$esjq~bi#6N6XdzmSA-sfyG*Id zU!mkh(!zpPH{`&mGXyw23pGc+S{05Z?Nax3^G{GT~zN-n}Curnrs1lA^O zE$qWM2~6E`x=|3xsF*3jGz7M`-xwZN&-vZj3JKeIQqV3zTCp^ves2q_=bCB@*YImz zS{OBIYvE~7|KK(HGMwjlL7o^|6Ukex2TZW$phG7Z*o1KG`8z|F^xUbcx3IV0IIrD1 z)0O=vEN}tPFH}V@Gn-)Q#|TdODvngswK2E5T12DwASO2&02}8DF;aF>e#6`Aeb`ES zp|zp?6SaZKAS1-1sBJI+IJ6NpbyjYJsEjQ;(@GBJk<0VZFn@2dFuHU!TU<{dJTa8P zq9F^LUaE&EuLz4z8USOZ;r&A!JFv2dXU2+ua7VK;D3Bo z#At6PG}$aAX9D+SP=hw7U+=?&)Ap$gnD%p1RUKq3yXxlJRcH2<{>SuP5wVv0y9JJD z$SUv?l*>2N!P&QiC!Qpt!yon}C`#tgWJt`}>OHCD+7u9MwNldP-`p1VPpZb(TwQ7z z(X-%O8wU*Bi}A)SONCN2#wmm?C;yw)t=w^Y*h9*4h<;?{_IY=FF+#zxt<=gat#Ht!yBT1$8x5h zv*eTh-#g!@Mgn*(s53PVPH%sU*uzF>0$s^2thCxQ$zE5Z70c`tiq z0isf*ShDtwh`8mHASm6d_dWC>IU+asYkffFnar0@z`t_VO^(j$Gh@pGF&`4f$|B`G z^hcyou5?Rm+tZowm~*uC@KtyJ?Bvii#}R4^N@RguTrZ7gQPdg~RShl(8oK?_OY9n| zEwK9;YhjE1pmAEH=H1XAsXAXui?lSL7c$=a9Mo@+!}>r_DHkf85PK^vQb#(v;>ff8 z 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

WNwI8f;slYbDiSIHa{&TX)48&a=tO7EH} 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%0Dvl)z3Ry%Lq*Rgn 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 createState() => _AnimatedButtonState(); +} + +class _AnimatedButtonState extends State + with TickerProviderStateMixin { + late AnimationController _scaleController; + late AnimationController _shimmerController; + late AnimationController _loadingController; + + late Animation _scaleAnimation; + late Animation _shimmerAnimation; + late Animation _loadingAnimation; + + bool _isPressed = false; + + @override + void initState() { + super.initState(); + + _scaleController = AnimationController( + duration: const Duration(milliseconds: 150), + vsync: this, + ); + + _shimmerController = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + ); + + _loadingController = AnimationController( + duration: const Duration(milliseconds: 1000), + vsync: this, + ); + + _scaleAnimation = Tween( + begin: 1.0, + end: 0.95, + ).animate(CurvedAnimation( + parent: _scaleController, + curve: Curves.easeInOut, + )); + + _shimmerAnimation = Tween( + begin: -1.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _shimmerController, + curve: Curves.easeInOut, + )); + + _loadingAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _loadingController, + curve: Curves.easeInOut, + )); + + if (widget.isLoading) { + _loadingController.repeat(); + } + } + + @override + void didUpdateWidget(AnimatedButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isLoading != oldWidget.isLoading) { + if (widget.isLoading) { + _loadingController.repeat(); + } else { + _loadingController.stop(); + _loadingController.reset(); + } + } + } + + @override + void dispose() { + _scaleController.dispose(); + _shimmerController.dispose(); + _loadingController.dispose(); + super.dispose(); + } + + void _onTapDown(TapDownDetails details) { + if (widget.onPressed != null && !widget.isLoading) { + setState(() => _isPressed = true); + _scaleController.forward(); + } + } + + void _onTapUp(TapUpDetails details) { + if (widget.onPressed != null && !widget.isLoading) { + setState(() => _isPressed = false); + _scaleController.reverse(); + _shimmerController.forward().then((_) { + _shimmerController.reset(); + }); + } + } + + void _onTapCancel() { + if (widget.onPressed != null && !widget.isLoading) { + setState(() => _isPressed = false); + _scaleController.reverse(); + } + } + + @override + Widget build(BuildContext context) { + final colors = _getColors(); + + return AnimatedBuilder( + animation: Listenable.merge([_scaleAnimation, _shimmerAnimation, _loadingAnimation]), + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: GestureDetector( + onTapDown: _onTapDown, + onTapUp: _onTapUp, + onTapCancel: _onTapCancel, + onTap: widget.onPressed != null && !widget.isLoading ? widget.onPressed : null, + child: Container( + width: widget.width, + height: widget.height ?? 56, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + colors.backgroundColor, + colors.backgroundColor.withOpacity(0.8), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: colors.backgroundColor.withOpacity(0.3), + blurRadius: _isPressed ? 4 : 8, + offset: Offset(0, _isPressed ? 2 : 4), + ), + ], + ), + child: Stack( + children: [ + // Effet shimmer + if (!widget.isLoading) + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: AnimatedBuilder( + animation: _shimmerAnimation, + builder: (context, child) { + return Transform.translate( + offset: Offset(_shimmerAnimation.value * 200, 0), + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.transparent, + Colors.white.withOpacity(0.2), + Colors.transparent, + ], + stops: const [0.0, 0.5, 1.0], + ), + ), + ), + ); + }, + ), + ), + ), + + // Contenu du bouton + Center( + child: widget.isLoading + ? _buildLoadingContent(colors) + : _buildNormalContent(colors), + ), + ], + ), + ), + ), + ); + }, + ); + } + + Widget _buildLoadingContent(_ButtonColors colors) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(colors.foregroundColor), + ), + ), + const SizedBox(width: 12), + Text( + 'Chargement...', + style: TextStyle( + color: colors.foregroundColor, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ); + } + + Widget _buildNormalContent(_ButtonColors colors) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.icon != null) ...[ + Icon( + widget.icon, + color: colors.foregroundColor, + size: 20, + ), + const SizedBox(width: 8), + ], + Text( + widget.text, + style: TextStyle( + color: colors.foregroundColor, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ); + } + + _ButtonColors _getColors() { + switch (widget.style) { + case AnimatedButtonStyle.primary: + return _ButtonColors( + backgroundColor: widget.backgroundColor ?? AppTheme.primaryColor, + foregroundColor: widget.foregroundColor ?? Colors.white, + ); + case AnimatedButtonStyle.secondary: + return _ButtonColors( + backgroundColor: widget.backgroundColor ?? AppTheme.secondaryColor, + foregroundColor: widget.foregroundColor ?? Colors.white, + ); + case AnimatedButtonStyle.success: + return _ButtonColors( + backgroundColor: widget.backgroundColor ?? AppTheme.successColor, + foregroundColor: widget.foregroundColor ?? Colors.white, + ); + case AnimatedButtonStyle.warning: + return _ButtonColors( + backgroundColor: widget.backgroundColor ?? AppTheme.warningColor, + foregroundColor: widget.foregroundColor ?? Colors.white, + ); + case AnimatedButtonStyle.error: + return _ButtonColors( + backgroundColor: widget.backgroundColor ?? AppTheme.errorColor, + foregroundColor: widget.foregroundColor ?? Colors.white, + ); + case AnimatedButtonStyle.outline: + return _ButtonColors( + backgroundColor: widget.backgroundColor ?? Colors.transparent, + foregroundColor: widget.foregroundColor ?? AppTheme.primaryColor, + ); + } + } +} + +class _ButtonColors { + final Color backgroundColor; + final Color foregroundColor; + + _ButtonColors({ + required this.backgroundColor, + required this.foregroundColor, + }); +} + +enum AnimatedButtonStyle { + primary, + secondary, + success, + warning, + error, + outline, +} diff --git a/unionflow-mobile-apps/lib/core/animations/animated_notifications.dart b/unionflow-mobile-apps/lib/core/animations/animated_notifications.dart new file mode 100644 index 0000000..918da9b --- /dev/null +++ b/unionflow-mobile-apps/lib/core/animations/animated_notifications.dart @@ -0,0 +1,352 @@ +import 'package:flutter/material.dart'; +import '../../shared/theme/app_theme.dart'; + +/// Service de notifications animées +class AnimatedNotifications { + static OverlayEntry? _currentOverlay; + + /// Affiche une notification de succès + static void showSuccess( + BuildContext context, + String message, { + Duration duration = const Duration(seconds: 3), + }) { + _showNotification( + context, + message, + NotificationType.success, + duration, + ); + } + + /// Affiche une notification d'erreur + static void showError( + BuildContext context, + String message, { + Duration duration = const Duration(seconds: 4), + }) { + _showNotification( + context, + message, + NotificationType.error, + duration, + ); + } + + /// Affiche une notification d'information + static void showInfo( + BuildContext context, + String message, { + Duration duration = const Duration(seconds: 3), + }) { + _showNotification( + context, + message, + NotificationType.info, + duration, + ); + } + + /// Affiche une notification d'avertissement + static void showWarning( + BuildContext context, + String message, { + Duration duration = const Duration(seconds: 3), + }) { + _showNotification( + context, + message, + NotificationType.warning, + duration, + ); + } + + static void _showNotification( + BuildContext context, + String message, + NotificationType type, + Duration duration, + ) { + // Supprimer la notification précédente si elle existe + _currentOverlay?.remove(); + + final overlay = Overlay.of(context); + late OverlayEntry overlayEntry; + + overlayEntry = OverlayEntry( + builder: (context) => AnimatedNotificationWidget( + message: message, + type: type, + onDismiss: () { + overlayEntry.remove(); + _currentOverlay = null; + }, + ), + ); + + _currentOverlay = overlayEntry; + overlay.insert(overlayEntry); + + // Auto-dismiss après la durée spécifiée + Future.delayed(duration, () { + if (_currentOverlay == overlayEntry) { + overlayEntry.remove(); + _currentOverlay = null; + } + }); + } + + /// Masque la notification actuelle + static void dismiss() { + _currentOverlay?.remove(); + _currentOverlay = null; + } +} + +/// Widget de notification animée +class AnimatedNotificationWidget extends StatefulWidget { + final String message; + final NotificationType type; + final VoidCallback onDismiss; + + const AnimatedNotificationWidget({ + super.key, + required this.message, + required this.type, + required this.onDismiss, + }); + + @override + State createState() => _AnimatedNotificationWidgetState(); +} + +class _AnimatedNotificationWidgetState extends State + with TickerProviderStateMixin { + late AnimationController _slideController; + late AnimationController _fadeController; + late AnimationController _scaleController; + + late Animation _slideAnimation; + late Animation _fadeAnimation; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + + _slideController = AnimationController( + duration: const Duration(milliseconds: 500), + vsync: this, + ); + + _fadeController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + + _scaleController = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + + _slideAnimation = Tween( + begin: const Offset(0, -1), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _slideController, + curve: Curves.elasticOut, + )); + + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _fadeController, + curve: Curves.easeOut, + )); + + _scaleAnimation = Tween( + begin: 1.0, + end: 1.05, + ).animate(CurvedAnimation( + parent: _scaleController, + curve: Curves.easeInOut, + )); + + // Démarrer les animations d'entrée + _fadeController.forward(); + _slideController.forward(); + } + + @override + void dispose() { + _slideController.dispose(); + _fadeController.dispose(); + _scaleController.dispose(); + super.dispose(); + } + + void _dismiss() async { + await _fadeController.reverse(); + widget.onDismiss(); + } + + @override + Widget build(BuildContext context) { + final colors = _getColors(); + + return Positioned( + top: MediaQuery.of(context).padding.top + 16, + left: 16, + right: 16, + child: AnimatedBuilder( + animation: Listenable.merge([ + _slideAnimation, + _fadeAnimation, + _scaleAnimation, + ]), + builder: (context, child) { + return SlideTransition( + position: _slideAnimation, + child: FadeTransition( + opacity: _fadeAnimation, + child: Transform.scale( + scale: _scaleAnimation.value, + child: GestureDetector( + onTap: () => _scaleController.forward().then((_) { + _scaleController.reverse(); + }), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + colors.backgroundColor, + colors.backgroundColor.withOpacity(0.9), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: colors.backgroundColor.withOpacity(0.3), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + children: [ + // Icône + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: colors.iconBackgroundColor, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + colors.icon, + color: colors.iconColor, + size: 24, + ), + ), + + const SizedBox(width: 12), + + // Message + Expanded( + child: Text( + widget.message, + style: TextStyle( + color: colors.textColor, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + + // Bouton de fermeture + GestureDetector( + onTap: _dismiss, + child: Container( + padding: const EdgeInsets.all(4), + child: Icon( + Icons.close, + color: colors.textColor.withOpacity(0.7), + size: 20, + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + }, + ), + ); + } + + _NotificationColors _getColors() { + switch (widget.type) { + case NotificationType.success: + return _NotificationColors( + backgroundColor: AppTheme.successColor, + textColor: Colors.white, + icon: Icons.check_circle, + iconColor: Colors.white, + iconBackgroundColor: Colors.white.withOpacity(0.2), + ); + case NotificationType.error: + return _NotificationColors( + backgroundColor: AppTheme.errorColor, + textColor: Colors.white, + icon: Icons.error, + iconColor: Colors.white, + iconBackgroundColor: Colors.white.withOpacity(0.2), + ); + case NotificationType.warning: + return _NotificationColors( + backgroundColor: AppTheme.warningColor, + textColor: Colors.white, + icon: Icons.warning, + iconColor: Colors.white, + iconBackgroundColor: Colors.white.withOpacity(0.2), + ); + case NotificationType.info: + return _NotificationColors( + backgroundColor: AppTheme.primaryColor, + textColor: Colors.white, + icon: Icons.info, + iconColor: Colors.white, + iconBackgroundColor: Colors.white.withOpacity(0.2), + ); + } + } +} + +class _NotificationColors { + final Color backgroundColor; + final Color textColor; + final IconData icon; + final Color iconColor; + final Color iconBackgroundColor; + + _NotificationColors({ + required this.backgroundColor, + required this.textColor, + required this.icon, + required this.iconColor, + required this.iconBackgroundColor, + }); +} + +enum NotificationType { + success, + error, + warning, + info, +} diff --git a/unionflow-mobile-apps/lib/core/animations/micro_interactions.dart b/unionflow-mobile-apps/lib/core/animations/micro_interactions.dart new file mode 100644 index 0000000..3f9840b --- /dev/null +++ b/unionflow-mobile-apps/lib/core/animations/micro_interactions.dart @@ -0,0 +1,368 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// Widget avec micro-interactions pour les boutons +class InteractiveButton extends StatefulWidget { + final Widget child; + final VoidCallback? onPressed; + final Color? backgroundColor; + final Color? foregroundColor; + final EdgeInsetsGeometry? padding; + final BorderRadius? borderRadius; + final bool enableHapticFeedback; + final bool enableSoundFeedback; + final Duration animationDuration; + + const InteractiveButton({ + super.key, + required this.child, + this.onPressed, + this.backgroundColor, + this.foregroundColor, + this.padding, + this.borderRadius, + this.enableHapticFeedback = true, + this.enableSoundFeedback = false, + this.animationDuration = const Duration(milliseconds: 150), + }); + + @override + State createState() => _InteractiveButtonState(); +} + +class _InteractiveButtonState extends State + with TickerProviderStateMixin { + late AnimationController _scaleController; + late AnimationController _rippleController; + late Animation _scaleAnimation; + late Animation _rippleAnimation; + + bool _isPressed = false; + + @override + void initState() { + super.initState(); + + _scaleController = AnimationController( + duration: widget.animationDuration, + vsync: this, + ); + + _rippleController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + + _scaleAnimation = Tween( + begin: 1.0, + end: 0.95, + ).animate(CurvedAnimation( + parent: _scaleController, + curve: Curves.easeInOut, + )); + + _rippleAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _rippleController, + curve: Curves.easeOut, + )); + } + + @override + void dispose() { + _scaleController.dispose(); + _rippleController.dispose(); + super.dispose(); + } + + void _handleTapDown(TapDownDetails details) { + if (widget.onPressed != null) { + setState(() => _isPressed = true); + _scaleController.forward(); + _rippleController.forward(); + + if (widget.enableHapticFeedback) { + HapticFeedback.lightImpact(); + } + } + } + + void _handleTapUp(TapUpDetails details) { + _handleTapEnd(); + } + + void _handleTapCancel() { + _handleTapEnd(); + } + + void _handleTapEnd() { + if (_isPressed) { + setState(() => _isPressed = false); + _scaleController.reverse(); + + Future.delayed(const Duration(milliseconds: 100), () { + _rippleController.reverse(); + }); + } + } + + void _handleTap() { + if (widget.onPressed != null) { + if (widget.enableSoundFeedback) { + SystemSound.play(SystemSoundType.click); + } + widget.onPressed!(); + } + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTapDown: _handleTapDown, + onTapUp: _handleTapUp, + onTapCancel: _handleTapCancel, + onTap: _handleTap, + child: AnimatedBuilder( + animation: Listenable.merge([_scaleAnimation, _rippleAnimation]), + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: Container( + padding: widget.padding ?? const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + decoration: BoxDecoration( + color: widget.backgroundColor ?? Theme.of(context).primaryColor, + borderRadius: widget.borderRadius ?? BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: (widget.backgroundColor ?? Theme.of(context).primaryColor) + .withOpacity(0.3), + blurRadius: _isPressed ? 8 : 12, + offset: Offset(0, _isPressed ? 2 : 4), + spreadRadius: _isPressed ? 0 : 2, + ), + ], + ), + child: Stack( + alignment: Alignment.center, + children: [ + // Effet de ripple + if (_rippleAnimation.value > 0) + Positioned.fill( + child: Container( + decoration: BoxDecoration( + borderRadius: widget.borderRadius ?? BorderRadius.circular(8), + color: Colors.white.withOpacity( + 0.2 * _rippleAnimation.value, + ), + ), + ), + ), + + // Contenu du bouton + DefaultTextStyle( + style: TextStyle( + color: widget.foregroundColor ?? Colors.white, + fontWeight: FontWeight.w600, + ), + child: widget.child, + ), + ], + ), + ), + ); + }, + ), + ); + } +} + +/// Widget avec effet de parallax pour les cartes +class ParallaxCard extends StatefulWidget { + final Widget child; + final double parallaxOffset; + final Duration animationDuration; + + const ParallaxCard({ + super.key, + required this.child, + this.parallaxOffset = 20.0, + this.animationDuration = const Duration(milliseconds: 200), + }); + + @override + State createState() => _ParallaxCardState(); +} + +class _ParallaxCardState extends State + with TickerProviderStateMixin { + late AnimationController _controller; + late Animation _offsetAnimation; + late Animation _elevationAnimation; + + @override + void initState() { + super.initState(); + + _controller = AnimationController( + duration: widget.animationDuration, + vsync: this, + ); + + _offsetAnimation = Tween( + begin: Offset.zero, + end: Offset(0, -widget.parallaxOffset), + ).animate(CurvedAnimation( + parent: _controller, + curve: Curves.easeOut, + )); + + _elevationAnimation = Tween( + begin: 4.0, + end: 12.0, + ).animate(CurvedAnimation( + parent: _controller, + curve: Curves.easeOut, + )); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) => _controller.forward(), + onExit: (_) => _controller.reverse(), + child: GestureDetector( + onTapDown: (_) => _controller.forward(), + onTapUp: (_) => _controller.reverse(), + onTapCancel: () => _controller.reverse(), + child: AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Transform.translate( + offset: _offsetAnimation.value, + child: Card( + elevation: _elevationAnimation.value, + child: widget.child, + ), + ); + }, + ), + ), + ); + } +} + +/// Widget avec effet de morphing pour les icônes +class MorphingIcon extends StatefulWidget { + final IconData icon; + final IconData? alternateIcon; + final double size; + final Color? color; + final Duration animationDuration; + final VoidCallback? onPressed; + + const MorphingIcon({ + super.key, + required this.icon, + this.alternateIcon, + this.size = 24.0, + this.color, + this.animationDuration = const Duration(milliseconds: 300), + this.onPressed, + }); + + @override + State createState() => _MorphingIconState(); +} + +class _MorphingIconState extends State + with TickerProviderStateMixin { + late AnimationController _controller; + late Animation _scaleAnimation; + late Animation _rotationAnimation; + + bool _isAlternate = false; + + @override + void initState() { + super.initState(); + + _controller = AnimationController( + duration: widget.animationDuration, + vsync: this, + ); + + _scaleAnimation = Tween( + begin: 1.0, + end: 0.0, + ).animate(CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.5, curve: Curves.easeIn), + )); + + _rotationAnimation = Tween( + begin: 0.0, + end: 0.5, + ).animate(CurvedAnimation( + parent: _controller, + curve: Curves.easeInOut, + )); + + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed) { + setState(() { + _isAlternate = !_isAlternate; + }); + _controller.reverse(); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _handleTap() { + if (widget.alternateIcon != null) { + _controller.forward(); + } + widget.onPressed?.call(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: _handleTap, + child: AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value == 0.0 ? 1.0 : _scaleAnimation.value, + child: Transform.rotate( + angle: _rotationAnimation.value * 3.14159, + child: Icon( + _isAlternate && widget.alternateIcon != null + ? widget.alternateIcon! + : widget.icon, + size: widget.size, + color: widget.color, + ), + ), + ); + }, + ), + ); + } +} diff --git a/unionflow-mobile-apps/lib/core/animations/page_transitions.dart b/unionflow-mobile-apps/lib/core/animations/page_transitions.dart index 474441e..b14bb6d 100644 --- a/unionflow-mobile-apps/lib/core/animations/page_transitions.dart +++ b/unionflow-mobile-apps/lib/core/animations/page_transitions.dart @@ -176,6 +176,72 @@ class PageTransitions { }, ); } + + /// Transition avec effet de morphing et blur + static PageRouteBuilder morphWithBlur(Widget page) { + return PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => page, + transitionDuration: const Duration(milliseconds: 500), + reverseTransitionDuration: const Duration(milliseconds: 400), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + final curvedAnimation = CurvedAnimation( + parent: animation, + curve: Curves.easeInOutCubic, + ); + + final scaleAnimation = Tween( + begin: 0.8, + end: 1.0, + ).animate(curvedAnimation); + + final fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: animation, + curve: const Interval(0.3, 1.0, curve: Curves.easeOut), + )); + + return FadeTransition( + opacity: fadeAnimation, + child: Transform.scale( + scale: scaleAnimation.value, + child: child, + ), + ); + }, + ); + } + + /// Transition avec effet de rotation 3D + static PageRouteBuilder rotate3D(Widget page) { + return PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => page, + transitionDuration: const Duration(milliseconds: 600), + reverseTransitionDuration: const Duration(milliseconds: 500), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + final curvedAnimation = CurvedAnimation( + parent: animation, + curve: Curves.easeInOutCubic, + ); + + return AnimatedBuilder( + animation: curvedAnimation, + builder: (context, child) { + final rotationY = (1.0 - curvedAnimation.value) * 0.5; + return Transform( + alignment: Alignment.center, + transform: Matrix4.identity() + ..setEntry(3, 2, 0.001) + ..rotateY(rotationY), + child: child, + ); + }, + child: child, + ); + }, + ); + } } /// Extensions pour faciliter l'utilisation des transitions @@ -209,6 +275,16 @@ extension NavigatorTransitions on NavigatorState { Future pushSlideWithParallax(Widget page) { return push(PageTransitions.slideWithParallax(page)); } + + /// Navigation avec transition de morphing + Future pushMorphWithBlur(Widget page) { + return push(PageTransitions.morphWithBlur(page)); + } + + /// Navigation avec transition de rotation 3D + Future pushRotate3D(Widget page) { + return push(PageTransitions.rotate3D(page)); + } } /// Widget d'animation pour les éléments de liste diff --git a/unionflow-mobile-apps/lib/core/auth/services/keycloak_webview_auth_service.dart b/unionflow-mobile-apps/lib/core/auth/services/keycloak_webview_auth_service.dart index e1e282c..9abada3 100644 --- a/unionflow-mobile-apps/lib/core/auth/services/keycloak_webview_auth_service.dart +++ b/unionflow-mobile-apps/lib/core/auth/services/keycloak_webview_auth_service.dart @@ -13,10 +13,10 @@ import 'package:dio/dio.dart'; @singleton class KeycloakWebViewAuthService { - static const String _keycloakBaseUrl = 'http://192.168.1.11:8180'; + static const String _keycloakBaseUrl = 'http://192.168.1.145:8180'; static const String _realm = 'unionflow'; static const String _clientId = 'unionflow-mobile'; - static const String _redirectUrl = 'http://192.168.1.11:8080/auth/callback'; + static const String _redirectUrl = 'http://192.168.1.145:8080/auth/callback'; final FlutterSecureStorage _secureStorage = const FlutterSecureStorage(); final Dio _dio = Dio(); diff --git a/unionflow-mobile-apps/lib/core/constants/app_constants.dart b/unionflow-mobile-apps/lib/core/constants/app_constants.dart index 7601f0c..4235475 100644 --- a/unionflow-mobile-apps/lib/core/constants/app_constants.dart +++ b/unionflow-mobile-apps/lib/core/constants/app_constants.dart @@ -1,6 +1,6 @@ class AppConstants { // API Configuration - static const String baseUrl = 'http://192.168.1.11:8080'; // Backend UnionFlow + static const String baseUrl = 'http://192.168.1.145:8080'; // Backend UnionFlow static const String apiVersion = '/api'; // Timeout diff --git a/unionflow-mobile-apps/lib/core/di/injection.config.dart b/unionflow-mobile-apps/lib/core/di/injection.config.dart index 0e30007..8318a2d 100644 --- a/unionflow-mobile-apps/lib/core/di/injection.config.dart +++ b/unionflow-mobile-apps/lib/core/di/injection.config.dart @@ -8,8 +8,11 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:flutter_local_notifications/flutter_local_notifications.dart' + as _i163; import 'package:get_it/get_it.dart' as _i174; import 'package:injectable/injectable.dart' as _i526; +import 'package:shared_preferences/shared_preferences.dart' as _i460; import 'package:unionflow_mobile_apps/core/auth/bloc/auth_bloc.dart' as _i635; import 'package:unionflow_mobile_apps/core/auth/services/auth_api_service.dart' as _i705; @@ -23,6 +26,18 @@ import 'package:unionflow_mobile_apps/core/network/auth_interceptor.dart' as _i772; import 'package:unionflow_mobile_apps/core/network/dio_client.dart' as _i978; import 'package:unionflow_mobile_apps/core/services/api_service.dart' as _i238; +import 'package:unionflow_mobile_apps/core/services/cache_service.dart' + as _i742; +import 'package:unionflow_mobile_apps/core/services/moov_money_service.dart' + as _i1053; +import 'package:unionflow_mobile_apps/core/services/notification_service.dart' + as _i421; +import 'package:unionflow_mobile_apps/core/services/orange_money_service.dart' + as _i135; +import 'package:unionflow_mobile_apps/core/services/payment_service.dart' + as _i132; +import 'package:unionflow_mobile_apps/core/services/wave_payment_service.dart' + as _i924; import 'package:unionflow_mobile_apps/features/cotisations/data/repositories/cotisation_repository_impl.dart' as _i991; import 'package:unionflow_mobile_apps/features/cotisations/domain/repositories/cotisation_repository.dart' @@ -62,25 +77,50 @@ extension GetItInjectableX on _i174.GetIt { () => _i705.AuthApiService(gh<_i978.DioClient>())); gh.singleton<_i238.ApiService>( () => _i238.ApiService(gh<_i978.DioClient>())); + gh.lazySingleton<_i742.CacheService>( + () => _i742.CacheService(gh<_i460.SharedPreferences>())); gh.singleton<_i423.AuthService>(() => _i423.AuthService( gh<_i394.SecureTokenStorage>(), gh<_i705.AuthApiService>(), gh<_i772.AuthInterceptor>(), gh<_i978.DioClient>(), )); - gh.singleton<_i635.AuthBloc>(() => _i635.AuthBloc(gh<_i423.AuthService>())); gh.lazySingleton<_i961.CotisationRepository>( - () => _i991.CotisationRepositoryImpl(gh<_i238.ApiService>())); + () => _i991.CotisationRepositoryImpl( + gh<_i238.ApiService>(), + gh<_i742.CacheService>(), + )); + gh.lazySingleton<_i1053.MoovMoneyService>( + () => _i1053.MoovMoneyService(gh<_i238.ApiService>())); + gh.lazySingleton<_i135.OrangeMoneyService>( + () => _i135.OrangeMoneyService(gh<_i238.ApiService>())); + gh.lazySingleton<_i924.WavePaymentService>( + () => _i924.WavePaymentService(gh<_i238.ApiService>())); + gh.singleton<_i635.AuthBloc>(() => _i635.AuthBloc(gh<_i423.AuthService>())); + gh.lazySingleton<_i421.NotificationService>(() => _i421.NotificationService( + gh<_i163.FlutterLocalNotificationsPlugin>(), + gh<_i460.SharedPreferences>(), + )); gh.lazySingleton<_i351.EvenementRepository>( () => _i947.EvenementRepositoryImpl(gh<_i238.ApiService>())); gh.lazySingleton<_i930.MembreRepository>( () => _i108.MembreRepositoryImpl(gh<_i238.ApiService>())); gh.factory<_i1001.EvenementBloc>( () => _i1001.EvenementBloc(gh<_i351.EvenementRepository>())); + gh.lazySingleton<_i132.PaymentService>(() => _i132.PaymentService( + gh<_i238.ApiService>(), + gh<_i742.CacheService>(), + gh<_i924.WavePaymentService>(), + gh<_i135.OrangeMoneyService>(), + gh<_i1053.MoovMoneyService>(), + )); gh.factory<_i41.MembresBloc>( () => _i41.MembresBloc(gh<_i930.MembreRepository>())); - gh.factory<_i919.CotisationsBloc>( - () => _i919.CotisationsBloc(gh<_i961.CotisationRepository>())); + gh.factory<_i919.CotisationsBloc>(() => _i919.CotisationsBloc( + gh<_i961.CotisationRepository>(), + gh<_i132.PaymentService>(), + gh<_i421.NotificationService>(), + )); return this; } } diff --git a/unionflow-mobile-apps/lib/core/di/injection.dart b/unionflow-mobile-apps/lib/core/di/injection.dart index 50034a2..e421a73 100644 --- a/unionflow-mobile-apps/lib/core/di/injection.dart +++ b/unionflow-mobile-apps/lib/core/di/injection.dart @@ -1,5 +1,8 @@ import 'package:get_it/get_it.dart'; import 'package:injectable/injectable.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; + import 'injection.config.dart'; @@ -9,6 +12,16 @@ final GetIt getIt = GetIt.instance; /// Configure l'injection de dépendances @InjectableInit() Future configureDependencies() async { + // Enregistrer SharedPreferences + final sharedPreferences = await SharedPreferences.getInstance(); + getIt.registerSingleton(sharedPreferences); + + // Enregistrer FlutterLocalNotificationsPlugin + getIt.registerSingleton( + FlutterLocalNotificationsPlugin(), + ); + + // Initialiser les autres dépendances getIt.init(); } diff --git a/unionflow-mobile-apps/lib/core/models/cotisation_filter_model.dart b/unionflow-mobile-apps/lib/core/models/cotisation_filter_model.dart new file mode 100644 index 0000000..ff46927 --- /dev/null +++ b/unionflow-mobile-apps/lib/core/models/cotisation_filter_model.dart @@ -0,0 +1,326 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'cotisation_filter_model.g.dart'; + +/// Modèle pour les filtres de recherche des cotisations +/// Permet de filtrer les cotisations selon différents critères +@JsonSerializable() +class CotisationFilterModel { + final String? membreId; + final String? nomMembre; + final String? numeroMembre; + final List? statuts; + final List? typesCotisation; + final DateTime? dateEcheanceMin; + final DateTime? dateEcheanceMax; + final DateTime? datePaiementMin; + final DateTime? datePaiementMax; + final double? montantMin; + final double? montantMax; + final int? annee; + final int? mois; + final String? periode; + final bool? recurrente; + final bool? enRetard; + final bool? echeanceProche; + final String? methodePaiement; + final String? recherche; + final String? triPar; + final String? ordretri; + final int page; + final int size; + + const CotisationFilterModel({ + this.membreId, + this.nomMembre, + this.numeroMembre, + this.statuts, + this.typesCotisation, + this.dateEcheanceMin, + this.dateEcheanceMax, + this.datePaiementMin, + this.datePaiementMax, + this.montantMin, + this.montantMax, + this.annee, + this.mois, + this.periode, + this.recurrente, + this.enRetard, + this.echeanceProche, + this.methodePaiement, + this.recherche, + this.triPar, + this.ordreTriPar, + this.page = 0, + this.size = 20, + }); + + /// Factory pour créer depuis JSON + factory CotisationFilterModel.fromJson(Map json) => + _$CotisationFilterModelFromJson(json); + + /// Convertit vers JSON + Map toJson() => _$CotisationFilterModelToJson(this); + + /// Crée un filtre vide + factory CotisationFilterModel.empty() { + return const CotisationFilterModel(); + } + + /// Crée un filtre pour les cotisations en retard + factory CotisationFilterModel.enRetard() { + return const CotisationFilterModel( + enRetard: true, + triPar: 'dateEcheance', + ordreTriPar: 'ASC', + ); + } + + /// Crée un filtre pour les cotisations avec échéance proche + factory CotisationFilterModel.echeanceProche() { + return const CotisationFilterModel( + echeanceProche: true, + triPar: 'dateEcheance', + ordreTriPar: 'ASC', + ); + } + + /// Crée un filtre pour un membre spécifique + factory CotisationFilterModel.parMembre(String membreId) { + return CotisationFilterModel( + membreId: membreId, + triPar: 'dateEcheance', + ordreTriPar: 'DESC', + ); + } + + /// Crée un filtre pour un statut spécifique + factory CotisationFilterModel.parStatut(String statut) { + return CotisationFilterModel( + statuts: [statut], + triPar: 'dateEcheance', + ordreTriPar: 'DESC', + ); + } + + /// Crée un filtre pour une période spécifique + factory CotisationFilterModel.parPeriode(int annee, [int? mois]) { + return CotisationFilterModel( + annee: annee, + mois: mois, + triPar: 'dateEcheance', + ordreTriPar: 'DESC', + ); + } + + /// Crée un filtre pour une recherche textuelle + factory CotisationFilterModel.recherche(String terme) { + return CotisationFilterModel( + recherche: terme, + triPar: 'dateCreation', + ordreTriPar: 'DESC', + ); + } + + /// Vérifie si le filtre est vide + bool get isEmpty { + return membreId == null && + nomMembre == null && + numeroMembre == null && + (statuts == null || statuts!.isEmpty) && + (typesCotisation == null || typesCotisation!.isEmpty) && + dateEcheanceMin == null && + dateEcheanceMax == null && + datePaiementMin == null && + datePaiementMax == null && + montantMin == null && + montantMax == null && + annee == null && + mois == null && + periode == null && + recurrente == null && + enRetard == null && + echeanceProche == null && + methodePaiement == null && + (recherche == null || recherche!.isEmpty); + } + + /// Vérifie si le filtre a des critères actifs + bool get hasActiveFilters => !isEmpty; + + /// Compte le nombre de filtres actifs + int get nombreFiltresActifs { + int count = 0; + if (membreId != null) count++; + if (nomMembre != null) count++; + if (numeroMembre != null) count++; + if (statuts != null && statuts!.isNotEmpty) count++; + if (typesCotisation != null && typesCotisation!.isNotEmpty) count++; + if (dateEcheanceMin != null || dateEcheanceMax != null) count++; + if (datePaiementMin != null || datePaiementMax != null) count++; + if (montantMin != null || montantMax != null) count++; + if (annee != null) count++; + if (mois != null) count++; + if (periode != null) count++; + if (recurrente != null) count++; + if (enRetard == true) count++; + if (echeanceProche == true) count++; + if (methodePaiement != null) count++; + if (recherche != null && recherche!.isNotEmpty) count++; + return count; + } + + /// Retourne une description textuelle des filtres actifs + String get descriptionFiltres { + List descriptions = []; + + if (statuts != null && statuts!.isNotEmpty) { + descriptions.add('Statut: ${statuts!.join(', ')}'); + } + + if (typesCotisation != null && typesCotisation!.isNotEmpty) { + descriptions.add('Type: ${typesCotisation!.join(', ')}'); + } + + if (annee != null) { + String periodeDesc = 'Année: $annee'; + if (mois != null) { + periodeDesc += ', Mois: $mois'; + } + descriptions.add(periodeDesc); + } + + if (enRetard == true) { + descriptions.add('En retard'); + } + + if (echeanceProche == true) { + descriptions.add('Échéance proche'); + } + + if (montantMin != null || montantMax != null) { + String montantDesc = 'Montant: '; + if (montantMin != null && montantMax != null) { + montantDesc += '${montantMin!.toStringAsFixed(0)} - ${montantMax!.toStringAsFixed(0)} XOF'; + } else if (montantMin != null) { + montantDesc += '≥ ${montantMin!.toStringAsFixed(0)} XOF'; + } else { + montantDesc += '≤ ${montantMax!.toStringAsFixed(0)} XOF'; + } + descriptions.add(montantDesc); + } + + if (recherche != null && recherche!.isNotEmpty) { + descriptions.add('Recherche: "$recherche"'); + } + + return descriptions.join(' • '); + } + + /// Convertit vers Map pour les paramètres de requête + Map toQueryParameters() { + Map params = {}; + + if (membreId != null) params['membreId'] = membreId; + if (statuts != null && statuts!.isNotEmpty) { + params['statut'] = statuts!.length == 1 ? statuts!.first : statuts!.join(','); + } + if (typesCotisation != null && typesCotisation!.isNotEmpty) { + params['typeCotisation'] = typesCotisation!.length == 1 ? typesCotisation!.first : typesCotisation!.join(','); + } + if (annee != null) params['annee'] = annee.toString(); + if (mois != null) params['mois'] = mois.toString(); + if (periode != null) params['periode'] = periode; + if (recurrente != null) params['recurrente'] = recurrente.toString(); + if (enRetard == true) params['enRetard'] = 'true'; + if (echeanceProche == true) params['echeanceProche'] = 'true'; + if (methodePaiement != null) params['methodePaiement'] = methodePaiement; + if (recherche != null && recherche!.isNotEmpty) params['q'] = recherche; + if (triPar != null) params['sortBy'] = triPar; + if (ordreTriPar != null) params['sortOrder'] = ordreTriPar; + + params['page'] = page.toString(); + params['size'] = size.toString(); + + return params; + } + + /// Copie avec modifications + CotisationFilterModel copyWith({ + String? membreId, + String? nomMembre, + String? numeroMembre, + List? statuts, + List? typesCotisation, + DateTime? dateEcheanceMin, + DateTime? dateEcheanceMax, + DateTime? datePaiementMin, + DateTime? datePaiementMax, + double? montantMin, + double? montantMax, + int? annee, + int? mois, + String? periode, + bool? recurrente, + bool? enRetard, + bool? echeanceProche, + String? methodePaiement, + String? recherche, + String? triPar, + String? ordreTriPar, + int? page, + int? size, + }) { + return CotisationFilterModel( + membreId: membreId ?? this.membreId, + nomMembre: nomMembre ?? this.nomMembre, + numeroMembre: numeroMembre ?? this.numeroMembre, + statuts: statuts ?? this.statuts, + typesCotisation: typesCotisation ?? this.typesCotisation, + dateEcheanceMin: dateEcheanceMin ?? this.dateEcheanceMin, + dateEcheanceMax: dateEcheanceMax ?? this.dateEcheanceMax, + datePaiementMin: datePaiementMin ?? this.datePaiementMin, + datePaiementMax: datePaiementMax ?? this.datePaiementMax, + montantMin: montantMin ?? this.montantMin, + montantMax: montantMax ?? this.montantMax, + annee: annee ?? this.annee, + mois: mois ?? this.mois, + periode: periode ?? this.periode, + recurrente: recurrente ?? this.recurrente, + enRetard: enRetard ?? this.enRetard, + echeanceProche: echeanceProche ?? this.echeanceProche, + methodePaiement: methodePaiement ?? this.methodePaiement, + recherche: recherche ?? this.recherche, + triPar: triPar ?? this.triPar, + ordreTriPar: ordreTriPar ?? this.ordreTriPar, + page: page ?? this.page, + size: size ?? this.size, + ); + } + + /// Réinitialise tous les filtres + CotisationFilterModel clear() { + return const CotisationFilterModel(); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is CotisationFilterModel && + other.membreId == membreId && + other.statuts == statuts && + other.typesCotisation == typesCotisation && + other.annee == annee && + other.mois == mois && + other.recherche == recherche; + } + + @override + int get hashCode => Object.hash(membreId, statuts, typesCotisation, annee, mois, recherche); + + @override + String toString() { + return 'CotisationFilterModel(filtres actifs: $nombreFiltresActifs)'; + } +} diff --git a/unionflow-mobile-apps/lib/core/models/cotisation_filter_model.g.dart b/unionflow-mobile-apps/lib/core/models/cotisation_filter_model.g.dart new file mode 100644 index 0000000..5b22337 --- /dev/null +++ b/unionflow-mobile-apps/lib/core/models/cotisation_filter_model.g.dart @@ -0,0 +1,72 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'cotisation_filter_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CotisationFilterModel _$CotisationFilterModelFromJson( + Map json) => + CotisationFilterModel( + membreId: json['membreId'] as String?, + nomMembre: json['nomMembre'] as String?, + numeroMembre: json['numeroMembre'] as String?, + statuts: + (json['statuts'] as List?)?.map((e) => e as String).toList(), + typesCotisation: (json['typesCotisation'] as List?) + ?.map((e) => e as String) + .toList(), + dateEcheanceMin: json['dateEcheanceMin'] == null + ? null + : DateTime.parse(json['dateEcheanceMin'] as String), + dateEcheanceMax: json['dateEcheanceMax'] == null + ? null + : DateTime.parse(json['dateEcheanceMax'] as String), + datePaiementMin: json['datePaiementMin'] == null + ? null + : DateTime.parse(json['datePaiementMin'] as String), + datePaiementMax: json['datePaiementMax'] == null + ? null + : DateTime.parse(json['datePaiementMax'] as String), + montantMin: (json['montantMin'] as num?)?.toDouble(), + montantMax: (json['montantMax'] as num?)?.toDouble(), + annee: (json['annee'] as num?)?.toInt(), + mois: (json['mois'] as num?)?.toInt(), + periode: json['periode'] as String?, + recurrente: json['recurrente'] as bool?, + enRetard: json['enRetard'] as bool?, + echeanceProche: json['echeanceProche'] as bool?, + methodePaiement: json['methodePaiement'] as String?, + recherche: json['recherche'] as String?, + triPar: json['triPar'] as String?, + page: (json['page'] as num?)?.toInt() ?? 0, + size: (json['size'] as num?)?.toInt() ?? 20, + ); + +Map _$CotisationFilterModelToJson( + CotisationFilterModel instance) => + { + 'membreId': instance.membreId, + 'nomMembre': instance.nomMembre, + 'numeroMembre': instance.numeroMembre, + 'statuts': instance.statuts, + 'typesCotisation': instance.typesCotisation, + 'dateEcheanceMin': instance.dateEcheanceMin?.toIso8601String(), + 'dateEcheanceMax': instance.dateEcheanceMax?.toIso8601String(), + 'datePaiementMin': instance.datePaiementMin?.toIso8601String(), + 'datePaiementMax': instance.datePaiementMax?.toIso8601String(), + 'montantMin': instance.montantMin, + 'montantMax': instance.montantMax, + 'annee': instance.annee, + 'mois': instance.mois, + 'periode': instance.periode, + 'recurrente': instance.recurrente, + 'enRetard': instance.enRetard, + 'echeanceProche': instance.echeanceProche, + 'methodePaiement': instance.methodePaiement, + 'recherche': instance.recherche, + 'triPar': instance.triPar, + 'page': instance.page, + 'size': instance.size, + }; diff --git a/unionflow-mobile-apps/lib/core/models/cotisation_model.dart b/unionflow-mobile-apps/lib/core/models/cotisation_model.dart index 212dbd2..186ce8b 100644 --- a/unionflow-mobile-apps/lib/core/models/cotisation_model.dart +++ b/unionflow-mobile-apps/lib/core/models/cotisation_model.dart @@ -88,6 +88,12 @@ class CotisationModel { return (montantPaye / montantDu * 100).clamp(0, 100); } + /// Calcule le nombre de jours de retard + int get joursRetard { + if (!isEnRetard) return 0; + return DateTime.now().difference(dateEcheance).inDays; + } + /// Retourne la couleur associée au statut String get couleurStatut { switch (statut) { diff --git a/unionflow-mobile-apps/lib/core/models/cotisation_statistics_model.dart b/unionflow-mobile-apps/lib/core/models/cotisation_statistics_model.dart new file mode 100644 index 0000000..cd220eb --- /dev/null +++ b/unionflow-mobile-apps/lib/core/models/cotisation_statistics_model.dart @@ -0,0 +1,295 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'cotisation_statistics_model.g.dart'; + +/// Modèle de données pour les statistiques des cotisations +/// Représente les métriques et analyses des cotisations +@JsonSerializable() +class CotisationStatisticsModel { + final int totalCotisations; + final double montantTotal; + final double montantPaye; + final double montantRestant; + final int cotisationsPayees; + final int cotisationsEnAttente; + final int cotisationsEnRetard; + final int cotisationsAnnulees; + final double tauxPaiement; + final double tauxRetard; + final double montantMoyenCotisation; + final double montantMoyenPaiement; + final Map? repartitionParType; + final Map? montantParType; + final Map? repartitionParStatut; + final Map? montantParStatut; + final Map? evolutionMensuelle; + final Map? chiffreAffaireMensuel; + final List? tendances; + final DateTime dateCalcul; + final String? periode; + final int? annee; + final int? mois; + + const CotisationStatisticsModel({ + required this.totalCotisations, + required this.montantTotal, + required this.montantPaye, + required this.montantRestant, + required this.cotisationsPayees, + required this.cotisationsEnAttente, + required this.cotisationsEnRetard, + required this.cotisationsAnnulees, + required this.tauxPaiement, + required this.tauxRetard, + required this.montantMoyenCotisation, + required this.montantMoyenPaiement, + this.repartitionParType, + this.montantParType, + this.repartitionParStatut, + this.montantParStatut, + this.evolutionMensuelle, + this.chiffreAffaireMensuel, + this.tendances, + required this.dateCalcul, + this.periode, + this.annee, + this.mois, + }); + + /// Factory pour créer depuis JSON + factory CotisationStatisticsModel.fromJson(Map json) => + _$CotisationStatisticsModelFromJson(json); + + /// Convertit vers JSON + Map toJson() => _$CotisationStatisticsModelToJson(this); + + /// Calcule le pourcentage de cotisations payées + double get pourcentageCotisationsPayees { + if (totalCotisations == 0) return 0; + return (cotisationsPayees / totalCotisations * 100); + } + + /// Calcule le pourcentage de cotisations en retard + double get pourcentageCotisationsEnRetard { + if (totalCotisations == 0) return 0; + return (cotisationsEnRetard / totalCotisations * 100); + } + + /// Calcule le pourcentage de cotisations en attente + double get pourcentageCotisationsEnAttente { + if (totalCotisations == 0) return 0; + return (cotisationsEnAttente / totalCotisations * 100); + } + + /// Retourne le statut de santé financière + String get statutSanteFinanciere { + if (tauxPaiement >= 90) return 'EXCELLENT'; + if (tauxPaiement >= 75) return 'BON'; + if (tauxPaiement >= 60) return 'MOYEN'; + if (tauxPaiement >= 40) return 'FAIBLE'; + return 'CRITIQUE'; + } + + /// Retourne la couleur associée au statut de santé + String get couleurSanteFinanciere { + switch (statutSanteFinanciere) { + case 'EXCELLENT': + return '#4CAF50'; // Vert + case 'BON': + return '#8BC34A'; // Vert clair + case 'MOYEN': + return '#FF9800'; // Orange + case 'FAIBLE': + return '#FF5722'; // Orange foncé + case 'CRITIQUE': + return '#F44336'; // Rouge + default: + return '#757575'; // Gris + } + } + + /// Retourne le libellé du statut de santé + String get libelleSanteFinanciere { + switch (statutSanteFinanciere) { + case 'EXCELLENT': + return 'Excellente santé financière'; + case 'BON': + return 'Bonne santé financière'; + case 'MOYEN': + return 'Santé financière moyenne'; + case 'FAIBLE': + return 'Santé financière faible'; + case 'CRITIQUE': + return 'Situation critique'; + default: + return 'Statut inconnu'; + } + } + + /// Calcule la progression par rapport à la période précédente + double? calculerProgression(CotisationStatisticsModel? precedent) { + if (precedent == null || precedent.montantPaye == 0) return null; + return ((montantPaye - precedent.montantPaye) / precedent.montantPaye * 100); + } + + /// Retourne les indicateurs clés de performance + Map get kpis { + return { + 'tauxRecouvrement': tauxPaiement, + 'tauxRetard': tauxRetard, + 'montantMoyenCotisation': montantMoyenCotisation, + 'montantMoyenPaiement': montantMoyenPaiement, + 'efficaciteRecouvrement': montantPaye / montantTotal * 100, + 'risqueImpaye': montantRestant / montantTotal * 100, + }; + } + + /// Retourne les alertes basées sur les seuils + List get alertes { + List alertes = []; + + if (tauxRetard > 20) { + alertes.add('Taux de retard élevé (${tauxRetard.toStringAsFixed(1)}%)'); + } + + if (tauxPaiement < 60) { + alertes.add('Taux de paiement faible (${tauxPaiement.toStringAsFixed(1)}%)'); + } + + if (cotisationsEnRetard > totalCotisations * 0.3) { + alertes.add('Trop de cotisations en retard ($cotisationsEnRetard)'); + } + + if (montantRestant > montantTotal * 0.4) { + alertes.add('Montant impayé important (${montantRestant.toStringAsFixed(0)} XOF)'); + } + + return alertes; + } + + /// Vérifie si des actions sont nécessaires + bool get actionRequise => alertes.isNotEmpty; + + /// Retourne les recommandations d'amélioration + List get recommandations { + List recommandations = []; + + if (tauxRetard > 15) { + recommandations.add('Mettre en place des rappels automatiques'); + recommandations.add('Contacter les membres en retard'); + } + + if (tauxPaiement < 70) { + recommandations.add('Faciliter les moyens de paiement'); + recommandations.add('Proposer des échéanciers personnalisés'); + } + + if (cotisationsEnRetard > 10) { + recommandations.add('Organiser une campagne de recouvrement'); + } + + return recommandations; + } + + /// Copie avec modifications + CotisationStatisticsModel copyWith({ + int? totalCotisations, + double? montantTotal, + double? montantPaye, + double? montantRestant, + int? cotisationsPayees, + int? cotisationsEnAttente, + int? cotisationsEnRetard, + int? cotisationsAnnulees, + double? tauxPaiement, + double? tauxRetard, + double? montantMoyenCotisation, + double? montantMoyenPaiement, + Map? repartitionParType, + Map? montantParType, + Map? repartitionParStatut, + Map? montantParStatut, + Map? evolutionMensuelle, + Map? chiffreAffaireMensuel, + List? tendances, + DateTime? dateCalcul, + String? periode, + int? annee, + int? mois, + }) { + return CotisationStatisticsModel( + totalCotisations: totalCotisations ?? this.totalCotisations, + montantTotal: montantTotal ?? this.montantTotal, + montantPaye: montantPaye ?? this.montantPaye, + montantRestant: montantRestant ?? this.montantRestant, + cotisationsPayees: cotisationsPayees ?? this.cotisationsPayees, + cotisationsEnAttente: cotisationsEnAttente ?? this.cotisationsEnAttente, + cotisationsEnRetard: cotisationsEnRetard ?? this.cotisationsEnRetard, + cotisationsAnnulees: cotisationsAnnulees ?? this.cotisationsAnnulees, + tauxPaiement: tauxPaiement ?? this.tauxPaiement, + tauxRetard: tauxRetard ?? this.tauxRetard, + montantMoyenCotisation: montantMoyenCotisation ?? this.montantMoyenCotisation, + montantMoyenPaiement: montantMoyenPaiement ?? this.montantMoyenPaiement, + repartitionParType: repartitionParType ?? this.repartitionParType, + montantParType: montantParType ?? this.montantParType, + repartitionParStatut: repartitionParStatut ?? this.repartitionParStatut, + montantParStatut: montantParStatut ?? this.montantParStatut, + evolutionMensuelle: evolutionMensuelle ?? this.evolutionMensuelle, + chiffreAffaireMensuel: chiffreAffaireMensuel ?? this.chiffreAffaireMensuel, + tendances: tendances ?? this.tendances, + dateCalcul: dateCalcul ?? this.dateCalcul, + periode: periode ?? this.periode, + annee: annee ?? this.annee, + mois: mois ?? this.mois, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is CotisationStatisticsModel && + other.dateCalcul == dateCalcul && + other.periode == periode && + other.annee == annee && + other.mois == mois; + } + + @override + int get hashCode => Object.hash(dateCalcul, periode, annee, mois); + + @override + String toString() { + return 'CotisationStatisticsModel(totalCotisations: $totalCotisations, ' + 'montantTotal: $montantTotal, tauxPaiement: $tauxPaiement%)'; + } +} + +/// Modèle pour les tendances des cotisations +@JsonSerializable() +class CotisationTrendModel { + final String periode; + final int totalCotisations; + final double montantTotal; + final double montantPaye; + final double tauxPaiement; + final DateTime date; + + const CotisationTrendModel({ + required this.periode, + required this.totalCotisations, + required this.montantTotal, + required this.montantPaye, + required this.tauxPaiement, + required this.date, + }); + + factory CotisationTrendModel.fromJson(Map json) => + _$CotisationTrendModelFromJson(json); + + Map toJson() => _$CotisationTrendModelToJson(this); + + @override + String toString() { + return 'CotisationTrendModel(periode: $periode, tauxPaiement: $tauxPaiement%)'; + } +} diff --git a/unionflow-mobile-apps/lib/core/models/cotisation_statistics_model.g.dart b/unionflow-mobile-apps/lib/core/models/cotisation_statistics_model.g.dart new file mode 100644 index 0000000..96a4a94 --- /dev/null +++ b/unionflow-mobile-apps/lib/core/models/cotisation_statistics_model.g.dart @@ -0,0 +1,105 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'cotisation_statistics_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CotisationStatisticsModel _$CotisationStatisticsModelFromJson( + Map json) => + CotisationStatisticsModel( + totalCotisations: (json['totalCotisations'] as num).toInt(), + montantTotal: (json['montantTotal'] as num).toDouble(), + montantPaye: (json['montantPaye'] as num).toDouble(), + montantRestant: (json['montantRestant'] as num).toDouble(), + cotisationsPayees: (json['cotisationsPayees'] as num).toInt(), + cotisationsEnAttente: (json['cotisationsEnAttente'] as num).toInt(), + cotisationsEnRetard: (json['cotisationsEnRetard'] as num).toInt(), + cotisationsAnnulees: (json['cotisationsAnnulees'] as num).toInt(), + tauxPaiement: (json['tauxPaiement'] as num).toDouble(), + tauxRetard: (json['tauxRetard'] as num).toDouble(), + montantMoyenCotisation: + (json['montantMoyenCotisation'] as num).toDouble(), + montantMoyenPaiement: (json['montantMoyenPaiement'] as num).toDouble(), + repartitionParType: + (json['repartitionParType'] as Map?)?.map( + (k, e) => MapEntry(k, (e as num).toInt()), + ), + montantParType: (json['montantParType'] as Map?)?.map( + (k, e) => MapEntry(k, (e as num).toDouble()), + ), + repartitionParStatut: + (json['repartitionParStatut'] as Map?)?.map( + (k, e) => MapEntry(k, (e as num).toInt()), + ), + montantParStatut: + (json['montantParStatut'] as Map?)?.map( + (k, e) => MapEntry(k, (e as num).toDouble()), + ), + evolutionMensuelle: + (json['evolutionMensuelle'] as Map?)?.map( + (k, e) => MapEntry(k, (e as num).toInt()), + ), + chiffreAffaireMensuel: + (json['chiffreAffaireMensuel'] as Map?)?.map( + (k, e) => MapEntry(k, (e as num).toDouble()), + ), + tendances: (json['tendances'] as List?) + ?.map((e) => CotisationTrendModel.fromJson(e as Map)) + .toList(), + dateCalcul: DateTime.parse(json['dateCalcul'] as String), + periode: json['periode'] as String?, + annee: (json['annee'] as num?)?.toInt(), + mois: (json['mois'] as num?)?.toInt(), + ); + +Map _$CotisationStatisticsModelToJson( + CotisationStatisticsModel instance) => + { + 'totalCotisations': instance.totalCotisations, + 'montantTotal': instance.montantTotal, + 'montantPaye': instance.montantPaye, + 'montantRestant': instance.montantRestant, + 'cotisationsPayees': instance.cotisationsPayees, + 'cotisationsEnAttente': instance.cotisationsEnAttente, + 'cotisationsEnRetard': instance.cotisationsEnRetard, + 'cotisationsAnnulees': instance.cotisationsAnnulees, + 'tauxPaiement': instance.tauxPaiement, + 'tauxRetard': instance.tauxRetard, + 'montantMoyenCotisation': instance.montantMoyenCotisation, + 'montantMoyenPaiement': instance.montantMoyenPaiement, + 'repartitionParType': instance.repartitionParType, + 'montantParType': instance.montantParType, + 'repartitionParStatut': instance.repartitionParStatut, + 'montantParStatut': instance.montantParStatut, + 'evolutionMensuelle': instance.evolutionMensuelle, + 'chiffreAffaireMensuel': instance.chiffreAffaireMensuel, + 'tendances': instance.tendances, + 'dateCalcul': instance.dateCalcul.toIso8601String(), + 'periode': instance.periode, + 'annee': instance.annee, + 'mois': instance.mois, + }; + +CotisationTrendModel _$CotisationTrendModelFromJson( + Map json) => + CotisationTrendModel( + periode: json['periode'] as String, + totalCotisations: (json['totalCotisations'] as num).toInt(), + montantTotal: (json['montantTotal'] as num).toDouble(), + montantPaye: (json['montantPaye'] as num).toDouble(), + tauxPaiement: (json['tauxPaiement'] as num).toDouble(), + date: DateTime.parse(json['date'] as String), + ); + +Map _$CotisationTrendModelToJson( + CotisationTrendModel instance) => + { + 'periode': instance.periode, + 'totalCotisations': instance.totalCotisations, + 'montantTotal': instance.montantTotal, + 'montantPaye': instance.montantPaye, + 'tauxPaiement': instance.tauxPaiement, + 'date': instance.date.toIso8601String(), + }; diff --git a/unionflow-mobile-apps/lib/core/models/payment_model.dart b/unionflow-mobile-apps/lib/core/models/payment_model.dart new file mode 100644 index 0000000..eb66f57 --- /dev/null +++ b/unionflow-mobile-apps/lib/core/models/payment_model.dart @@ -0,0 +1,279 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'payment_model.g.dart'; + +/// Modèle de données pour les paiements +/// Représente une transaction de paiement de cotisation +@JsonSerializable() +class PaymentModel { + final String id; + final String cotisationId; + final String numeroReference; + final double montant; + final String codeDevise; + final String methodePaiement; + final String statut; + final DateTime dateTransaction; + final String? numeroTransaction; + final String? referencePaiement; + final String? description; + final Map? metadonnees; + final String? operateurMobileMoney; + final String? numeroTelephone; + final String? nomPayeur; + final String? emailPayeur; + final double? fraisTransaction; + final String? codeAutorisation; + final String? messageErreur; + final int? nombreTentatives; + final DateTime? dateEcheance; + final DateTime dateCreation; + final DateTime? dateModification; + + const PaymentModel({ + required this.id, + required this.cotisationId, + required this.numeroReference, + required this.montant, + required this.codeDevise, + required this.methodePaiement, + required this.statut, + required this.dateTransaction, + this.numeroTransaction, + this.referencePaiement, + this.description, + this.metadonnees, + this.operateurMobileMoney, + this.numeroTelephone, + this.nomPayeur, + this.emailPayeur, + this.fraisTransaction, + this.codeAutorisation, + this.messageErreur, + this.nombreTentatives, + this.dateEcheance, + required this.dateCreation, + this.dateModification, + }); + + /// Factory pour créer depuis JSON + factory PaymentModel.fromJson(Map json) => + _$PaymentModelFromJson(json); + + /// Convertit vers JSON + Map toJson() => _$PaymentModelToJson(this); + + /// Vérifie si le paiement est réussi + bool get isSuccessful => statut == 'COMPLETED' || statut == 'SUCCESS'; + + /// Vérifie si le paiement est en cours + bool get isPending => statut == 'PENDING' || statut == 'PROCESSING'; + + /// Vérifie si le paiement a échoué + bool get isFailed => statut == 'FAILED' || statut == 'ERROR' || statut == 'CANCELLED'; + + /// Retourne la couleur associée au statut + String get couleurStatut { + switch (statut) { + case 'COMPLETED': + case 'SUCCESS': + return '#4CAF50'; // Vert + case 'PENDING': + case 'PROCESSING': + return '#FF9800'; // Orange + case 'FAILED': + case 'ERROR': + return '#F44336'; // Rouge + case 'CANCELLED': + return '#9E9E9E'; // Gris + default: + return '#757575'; // Gris foncé + } + } + + /// Retourne le libellé du statut en français + String get libelleStatut { + switch (statut) { + case 'COMPLETED': + case 'SUCCESS': + return 'Réussi'; + case 'PENDING': + return 'En attente'; + case 'PROCESSING': + return 'En cours'; + case 'FAILED': + return 'Échoué'; + case 'ERROR': + return 'Erreur'; + case 'CANCELLED': + return 'Annulé'; + default: + return statut; + } + } + + /// Retourne le libellé de la méthode de paiement + String get libelleMethodePaiement { + switch (methodePaiement) { + case 'MOBILE_MONEY': + return 'Mobile Money'; + case 'ORANGE_MONEY': + return 'Orange Money'; + case 'WAVE': + return 'Wave'; + case 'MOOV_MONEY': + return 'Moov Money'; + case 'CARTE_BANCAIRE': + return 'Carte bancaire'; + case 'VIREMENT': + return 'Virement bancaire'; + case 'ESPECES': + return 'Espèces'; + case 'CHEQUE': + return 'Chèque'; + default: + return methodePaiement; + } + } + + /// Retourne l'icône associée à la méthode de paiement + String get iconeMethodePaiement { + switch (methodePaiement) { + case 'MOBILE_MONEY': + case 'ORANGE_MONEY': + case 'WAVE': + case 'MOOV_MONEY': + return '📱'; + case 'CARTE_BANCAIRE': + return '💳'; + case 'VIREMENT': + return '🏦'; + case 'ESPECES': + return '💵'; + case 'CHEQUE': + return '📝'; + default: + return '💰'; + } + } + + /// Calcule le montant net (montant - frais) + double get montantNet { + return montant - (fraisTransaction ?? 0); + } + + /// Vérifie si des frais sont appliqués + bool get hasFrais => fraisTransaction != null && fraisTransaction! > 0; + + /// Retourne le pourcentage de frais + double get pourcentageFrais { + if (montant == 0 || fraisTransaction == null) return 0; + return (fraisTransaction! / montant * 100); + } + + /// Vérifie si le paiement est expiré + bool get isExpired { + if (dateEcheance == null) return false; + return DateTime.now().isAfter(dateEcheance!) && !isSuccessful; + } + + /// Retourne le temps restant avant expiration + Duration? get tempsRestant { + if (dateEcheance == null || isExpired) return null; + return dateEcheance!.difference(DateTime.now()); + } + + /// Retourne un message d'état détaillé + String get messageStatut { + switch (statut) { + case 'COMPLETED': + case 'SUCCESS': + return 'Paiement effectué avec succès'; + case 'PENDING': + return 'Paiement en attente de confirmation'; + case 'PROCESSING': + return 'Traitement du paiement en cours'; + case 'FAILED': + return messageErreur ?? 'Le paiement a échoué'; + case 'ERROR': + return messageErreur ?? 'Erreur lors du paiement'; + case 'CANCELLED': + return 'Paiement annulé par l\'utilisateur'; + default: + return 'Statut inconnu'; + } + } + + /// Vérifie si le paiement peut être retenté + bool get canRetry { + return isFailed && (nombreTentatives ?? 0) < 3 && !isExpired; + } + + /// Copie avec modifications + PaymentModel copyWith({ + String? id, + String? cotisationId, + String? numeroReference, + double? montant, + String? codeDevise, + String? methodePaiement, + String? statut, + DateTime? dateTransaction, + String? numeroTransaction, + String? referencePaiement, + String? description, + Map? metadonnees, + String? operateurMobileMoney, + String? numeroTelephone, + String? nomPayeur, + String? emailPayeur, + double? fraisTransaction, + String? codeAutorisation, + String? messageErreur, + int? nombreTentatives, + DateTime? dateEcheance, + DateTime? dateCreation, + DateTime? dateModification, + }) { + return PaymentModel( + id: id ?? this.id, + cotisationId: cotisationId ?? this.cotisationId, + numeroReference: numeroReference ?? this.numeroReference, + montant: montant ?? this.montant, + codeDevise: codeDevise ?? this.codeDevise, + methodePaiement: methodePaiement ?? this.methodePaiement, + statut: statut ?? this.statut, + dateTransaction: dateTransaction ?? this.dateTransaction, + numeroTransaction: numeroTransaction ?? this.numeroTransaction, + referencePaiement: referencePaiement ?? this.referencePaiement, + description: description ?? this.description, + metadonnees: metadonnees ?? this.metadonnees, + operateurMobileMoney: operateurMobileMoney ?? this.operateurMobileMoney, + numeroTelephone: numeroTelephone ?? this.numeroTelephone, + nomPayeur: nomPayeur ?? this.nomPayeur, + emailPayeur: emailPayeur ?? this.emailPayeur, + fraisTransaction: fraisTransaction ?? this.fraisTransaction, + codeAutorisation: codeAutorisation ?? this.codeAutorisation, + messageErreur: messageErreur ?? this.messageErreur, + nombreTentatives: nombreTentatives ?? this.nombreTentatives, + dateEcheance: dateEcheance ?? this.dateEcheance, + dateCreation: dateCreation ?? this.dateCreation, + dateModification: dateModification ?? this.dateModification, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is PaymentModel && other.id == id; + } + + @override + int get hashCode => id.hashCode; + + @override + String toString() { + return 'PaymentModel(id: $id, numeroReference: $numeroReference, ' + 'montant: $montant, methodePaiement: $methodePaiement, statut: $statut)'; + } +} diff --git a/unionflow-mobile-apps/lib/core/models/payment_model.g.dart b/unionflow-mobile-apps/lib/core/models/payment_model.g.dart new file mode 100644 index 0000000..ba0bbdf --- /dev/null +++ b/unionflow-mobile-apps/lib/core/models/payment_model.g.dart @@ -0,0 +1,64 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'payment_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PaymentModel _$PaymentModelFromJson(Map json) => PaymentModel( + id: json['id'] as String, + cotisationId: json['cotisationId'] as String, + numeroReference: json['numeroReference'] as String, + montant: (json['montant'] as num).toDouble(), + codeDevise: json['codeDevise'] as String, + methodePaiement: json['methodePaiement'] as String, + statut: json['statut'] as String, + dateTransaction: DateTime.parse(json['dateTransaction'] as String), + numeroTransaction: json['numeroTransaction'] as String?, + referencePaiement: json['referencePaiement'] as String?, + description: json['description'] as String?, + metadonnees: json['metadonnees'] as Map?, + operateurMobileMoney: json['operateurMobileMoney'] as String?, + numeroTelephone: json['numeroTelephone'] as String?, + nomPayeur: json['nomPayeur'] as String?, + emailPayeur: json['emailPayeur'] as String?, + fraisTransaction: (json['fraisTransaction'] as num?)?.toDouble(), + codeAutorisation: json['codeAutorisation'] as String?, + messageErreur: json['messageErreur'] as String?, + nombreTentatives: (json['nombreTentatives'] as num?)?.toInt(), + dateEcheance: json['dateEcheance'] == null + ? null + : DateTime.parse(json['dateEcheance'] as String), + dateCreation: DateTime.parse(json['dateCreation'] as String), + dateModification: json['dateModification'] == null + ? null + : DateTime.parse(json['dateModification'] as String), + ); + +Map _$PaymentModelToJson(PaymentModel instance) => + { + 'id': instance.id, + 'cotisationId': instance.cotisationId, + 'numeroReference': instance.numeroReference, + 'montant': instance.montant, + 'codeDevise': instance.codeDevise, + 'methodePaiement': instance.methodePaiement, + 'statut': instance.statut, + 'dateTransaction': instance.dateTransaction.toIso8601String(), + 'numeroTransaction': instance.numeroTransaction, + 'referencePaiement': instance.referencePaiement, + 'description': instance.description, + 'metadonnees': instance.metadonnees, + 'operateurMobileMoney': instance.operateurMobileMoney, + 'numeroTelephone': instance.numeroTelephone, + 'nomPayeur': instance.nomPayeur, + 'emailPayeur': instance.emailPayeur, + 'fraisTransaction': instance.fraisTransaction, + 'codeAutorisation': instance.codeAutorisation, + 'messageErreur': instance.messageErreur, + 'nombreTentatives': instance.nombreTentatives, + 'dateEcheance': instance.dateEcheance?.toIso8601String(), + 'dateCreation': instance.dateCreation.toIso8601String(), + 'dateModification': instance.dateModification?.toIso8601String(), + }; diff --git a/unionflow-mobile-apps/lib/core/network/dio_client.dart b/unionflow-mobile-apps/lib/core/network/dio_client.dart index 809ca3f..c1187c7 100644 --- a/unionflow-mobile-apps/lib/core/network/dio_client.dart +++ b/unionflow-mobile-apps/lib/core/network/dio_client.dart @@ -19,7 +19,7 @@ class DioClient { void _configureOptions() { _dio.options = BaseOptions( // URL de base de l'API - baseUrl: 'http://192.168.1.11:8080', // Adresse de votre API Quarkus + baseUrl: 'http://192.168.1.145:8080', // Adresse de votre API Quarkus // Timeouts connectTimeout: const Duration(seconds: 30), diff --git a/unionflow-mobile-apps/lib/core/services/api_service.dart b/unionflow-mobile-apps/lib/core/services/api_service.dart index f84e2ce..26b0f92 100644 --- a/unionflow-mobile-apps/lib/core/services/api_service.dart +++ b/unionflow-mobile-apps/lib/core/services/api_service.dart @@ -4,6 +4,7 @@ import '../models/membre_model.dart'; import '../models/cotisation_model.dart'; import '../models/evenement_model.dart'; import '../models/wave_checkout_session_model.dart'; +import '../models/payment_model.dart'; import '../network/dio_client.dart'; /// Service API principal pour communiquer avec le serveur UnionFlow @@ -438,7 +439,7 @@ class ApiService { }) async { try { final response = await _dio.get( - '/api/evenements/a-venir', + '/api/evenements/a-venir-public', queryParameters: { 'page': page, 'size': size, @@ -640,4 +641,75 @@ class ApiService { throw _handleDioException(e, 'Erreur lors de la récupération des statistiques'); } } + + // ======================================== + // PAIEMENTS + // ======================================== + + /// Initie un paiement + Future initiatePayment(Map paymentData) async { + try { + final response = await _dio.post('/api/paiements/initier', data: paymentData); + return PaymentModel.fromJson(response.data as Map); + } on DioException catch (e) { + throw _handleDioException(e, 'Erreur lors de l\'initiation du paiement'); + } + } + + /// Récupère le statut d'un paiement + Future getPaymentStatus(String paymentId) async { + try { + final response = await _dio.get('/api/paiements/$paymentId/statut'); + return PaymentModel.fromJson(response.data as Map); + } on DioException catch (e) { + throw _handleDioException(e, 'Erreur lors de la vérification du statut'); + } + } + + /// Annule un paiement + Future cancelPayment(String paymentId) async { + try { + final response = await _dio.post('/api/paiements/$paymentId/annuler'); + return response.statusCode == 200; + } on DioException catch (e) { + throw _handleDioException(e, 'Erreur lors de l\'annulation du paiement'); + } + } + + /// Récupère l'historique des paiements + Future> getPaymentHistory(Map filters) async { + try { + final response = await _dio.get('/api/paiements/historique', queryParameters: filters); + + if (response.data is List) { + return (response.data as List) + .map((json) => PaymentModel.fromJson(json as Map)) + .toList(); + } + + throw Exception('Format de réponse invalide pour l\'historique des paiements'); + } on DioException catch (e) { + throw _handleDioException(e, 'Erreur lors de la récupération de l\'historique'); + } + } + + /// Vérifie le statut d'un service de paiement + Future> checkServiceStatus(String serviceType) async { + try { + final response = await _dio.get('/api/paiements/services/$serviceType/statut'); + return response.data as Map; + } on DioException catch (e) { + throw _handleDioException(e, 'Erreur lors de la vérification du service'); + } + } + + /// Récupère les statistiques de paiement + Future> getPaymentStatistics(Map filters) async { + try { + final response = await _dio.get('/api/paiements/statistiques', queryParameters: filters); + return response.data as Map; + } on DioException catch (e) { + throw _handleDioException(e, 'Erreur lors de la récupération des statistiques'); + } + } } diff --git a/unionflow-mobile-apps/lib/core/services/cache_service.dart b/unionflow-mobile-apps/lib/core/services/cache_service.dart new file mode 100644 index 0000000..8332bd7 --- /dev/null +++ b/unionflow-mobile-apps/lib/core/services/cache_service.dart @@ -0,0 +1,249 @@ +import 'dart:convert'; +import 'package:injectable/injectable.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../models/cotisation_model.dart'; +import '../models/cotisation_statistics_model.dart'; +import '../models/payment_model.dart'; + +/// Service de gestion du cache local +/// Permet de stocker et récupérer des données en mode hors-ligne +@LazySingleton() +class CacheService { + static const String _cotisationsCacheKey = 'cotisations_cache'; + static const String _cotisationsStatsCacheKey = 'cotisations_stats_cache'; + static const String _paymentsCacheKey = 'payments_cache'; + static const String _lastSyncKey = 'last_sync_timestamp'; + static const Duration _cacheValidityDuration = Duration(minutes: 30); + + final SharedPreferences _prefs; + + CacheService(this._prefs); + + /// Sauvegarde une liste de cotisations dans le cache + Future saveCotisations(List cotisations, {String? key}) async { + final cacheKey = key ?? _cotisationsCacheKey; + final jsonList = cotisations.map((c) => c.toJson()).toList(); + final jsonString = jsonEncode({ + 'data': jsonList, + 'timestamp': DateTime.now().millisecondsSinceEpoch, + }); + await _prefs.setString(cacheKey, jsonString); + } + + /// Récupère une liste de cotisations depuis le cache + Future?> getCotisations({String? key}) async { + final cacheKey = key ?? _cotisationsCacheKey; + final jsonString = _prefs.getString(cacheKey); + + if (jsonString == null) return null; + + try { + final jsonData = jsonDecode(jsonString) as Map; + final timestamp = DateTime.fromMillisecondsSinceEpoch(jsonData['timestamp'] as int); + + // Vérifier si le cache est encore valide + if (DateTime.now().difference(timestamp) > _cacheValidityDuration) { + await clearCotisations(key: key); + return null; + } + + final jsonList = jsonData['data'] as List; + return jsonList.map((json) => CotisationModel.fromJson(json as Map)).toList(); + } catch (e) { + // En cas d'erreur, nettoyer le cache corrompu + await clearCotisations(key: key); + return null; + } + } + + /// Sauvegarde les statistiques des cotisations + Future saveCotisationsStats(CotisationStatisticsModel stats) async { + final jsonString = jsonEncode({ + 'data': stats.toJson(), + 'timestamp': DateTime.now().millisecondsSinceEpoch, + }); + await _prefs.setString(_cotisationsStatsCacheKey, jsonString); + } + + /// Récupère les statistiques des cotisations depuis le cache + Future getCotisationsStats() async { + final jsonString = _prefs.getString(_cotisationsStatsCacheKey); + + if (jsonString == null) return null; + + try { + final jsonData = jsonDecode(jsonString) as Map; + final timestamp = DateTime.fromMillisecondsSinceEpoch(jsonData['timestamp'] as int); + + // Vérifier si le cache est encore valide + if (DateTime.now().difference(timestamp) > _cacheValidityDuration) { + await clearCotisationsStats(); + return null; + } + + return CotisationStatisticsModel.fromJson(jsonData['data'] as Map); + } catch (e) { + await clearCotisationsStats(); + return null; + } + } + + /// Sauvegarde une liste de paiements dans le cache + Future savePayments(List payments) async { + final jsonList = payments.map((p) => p.toJson()).toList(); + final jsonString = jsonEncode({ + 'data': jsonList, + 'timestamp': DateTime.now().millisecondsSinceEpoch, + }); + await _prefs.setString(_paymentsCacheKey, jsonString); + } + + /// Récupère une liste de paiements depuis le cache + Future?> getPayments() async { + final jsonString = _prefs.getString(_paymentsCacheKey); + + if (jsonString == null) return null; + + try { + final jsonData = jsonDecode(jsonString) as Map; + final timestamp = DateTime.fromMillisecondsSinceEpoch(jsonData['timestamp'] as int); + + // Vérifier si le cache est encore valide + if (DateTime.now().difference(timestamp) > _cacheValidityDuration) { + await clearPayments(); + return null; + } + + final jsonList = jsonData['data'] as List; + return jsonList.map((json) => PaymentModel.fromJson(json as Map)).toList(); + } catch (e) { + await clearPayments(); + return null; + } + } + + /// Sauvegarde une cotisation individuelle dans le cache + Future saveCotisation(CotisationModel cotisation) async { + final key = 'cotisation_${cotisation.id}'; + final jsonString = jsonEncode({ + 'data': cotisation.toJson(), + 'timestamp': DateTime.now().millisecondsSinceEpoch, + }); + await _prefs.setString(key, jsonString); + } + + /// Récupère une cotisation individuelle depuis le cache + Future getCotisation(String id) async { + final key = 'cotisation_$id'; + final jsonString = _prefs.getString(key); + + if (jsonString == null) return null; + + try { + final jsonData = jsonDecode(jsonString) as Map; + final timestamp = DateTime.fromMillisecondsSinceEpoch(jsonData['timestamp'] as int); + + // Vérifier si le cache est encore valide + if (DateTime.now().difference(timestamp) > _cacheValidityDuration) { + await clearCotisation(id); + return null; + } + + return CotisationModel.fromJson(jsonData['data'] as Map); + } catch (e) { + await clearCotisation(id); + return null; + } + } + + /// Met à jour le timestamp de la dernière synchronisation + Future updateLastSyncTimestamp() async { + await _prefs.setInt(_lastSyncKey, DateTime.now().millisecondsSinceEpoch); + } + + /// Récupère le timestamp de la dernière synchronisation + DateTime? getLastSyncTimestamp() { + final timestamp = _prefs.getInt(_lastSyncKey); + return timestamp != null ? DateTime.fromMillisecondsSinceEpoch(timestamp) : null; + } + + /// Vérifie si une synchronisation est nécessaire + bool needsSync() { + final lastSync = getLastSyncTimestamp(); + if (lastSync == null) return true; + + return DateTime.now().difference(lastSync) > const Duration(minutes: 15); + } + + /// Nettoie le cache des cotisations + Future clearCotisations({String? key}) async { + final cacheKey = key ?? _cotisationsCacheKey; + await _prefs.remove(cacheKey); + } + + /// Nettoie le cache des statistiques + Future clearCotisationsStats() async { + await _prefs.remove(_cotisationsStatsCacheKey); + } + + /// Nettoie le cache des paiements + Future clearPayments() async { + await _prefs.remove(_paymentsCacheKey); + } + + /// Nettoie une cotisation individuelle du cache + Future clearCotisation(String id) async { + final key = 'cotisation_$id'; + await _prefs.remove(key); + } + + /// Nettoie tout le cache des cotisations + Future clearAllCotisationsCache() async { + final keys = _prefs.getKeys().where((key) => + key.startsWith('cotisation') || + key == _cotisationsStatsCacheKey || + key == _paymentsCacheKey + ).toList(); + + for (final key in keys) { + await _prefs.remove(key); + } + } + + /// Retourne la taille du cache en octets (approximation) + int getCacheSize() { + int totalSize = 0; + final keys = _prefs.getKeys().where((key) => + key.startsWith('cotisation') || + key == _cotisationsStatsCacheKey || + key == _paymentsCacheKey + ); + + for (final key in keys) { + final value = _prefs.getString(key); + if (value != null) { + totalSize += value.length * 2; // Approximation UTF-16 + } + } + + return totalSize; + } + + /// Retourne des informations sur le cache + Map getCacheInfo() { + final lastSync = getLastSyncTimestamp(); + return { + 'lastSync': lastSync?.toIso8601String(), + 'needsSync': needsSync(), + 'cacheSize': getCacheSize(), + 'cacheSizeFormatted': _formatBytes(getCacheSize()), + }; + } + + /// Formate la taille en octets en format lisible + String _formatBytes(int bytes) { + if (bytes < 1024) return '$bytes B'; + if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; + return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; + } +} diff --git a/unionflow-mobile-apps/lib/core/services/moov_money_service.dart b/unionflow-mobile-apps/lib/core/services/moov_money_service.dart new file mode 100644 index 0000000..9192bfc --- /dev/null +++ b/unionflow-mobile-apps/lib/core/services/moov_money_service.dart @@ -0,0 +1,280 @@ +import 'package:injectable/injectable.dart'; +import '../models/payment_model.dart'; +import 'api_service.dart'; + +/// Service d'intégration avec Moov Money +/// Gère les paiements via Moov Money pour la Côte d'Ivoire +@LazySingleton() +class MoovMoneyService { + final ApiService _apiService; + + MoovMoneyService(this._apiService); + + /// Initie un paiement Moov Money pour une cotisation + Future initiatePayment({ + required String cotisationId, + required double montant, + required String numeroTelephone, + String? nomPayeur, + String? emailPayeur, + }) async { + try { + final paymentData = { + 'cotisationId': cotisationId, + 'montant': montant, + 'methodePaiement': 'MOOV_MONEY', + 'numeroTelephone': numeroTelephone, + 'nomPayeur': nomPayeur, + 'emailPayeur': emailPayeur, + }; + + // Appel API pour initier le paiement Moov Money + final payment = await _apiService.initiatePayment(paymentData); + + return payment; + } catch (e) { + throw MoovMoneyException('Erreur lors de l\'initiation du paiement Moov Money: ${e.toString()}'); + } + } + + /// Vérifie le statut d'un paiement Moov Money + Future checkPaymentStatus(String paymentId) async { + try { + return await _apiService.getPaymentStatus(paymentId); + } catch (e) { + throw MoovMoneyException('Erreur lors de la vérification du statut: ${e.toString()}'); + } + } + + /// Calcule les frais Moov Money selon le barème officiel + double calculateMoovMoneyFees(double montant) { + // Barème Moov Money Côte d'Ivoire (2024) + if (montant <= 1000) return 0; // Gratuit jusqu'à 1000 XOF + if (montant <= 5000) return 30; // 30 XOF de 1001 à 5000 + if (montant <= 15000) return 75; // 75 XOF de 5001 à 15000 + if (montant <= 50000) return 150; // 150 XOF de 15001 à 50000 + if (montant <= 100000) return 300; // 300 XOF de 50001 à 100000 + if (montant <= 250000) return 600; // 600 XOF de 100001 à 250000 + if (montant <= 500000) return 1200; // 1200 XOF de 250001 à 500000 + + // Au-delà de 500000 XOF: 0.4% du montant + return montant * 0.004; + } + + /// Valide un numéro de téléphone Moov Money + bool validatePhoneNumber(String numeroTelephone) { + // Nettoyer le numéro + final cleanNumber = numeroTelephone.replaceAll(RegExp(r'[^\d]'), ''); + + // Moov Money: 01, 02, 03 (Côte d'Ivoire) + // Format: 225XXXXXXXX ou 0XXXXXXXX + return RegExp(r'^(225)?(0[123])\d{8}$').hasMatch(cleanNumber); + } + + /// Obtient les limites de transaction Moov Money + Map getTransactionLimits() { + return { + 'montantMinimum': 100.0, // 100 XOF minimum + 'montantMaximum': 1500000.0, // 1.5 million XOF maximum + 'fraisMinimum': 0.0, + 'fraisMaximum': 6000.0, // Frais maximum théorique + }; + } + + /// Vérifie si un montant est dans les limites autorisées + bool isAmountValid(double montant) { + final limits = getTransactionLimits(); + return montant >= limits['montantMinimum']! && + montant <= limits['montantMaximum']!; + } + + /// Formate un numéro de téléphone pour Moov Money + String formatPhoneNumber(String numeroTelephone) { + final cleanNumber = numeroTelephone.replaceAll(RegExp(r'[^\d]'), ''); + + // Si le numéro commence par 225, le garder tel quel + if (cleanNumber.startsWith('225')) { + return cleanNumber; + } + + // Si le numéro commence par 0, ajouter 225 + if (cleanNumber.startsWith('0')) { + return '225$cleanNumber'; + } + + // Sinon, ajouter 2250 + return '2250$cleanNumber'; + } + + /// Obtient les informations de l'opérateur + Map getOperatorInfo() { + return { + 'nom': 'Moov Money', + 'code': 'MOOV_MONEY', + 'couleur': '#0066CC', + 'icone': '💙', + 'description': 'Paiement via Moov Money', + 'prefixes': ['01', '02', '03'], + 'pays': 'Côte d\'Ivoire', + 'devise': 'XOF', + }; + } + + /// Génère un message de confirmation pour l'utilisateur + String generateConfirmationMessage({ + required double montant, + required String numeroTelephone, + required double frais, + }) { + final total = montant + frais; + final formattedPhone = formatPhoneNumber(numeroTelephone); + + return ''' +Confirmation de paiement Moov Money + +Montant: ${montant.toStringAsFixed(0)} XOF +Frais: ${frais.toStringAsFixed(0)} XOF +Total: ${total.toStringAsFixed(0)} XOF + +Numéro: $formattedPhone + +Vous allez recevoir un SMS avec le code de confirmation. +Composez *155# pour finaliser le paiement. +'''; + } + + /// Annule un paiement Moov Money (si possible) + Future cancelPayment(String paymentId) async { + try { + // Vérifier le statut du paiement + final payment = await checkPaymentStatus(paymentId); + + // Un paiement peut être annulé seulement s'il est en attente + if (payment.statut == 'EN_ATTENTE') { + // Appeler l'API d'annulation + await _apiService.cancelPayment(paymentId); + return true; + } + + return false; + } catch (e) { + return false; + } + } + + /// Obtient l'historique des paiements Moov Money + Future> getPaymentHistory({ + String? cotisationId, + DateTime? dateDebut, + DateTime? dateFin, + int? limit, + }) async { + try { + final filters = { + 'methodePaiement': 'MOOV_MONEY', + if (cotisationId != null) 'cotisationId': cotisationId, + if (dateDebut != null) 'dateDebut': dateDebut.toIso8601String(), + if (dateFin != null) 'dateFin': dateFin.toIso8601String(), + if (limit != null) 'limit': limit, + }; + + return await _apiService.getPaymentHistory(filters); + } catch (e) { + throw MoovMoneyException('Erreur lors de la récupération de l\'historique: ${e.toString()}'); + } + } + + /// Vérifie la disponibilité du service Moov Money + Future checkServiceAvailability() async { + try { + // Appel API pour vérifier la disponibilité + final response = await _apiService.checkServiceStatus('MOOV_MONEY'); + return response['available'] == true; + } catch (e) { + // En cas d'erreur, considérer le service comme indisponible + return false; + } + } + + /// Obtient les statistiques des paiements Moov Money + Future> getPaymentStatistics({ + DateTime? dateDebut, + DateTime? dateFin, + }) async { + try { + final filters = { + 'methodePaiement': 'MOOV_MONEY', + if (dateDebut != null) 'dateDebut': dateDebut.toIso8601String(), + if (dateFin != null) 'dateFin': dateFin.toIso8601String(), + }; + + return await _apiService.getPaymentStatistics(filters); + } catch (e) { + throw MoovMoneyException('Erreur lors de la récupération des statistiques: ${e.toString()}'); + } + } + + /// Détecte automatiquement l'opérateur à partir du numéro + static String? detectOperatorFromNumber(String numeroTelephone) { + final cleanNumber = numeroTelephone.replaceAll(RegExp(r'[^\d]'), ''); + + // Extraire les 2 premiers chiffres après 225 ou le préfixe 0 + String prefix = ''; + if (cleanNumber.startsWith('225') && cleanNumber.length >= 5) { + prefix = cleanNumber.substring(3, 5); + } else if (cleanNumber.startsWith('0') && cleanNumber.length >= 2) { + prefix = cleanNumber.substring(0, 2); + } + + // Vérifier si c'est Moov Money + if (['01', '02', '03'].contains(prefix)) { + return 'MOOV_MONEY'; + } + + return null; + } + + /// Obtient les horaires de service + Map getServiceHours() { + return { + 'ouverture': '06:00', + 'fermeture': '23:00', + 'jours': ['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche'], + 'maintenance': { + 'debut': '02:00', + 'fin': '04:00', + 'description': 'Maintenance technique quotidienne' + } + }; + } + + /// Vérifie si le service est disponible à l'heure actuelle + bool isServiceAvailableNow() { + final now = DateTime.now(); + final hour = now.hour; + + // Service disponible de 6h à 23h + // Maintenance de 2h à 4h + if (hour >= 2 && hour < 4) { + return false; // Maintenance + } + + return hour >= 6 && hour < 23; + } +} + +/// Exception personnalisée pour les erreurs Moov Money +class MoovMoneyException implements Exception { + final String message; + final String? errorCode; + final dynamic originalError; + + MoovMoneyException( + this.message, { + this.errorCode, + this.originalError, + }); + + @override + String toString() => 'MoovMoneyException: $message'; +} diff --git a/unionflow-mobile-apps/lib/core/services/notification_service.dart b/unionflow-mobile-apps/lib/core/services/notification_service.dart new file mode 100644 index 0000000..ba6d009 --- /dev/null +++ b/unionflow-mobile-apps/lib/core/services/notification_service.dart @@ -0,0 +1,362 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:injectable/injectable.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../models/cotisation_model.dart'; + +/// Service de gestion des notifications +/// Gère les notifications locales et push pour les cotisations +@LazySingleton() +class NotificationService { + static const String _notificationsEnabledKey = 'notifications_enabled'; + static const String _reminderDaysKey = 'reminder_days'; + static const String _scheduledNotificationsKey = 'scheduled_notifications'; + + final FlutterLocalNotificationsPlugin _localNotifications; + final SharedPreferences _prefs; + + NotificationService(this._localNotifications, this._prefs); + + /// Initialise le service de notifications + Future initialize() async { + const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); + const iosSettings = DarwinInitializationSettings( + requestAlertPermission: true, + requestBadgePermission: true, + requestSoundPermission: true, + ); + + const initSettings = InitializationSettings( + android: androidSettings, + iOS: iosSettings, + ); + + await _localNotifications.initialize( + initSettings, + onDidReceiveNotificationResponse: _onNotificationTapped, + ); + + // Demander les permissions sur iOS + await _requestPermissions(); + } + + /// Demande les permissions de notification + Future _requestPermissions() async { + final result = await _localNotifications + .resolvePlatformSpecificImplementation() + ?.requestPermissions( + alert: true, + badge: true, + sound: true, + ); + return result ?? true; + } + + /// Planifie une notification de rappel pour une cotisation + Future schedulePaymentReminder(CotisationModel cotisation) async { + if (!await isNotificationsEnabled()) return; + + final reminderDays = await getReminderDays(); + final notificationDate = cotisation.dateEcheance.subtract(Duration(days: reminderDays)); + + // Ne pas planifier si la date est déjà passée + if (notificationDate.isBefore(DateTime.now())) return; + + const androidDetails = AndroidNotificationDetails( + 'payment_reminders', + 'Rappels de paiement', + channelDescription: 'Notifications de rappel pour les cotisations à payer', + importance: Importance.high, + priority: Priority.high, + icon: '@mipmap/ic_launcher', + color: Color(0xFF2196F3), + playSound: true, + enableVibration: true, + ); + + const iosDetails = DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + ); + + const notificationDetails = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + final notificationId = _generateNotificationId(cotisation.id, 'reminder'); + + await _localNotifications.zonedSchedule( + notificationId, + 'Rappel de cotisation', + 'Votre cotisation ${cotisation.typeCotisation} de ${cotisation.montantDu.toStringAsFixed(0)} XOF arrive à échéance le ${_formatDate(cotisation.dateEcheance)}', + _convertToTZDateTime(notificationDate), + notificationDetails, + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime, + payload: jsonEncode({ + 'type': 'payment_reminder', + 'cotisationId': cotisation.id, + 'action': 'open_cotisation', + }), + ); + + // Sauvegarder la notification planifiée + await _saveScheduledNotification(notificationId, cotisation.id, 'reminder', notificationDate); + } + + /// Planifie une notification d'échéance le jour J + Future scheduleDueDateNotification(CotisationModel cotisation) async { + if (!await isNotificationsEnabled()) return; + + final notificationDate = DateTime( + cotisation.dateEcheance.year, + cotisation.dateEcheance.month, + cotisation.dateEcheance.day, + 9, // 9h du matin + ); + + // Ne pas planifier si la date est déjà passée + if (notificationDate.isBefore(DateTime.now())) return; + + const androidDetails = AndroidNotificationDetails( + 'due_date_notifications', + 'Échéances du jour', + channelDescription: 'Notifications pour les cotisations qui arrivent à échéance', + importance: Importance.max, + priority: Priority.max, + icon: '@mipmap/ic_launcher', + color: Color(0xFFFF5722), + playSound: true, + enableVibration: true, + ongoing: true, + ); + + const iosDetails = DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + interruptionLevel: InterruptionLevel.critical, + ); + + const notificationDetails = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + final notificationId = _generateNotificationId(cotisation.id, 'due_date'); + + await _localNotifications.zonedSchedule( + notificationId, + 'Échéance aujourd\'hui !', + 'Votre cotisation ${cotisation.typeCotisation} de ${cotisation.montantDu.toStringAsFixed(0)} XOF arrive à échéance aujourd\'hui', + _convertToTZDateTime(notificationDate), + notificationDetails, + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime, + payload: jsonEncode({ + 'type': 'due_date', + 'cotisationId': cotisation.id, + 'action': 'pay_now', + }), + ); + + await _saveScheduledNotification(notificationId, cotisation.id, 'due_date', notificationDate); + } + + /// Envoie une notification immédiate de confirmation de paiement + Future showPaymentConfirmation(CotisationModel cotisation, double montantPaye) async { + const androidDetails = AndroidNotificationDetails( + 'payment_confirmations', + 'Confirmations de paiement', + channelDescription: 'Notifications de confirmation après paiement', + importance: Importance.high, + priority: Priority.high, + icon: '@mipmap/ic_launcher', + color: Color(0xFF4CAF50), + playSound: true, + enableVibration: true, + ); + + const iosDetails = DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + ); + + const notificationDetails = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + await _localNotifications.show( + _generateNotificationId(cotisation.id, 'payment_success'), + 'Paiement confirmé ✅', + 'Votre paiement de ${montantPaye.toStringAsFixed(0)} XOF pour la cotisation ${cotisation.typeCotisation} a été confirmé', + notificationDetails, + payload: jsonEncode({ + 'type': 'payment_success', + 'cotisationId': cotisation.id, + 'action': 'view_receipt', + }), + ); + } + + /// Envoie une notification d'échec de paiement + Future showPaymentFailure(CotisationModel cotisation, String raison) async { + const androidDetails = AndroidNotificationDetails( + 'payment_failures', + 'Échecs de paiement', + channelDescription: 'Notifications d\'échec de paiement', + importance: Importance.high, + priority: Priority.high, + icon: '@mipmap/ic_launcher', + color: Color(0xFFF44336), + playSound: true, + enableVibration: true, + ); + + const iosDetails = DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + ); + + const notificationDetails = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + await _localNotifications.show( + _generateNotificationId(cotisation.id, 'payment_failure'), + 'Échec de paiement ❌', + 'Le paiement pour la cotisation ${cotisation.typeCotisation} a échoué: $raison', + notificationDetails, + payload: jsonEncode({ + 'type': 'payment_failure', + 'cotisationId': cotisation.id, + 'action': 'retry_payment', + }), + ); + } + + /// Annule toutes les notifications pour une cotisation + Future cancelCotisationNotifications(String cotisationId) async { + final scheduledNotifications = await getScheduledNotifications(); + final notificationsToCancel = scheduledNotifications + .where((n) => n['cotisationId'] == cotisationId) + .toList(); + + for (final notification in notificationsToCancel) { + await _localNotifications.cancel(notification['id'] as int); + } + + // Supprimer de la liste des notifications planifiées + final updatedNotifications = scheduledNotifications + .where((n) => n['cotisationId'] != cotisationId) + .toList(); + + await _prefs.setString(_scheduledNotificationsKey, jsonEncode(updatedNotifications)); + } + + /// Planifie les notifications pour toutes les cotisations actives + Future scheduleAllCotisationsNotifications(List cotisations) async { + // Annuler toutes les notifications existantes + await _localNotifications.cancelAll(); + await _clearScheduledNotifications(); + + // Planifier pour chaque cotisation non payée + for (final cotisation in cotisations) { + if (!cotisation.isEntierementPayee && !cotisation.isEnRetard) { + await schedulePaymentReminder(cotisation); + await scheduleDueDateNotification(cotisation); + } + } + } + + /// Configuration des notifications + + Future isNotificationsEnabled() async { + return _prefs.getBool(_notificationsEnabledKey) ?? true; + } + + Future setNotificationsEnabled(bool enabled) async { + await _prefs.setBool(_notificationsEnabledKey, enabled); + + if (!enabled) { + await _localNotifications.cancelAll(); + await _clearScheduledNotifications(); + } + } + + Future getReminderDays() async { + return _prefs.getInt(_reminderDaysKey) ?? 3; // 3 jours par défaut + } + + Future setReminderDays(int days) async { + await _prefs.setInt(_reminderDaysKey, days); + } + + Future>> getScheduledNotifications() async { + final jsonString = _prefs.getString(_scheduledNotificationsKey); + if (jsonString == null) return []; + + try { + final List jsonList = jsonDecode(jsonString); + return jsonList.cast>(); + } catch (e) { + return []; + } + } + + /// Méthodes privées + + void _onNotificationTapped(NotificationResponse response) { + if (response.payload != null) { + try { + final payload = jsonDecode(response.payload!); + // TODO: Implémenter la navigation selon l'action + // NavigationService.navigateToAction(payload); + } catch (e) { + // Ignorer les erreurs de parsing + } + } + } + + int _generateNotificationId(String cotisationId, String type) { + return '${cotisationId}_$type'.hashCode; + } + + String _formatDate(DateTime date) { + return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}'; + } + + // Note: Cette méthode nécessite le package timezone + // Pour simplifier, on utilise DateTime directement + dynamic _convertToTZDateTime(DateTime dateTime) { + return dateTime; // Simplification - en production, utiliser TZDateTime + } + + Future _saveScheduledNotification( + int notificationId, + String cotisationId, + String type, + DateTime scheduledDate, + ) async { + final notifications = await getScheduledNotifications(); + notifications.add({ + 'id': notificationId, + 'cotisationId': cotisationId, + 'type': type, + 'scheduledDate': scheduledDate.toIso8601String(), + }); + + await _prefs.setString(_scheduledNotificationsKey, jsonEncode(notifications)); + } + + Future _clearScheduledNotifications() async { + await _prefs.remove(_scheduledNotificationsKey); + } +} diff --git a/unionflow-mobile-apps/lib/core/services/orange_money_service.dart b/unionflow-mobile-apps/lib/core/services/orange_money_service.dart new file mode 100644 index 0000000..274b7bc --- /dev/null +++ b/unionflow-mobile-apps/lib/core/services/orange_money_service.dart @@ -0,0 +1,233 @@ +import 'package:injectable/injectable.dart'; +import '../models/payment_model.dart'; +import 'api_service.dart'; + +/// Service d'intégration avec Orange Money +/// Gère les paiements via Orange Money pour la Côte d'Ivoire +@LazySingleton() +class OrangeMoneyService { + final ApiService _apiService; + + OrangeMoneyService(this._apiService); + + /// Initie un paiement Orange Money pour une cotisation + Future initiatePayment({ + required String cotisationId, + required double montant, + required String numeroTelephone, + String? nomPayeur, + String? emailPayeur, + }) async { + try { + final paymentData = { + 'cotisationId': cotisationId, + 'montant': montant, + 'methodePaiement': 'ORANGE_MONEY', + 'numeroTelephone': numeroTelephone, + 'nomPayeur': nomPayeur, + 'emailPayeur': emailPayeur, + }; + + // Appel API pour initier le paiement Orange Money + final payment = await _apiService.initiatePayment(paymentData); + + return payment; + } catch (e) { + throw OrangeMoneyException('Erreur lors de l\'initiation du paiement Orange Money: ${e.toString()}'); + } + } + + /// Vérifie le statut d'un paiement Orange Money + Future checkPaymentStatus(String paymentId) async { + try { + return await _apiService.getPaymentStatus(paymentId); + } catch (e) { + throw OrangeMoneyException('Erreur lors de la vérification du statut: ${e.toString()}'); + } + } + + /// Calcule les frais Orange Money selon le barème officiel + double calculateOrangeMoneyFees(double montant) { + // Barème Orange Money Côte d'Ivoire (2024) + if (montant <= 1000) return 0; // Gratuit jusqu'à 1000 XOF + if (montant <= 5000) return 25; // 25 XOF de 1001 à 5000 + if (montant <= 10000) return 50; // 50 XOF de 5001 à 10000 + if (montant <= 25000) return 100; // 100 XOF de 10001 à 25000 + if (montant <= 50000) return 200; // 200 XOF de 25001 à 50000 + if (montant <= 100000) return 400; // 400 XOF de 50001 à 100000 + if (montant <= 250000) return 750; // 750 XOF de 100001 à 250000 + if (montant <= 500000) return 1500; // 1500 XOF de 250001 à 500000 + + // Au-delà de 500000 XOF: 0.5% du montant + return montant * 0.005; + } + + /// Valide un numéro de téléphone Orange Money + bool validatePhoneNumber(String numeroTelephone) { + // Nettoyer le numéro + final cleanNumber = numeroTelephone.replaceAll(RegExp(r'[^\d]'), ''); + + // Orange Money: 07, 08, 09 (Côte d'Ivoire) + // Format: 225XXXXXXXX ou 0XXXXXXXX + return RegExp(r'^(225)?(0[789])\d{8}$').hasMatch(cleanNumber); + } + + /// Obtient les limites de transaction Orange Money + Map getTransactionLimits() { + return { + 'montantMinimum': 100.0, // 100 XOF minimum + 'montantMaximum': 1000000.0, // 1 million XOF maximum + 'fraisMinimum': 0.0, + 'fraisMaximum': 5000.0, // Frais maximum théorique + }; + } + + /// Vérifie si un montant est dans les limites autorisées + bool isAmountValid(double montant) { + final limits = getTransactionLimits(); + return montant >= limits['montantMinimum']! && + montant <= limits['montantMaximum']!; + } + + /// Formate un numéro de téléphone pour Orange Money + String formatPhoneNumber(String numeroTelephone) { + final cleanNumber = numeroTelephone.replaceAll(RegExp(r'[^\d]'), ''); + + // Si le numéro commence par 225, le garder tel quel + if (cleanNumber.startsWith('225')) { + return cleanNumber; + } + + // Si le numéro commence par 0, ajouter 225 + if (cleanNumber.startsWith('0')) { + return '225$cleanNumber'; + } + + // Sinon, ajouter 2250 + return '2250$cleanNumber'; + } + + /// Obtient les informations de l'opérateur + Map getOperatorInfo() { + return { + 'nom': 'Orange Money', + 'code': 'ORANGE_MONEY', + 'couleur': '#FF6600', + 'icone': '📱', + 'description': 'Paiement via Orange Money', + 'prefixes': ['07', '08', '09'], + 'pays': 'Côte d\'Ivoire', + 'devise': 'XOF', + }; + } + + /// Génère un message de confirmation pour l'utilisateur + String generateConfirmationMessage({ + required double montant, + required String numeroTelephone, + required double frais, + }) { + final total = montant + frais; + final formattedPhone = formatPhoneNumber(numeroTelephone); + + return ''' +Confirmation de paiement Orange Money + +Montant: ${montant.toStringAsFixed(0)} XOF +Frais: ${frais.toStringAsFixed(0)} XOF +Total: ${total.toStringAsFixed(0)} XOF + +Numéro: $formattedPhone + +Vous allez recevoir un SMS avec le code de confirmation. +Suivez les instructions pour finaliser le paiement. +'''; + } + + /// Annule un paiement Orange Money (si possible) + Future cancelPayment(String paymentId) async { + try { + // Vérifier le statut du paiement + final payment = await checkPaymentStatus(paymentId); + + // Un paiement peut être annulé seulement s'il est en attente + if (payment.statut == 'EN_ATTENTE') { + // Appeler l'API d'annulation + await _apiService.cancelPayment(paymentId); + return true; + } + + return false; + } catch (e) { + return false; + } + } + + /// Obtient l'historique des paiements Orange Money + Future> getPaymentHistory({ + String? cotisationId, + DateTime? dateDebut, + DateTime? dateFin, + int? limit, + }) async { + try { + final filters = { + 'methodePaiement': 'ORANGE_MONEY', + if (cotisationId != null) 'cotisationId': cotisationId, + if (dateDebut != null) 'dateDebut': dateDebut.toIso8601String(), + if (dateFin != null) 'dateFin': dateFin.toIso8601String(), + if (limit != null) 'limit': limit, + }; + + return await _apiService.getPaymentHistory(filters); + } catch (e) { + throw OrangeMoneyException('Erreur lors de la récupération de l\'historique: ${e.toString()}'); + } + } + + /// Vérifie la disponibilité du service Orange Money + Future checkServiceAvailability() async { + try { + // Appel API pour vérifier la disponibilité + final response = await _apiService.checkServiceStatus('ORANGE_MONEY'); + return response['available'] == true; + } catch (e) { + // En cas d'erreur, considérer le service comme indisponible + return false; + } + } + + /// Obtient les statistiques des paiements Orange Money + Future> getPaymentStatistics({ + DateTime? dateDebut, + DateTime? dateFin, + }) async { + try { + final filters = { + 'methodePaiement': 'ORANGE_MONEY', + if (dateDebut != null) 'dateDebut': dateDebut.toIso8601String(), + if (dateFin != null) 'dateFin': dateFin.toIso8601String(), + }; + + return await _apiService.getPaymentStatistics(filters); + } catch (e) { + throw OrangeMoneyException('Erreur lors de la récupération des statistiques: ${e.toString()}'); + } + } +} + +/// Exception personnalisée pour les erreurs Orange Money +class OrangeMoneyException implements Exception { + final String message; + final String? errorCode; + final dynamic originalError; + + OrangeMoneyException( + this.message, { + this.errorCode, + this.originalError, + }); + + @override + String toString() => 'OrangeMoneyException: $message'; +} diff --git a/unionflow-mobile-apps/lib/core/services/payment_service.dart b/unionflow-mobile-apps/lib/core/services/payment_service.dart new file mode 100644 index 0000000..665ac73 --- /dev/null +++ b/unionflow-mobile-apps/lib/core/services/payment_service.dart @@ -0,0 +1,428 @@ +import 'package:injectable/injectable.dart'; +import '../models/payment_model.dart'; +import '../models/cotisation_model.dart'; +import 'api_service.dart'; +import 'cache_service.dart'; +import 'wave_payment_service.dart'; +import 'orange_money_service.dart'; +import 'moov_money_service.dart'; + +/// Service de gestion des paiements +/// Gère les transactions de paiement avec différents opérateurs +@LazySingleton() +class PaymentService { + final ApiService _apiService; + final CacheService _cacheService; + final WavePaymentService _waveService; + final OrangeMoneyService _orangeService; + final MoovMoneyService _moovService; + + PaymentService( + this._apiService, + this._cacheService, + this._waveService, + this._orangeService, + this._moovService, + ); + + /// Initie un paiement pour une cotisation + Future initiatePayment({ + required String cotisationId, + required double montant, + required String methodePaiement, + required String numeroTelephone, + String? nomPayeur, + String? emailPayeur, + }) async { + try { + PaymentModel payment; + + // Déléguer au service spécialisé selon la méthode de paiement + switch (methodePaiement) { + case 'WAVE': + payment = await _waveService.initiatePayment( + cotisationId: cotisationId, + montant: montant, + numeroTelephone: numeroTelephone, + nomPayeur: nomPayeur, + emailPayeur: emailPayeur, + ); + break; + case 'ORANGE_MONEY': + payment = await _orangeService.initiatePayment( + cotisationId: cotisationId, + montant: montant, + numeroTelephone: numeroTelephone, + nomPayeur: nomPayeur, + emailPayeur: emailPayeur, + ); + break; + case 'MOOV_MONEY': + payment = await _moovService.initiatePayment( + cotisationId: cotisationId, + montant: montant, + numeroTelephone: numeroTelephone, + nomPayeur: nomPayeur, + emailPayeur: emailPayeur, + ); + break; + default: + throw PaymentException('Méthode de paiement non supportée: $methodePaiement'); + } + + // Sauvegarder en cache + await _cachePayment(payment); + + return payment; + } catch (e) { + if (e is PaymentException) rethrow; + throw PaymentException('Erreur lors de l\'initiation du paiement: ${e.toString()}'); + } + } + + /// Vérifie le statut d'un paiement + Future checkPaymentStatus(String paymentId) async { + try { + // Essayer le cache d'abord + final cachedPayment = await _getCachedPayment(paymentId); + + // Si le paiement est déjà terminé (succès ou échec), retourner le cache + if (cachedPayment != null && + (cachedPayment.isSuccessful || cachedPayment.isFailed)) { + return cachedPayment; + } + + // Déterminer le service à utiliser selon la méthode de paiement + PaymentModel payment; + if (cachedPayment != null) { + switch (cachedPayment.methodePaiement) { + case 'WAVE': + payment = await _waveService.checkPaymentStatus(paymentId); + break; + case 'ORANGE_MONEY': + payment = await _orangeService.checkPaymentStatus(paymentId); + break; + case 'MOOV_MONEY': + payment = await _moovService.checkPaymentStatus(paymentId); + break; + default: + throw PaymentException('Méthode de paiement inconnue: ${cachedPayment.methodePaiement}'); + } + } else { + // Si pas de cache, essayer tous les services (peu probable) + throw PaymentException('Paiement non trouvé en cache'); + } + + // Mettre à jour le cache + await _cachePayment(payment); + + return payment; + } catch (e) { + // En cas d'erreur réseau, retourner le cache si disponible + final cachedPayment = await _getCachedPayment(paymentId); + if (cachedPayment != null) { + return cachedPayment; + } + throw PaymentException('Erreur lors de la vérification du paiement: ${e.toString()}'); + } + } + + /// Annule un paiement en cours + Future cancelPayment(String paymentId) async { + try { + // Récupérer le paiement en cache pour connaître la méthode + final cachedPayment = await _getCachedPayment(paymentId); + if (cachedPayment == null) { + throw PaymentException('Paiement non trouvé'); + } + + // Déléguer au service approprié + bool cancelled = false; + switch (cachedPayment.methodePaiement) { + case 'WAVE': + cancelled = await _waveService.cancelPayment(paymentId); + break; + case 'ORANGE_MONEY': + cancelled = await _orangeService.cancelPayment(paymentId); + break; + case 'MOOV_MONEY': + cancelled = await _moovService.cancelPayment(paymentId); + break; + default: + throw PaymentException('Méthode de paiement non supportée pour l\'annulation'); + } + + return cancelled; + } catch (e) { + if (e is PaymentException) rethrow; + throw PaymentException('Erreur lors de l\'annulation du paiement: ${e.toString()}'); + } + } + + /// Retente un paiement échoué + Future retryPayment(String paymentId) async { + try { + // Récupérer le paiement original + final originalPayment = await _getCachedPayment(paymentId); + if (originalPayment == null) { + throw PaymentException('Paiement original non trouvé'); + } + + // Réinitier le paiement avec les mêmes paramètres + return await initiatePayment( + cotisationId: originalPayment.cotisationId, + montant: originalPayment.montant, + methodePaiement: originalPayment.methodePaiement, + numeroTelephone: originalPayment.numeroTelephone ?? '', + nomPayeur: originalPayment.nomPayeur, + emailPayeur: originalPayment.emailPayeur, + ); + } catch (e) { + if (e is PaymentException) rethrow; + throw PaymentException('Erreur lors de la nouvelle tentative de paiement: ${e.toString()}'); + } + } + + /// Récupère l'historique des paiements d'une cotisation + Future> getPaymentHistory(String cotisationId) async { + try { + // Essayer le cache d'abord + final cachedPayments = await _cacheService.getPayments(); + if (cachedPayments != null) { + final filteredPayments = cachedPayments + .where((p) => p.cotisationId == cotisationId) + .toList(); + + if (filteredPayments.isNotEmpty) { + return filteredPayments; + } + } + + // Si pas de cache, retourner une liste vide + // En production, on pourrait appeler l'API ici + return []; + } catch (e) { + throw PaymentException('Erreur lors de la récupération de l\'historique: ${e.toString()}'); + } + } + + /// Valide les données de paiement avant envoi + bool validatePaymentData({ + required String cotisationId, + required double montant, + required String methodePaiement, + required String numeroTelephone, + }) { + // Validation du montant + if (montant <= 0) return false; + + // Validation du numéro de téléphone selon l'opérateur + if (!_validatePhoneNumber(numeroTelephone, methodePaiement)) { + return false; + } + + // Validation de la méthode de paiement + if (!_isValidPaymentMethod(methodePaiement)) { + return false; + } + + return true; + } + + /// Calcule les frais de transaction selon la méthode + double calculateTransactionFees(double montant, String methodePaiement) { + switch (methodePaiement) { + case 'ORANGE_MONEY': + return _calculateOrangeMoneyFees(montant); + case 'WAVE': + return _calculateWaveFees(montant); + case 'MOOV_MONEY': + return _calculateMoovMoneyFees(montant); + case 'CARTE_BANCAIRE': + return _calculateCardFees(montant); + default: + return 0.0; + } + } + + /// Retourne les méthodes de paiement disponibles + List getAvailablePaymentMethods() { + return [ + PaymentMethod( + id: 'ORANGE_MONEY', + nom: 'Orange Money', + icone: '📱', + couleur: '#FF6600', + description: 'Paiement via Orange Money', + fraisMinimum: 0, + fraisMaximum: 1000, + montantMinimum: 100, + montantMaximum: 1000000, + ), + PaymentMethod( + id: 'WAVE', + nom: 'Wave', + icone: '🌊', + couleur: '#00D4FF', + description: 'Paiement via Wave', + fraisMinimum: 0, + fraisMaximum: 500, + montantMinimum: 100, + montantMaximum: 2000000, + ), + PaymentMethod( + id: 'MOOV_MONEY', + nom: 'Moov Money', + icone: '💙', + couleur: '#0066CC', + description: 'Paiement via Moov Money', + fraisMinimum: 0, + fraisMaximum: 800, + montantMinimum: 100, + montantMaximum: 1500000, + ), + PaymentMethod( + id: 'CARTE_BANCAIRE', + nom: 'Carte bancaire', + icone: '💳', + couleur: '#4CAF50', + description: 'Paiement par carte bancaire', + fraisMinimum: 100, + fraisMaximum: 2000, + montantMinimum: 500, + montantMaximum: 5000000, + ), + ]; + } + + /// Méthodes privées + + Future _cachePayment(PaymentModel payment) async { + try { + // Utiliser le service de cache pour sauvegarder + final payments = await _cacheService.getPayments() ?? []; + + // Remplacer ou ajouter le paiement + final index = payments.indexWhere((p) => p.id == payment.id); + if (index >= 0) { + payments[index] = payment; + } else { + payments.add(payment); + } + + await _cacheService.savePayments(payments); + } catch (e) { + // Ignorer les erreurs de cache + } + } + + Future _getCachedPayment(String paymentId) async { + try { + final payments = await _cacheService.getPayments(); + if (payments != null) { + return payments.firstWhere( + (p) => p.id == paymentId, + orElse: () => throw StateError('Payment not found'), + ); + } + return null; + } catch (e) { + return null; + } + } + + bool _validatePhoneNumber(String numero, String operateur) { + // Supprimer les espaces et caractères spéciaux + final cleanNumber = numero.replaceAll(RegExp(r'[^\d]'), ''); + + switch (operateur) { + case 'ORANGE_MONEY': + // Orange: 07, 08, 09 (Côte d'Ivoire) + return RegExp(r'^(225)?(0[789])\d{8}$').hasMatch(cleanNumber); + case 'WAVE': + // Wave accepte tous les numéros ivoiriens + return RegExp(r'^(225)?(0[1-9])\d{8}$').hasMatch(cleanNumber); + case 'MOOV_MONEY': + // Moov: 01, 02, 03 + return RegExp(r'^(225)?(0[123])\d{8}$').hasMatch(cleanNumber); + default: + return cleanNumber.length >= 8; + } + } + + bool _isValidPaymentMethod(String methode) { + const validMethods = [ + 'ORANGE_MONEY', + 'WAVE', + 'MOOV_MONEY', + 'CARTE_BANCAIRE', + 'VIREMENT', + 'ESPECES' + ]; + return validMethods.contains(methode); + } + + double _calculateOrangeMoneyFees(double montant) { + if (montant <= 1000) return 0; + if (montant <= 5000) return 25; + if (montant <= 10000) return 50; + if (montant <= 25000) return 100; + if (montant <= 50000) return 200; + return montant * 0.005; // 0.5% + } + + double _calculateWaveFees(double montant) { + // Wave a généralement des frais plus bas + if (montant <= 2000) return 0; + if (montant <= 10000) return 25; + if (montant <= 50000) return 100; + return montant * 0.003; // 0.3% + } + + double _calculateMoovMoneyFees(double montant) { + if (montant <= 1000) return 0; + if (montant <= 5000) return 30; + if (montant <= 15000) return 75; + if (montant <= 50000) return 150; + return montant * 0.004; // 0.4% + } + + double _calculateCardFees(double montant) { + // Frais fixes + pourcentage pour les cartes + return 100 + (montant * 0.025); // 100 XOF + 2.5% + } +} + +/// Modèle pour les méthodes de paiement disponibles +class PaymentMethod { + final String id; + final String nom; + final String icone; + final String couleur; + final String description; + final double fraisMinimum; + final double fraisMaximum; + final double montantMinimum; + final double montantMaximum; + + PaymentMethod({ + required this.id, + required this.nom, + required this.icone, + required this.couleur, + required this.description, + required this.fraisMinimum, + required this.fraisMaximum, + required this.montantMinimum, + required this.montantMaximum, + }); +} + +/// Exception personnalisée pour les erreurs de paiement +class PaymentException implements Exception { + final String message; + PaymentException(this.message); + + @override + String toString() => 'PaymentException: $message'; +} diff --git a/unionflow-mobile-apps/lib/core/services/wave_payment_service.dart b/unionflow-mobile-apps/lib/core/services/wave_payment_service.dart new file mode 100644 index 0000000..56751dc --- /dev/null +++ b/unionflow-mobile-apps/lib/core/services/wave_payment_service.dart @@ -0,0 +1,229 @@ +import 'package:injectable/injectable.dart'; +import '../models/payment_model.dart'; +import '../models/wave_checkout_session_model.dart'; +import 'api_service.dart'; + +/// Service d'intégration avec l'API Wave Money +/// Gère les paiements via Wave Money pour la Côte d'Ivoire +@LazySingleton() +class WavePaymentService { + final ApiService _apiService; + + WavePaymentService(this._apiService); + + /// Crée une session de checkout Wave via notre API backend + Future createCheckoutSession({ + required double montant, + required String devise, + required String successUrl, + required String errorUrl, + String? organisationId, + String? membreId, + String? typePaiement, + String? description, + String? referenceExterne, + }) async { + try { + // Utiliser notre API backend + return await _apiService.createWaveSession( + montant: montant, + devise: devise, + successUrl: successUrl, + errorUrl: errorUrl, + organisationId: organisationId, + membreId: membreId, + typePaiement: typePaiement, + description: description, + ); + } catch (e) { + throw WavePaymentException('Erreur lors de la création de la session Wave: ${e.toString()}'); + } + } + + /// Vérifie le statut d'une session de checkout + Future getCheckoutSession(String sessionId) async { + try { + return await _apiService.getWaveSession(sessionId); + } catch (e) { + throw WavePaymentException('Erreur lors de la récupération de la session: ${e.toString()}'); + } + } + + /// Initie un paiement Wave pour une cotisation + Future initiatePayment({ + required String cotisationId, + required double montant, + required String numeroTelephone, + String? nomPayeur, + String? emailPayeur, + }) async { + try { + // Générer les URLs de callback + const successUrl = 'https://unionflow.app/payment/success'; + const errorUrl = 'https://unionflow.app/payment/error'; + + // Créer la session Wave + final session = await createCheckoutSession( + montant: montant, + devise: 'XOF', // Franc CFA + successUrl: successUrl, + errorUrl: errorUrl, + typePaiement: 'COTISATION', + description: 'Paiement cotisation $cotisationId', + referenceExterne: cotisationId, + ); + + // Convertir en PaymentModel pour l'uniformité + return PaymentModel( + id: session.id ?? session.waveSessionId, + cotisationId: cotisationId, + numeroReference: session.waveSessionId, + montant: montant, + codeDevise: 'XOF', + methodePaiement: 'WAVE', + statut: _mapWaveStatusToPaymentStatus(session.statut), + dateTransaction: DateTime.now(), + numeroTransaction: session.waveSessionId, + referencePaiement: session.referenceExterne, + operateurMobileMoney: 'WAVE', + numeroTelephone: numeroTelephone, + nomPayeur: nomPayeur, + emailPayeur: emailPayeur, + metadonnees: { + 'wave_session_id': session.waveSessionId, + 'wave_checkout_url': session.waveUrl, + 'wave_status': session.statut, + 'cotisation_id': cotisationId, + 'numero_telephone': numeroTelephone, + 'source': 'unionflow_mobile', + }, + dateCreation: DateTime.now(), + ); + } catch (e) { + if (e is WavePaymentException) { + rethrow; + } + throw WavePaymentException('Erreur lors de l\'initiation du paiement Wave: ${e.toString()}'); + } + } + + /// Vérifie le statut d'un paiement Wave + Future checkPaymentStatus(String paymentId) async { + try { + final session = await getCheckoutSession(paymentId); + + return PaymentModel( + id: session.id ?? session.waveSessionId, + cotisationId: session.referenceExterne ?? '', + numeroReference: session.waveSessionId, + montant: session.montant, + codeDevise: session.devise, + methodePaiement: 'WAVE', + statut: _mapWaveStatusToPaymentStatus(session.statut), + dateTransaction: session.dateModification ?? DateTime.now(), + numeroTransaction: session.waveSessionId, + referencePaiement: session.referenceExterne, + operateurMobileMoney: 'WAVE', + metadonnees: { + 'wave_session_id': session.waveSessionId, + 'wave_checkout_url': session.waveUrl, + 'wave_status': session.statut, + 'organisation_id': session.organisationId, + 'membre_id': session.membreId, + 'type_paiement': session.typePaiement, + }, + dateCreation: session.dateCreation, + dateModification: session.dateModification, + ); + } catch (e) { + if (e is WavePaymentException) { + rethrow; + } + throw WavePaymentException('Erreur lors de la vérification du statut: ${e.toString()}'); + } + } + + /// Calcule les frais Wave selon le barème officiel + double calculateWaveFees(double montant) { + // Barème Wave Côte d'Ivoire (2024) + if (montant <= 2000) return 0; // Gratuit jusqu'à 2000 XOF + if (montant <= 10000) return 25; // 25 XOF de 2001 à 10000 + if (montant <= 50000) return 100; // 100 XOF de 10001 à 50000 + if (montant <= 100000) return 200; // 200 XOF de 50001 à 100000 + if (montant <= 500000) return 500; // 500 XOF de 100001 à 500000 + + // Au-delà de 500000 XOF: 0.1% du montant + return montant * 0.001; + } + + /// Valide un numéro de téléphone pour Wave + bool validatePhoneNumber(String numeroTelephone) { + // Nettoyer le numéro + final cleanNumber = numeroTelephone.replaceAll(RegExp(r'[^\d]'), ''); + + // Wave accepte tous les numéros ivoiriens + // Format: 225XXXXXXXX ou 0XXXXXXXX + return RegExp(r'^(225)?(0[1-9])\d{8}$').hasMatch(cleanNumber) || + RegExp(r'^[1-9]\d{7}$').hasMatch(cleanNumber); // Format court + } + + /// Obtient l'URL de checkout pour redirection + String getCheckoutUrl(String sessionId) { + return 'https://checkout.wave.com/checkout/$sessionId'; + } + + /// Annule une session de paiement (si possible) + Future cancelPayment(String sessionId) async { + try { + // Vérifier le statut de la session + final session = await getCheckoutSession(sessionId); + + // Une session peut être considérée comme annulée si elle a expiré + return session.statut.toLowerCase() == 'expired' || + session.statut.toLowerCase() == 'cancelled' || + session.estExpiree; + } catch (e) { + return false; + } + } + + /// Méthodes utilitaires privées + + String _mapWaveStatusToPaymentStatus(String waveStatus) { + switch (waveStatus.toLowerCase()) { + case 'pending': + case 'en_attente': + return 'EN_ATTENTE'; + case 'successful': + case 'completed': + case 'success': + case 'reussie': + return 'REUSSIE'; + case 'failed': + case 'echec': + return 'ECHOUEE'; + case 'expired': + case 'cancelled': + case 'annulee': + return 'ANNULEE'; + default: + return 'EN_ATTENTE'; + } + } +} + +/// Exception personnalisée pour les erreurs Wave +class WavePaymentException implements Exception { + final String message; + final String? errorCode; + final dynamic originalError; + + WavePaymentException( + this.message, { + this.errorCode, + this.originalError, + }); + + @override + String toString() => 'WavePaymentException: $message'; +} diff --git a/unionflow-mobile-apps/lib/features/auth/presentation/pages/forgot_password_screen.dart b/unionflow-mobile-apps/lib/features/auth/presentation/pages/forgot_password_screen.dart index a0b8e00..77651d6 100644 --- a/unionflow-mobile-apps/lib/features/auth/presentation/pages/forgot_password_screen.dart +++ b/unionflow-mobile-apps/lib/features/auth/presentation/pages/forgot_password_screen.dart @@ -163,7 +163,7 @@ class _ForgotPasswordScreenState extends State const SizedBox(height: 16), // Message de succès - Text( + const Text( 'Nous avons envoyé un lien de réinitialisation à :', style: TextStyle( fontSize: 16, @@ -196,15 +196,15 @@ class _ForgotPasswordScreenState extends State color: AppTheme.infoColor.withOpacity(0.2), ), ), - child: Column( + child: const Column( children: [ - const Icon( + Icon( Icons.info_outline, color: AppTheme.infoColor, size: 24, ), - const SizedBox(height: 12), - const Text( + SizedBox(height: 12), + Text( 'Instructions', style: TextStyle( fontSize: 16, @@ -212,7 +212,7 @@ class _ForgotPasswordScreenState extends State color: AppTheme.textPrimary, ), ), - const SizedBox(height: 8), + SizedBox(height: 8), Text( '1. Vérifiez votre boîte email (et vos spams)\n' '2. Cliquez sur le lien de réinitialisation\n' @@ -291,7 +291,7 @@ class _ForgotPasswordScreenState extends State const SizedBox(height: 8), // Sous-titre - Text( + const Text( 'Pas de problème ! Nous allons vous aider à le récupérer.', style: TextStyle( fontSize: 16, @@ -328,11 +328,11 @@ class _ForgotPasswordScreenState extends State ), ), const SizedBox(width: 16), - Expanded( + const Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( 'Comment ça marche ?', style: TextStyle( fontSize: 16, @@ -340,7 +340,7 @@ class _ForgotPasswordScreenState extends State color: AppTheme.textPrimary, ), ), - const SizedBox(height: 4), + SizedBox(height: 4), Text( 'Saisissez votre email et nous vous enverrons un lien sécurisé pour réinitialiser votre mot de passe.', style: TextStyle( @@ -388,7 +388,7 @@ class _ForgotPasswordScreenState extends State return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( + const Text( 'Vous vous souvenez de votre mot de passe ? ', style: TextStyle( color: AppTheme.textSecondary, diff --git a/unionflow-mobile-apps/lib/features/auth/presentation/pages/login_page_temp.dart b/unionflow-mobile-apps/lib/features/auth/presentation/pages/login_page_temp.dart index 7bff1a9..0f3b39d 100644 --- a/unionflow-mobile-apps/lib/features/auth/presentation/pages/login_page_temp.dart +++ b/unionflow-mobile-apps/lib/features/auth/presentation/pages/login_page_temp.dart @@ -190,7 +190,7 @@ class _TempLoginPageState extends State decoration: InputDecoration( labelText: 'Adresse email', hintText: 'votre.email@exemple.com', - prefixIcon: Icon( + prefixIcon: const Icon( Icons.email_outlined, color: AppTheme.primaryColor, ), @@ -202,7 +202,7 @@ class _TempLoginPageState extends State ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( + borderSide: const BorderSide( color: AppTheme.primaryColor, width: 2, ), @@ -234,7 +234,7 @@ class _TempLoginPageState extends State decoration: InputDecoration( labelText: 'Mot de passe', hintText: 'Saisissez votre mot de passe', - prefixIcon: Icon( + prefixIcon: const Icon( Icons.lock_outlined, color: AppTheme.primaryColor, ), @@ -260,7 +260,7 @@ class _TempLoginPageState extends State ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( + borderSide: const BorderSide( color: AppTheme.primaryColor, width: 2, ), @@ -320,7 +320,7 @@ class _TempLoginPageState extends State : null, ), const SizedBox(width: 8), - Text( + const Text( 'Se souvenir de moi', style: TextStyle( fontSize: 14, @@ -340,7 +340,7 @@ class _TempLoginPageState extends State color: AppTheme.infoColor.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), - child: Text( + child: const Text( 'Compte de test', style: TextStyle( fontSize: 12, @@ -376,12 +376,12 @@ class _TempLoginPageState extends State strokeWidth: 2, ), ) - : Row( + : const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(Icons.login, size: 20), - const SizedBox(width: 8), - const Text( + Icon(Icons.login, size: 20), + SizedBox(width: 8), + Text( 'Se connecter', style: TextStyle( fontSize: 16, diff --git a/unionflow-mobile-apps/lib/features/auth/presentation/pages/login_screen.dart b/unionflow-mobile-apps/lib/features/auth/presentation/pages/login_screen.dart index ea7bbe6..e33a1cf 100644 --- a/unionflow-mobile-apps/lib/features/auth/presentation/pages/login_screen.dart +++ b/unionflow-mobile-apps/lib/features/auth/presentation/pages/login_screen.dart @@ -161,7 +161,7 @@ class _LoginScreenState extends State const SizedBox(height: 8), // Sous-titre - Text( + const Text( 'Connectez-vous à votre compte UnionFlow', style: TextStyle( fontSize: 16, @@ -269,11 +269,11 @@ class _LoginScreenState extends State } Widget _buildDivider() { - return Row( + return const Row( children: [ - const Expanded(child: Divider()), + Expanded(child: Divider()), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: EdgeInsets.symmetric(horizontal: 16), child: Text( 'ou', style: TextStyle( @@ -282,7 +282,7 @@ class _LoginScreenState extends State ), ), ), - const Expanded(child: Divider()), + Expanded(child: Divider()), ], ); } @@ -348,7 +348,7 @@ class _LoginScreenState extends State return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( + const Text( 'Pas encore de compte ? ', style: TextStyle( color: AppTheme.textSecondary, diff --git a/unionflow-mobile-apps/lib/features/auth/presentation/pages/register_screen.dart b/unionflow-mobile-apps/lib/features/auth/presentation/pages/register_screen.dart index f1975e2..0814657 100644 --- a/unionflow-mobile-apps/lib/features/auth/presentation/pages/register_screen.dart +++ b/unionflow-mobile-apps/lib/features/auth/presentation/pages/register_screen.dart @@ -163,7 +163,7 @@ class _RegisterScreenState extends State const SizedBox(height: 8), // Sous-titre - Text( + const Text( 'Rejoignez UnionFlow et gérez votre association', style: TextStyle( fontSize: 16, @@ -386,25 +386,25 @@ class _RegisterScreenState extends State ), Expanded( child: RichText( - text: TextSpan( - style: const TextStyle( + text: const TextSpan( + style: TextStyle( color: AppTheme.textSecondary, fontSize: 14, ), children: [ - const TextSpan(text: 'J\'accepte les '), + TextSpan(text: 'J\'accepte les '), TextSpan( text: 'Conditions d\'utilisation', - style: const TextStyle( + style: TextStyle( color: AppTheme.primaryColor, fontWeight: FontWeight.w600, decoration: TextDecoration.underline, ), ), - const TextSpan(text: ' et la '), + TextSpan(text: ' et la '), TextSpan( text: 'Politique de confidentialité', - style: const TextStyle( + style: TextStyle( color: AppTheme.primaryColor, fontWeight: FontWeight.w600, decoration: TextDecoration.underline, @@ -459,7 +459,7 @@ class _RegisterScreenState extends State return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( + const Text( 'Déjà un compte ? ', style: TextStyle( color: AppTheme.textSecondary, diff --git a/unionflow-mobile-apps/lib/features/auth/presentation/widgets/login_footer.dart b/unionflow-mobile-apps/lib/features/auth/presentation/widgets/login_footer.dart index 8847771..2c172d8 100644 --- a/unionflow-mobile-apps/lib/features/auth/presentation/widgets/login_footer.dart +++ b/unionflow-mobile-apps/lib/features/auth/presentation/widgets/login_footer.dart @@ -87,7 +87,7 @@ class LoginFooter extends StatelessWidget { color: AppTheme.textSecondary.withOpacity(0.1), ), ), - child: Column( + child: const Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.center, @@ -97,7 +97,7 @@ class LoginFooter extends StatelessWidget { size: 20, color: AppTheme.successColor, ), - const SizedBox(width: 8), + SizedBox(width: 8), Text( 'Connexion sécurisée', style: TextStyle( @@ -108,7 +108,7 @@ class LoginFooter extends StatelessWidget { ), ], ), - const SizedBox(height: 8), + SizedBox(height: 8), Text( 'Vos données sont protégées par un cryptage de niveau bancaire', textAlign: TextAlign.center, @@ -174,7 +174,7 @@ class LoginFooter extends StatelessWidget { const SizedBox(width: 6), Text( label, - style: TextStyle( + style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary, fontWeight: FontWeight.w500, @@ -216,14 +216,14 @@ class LoginFooter extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), - title: Row( + title: const Row( children: [ Icon( Icons.help_outline, color: AppTheme.infoColor, ), - const SizedBox(width: 12), - const Text('Aide'), + SizedBox(width: 12), + Text('Aide'), ], ), content: Column( @@ -249,7 +249,7 @@ class LoginFooter extends StatelessWidget { actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: Text( + child: const Text( 'Fermer', style: TextStyle( color: AppTheme.primaryColor, @@ -269,14 +269,14 @@ class LoginFooter extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), - title: Row( + title: const Row( children: [ Icon( Icons.info_outline, color: AppTheme.primaryColor, ), - const SizedBox(width: 12), - const Text('À propos'), + SizedBox(width: 12), + Text('À propos'), ], ), content: const Text( @@ -286,7 +286,7 @@ class LoginFooter extends StatelessWidget { actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: Text( + child: const Text( 'Fermer', style: TextStyle( color: AppTheme.primaryColor, @@ -306,14 +306,14 @@ class LoginFooter extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), - title: Row( + title: const Row( children: [ Icon( Icons.privacy_tip_outlined, color: AppTheme.warningColor, ), - const SizedBox(width: 12), - const Text('Confidentialité'), + SizedBox(width: 12), + Text('Confidentialité'), ], ), content: const Text( @@ -323,7 +323,7 @@ class LoginFooter extends StatelessWidget { actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: Text( + child: const Text( 'Compris', style: TextStyle( color: AppTheme.primaryColor, @@ -342,7 +342,7 @@ class LoginFooter extends StatelessWidget { children: [ Text( title, - style: TextStyle( + style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, @@ -351,7 +351,7 @@ class LoginFooter extends StatelessWidget { const SizedBox(height: 4), Text( description, - style: TextStyle( + style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary, ), diff --git a/unionflow-mobile-apps/lib/features/auth/presentation/widgets/login_form.dart b/unionflow-mobile-apps/lib/features/auth/presentation/widgets/login_form.dart index 1ffd488..27fe212 100644 --- a/unionflow-mobile-apps/lib/features/auth/presentation/widgets/login_form.dart +++ b/unionflow-mobile-apps/lib/features/auth/presentation/widgets/login_form.dart @@ -189,21 +189,21 @@ class _LoginFormState extends State ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( + borderSide: const BorderSide( color: AppTheme.primaryColor, width: 2, ), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( + borderSide: const BorderSide( color: AppTheme.errorColor, width: 2, ), ), focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( + borderSide: const BorderSide( color: AppTheme.errorColor, width: 2, ), @@ -281,21 +281,21 @@ class _LoginFormState extends State ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( + borderSide: const BorderSide( color: AppTheme.primaryColor, width: 2, ), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( + borderSide: const BorderSide( color: AppTheme.errorColor, width: 2, ), ), focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide( + borderSide: const BorderSide( color: AppTheme.errorColor, width: 2, ), @@ -344,7 +344,7 @@ class _LoginFormState extends State : Colors.transparent, ), child: widget.rememberMe - ? Icon( + ? const Icon( Icons.check, size: 14, color: Colors.white, @@ -352,7 +352,7 @@ class _LoginFormState extends State : null, ), const SizedBox(width: 8), - Flexible( + const Flexible( child: Text( 'Se souvenir de moi', style: TextStyle( @@ -374,7 +374,7 @@ class _LoginFormState extends State HapticFeedback.selectionClick(); _showForgotPasswordDialog(); }, - child: Text( + child: const Text( 'Mot de passe oublié ?', style: TextStyle( fontSize: 14, @@ -413,14 +413,14 @@ class _LoginFormState extends State shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), - title: Row( + title: const Row( children: [ Icon( Icons.help_outline, color: AppTheme.primaryColor, ), - const SizedBox(width: 12), - const Text('Mot de passe oublié'), + SizedBox(width: 12), + Text('Mot de passe oublié'), ], ), content: const Text( @@ -429,7 +429,7 @@ class _LoginFormState extends State actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: Text( + child: const Text( 'Compris', style: TextStyle( color: AppTheme.primaryColor, diff --git a/unionflow-mobile-apps/lib/features/cotisations/data/repositories/cotisation_repository_impl.dart b/unionflow-mobile-apps/lib/features/cotisations/data/repositories/cotisation_repository_impl.dart index 278fa1f..40693d9 100644 --- a/unionflow-mobile-apps/lib/features/cotisations/data/repositories/cotisation_repository_impl.dart +++ b/unionflow-mobile-apps/lib/features/cotisations/data/repositories/cotisation_repository_impl.dart @@ -1,15 +1,17 @@ import 'package:injectable/injectable.dart'; import '../../../../core/models/cotisation_model.dart'; import '../../../../core/services/api_service.dart'; +import '../../../../core/services/cache_service.dart'; import '../../../cotisations/domain/repositories/cotisation_repository.dart'; /// Implémentation du repository des cotisations -/// Utilise ApiService pour communiquer avec le backend +/// Utilise ApiService pour communiquer avec le backend et CacheService pour le cache local @LazySingleton(as: CotisationRepository) class CotisationRepositoryImpl implements CotisationRepository { final ApiService _apiService; + final CacheService _cacheService; - CotisationRepositoryImpl(this._apiService); + CotisationRepositoryImpl(this._apiService, this._cacheService); @override Future> getCotisations({int page = 0, int size = 20}) async { @@ -79,6 +81,54 @@ class CotisationRepositoryImpl implements CotisationRepository { @override Future> getCotisationsStats() async { - return await _apiService.getCotisationsStats(); + // Essayer de récupérer depuis le cache d'abord + final cachedStats = await _cacheService.getCotisationsStats(); + if (cachedStats != null) { + return cachedStats.toJson(); + } + + try { + final stats = await _apiService.getCotisationsStats(); + + // Sauvegarder en cache si possible + // Note: Conversion nécessaire selon la structure des stats du backend + // await _cacheService.saveCotisationsStats(statsModel); + + return stats; + } catch (e) { + // En cas d'erreur, retourner le cache si disponible + if (cachedStats != null) { + return cachedStats.toJson(); + } + rethrow; + } + } + + /// Invalide tous les caches de listes de cotisations + Future _invalidateListCaches() async { + // Nettoyer les caches de listes paginées + final keys = ['cotisations_page_0_size_20', 'cotisations_cache']; + for (final key in keys) { + await _cacheService.clearCotisations(key: key); + } + + // Nettoyer le cache des statistiques + await _cacheService.clearCotisationsStats(); + } + + /// Force la synchronisation avec le serveur + Future forceSync() async { + await _cacheService.clearAllCotisationsCache(); + await _cacheService.updateLastSyncTimestamp(); + } + + /// Vérifie si une synchronisation est nécessaire + bool needsSync() { + return _cacheService.needsSync(); + } + + /// Retourne des informations sur le cache + Map getCacheInfo() { + return _cacheService.getCacheInfo(); } } diff --git a/unionflow-mobile-apps/lib/features/cotisations/presentation/bloc/cotisations_bloc.dart b/unionflow-mobile-apps/lib/features/cotisations/presentation/bloc/cotisations_bloc.dart index 5048538..b85497c 100644 --- a/unionflow-mobile-apps/lib/features/cotisations/presentation/bloc/cotisations_bloc.dart +++ b/unionflow-mobile-apps/lib/features/cotisations/presentation/bloc/cotisations_bloc.dart @@ -1,6 +1,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:injectable/injectable.dart'; import '../../../../core/models/cotisation_model.dart'; +import '../../../../core/models/payment_model.dart'; +import '../../../../core/services/payment_service.dart'; +import '../../../../core/services/notification_service.dart'; import '../../domain/repositories/cotisation_repository.dart'; import 'cotisations_event.dart'; import 'cotisations_state.dart'; @@ -10,8 +13,14 @@ import 'cotisations_state.dart'; @injectable class CotisationsBloc extends Bloc { final CotisationRepository _cotisationRepository; + final PaymentService _paymentService; + final NotificationService _notificationService; - CotisationsBloc(this._cotisationRepository) : super(const CotisationsInitial()) { + CotisationsBloc( + this._cotisationRepository, + this._paymentService, + this._notificationService, + ) : super(const CotisationsInitial()) { // Enregistrement des handlers d'événements on(_onLoadCotisations); on(_onLoadCotisationById); @@ -28,6 +37,15 @@ class CotisationsBloc extends Bloc { on(_onResetCotisationsState); on(_onFilterCotisations); on(_onSortCotisations); + + // Nouveaux handlers pour les paiements et fonctionnalités avancées + on(_onInitiatePayment); + on(_onCheckPaymentStatus); + on(_onCancelPayment); + on(_onScheduleNotifications); + on(_onSyncWithServer); + on(_onApplyAdvancedFilters); + on(_onExportCotisations); } /// Handler pour charger la liste des cotisations @@ -506,4 +524,207 @@ class CotisationsBloc extends Bloc { emit(currentState.copyWith(filteredCotisations: sortedList)); } } + + /// Handler pour initier un paiement + Future _onInitiatePayment( + InitiatePayment event, + Emitter emit, + ) async { + try { + // Valider les données de paiement + if (!_paymentService.validatePaymentData( + cotisationId: event.cotisationId, + montant: event.montant, + methodePaiement: event.methodePaiement, + numeroTelephone: event.numeroTelephone, + )) { + emit(PaymentFailure( + cotisationId: event.cotisationId, + paymentId: '', + errorMessage: 'Données de paiement invalides', + errorCode: 'INVALID_DATA', + )); + return; + } + + // Initier le paiement + final payment = await _paymentService.initiatePayment( + cotisationId: event.cotisationId, + montant: event.montant, + methodePaiement: event.methodePaiement, + numeroTelephone: event.numeroTelephone, + nomPayeur: event.nomPayeur, + emailPayeur: event.emailPayeur, + ); + + emit(PaymentInProgress( + cotisationId: event.cotisationId, + paymentId: payment.id, + methodePaiement: event.methodePaiement, + montant: event.montant, + )); + + } catch (e) { + emit(PaymentFailure( + cotisationId: event.cotisationId, + paymentId: '', + errorMessage: e.toString(), + )); + } + } + + /// Handler pour vérifier le statut d'un paiement + Future _onCheckPaymentStatus( + CheckPaymentStatus event, + Emitter emit, + ) async { + try { + final payment = await _paymentService.checkPaymentStatus(event.paymentId); + + if (payment.isSuccessful) { + // Récupérer la cotisation mise à jour + final cotisation = await _cotisationRepository.getCotisationById(payment.cotisationId); + + emit(PaymentSuccess( + cotisationId: payment.cotisationId, + payment: payment, + updatedCotisation: cotisation, + )); + + // Envoyer notification de succès + await _notificationService.showPaymentConfirmation(cotisation, payment.montant); + + } else if (payment.isFailed) { + emit(PaymentFailure( + cotisationId: payment.cotisationId, + paymentId: payment.id, + errorMessage: payment.messageErreur ?? 'Paiement échoué', + )); + + // Envoyer notification d'échec + final cotisation = await _cotisationRepository.getCotisationById(payment.cotisationId); + await _notificationService.showPaymentFailure(cotisation, payment.messageErreur ?? 'Erreur inconnue'); + } + } catch (e) { + emit(CotisationsError('Erreur lors de la vérification du paiement: ${e.toString()}')); + } + } + + /// Handler pour annuler un paiement + Future _onCancelPayment( + CancelPayment event, + Emitter emit, + ) async { + try { + final cancelled = await _paymentService.cancelPayment(event.paymentId); + + if (cancelled) { + emit(PaymentCancelled( + cotisationId: event.cotisationId, + paymentId: event.paymentId, + )); + } else { + emit(const CotisationsError('Impossible d\'annuler le paiement')); + } + } catch (e) { + emit(CotisationsError('Erreur lors de l\'annulation du paiement: ${e.toString()}')); + } + } + + /// Handler pour programmer les notifications + Future _onScheduleNotifications( + ScheduleNotifications event, + Emitter emit, + ) async { + try { + await _notificationService.scheduleAllCotisationsNotifications(event.cotisations); + + emit(NotificationsScheduled( + notificationsCount: event.cotisations.length * 2, + cotisationIds: event.cotisations.map((c) => c.id).toList(), + )); + } catch (e) { + emit(CotisationsError('Erreur lors de la programmation des notifications: ${e.toString()}')); + } + } + + /// Handler pour synchroniser avec le serveur + Future _onSyncWithServer( + SyncWithServer event, + Emitter emit, + ) async { + try { + emit(const SyncInProgress('Synchronisation en cours...')); + + // Recharger les données + final cotisations = await _cotisationRepository.getCotisations(); + + emit(SyncCompleted( + itemsSynced: cotisations.length, + syncTime: DateTime.now(), + )); + + // Émettre l'état chargé avec les nouvelles données + emit(CotisationsLoaded( + cotisations: cotisations, + filteredCotisations: cotisations, + )); + + } catch (e) { + emit(CotisationsError('Erreur lors de la synchronisation: ${e.toString()}')); + } + } + + /// Handler pour appliquer des filtres avancés + Future _onApplyAdvancedFilters( + ApplyAdvancedFilters event, + Emitter emit, + ) async { + try { + emit(const CotisationsLoading()); + + final cotisations = await _cotisationRepository.rechercherCotisations( + membreId: event.filters['membreId'], + statut: event.filters['statut'], + typeCotisation: event.filters['typeCotisation'], + annee: event.filters['annee'], + mois: event.filters['mois'], + ); + + emit(CotisationsSearchResults( + cotisations: cotisations, + searchCriteria: event.filters, + )); + + } catch (e) { + emit(CotisationsError('Erreur lors de l\'application des filtres: ${e.toString()}')); + } + } + + /// Handler pour exporter les cotisations + Future _onExportCotisations( + ExportCotisations event, + Emitter emit, + ) async { + try { + final cotisations = event.cotisations ?? []; + + emit(ExportInProgress( + format: event.format, + totalItems: cotisations.length, + )); + + // TODO: Implémenter l'export réel selon le format + await Future.delayed(const Duration(seconds: 2)); // Simulation + + emit(ExportCompleted( + format: event.format, + filePath: '/storage/emulated/0/Download/cotisations.${event.format}', + itemsExported: cotisations.length, + )); + + } catch (e) { + emit(CotisationsError('Erreur lors de l\'export: ${e.toString()}')); + } + } } diff --git a/unionflow-mobile-apps/lib/features/cotisations/presentation/bloc/cotisations_event.dart b/unionflow-mobile-apps/lib/features/cotisations/presentation/bloc/cotisations_event.dart index 7c5727e..8a47cec 100644 --- a/unionflow-mobile-apps/lib/features/cotisations/presentation/bloc/cotisations_event.dart +++ b/unionflow-mobile-apps/lib/features/cotisations/presentation/bloc/cotisations_event.dart @@ -204,3 +204,97 @@ class SortCotisations extends CotisationsEvent { @override List get props => [sortBy, ascending]; } + +/// Événement pour initier un paiement +class InitiatePayment extends CotisationsEvent { + final String cotisationId; + final double montant; + final String methodePaiement; + final String numeroTelephone; + final String? nomPayeur; + final String? emailPayeur; + + const InitiatePayment({ + required this.cotisationId, + required this.montant, + required this.methodePaiement, + required this.numeroTelephone, + this.nomPayeur, + this.emailPayeur, + }); + + @override + List get props => [ + cotisationId, + montant, + methodePaiement, + numeroTelephone, + nomPayeur, + emailPayeur, + ]; +} + +/// Événement pour vérifier le statut d'un paiement +class CheckPaymentStatus extends CotisationsEvent { + final String paymentId; + + const CheckPaymentStatus(this.paymentId); + + @override + List get props => [paymentId]; +} + +/// Événement pour annuler un paiement +class CancelPayment extends CotisationsEvent { + final String paymentId; + final String cotisationId; + + const CancelPayment({ + required this.paymentId, + required this.cotisationId, + }); + + @override + List get props => [paymentId, cotisationId]; +} + +/// Événement pour programmer des notifications +class ScheduleNotifications extends CotisationsEvent { + final List cotisations; + + const ScheduleNotifications(this.cotisations); + + @override + List get props => [cotisations]; +} + +/// Événement pour synchroniser avec le serveur +class SyncWithServer extends CotisationsEvent { + final bool forceSync; + + const SyncWithServer({this.forceSync = false}); + + @override + List get props => [forceSync]; +} + +/// Événement pour appliquer des filtres avancés +class ApplyAdvancedFilters extends CotisationsEvent { + final Map filters; + + const ApplyAdvancedFilters(this.filters); + + @override + List get props => [filters]; +} + +/// Événement pour exporter des données +class ExportCotisations extends CotisationsEvent { + final String format; // 'pdf', 'excel', 'csv' + final List? cotisations; + + const ExportCotisations(this.format, {this.cotisations}); + + @override + List get props => [format, cotisations]; +} diff --git a/unionflow-mobile-apps/lib/features/cotisations/presentation/bloc/cotisations_state.dart b/unionflow-mobile-apps/lib/features/cotisations/presentation/bloc/cotisations_state.dart index 7755fac..10076eb 100644 --- a/unionflow-mobile-apps/lib/features/cotisations/presentation/bloc/cotisations_state.dart +++ b/unionflow-mobile-apps/lib/features/cotisations/presentation/bloc/cotisations_state.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; import '../../../../core/models/cotisation_model.dart'; +import '../../../../core/models/payment_model.dart'; /// États du BLoC des cotisations abstract class CotisationsState extends Equatable { @@ -245,3 +246,137 @@ class CotisationsSearchResults extends CotisationsState { @override List get props => [cotisations, searchCriteria, hasReachedMax, currentPage]; } + +/// État pour un paiement en cours +class PaymentInProgress extends CotisationsState { + final String cotisationId; + final String paymentId; + final String methodePaiement; + final double montant; + + const PaymentInProgress({ + required this.cotisationId, + required this.paymentId, + required this.methodePaiement, + required this.montant, + }); + + @override + List get props => [cotisationId, paymentId, methodePaiement, montant]; +} + +/// État pour un paiement réussi +class PaymentSuccess extends CotisationsState { + final String cotisationId; + final PaymentModel payment; + final CotisationModel updatedCotisation; + + const PaymentSuccess({ + required this.cotisationId, + required this.payment, + required this.updatedCotisation, + }); + + @override + List get props => [cotisationId, payment, updatedCotisation]; +} + +/// État pour un paiement échoué +class PaymentFailure extends CotisationsState { + final String cotisationId; + final String paymentId; + final String errorMessage; + final String? errorCode; + + const PaymentFailure({ + required this.cotisationId, + required this.paymentId, + required this.errorMessage, + this.errorCode, + }); + + @override + List get props => [cotisationId, paymentId, errorMessage, errorCode]; +} + +/// État pour un paiement annulé +class PaymentCancelled extends CotisationsState { + final String cotisationId; + final String paymentId; + + const PaymentCancelled({ + required this.cotisationId, + required this.paymentId, + }); + + @override + List get props => [cotisationId, paymentId]; +} + +/// État pour la synchronisation en cours +class SyncInProgress extends CotisationsState { + final String message; + + const SyncInProgress(this.message); + + @override + List get props => [message]; +} + +/// État pour la synchronisation terminée +class SyncCompleted extends CotisationsState { + final int itemsSynced; + final DateTime syncTime; + + const SyncCompleted({ + required this.itemsSynced, + required this.syncTime, + }); + + @override + List get props => [itemsSynced, syncTime]; +} + +/// État pour l'export en cours +class ExportInProgress extends CotisationsState { + final String format; + final int totalItems; + + const ExportInProgress({ + required this.format, + required this.totalItems, + }); + + @override + List get props => [format, totalItems]; +} + +/// État pour l'export terminé +class ExportCompleted extends CotisationsState { + final String format; + final String filePath; + final int itemsExported; + + const ExportCompleted({ + required this.format, + required this.filePath, + required this.itemsExported, + }); + + @override + List get props => [format, filePath, itemsExported]; +} + +/// État pour les notifications programmées +class NotificationsScheduled extends CotisationsState { + final int notificationsCount; + final List cotisationIds; + + const NotificationsScheduled({ + required this.notificationsCount, + required this.cotisationIds, + }); + + @override + List get props => [notificationsCount, cotisationIds]; +} diff --git a/unionflow-mobile-apps/lib/features/cotisations/presentation/pages/cotisation_detail_page.dart b/unionflow-mobile-apps/lib/features/cotisations/presentation/pages/cotisation_detail_page.dart new file mode 100644 index 0000000..063134a --- /dev/null +++ b/unionflow-mobile-apps/lib/features/cotisations/presentation/pages/cotisation_detail_page.dart @@ -0,0 +1,708 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../../core/di/injection.dart'; +import '../../../../core/models/cotisation_model.dart'; +import '../../../../core/models/payment_model.dart'; +import '../../../../shared/theme/app_theme.dart'; +import '../../../../shared/widgets/buttons/buttons.dart'; +import '../../../../shared/widgets/buttons/primary_button.dart'; +import '../bloc/cotisations_bloc.dart'; +import '../bloc/cotisations_event.dart'; +import '../bloc/cotisations_state.dart'; +import '../widgets/payment_method_selector.dart'; +import '../widgets/payment_form_widget.dart'; +import '../widgets/cotisation_timeline_widget.dart'; + +/// Page de détail d'une cotisation +class CotisationDetailPage extends StatefulWidget { + final CotisationModel cotisation; + + const CotisationDetailPage({ + super.key, + required this.cotisation, + }); + + @override + State createState() => _CotisationDetailPageState(); +} + +class _CotisationDetailPageState extends State + with TickerProviderStateMixin { + late final CotisationsBloc _cotisationsBloc; + late final TabController _tabController; + late final AnimationController _animationController; + late final Animation _fadeAnimation; + + @override + void initState() { + super.initState(); + _cotisationsBloc = getIt(); + _tabController = TabController(length: 3, vsync: this); + _animationController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + ); + + _animationController.forward(); + } + + @override + void dispose() { + _tabController.dispose(); + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cotisationsBloc, + child: Scaffold( + backgroundColor: AppTheme.backgroundLight, + body: BlocListener( + listener: (context, state) { + if (state is PaymentSuccess) { + _showPaymentSuccessDialog(state); + } else if (state is PaymentFailure) { + _showPaymentErrorDialog(state); + } else if (state is PaymentInProgress) { + _showPaymentProgressDialog(state); + } + }, + child: FadeTransition( + opacity: _fadeAnimation, + child: CustomScrollView( + slivers: [ + _buildAppBar(), + SliverToBoxAdapter( + child: Column( + children: [ + _buildStatusCard(), + const SizedBox(height: 16), + _buildTabSection(), + ], + ), + ), + ], + ), + ), + ), + bottomNavigationBar: _buildBottomActions(), + ), + ); + } + + Widget _buildAppBar() { + return SliverAppBar( + expandedHeight: 200, + pinned: true, + backgroundColor: _getStatusColor(), + flexibleSpace: FlexibleSpaceBar( + title: Text( + widget.cotisation.typeCotisation, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + background: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + _getStatusColor(), + _getStatusColor().withOpacity(0.8), + ], + ), + ), + child: Stack( + children: [ + Positioned( + right: -50, + top: -50, + child: Container( + width: 200, + height: 200, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white.withOpacity(0.1), + ), + ), + ), + Positioned( + right: 20, + bottom: 20, + child: Icon( + _getStatusIcon(), + size: 80, + color: Colors.white.withOpacity(0.3), + ), + ), + ], + ), + ), + ), + actions: [ + IconButton( + icon: const Icon(Icons.share, color: Colors.white), + onPressed: _shareReceipt, + ), + PopupMenuButton( + icon: const Icon(Icons.more_vert, color: Colors.white), + onSelected: _handleMenuAction, + itemBuilder: (context) => [ + const PopupMenuItem( + value: 'export', + child: Row( + children: [ + Icon(Icons.download), + SizedBox(width: 8), + Text('Exporter'), + ], + ), + ), + const PopupMenuItem( + value: 'print', + child: Row( + children: [ + Icon(Icons.print), + SizedBox(width: 8), + Text('Imprimer'), + ], + ), + ), + const PopupMenuItem( + value: 'history', + child: Row( + children: [ + Icon(Icons.history), + SizedBox(width: 8), + Text('Historique'), + ], + ), + ), + ], + ), + ], + ); + } + + Widget _buildStatusCard() { + return Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 20, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Montant à payer', + style: TextStyle( + fontSize: 14, + color: AppTheme.textSecondary, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + Text( + '${widget.cotisation.montantDu.toStringAsFixed(0)} XOF', + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: AppTheme.textPrimary, + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: _getStatusColor().withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _getStatusIcon(), + size: 16, + color: _getStatusColor(), + ), + const SizedBox(width: 4), + Text( + widget.cotisation.statut, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: _getStatusColor(), + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 20), + _buildInfoRow('Membre', widget.cotisation.nomMembre ?? 'N/A'), + _buildInfoRow('Période', _formatPeriode()), + _buildInfoRow('Échéance', _formatDate(widget.cotisation.dateEcheance)), + if (widget.cotisation.montantPaye > 0) + _buildInfoRow('Montant payé', '${widget.cotisation.montantPaye.toStringAsFixed(0)} XOF'), + if (widget.cotisation.isEnRetard) + _buildInfoRow('Retard', '${widget.cotisation.joursRetard} jours', isWarning: true), + ], + ), + ); + } + + Widget _buildInfoRow(String label, String value, {bool isWarning = false}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: TextStyle( + fontSize: 14, + color: AppTheme.textSecondary, + ), + ), + Text( + value, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: isWarning ? AppTheme.warningColor : AppTheme.textPrimary, + ), + ), + ], + ), + ); + } + + Widget _buildTabSection() { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 20, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + children: [ + TabBar( + controller: _tabController, + labelColor: AppTheme.primaryColor, + unselectedLabelColor: AppTheme.textSecondary, + indicatorColor: AppTheme.primaryColor, + tabs: const [ + Tab(text: 'Détails', icon: Icon(Icons.info_outline)), + Tab(text: 'Paiement', icon: Icon(Icons.payment)), + Tab(text: 'Historique', icon: Icon(Icons.history)), + ], + ), + SizedBox( + height: 400, + child: TabBarView( + controller: _tabController, + children: [ + _buildDetailsTab(), + _buildPaymentTab(), + _buildHistoryTab(), + ], + ), + ), + ], + ), + ); + } + + Widget _buildDetailsTab() { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDetailSection('Informations générales', [ + _buildDetailItem('Type', widget.cotisation.typeCotisation), + _buildDetailItem('Référence', widget.cotisation.numeroReference), + _buildDetailItem('Date création', _formatDate(widget.cotisation.dateCreation)), + _buildDetailItem('Statut', widget.cotisation.statut), + ]), + const SizedBox(height: 20), + _buildDetailSection('Montants', [ + _buildDetailItem('Montant dû', '${widget.cotisation.montantDu.toStringAsFixed(0)} XOF'), + _buildDetailItem('Montant payé', '${widget.cotisation.montantPaye.toStringAsFixed(0)} XOF'), + _buildDetailItem('Reste à payer', '${(widget.cotisation.montantDu - widget.cotisation.montantPaye).toStringAsFixed(0)} XOF'), + ]), + if (widget.cotisation.description?.isNotEmpty == true) ...[ + const SizedBox(height: 20), + _buildDetailSection('Description', [ + Text( + widget.cotisation.description!, + style: const TextStyle( + fontSize: 14, + color: AppTheme.textSecondary, + ), + ), + ]), + ], + ], + ), + ); + } + + Widget _buildPaymentTab() { + if (widget.cotisation.isEntierementPayee) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.check_circle, + size: 64, + color: AppTheme.successColor, + ), + SizedBox(height: 16), + Text( + 'Cotisation entièrement payée', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppTheme.successColor, + ), + ), + ], + ), + ); + } + + return BlocBuilder( + builder: (context, state) { + if (state is PaymentInProgress) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Traitement du paiement en cours...'), + ], + ), + ); + } + + return PaymentFormWidget( + cotisation: widget.cotisation, + onPaymentInitiated: (paymentData) { + _cotisationsBloc.add(InitiatePayment( + cotisationId: widget.cotisation.id, + montant: paymentData['montant'], + methodePaiement: paymentData['methodePaiement'], + numeroTelephone: paymentData['numeroTelephone'], + nomPayeur: paymentData['nomPayeur'], + emailPayeur: paymentData['emailPayeur'], + )); + }, + ); + }, + ); + } + + Widget _buildHistoryTab() { + return CotisationTimelineWidget(cotisation: widget.cotisation); + } + + Widget _buildDetailSection(String title, List children) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppTheme.textPrimary, + ), + ), + const SizedBox(height: 12), + ...children, + ], + ); + } + + Widget _buildDetailItem(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 14, + color: AppTheme.textSecondary, + ), + ), + Text( + value, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppTheme.textPrimary, + ), + ), + ], + ), + ); + } + + Widget _buildBottomActions() { + if (widget.cotisation.isEntierementPayee) { + return Container( + padding: const EdgeInsets.all(16), + decoration: const BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black12, + blurRadius: 10, + offset: Offset(0, -2), + ), + ], + ), + child: PrimaryButton( + text: 'Télécharger le reçu', + icon: Icons.download, + onPressed: _downloadReceipt, + ), + ); + } + + return Container( + padding: const EdgeInsets.all(16), + decoration: const BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black12, + blurRadius: 10, + offset: Offset(0, -2), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: _scheduleReminder, + icon: const Icon(Icons.notifications), + label: const Text('Rappel'), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + flex: 2, + child: PrimaryButton( + text: 'Payer maintenant', + icon: Icons.payment, + onPressed: () { + _tabController.animateTo(1); // Aller à l'onglet paiement + }, + ), + ), + ], + ), + ); + } + + // Méthodes utilitaires + Color _getStatusColor() { + switch (widget.cotisation.statut.toLowerCase()) { + case 'payee': + return AppTheme.successColor; + case 'en_retard': + return AppTheme.errorColor; + case 'en_attente': + return AppTheme.warningColor; + default: + return AppTheme.primaryColor; + } + } + + IconData _getStatusIcon() { + switch (widget.cotisation.statut.toLowerCase()) { + case 'payee': + return Icons.check_circle; + case 'en_retard': + return Icons.warning; + case 'en_attente': + return Icons.schedule; + default: + return Icons.payment; + } + } + + String _formatDate(DateTime date) { + return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}'; + } + + String _formatPeriode() { + return '${widget.cotisation.mois}/${widget.cotisation.annee}'; + } + + // Actions + void _shareReceipt() { + // TODO: Implémenter le partage + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Partage - En cours de développement')), + ); + } + + void _handleMenuAction(String action) { + switch (action) { + case 'export': + _exportReceipt(); + break; + case 'print': + _printReceipt(); + break; + case 'history': + _showFullHistory(); + break; + } + } + + void _exportReceipt() { + _cotisationsBloc.add(ExportCotisations('pdf', cotisations: [widget.cotisation])); + } + + void _printReceipt() { + // TODO: Implémenter l'impression + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Impression - En cours de développement')), + ); + } + + void _showFullHistory() { + // TODO: Naviguer vers l'historique complet + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Historique complet - En cours de développement')), + ); + } + + void _downloadReceipt() { + _exportReceipt(); + } + + void _scheduleReminder() { + _cotisationsBloc.add(ScheduleNotifications([widget.cotisation])); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Rappel programmé avec succès'), + backgroundColor: AppTheme.successColor, + ), + ); + } + + // Dialogs + void _showPaymentSuccessDialog(PaymentSuccess state) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: const Row( + children: [ + Icon(Icons.check_circle, color: AppTheme.successColor), + SizedBox(width: 8), + Text('Paiement réussi'), + ], + ), + content: Text('Votre paiement de ${state.payment.montant.toStringAsFixed(0)} XOF a été confirmé.'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + Navigator.of(context).pop(); // Retour à la liste + }, + child: const Text('OK'), + ), + ], + ), + ); + } + + void _showPaymentErrorDialog(PaymentFailure state) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Row( + children: [ + Icon(Icons.error, color: AppTheme.errorColor), + SizedBox(width: 8), + Text('Échec du paiement'), + ], + ), + content: Text(state.errorMessage), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ), + ); + } + + void _showPaymentProgressDialog(PaymentInProgress state) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 16), + Text('Traitement du paiement de ${state.montant.toStringAsFixed(0)} XOF...'), + const SizedBox(height: 8), + Text('Méthode: ${state.methodePaiement}'), + ], + ), + ), + ); + } +} diff --git a/unionflow-mobile-apps/lib/features/cotisations/presentation/pages/cotisations_list_page.dart b/unionflow-mobile-apps/lib/features/cotisations/presentation/pages/cotisations_list_page.dart index 21894fa..a45495d 100644 --- a/unionflow-mobile-apps/lib/features/cotisations/presentation/pages/cotisations_list_page.dart +++ b/unionflow-mobile-apps/lib/features/cotisations/presentation/pages/cotisations_list_page.dart @@ -8,6 +8,8 @@ import '../bloc/cotisations_event.dart'; import '../bloc/cotisations_state.dart'; import '../widgets/cotisation_card.dart'; import '../widgets/cotisations_stats_card.dart'; +import 'cotisation_detail_page.dart'; +import 'cotisations_search_page.dart'; /// Page principale pour la liste des cotisations class CotisationsListPage extends StatefulWidget { @@ -155,13 +157,23 @@ class _CotisationsListPageState extends State { IconButton( icon: const Icon(Icons.search, color: Colors.white), onPressed: () { - // TODO: Implémenter la recherche + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const CotisationsSearchPage(), + ), + ); }, ), IconButton( icon: const Icon(Icons.filter_list, color: Colors.white), onPressed: () { - // TODO: Implémenter les filtres + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const CotisationsSearchPage(), + ), + ); }, ), ], @@ -264,14 +276,22 @@ class _CotisationsListPageState extends State { child: CotisationCard( cotisation: cotisation, onTap: () { - // TODO: Naviguer vers le détail + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CotisationDetailPage( + cotisation: cotisation, + ), + ), + ); }, onPay: () { - // TODO: Implémenter le paiement - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Paiement - En cours de développement'), - backgroundColor: AppTheme.successColor, + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CotisationDetailPage( + cotisation: cotisation, + ), ), ); }, diff --git a/unionflow-mobile-apps/lib/features/cotisations/presentation/pages/cotisations_search_page.dart b/unionflow-mobile-apps/lib/features/cotisations/presentation/pages/cotisations_search_page.dart new file mode 100644 index 0000000..c41cfbc --- /dev/null +++ b/unionflow-mobile-apps/lib/features/cotisations/presentation/pages/cotisations_search_page.dart @@ -0,0 +1,498 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../../core/di/injection.dart'; +import '../../../../core/models/cotisation_model.dart'; +import '../../../../shared/theme/app_theme.dart'; +import '../../../../shared/widgets/buttons/buttons.dart'; +import '../../../../shared/widgets/buttons/primary_button.dart'; +import '../bloc/cotisations_bloc.dart'; +import '../bloc/cotisations_event.dart'; +import '../bloc/cotisations_state.dart'; +import '../widgets/cotisation_card.dart'; +import 'cotisation_detail_page.dart'; + +/// Page de recherche et filtrage des cotisations +class CotisationsSearchPage extends StatefulWidget { + const CotisationsSearchPage({super.key}); + + @override + State createState() => _CotisationsSearchPageState(); +} + +class _CotisationsSearchPageState extends State + with TickerProviderStateMixin { + late final CotisationsBloc _cotisationsBloc; + late final TabController _tabController; + late final AnimationController _animationController; + + final _searchController = TextEditingController(); + final _scrollController = ScrollController(); + + String? _selectedStatut; + String? _selectedType; + int? _selectedAnnee; + int? _selectedMois; + bool _showAdvancedFilters = false; + + @override + void initState() { + super.initState(); + _cotisationsBloc = getIt(); + _tabController = TabController(length: 4, vsync: this); + _animationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + + _scrollController.addListener(_onScroll); + _animationController.forward(); + } + + @override + void dispose() { + _searchController.dispose(); + _scrollController.dispose(); + _tabController.dispose(); + _animationController.dispose(); + super.dispose(); + } + + void _onScroll() { + if (_isBottom) { + final currentState = _cotisationsBloc.state; + if (currentState is CotisationsSearchResults && !currentState.hasReachedMax) { + _performSearch(page: currentState.currentPage + 1); + } + } + } + + bool get _isBottom { + if (!_scrollController.hasClients) return false; + final maxScroll = _scrollController.position.maxScrollExtent; + final currentScroll = _scrollController.offset; + return currentScroll >= (maxScroll * 0.9); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cotisationsBloc, + child: Scaffold( + backgroundColor: AppTheme.backgroundLight, + appBar: AppBar( + title: const Text('Recherche'), + backgroundColor: AppTheme.primaryColor, + foregroundColor: Colors.white, + bottom: TabBar( + controller: _tabController, + labelColor: Colors.white, + unselectedLabelColor: Colors.white70, + indicatorColor: Colors.white, + tabs: const [ + Tab(text: 'Toutes', icon: Icon(Icons.list)), + Tab(text: 'En attente', icon: Icon(Icons.schedule)), + Tab(text: 'En retard', icon: Icon(Icons.warning)), + Tab(text: 'Payées', icon: Icon(Icons.check_circle)), + ], + onTap: (index) => _onTabChanged(index), + ), + ), + body: Column( + children: [ + _buildSearchHeader(), + if (_showAdvancedFilters) _buildAdvancedFilters(), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildSearchResults(), + _buildSearchResults(statut: 'EN_ATTENTE'), + _buildSearchResults(statut: 'EN_RETARD'), + _buildSearchResults(statut: 'PAYEE'), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildSearchHeader() { + return Container( + padding: const EdgeInsets.all(16), + decoration: const BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black12, + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + // Barre de recherche + TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: 'Rechercher par nom, référence...', + prefixIcon: const Icon(Icons.search), + suffixIcon: _searchController.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + _searchController.clear(); + _performSearch(); + }, + ) + : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + filled: true, + fillColor: AppTheme.backgroundLight, + ), + onChanged: (value) { + setState(() {}); + _performSearch(); + }, + ), + + const SizedBox(height: 12), + + // Boutons d'action + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () { + setState(() { + _showAdvancedFilters = !_showAdvancedFilters; + }); + if (_showAdvancedFilters) { + _animationController.forward(); + } else { + _animationController.reverse(); + } + }, + icon: Icon(_showAdvancedFilters ? Icons.expand_less : Icons.tune), + label: Text(_showAdvancedFilters ? 'Masquer filtres' : 'Filtres avancés'), + ), + ), + const SizedBox(width: 12), + OutlinedButton.icon( + onPressed: _clearAllFilters, + icon: const Icon(Icons.clear_all), + label: const Text('Effacer'), + ), + ], + ), + ], + ), + ); + } + + Widget _buildAdvancedFilters() { + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + height: _showAdvancedFilters ? null : 0, + child: Container( + padding: const EdgeInsets.all(16), + decoration: const BoxDecoration( + color: Colors.white, + border: Border( + bottom: BorderSide(color: AppTheme.borderLight), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Filtres avancés', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppTheme.textPrimary, + ), + ), + const SizedBox(height: 16), + + // Grille de filtres + GridView.count( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 2, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: 3, + children: [ + _buildFilterDropdown( + 'Type', + _selectedType, + ['Mensuelle', 'Annuelle', 'Exceptionnelle', 'Adhésion'], + (value) => setState(() => _selectedType = value), + ), + _buildFilterDropdown( + 'Année', + _selectedAnnee?.toString(), + List.generate(5, (i) => (DateTime.now().year - i).toString()), + (value) => setState(() => _selectedAnnee = int.tryParse(value ?? '')), + ), + ], + ), + + const SizedBox(height: 16), + + // Bouton d'application des filtres + SizedBox( + width: double.infinity, + child: PrimaryButton( + text: 'Appliquer les filtres', + onPressed: _applyAdvancedFilters, + ), + ), + ], + ), + ), + ); + } + + Widget _buildFilterDropdown( + String label, + String? value, + List items, + Function(String?) onChanged, + ) { + return DropdownButtonFormField( + value: value, + decoration: InputDecoration( + labelText: label, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + ), + items: [ + DropdownMenuItem( + value: null, + child: Text('Tous les ${label.toLowerCase()}s'), + ), + ...items.map((item) => DropdownMenuItem( + value: item, + child: Text(item), + )), + ], + onChanged: onChanged, + ); + } + + Widget _buildSearchResults({String? statut}) { + return BlocBuilder( + builder: (context, state) { + if (state is CotisationsLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (state is CotisationsError) { + return _buildErrorState(state); + } + + if (state is CotisationsSearchResults) { + final filteredResults = statut != null + ? state.cotisations.where((c) => c.statut == statut).toList() + : state.cotisations; + + if (filteredResults.isEmpty) { + return _buildEmptyState(); + } + + return RefreshIndicator( + onRefresh: () async => _performSearch(refresh: true), + child: ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.all(16), + itemCount: filteredResults.length + (state.hasReachedMax ? 0 : 1), + itemBuilder: (context, index) { + if (index >= filteredResults.length) { + return const Padding( + padding: EdgeInsets.all(16), + child: Center(child: CircularProgressIndicator()), + ); + } + + final cotisation = filteredResults[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: CotisationCard( + cotisation: cotisation, + onTap: () => _navigateToDetail(cotisation), + onPay: () => _navigateToDetail(cotisation), + ), + ); + }, + ), + ); + } + + return _buildInitialState(); + }, + ); + } + + Widget _buildInitialState() { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.search, + size: 64, + color: AppTheme.textHint, + ), + SizedBox(height: 16), + Text( + 'Recherchez des cotisations', + style: TextStyle( + fontSize: 18, + color: AppTheme.textSecondary, + ), + ), + SizedBox(height: 8), + Text( + 'Utilisez la barre de recherche ou les filtres', + style: TextStyle( + fontSize: 14, + color: AppTheme.textHint, + ), + ), + ], + ), + ); + } + + Widget _buildEmptyState() { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.search_off, + size: 64, + color: AppTheme.textHint, + ), + SizedBox(height: 16), + Text( + 'Aucun résultat trouvé', + style: TextStyle( + fontSize: 18, + color: AppTheme.textSecondary, + ), + ), + SizedBox(height: 8), + Text( + 'Essayez de modifier vos critères de recherche', + style: TextStyle( + fontSize: 14, + color: AppTheme.textHint, + ), + ), + ], + ), + ); + } + + Widget _buildErrorState(CotisationsError state) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + size: 64, + color: AppTheme.errorColor, + ), + const SizedBox(height: 16), + Text( + 'Erreur de recherche', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppTheme.textPrimary, + ), + ), + const SizedBox(height: 8), + Text( + state.message, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 14, + color: AppTheme.textSecondary, + ), + ), + const SizedBox(height: 24), + PrimaryButton( + text: 'Réessayer', + onPressed: () => _performSearch(refresh: true), + ), + ], + ), + ); + } + + // Actions + void _onTabChanged(int index) { + _performSearch(refresh: true); + } + + void _performSearch({int page = 0, bool refresh = false}) { + final query = _searchController.text.trim(); + + if (query.isEmpty && !_hasActiveFilters()) { + return; + } + + final filters = { + if (query.isNotEmpty) 'query': query, + if (_selectedStatut != null) 'statut': _selectedStatut, + if (_selectedType != null) 'typeCotisation': _selectedType, + if (_selectedAnnee != null) 'annee': _selectedAnnee, + if (_selectedMois != null) 'mois': _selectedMois, + }; + + _cotisationsBloc.add(ApplyAdvancedFilters(filters)); + } + + void _applyAdvancedFilters() { + _performSearch(refresh: true); + } + + void _clearAllFilters() { + setState(() { + _searchController.clear(); + _selectedStatut = null; + _selectedType = null; + _selectedAnnee = null; + _selectedMois = null; + }); + _cotisationsBloc.add(const ResetCotisationsState()); + } + + bool _hasActiveFilters() { + return _selectedStatut != null || + _selectedType != null || + _selectedAnnee != null || + _selectedMois != null; + } + + void _navigateToDetail(CotisationModel cotisation) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CotisationDetailPage(cotisation: cotisation), + ), + ); + } +} diff --git a/unionflow-mobile-apps/lib/features/cotisations/presentation/widgets/animated_cotisation_list.dart b/unionflow-mobile-apps/lib/features/cotisations/presentation/widgets/animated_cotisation_list.dart new file mode 100644 index 0000000..e87e8e7 --- /dev/null +++ b/unionflow-mobile-apps/lib/features/cotisations/presentation/widgets/animated_cotisation_list.dart @@ -0,0 +1,244 @@ +import 'package:flutter/material.dart'; +import '../../../../core/models/cotisation_model.dart'; +import '../../../../core/animations/loading_animations.dart'; +import 'cotisation_card.dart'; + +/// Widget animé pour afficher une liste de cotisations avec animations d'apparition +class AnimatedCotisationList extends StatefulWidget { + final List cotisations; + final Function(CotisationModel)? onCotisationTap; + final bool isLoading; + final VoidCallback? onRefresh; + final ScrollController? scrollController; + + const AnimatedCotisationList({ + super.key, + required this.cotisations, + this.onCotisationTap, + this.isLoading = false, + this.onRefresh, + this.scrollController, + }); + + @override + State createState() => _AnimatedCotisationListState(); +} + +class _AnimatedCotisationListState extends State + with TickerProviderStateMixin { + late AnimationController _listController; + List _itemControllers = []; + List> _itemAnimations = []; + List> _slideAnimations = []; + + @override + void initState() { + super.initState(); + _initializeAnimations(); + } + + @override + void didUpdateWidget(AnimatedCotisationList oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.cotisations.length != oldWidget.cotisations.length) { + _updateAnimations(); + } + } + + @override + void dispose() { + _listController.dispose(); + for (final controller in _itemControllers) { + controller.dispose(); + } + super.dispose(); + } + + void _initializeAnimations() { + _listController = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + ); + + _updateAnimations(); + _listController.forward(); + } + + void _updateAnimations() { + // Dispose des anciens controllers s'ils existent + if (_itemControllers.isNotEmpty) { + for (final controller in _itemControllers) { + controller.dispose(); + } + } + + // Créer de nouveaux controllers pour chaque élément + _itemControllers = List.generate( + widget.cotisations.length, + (index) => AnimationController( + duration: Duration(milliseconds: 400 + (index * 80)), + vsync: this, + ), + ); + + // Animations de fade et scale + _itemAnimations = _itemControllers.map((controller) { + return Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: controller, + curve: Curves.easeOutCubic, + ), + ); + }).toList(); + + // Animations de slide depuis la gauche + _slideAnimations = _itemControllers.map((controller) { + return Tween( + begin: const Offset(-0.3, 0), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: controller, + curve: Curves.easeOutCubic, + ), + ); + }).toList(); + + // Démarrer les animations avec un délai progressif + for (int i = 0; i < _itemControllers.length; i++) { + Future.delayed(Duration(milliseconds: i * 120), () { + if (mounted) { + _itemControllers[i].forward(); + } + }); + } + } + + @override + Widget build(BuildContext context) { + if (widget.isLoading && widget.cotisations.isEmpty) { + return _buildLoadingState(); + } + + if (widget.cotisations.isEmpty) { + return _buildEmptyState(); + } + + return RefreshIndicator( + onRefresh: () async { + widget.onRefresh?.call(); + await Future.delayed(const Duration(milliseconds: 500)); + }, + child: ListView.builder( + controller: widget.scrollController, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + itemCount: widget.cotisations.length + (widget.isLoading ? 1 : 0), + itemBuilder: (context, index) { + if (index >= widget.cotisations.length) { + return _buildLoadingIndicator(); + } + + return _buildAnimatedItem(index); + }, + ), + ); + } + + Widget _buildAnimatedItem(int index) { + final cotisation = widget.cotisations[index]; + + if (index >= _itemAnimations.length) { + // Fallback pour les nouveaux éléments + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: CotisationCard( + cotisation: cotisation, + onTap: () => widget.onCotisationTap?.call(cotisation), + ), + ); + } + + return AnimatedBuilder( + animation: _itemAnimations[index], + builder: (context, child) { + return SlideTransition( + position: _slideAnimations[index], + child: FadeTransition( + opacity: _itemAnimations[index], + child: Transform.scale( + scale: 0.9 + (0.1 * _itemAnimations[index].value), + child: Padding( + padding: const EdgeInsets.only(bottom: 16), + child: CotisationCard( + cotisation: cotisation, + onTap: () => widget.onCotisationTap?.call(cotisation), + ), + ), + ), + ), + ); + }, + ); + } + + Widget _buildLoadingState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + LoadingAnimations.pulse(), + const SizedBox(height: 24), + const Text( + 'Chargement des cotisations...', + style: TextStyle( + fontSize: 16, + color: Colors.grey, + ), + ), + ], + ), + ); + } + + Widget _buildEmptyState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.payment_outlined, + size: 80, + color: Colors.grey[400], + ), + const SizedBox(height: 24), + Text( + 'Aucune cotisation trouvée', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 8), + Text( + 'Les cotisations apparaîtront ici', + style: TextStyle( + fontSize: 14, + color: Colors.grey[500], + ), + ), + ], + ), + ); + } + + Widget _buildLoadingIndicator() { + return Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: LoadingAnimations.spinner(), + ), + ); + } +} diff --git a/unionflow-mobile-apps/lib/features/cotisations/presentation/widgets/cotisation_card.dart b/unionflow-mobile-apps/lib/features/cotisations/presentation/widgets/cotisation_card.dart index 5bec373..82151cd 100644 --- a/unionflow-mobile-apps/lib/features/cotisations/presentation/widgets/cotisation_card.dart +++ b/unionflow-mobile-apps/lib/features/cotisations/presentation/widgets/cotisation_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; import '../../../../core/models/cotisation_model.dart'; import '../../../../shared/theme/app_theme.dart'; @@ -41,7 +42,10 @@ class CotisationCard extends StatelessWidget { ), ), child: InkWell( - onTap: onTap, + onTap: () { + HapticFeedback.lightImpact(); + onTap?.call(); + }, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(16), @@ -71,7 +75,10 @@ class CotisationCard extends StatelessWidget { // Actions if (cotisation.statut == 'EN_ATTENTE' || cotisation.statut == 'EN_RETARD') IconButton( - onPressed: onPay, + onPressed: () { + HapticFeedback.lightImpact(); + onPay?.call(); + }, icon: const Icon(Icons.payment, size: 20), color: AppTheme.successColor, tooltip: 'Payer', diff --git a/unionflow-mobile-apps/lib/features/cotisations/presentation/widgets/cotisation_timeline_widget.dart b/unionflow-mobile-apps/lib/features/cotisations/presentation/widgets/cotisation_timeline_widget.dart new file mode 100644 index 0000000..c1e6ee8 --- /dev/null +++ b/unionflow-mobile-apps/lib/features/cotisations/presentation/widgets/cotisation_timeline_widget.dart @@ -0,0 +1,417 @@ +import 'package:flutter/material.dart'; +import '../../../../core/models/cotisation_model.dart'; +import '../../../../shared/theme/app_theme.dart'; + +/// Widget d'affichage de la timeline d'une cotisation +class CotisationTimelineWidget extends StatefulWidget { + final CotisationModel cotisation; + + const CotisationTimelineWidget({ + super.key, + required this.cotisation, + }); + + @override + State createState() => _CotisationTimelineWidgetState(); +} + +class _CotisationTimelineWidgetState extends State + with TickerProviderStateMixin { + late final AnimationController _animationController; + late final List> _itemAnimations; + + List _timelineEvents = []; + + @override + void initState() { + super.initState(); + _generateTimelineEvents(); + + _animationController = AnimationController( + duration: Duration(milliseconds: 300 * _timelineEvents.length), + vsync: this, + ); + + _itemAnimations = List.generate( + _timelineEvents.length, + (index) => Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _animationController, + curve: Interval( + index / _timelineEvents.length, + (index + 1) / _timelineEvents.length, + curve: Curves.easeOutCubic, + ), + ), + ), + ); + + _animationController.forward(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + void _generateTimelineEvents() { + _timelineEvents = [ + TimelineEvent( + title: 'Cotisation créée', + description: 'Cotisation ${widget.cotisation.typeCotisation} créée pour ${widget.cotisation.nomMembre}', + date: widget.cotisation.dateCreation, + icon: Icons.add_circle, + color: AppTheme.primaryColor, + isCompleted: true, + ), + ]; + + // Ajouter l'événement d'échéance + final now = DateTime.now(); + final isOverdue = widget.cotisation.dateEcheance.isBefore(now); + + _timelineEvents.add( + TimelineEvent( + title: isOverdue ? 'Échéance dépassée' : 'Échéance prévue', + description: 'Date limite de paiement: ${_formatDate(widget.cotisation.dateEcheance)}', + date: widget.cotisation.dateEcheance, + icon: isOverdue ? Icons.warning : Icons.schedule, + color: isOverdue ? AppTheme.errorColor : AppTheme.warningColor, + isCompleted: isOverdue, + isWarning: isOverdue, + ), + ); + + // Ajouter les événements de paiement (simulés) + if (widget.cotisation.montantPaye > 0) { + _timelineEvents.add( + TimelineEvent( + title: 'Paiement partiel reçu', + description: 'Montant: ${widget.cotisation.montantPaye.toStringAsFixed(0)} XOF', + date: widget.cotisation.dateCreation.add(const Duration(days: 5)), // Simulé + icon: Icons.payment, + color: AppTheme.successColor, + isCompleted: true, + ), + ); + } + + if (widget.cotisation.isEntierementPayee) { + _timelineEvents.add( + TimelineEvent( + title: 'Paiement complet', + description: 'Cotisation entièrement payée', + date: widget.cotisation.dateCreation.add(const Duration(days: 10)), // Simulé + icon: Icons.check_circle, + color: AppTheme.successColor, + isCompleted: true, + isSuccess: true, + ), + ); + } else { + // Ajouter les événements futurs + if (!isOverdue) { + _timelineEvents.add( + TimelineEvent( + title: 'Rappel automatique', + description: 'Rappel envoyé 3 jours avant l\'échéance', + date: widget.cotisation.dateEcheance.subtract(const Duration(days: 3)), + icon: Icons.notifications, + color: AppTheme.infoColor, + isCompleted: false, + isFuture: true, + ), + ); + } + + _timelineEvents.add( + TimelineEvent( + title: 'Paiement en attente', + description: 'En attente du paiement complet', + date: DateTime.now(), + icon: Icons.hourglass_empty, + color: AppTheme.textSecondary, + isCompleted: false, + isFuture: true, + ), + ); + } + + // Trier par date + _timelineEvents.sort((a, b) => a.date.compareTo(b.date)); + } + + @override + Widget build(BuildContext context) { + if (_timelineEvents.isEmpty) { + return const Center( + child: Text( + 'Aucun historique disponible', + style: TextStyle( + fontSize: 16, + color: AppTheme.textSecondary, + ), + ), + ); + } + + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Historique de la cotisation', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppTheme.textPrimary, + ), + ), + const SizedBox(height: 20), + + Expanded( + child: ListView.builder( + itemCount: _timelineEvents.length, + itemBuilder: (context, index) { + return AnimatedBuilder( + animation: _itemAnimations[index], + builder: (context, child) { + return Transform.translate( + offset: Offset( + 0, + 50 * (1 - _itemAnimations[index].value), + ), + child: Opacity( + opacity: _itemAnimations[index].value, + child: _buildTimelineItem( + _timelineEvents[index], + index, + index == _timelineEvents.length - 1, + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ); + } + + Widget _buildTimelineItem(TimelineEvent event, int index, bool isLast) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Timeline indicator + Column( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: event.isCompleted + ? event.color + : event.color.withOpacity(0.2), + border: Border.all( + color: event.color, + width: event.isCompleted ? 0 : 2, + ), + ), + child: Icon( + event.icon, + size: 20, + color: event.isCompleted + ? Colors.white + : event.color, + ), + ), + if (!isLast) + Container( + width: 2, + height: 60, + color: event.isCompleted + ? event.color.withOpacity(0.3) + : AppTheme.borderLight, + ), + ], + ), + const SizedBox(width: 16), + + // Event content + Expanded( + child: Container( + margin: const EdgeInsets.only(bottom: 20), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _getEventBackgroundColor(event), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: event.color.withOpacity(0.2), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + event.title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: event.isCompleted + ? AppTheme.textPrimary + : AppTheme.textSecondary, + ), + ), + ), + if (event.isSuccess) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: AppTheme.successColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: const Text( + 'Terminé', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: AppTheme.successColor, + ), + ), + ), + if (event.isWarning) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: AppTheme.errorColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: const Text( + 'En retard', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: AppTheme.errorColor, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + event.description, + style: TextStyle( + fontSize: 14, + color: event.isCompleted + ? AppTheme.textSecondary + : AppTheme.textHint, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.access_time, + size: 16, + color: AppTheme.textHint, + ), + const SizedBox(width: 4), + Text( + _formatDateTime(event.date), + style: const TextStyle( + fontSize: 12, + color: AppTheme.textHint, + ), + ), + if (event.isFuture) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: AppTheme.infoColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: const Text( + 'À venir', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w600, + color: AppTheme.infoColor, + ), + ), + ), + ], + ], + ), + ], + ), + ), + ), + ], + ); + } + + Color _getEventBackgroundColor(TimelineEvent event) { + if (event.isSuccess) { + return AppTheme.successColor.withOpacity(0.05); + } + if (event.isWarning) { + return AppTheme.errorColor.withOpacity(0.05); + } + if (event.isFuture) { + return AppTheme.infoColor.withOpacity(0.05); + } + return Colors.white; + } + + String _formatDate(DateTime date) { + return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}'; + } + + String _formatDateTime(DateTime date) { + return '${_formatDate(date)} à ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}'; + } +} + +/// Modèle pour les événements de la timeline +class TimelineEvent { + final String title; + final String description; + final DateTime date; + final IconData icon; + final Color color; + final bool isCompleted; + final bool isSuccess; + final bool isWarning; + final bool isFuture; + + TimelineEvent({ + required this.title, + required this.description, + required this.date, + required this.icon, + required this.color, + this.isCompleted = false, + this.isSuccess = false, + this.isWarning = false, + this.isFuture = false, + }); +} diff --git a/unionflow-mobile-apps/lib/features/cotisations/presentation/widgets/payment_form_widget.dart b/unionflow-mobile-apps/lib/features/cotisations/presentation/widgets/payment_form_widget.dart new file mode 100644 index 0000000..eb840f3 --- /dev/null +++ b/unionflow-mobile-apps/lib/features/cotisations/presentation/widgets/payment_form_widget.dart @@ -0,0 +1,457 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import '../../../../core/models/cotisation_model.dart'; +import '../../../../shared/theme/app_theme.dart'; +import '../../../../shared/widgets/buttons/buttons.dart'; +import '../../../../shared/widgets/buttons/primary_button.dart'; +import 'payment_method_selector.dart'; + +/// Widget de formulaire de paiement +class PaymentFormWidget extends StatefulWidget { + final CotisationModel cotisation; + final Function(Map) onPaymentInitiated; + + const PaymentFormWidget({ + super.key, + required this.cotisation, + required this.onPaymentInitiated, + }); + + @override + State createState() => _PaymentFormWidgetState(); +} + +class _PaymentFormWidgetState extends State + with TickerProviderStateMixin { + final _formKey = GlobalKey(); + final _phoneController = TextEditingController(); + final _nameController = TextEditingController(); + final _emailController = TextEditingController(); + final _amountController = TextEditingController(); + + late final AnimationController _animationController; + late final Animation _slideAnimation; + + String? _selectedPaymentMethod; + bool _isProcessing = false; + bool _acceptTerms = false; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + ); + _slideAnimation = Tween( + begin: const Offset(0, 0.3), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeOutCubic, + )); + + // Initialiser le montant avec le montant restant à payer + final remainingAmount = widget.cotisation.montantDu - widget.cotisation.montantPaye; + _amountController.text = remainingAmount.toStringAsFixed(0); + + _animationController.forward(); + } + + @override + void dispose() { + _phoneController.dispose(); + _nameController.dispose(); + _emailController.dispose(); + _amountController.dispose(); + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SlideTransition( + position: _slideAnimation, + child: Padding( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Sélection de la méthode de paiement + PaymentMethodSelector( + selectedMethod: _selectedPaymentMethod, + montant: double.tryParse(_amountController.text) ?? 0, + onMethodSelected: (method) { + setState(() { + _selectedPaymentMethod = method; + }); + }, + ), + + if (_selectedPaymentMethod != null) ...[ + const SizedBox(height: 24), + _buildPaymentForm(), + ], + ], + ), + ), + ), + ); + } + + Widget _buildPaymentForm() { + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Informations de paiement', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppTheme.textPrimary, + ), + ), + const SizedBox(height: 16), + + // Montant à payer + _buildAmountField(), + const SizedBox(height: 16), + + // Numéro de téléphone (pour Mobile Money) + if (_isMobileMoneyMethod()) ...[ + _buildPhoneField(), + const SizedBox(height: 16), + ], + + // Nom du payeur + _buildNameField(), + const SizedBox(height: 16), + + // Email (optionnel) + _buildEmailField(), + const SizedBox(height: 20), + + // Conditions d'utilisation + _buildTermsCheckbox(), + const SizedBox(height: 24), + + // Bouton de paiement + _buildPaymentButton(), + ], + ), + ); + } + + Widget _buildAmountField() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Montant à payer', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppTheme.textSecondary, + ), + ), + const SizedBox(height: 8), + TextFormField( + controller: _amountController, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(8), + ], + decoration: InputDecoration( + hintText: 'Entrez le montant', + suffixText: 'XOF', + prefixIcon: const Icon(Icons.attach_money), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppTheme.primaryColor, width: 2), + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer un montant'; + } + final amount = double.tryParse(value); + if (amount == null || amount <= 0) { + return 'Montant invalide'; + } + final remaining = widget.cotisation.montantDu - widget.cotisation.montantPaye; + if (amount > remaining) { + return 'Montant supérieur au solde restant (${remaining.toStringAsFixed(0)} XOF)'; + } + return null; + }, + onChanged: (value) { + setState(() {}); // Recalculer les frais + }, + ), + ], + ); + } + + Widget _buildPhoneField() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Numéro ${_getPaymentMethodName()}', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppTheme.textSecondary, + ), + ), + const SizedBox(height: 8), + TextFormField( + controller: _phoneController, + keyboardType: TextInputType.phone, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(10), + ], + decoration: InputDecoration( + hintText: 'Ex: 0123456789', + prefixIcon: const Icon(Icons.phone), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppTheme.primaryColor, width: 2), + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer votre numéro de téléphone'; + } + if (value.length < 8) { + return 'Numéro de téléphone invalide'; + } + if (!_validatePhoneForMethod(value)) { + return 'Ce numéro n\'est pas compatible avec ${_getPaymentMethodName()}'; + } + return null; + }, + ), + ], + ); + } + + Widget _buildNameField() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Nom du payeur', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppTheme.textSecondary, + ), + ), + const SizedBox(height: 8), + TextFormField( + controller: _nameController, + textCapitalization: TextCapitalization.words, + decoration: InputDecoration( + hintText: 'Entrez votre nom complet', + prefixIcon: const Icon(Icons.person), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppTheme.primaryColor, width: 2), + ), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Veuillez entrer votre nom'; + } + if (value.trim().length < 2) { + return 'Nom trop court'; + } + return null; + }, + ), + ], + ); + } + + Widget _buildEmailField() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Email (optionnel)', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppTheme.textSecondary, + ), + ), + const SizedBox(height: 8), + TextFormField( + controller: _emailController, + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration( + hintText: 'exemple@email.com', + prefixIcon: const Icon(Icons.email), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppTheme.primaryColor, width: 2), + ), + ), + validator: (value) { + if (value != null && value.isNotEmpty) { + if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { + return 'Email invalide'; + } + } + return null; + }, + ), + ], + ); + } + + Widget _buildTermsCheckbox() { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Checkbox( + value: _acceptTerms, + onChanged: (value) { + setState(() { + _acceptTerms = value ?? false; + }); + }, + activeColor: AppTheme.primaryColor, + ), + Expanded( + child: GestureDetector( + onTap: () { + setState(() { + _acceptTerms = !_acceptTerms; + }); + }, + child: const Text( + 'J\'accepte les conditions d\'utilisation et la politique de confidentialité', + style: TextStyle( + fontSize: 14, + color: AppTheme.textSecondary, + ), + ), + ), + ), + ], + ); + } + + Widget _buildPaymentButton() { + return SizedBox( + width: double.infinity, + child: PrimaryButton( + text: _isProcessing + ? 'Traitement en cours...' + : 'Confirmer le paiement', + icon: _isProcessing ? null : Icons.payment, + onPressed: _canProceedPayment() ? _processPayment : null, + isLoading: _isProcessing, + ), + ); + } + + bool _canProceedPayment() { + return _selectedPaymentMethod != null && + _acceptTerms && + !_isProcessing && + _amountController.text.isNotEmpty; + } + + bool _isMobileMoneyMethod() { + return _selectedPaymentMethod == 'ORANGE_MONEY' || + _selectedPaymentMethod == 'WAVE' || + _selectedPaymentMethod == 'MOOV_MONEY'; + } + + String _getPaymentMethodName() { + switch (_selectedPaymentMethod) { + case 'ORANGE_MONEY': + return 'Orange Money'; + case 'WAVE': + return 'Wave'; + case 'MOOV_MONEY': + return 'Moov Money'; + case 'CARTE_BANCAIRE': + return 'Carte bancaire'; + default: + return 'Paiement'; + } + } + + bool _validatePhoneForMethod(String phone) { + final cleanNumber = phone.replaceAll(RegExp(r'[^\d]'), ''); + + switch (_selectedPaymentMethod) { + case 'ORANGE_MONEY': + // Orange: 07, 08, 09 + return RegExp(r'^(225)?(0[789])\d{8}$').hasMatch(cleanNumber); + case 'WAVE': + // Wave accepte tous les numéros ivoiriens + return RegExp(r'^(225)?(0[1-9])\d{8}$').hasMatch(cleanNumber); + case 'MOOV_MONEY': + // Moov: 01, 02, 03 + return RegExp(r'^(225)?(0[123])\d{8}$').hasMatch(cleanNumber); + default: + return cleanNumber.length >= 8; + } + } + + void _processPayment() { + if (!_formKey.currentState!.validate()) { + return; + } + + setState(() { + _isProcessing = true; + }); + + // Préparer les données de paiement + final paymentData = { + 'montant': double.parse(_amountController.text), + 'methodePaiement': _selectedPaymentMethod!, + 'numeroTelephone': _phoneController.text, + 'nomPayeur': _nameController.text.trim(), + 'emailPayeur': _emailController.text.trim().isEmpty + ? null + : _emailController.text.trim(), + }; + + // Déclencher le paiement + widget.onPaymentInitiated(paymentData); + + // Simuler un délai de traitement + Future.delayed(const Duration(seconds: 2), () { + if (mounted) { + setState(() { + _isProcessing = false; + }); + } + }); + } +} diff --git a/unionflow-mobile-apps/lib/features/cotisations/presentation/widgets/payment_method_selector.dart b/unionflow-mobile-apps/lib/features/cotisations/presentation/widgets/payment_method_selector.dart new file mode 100644 index 0000000..4f56555 --- /dev/null +++ b/unionflow-mobile-apps/lib/features/cotisations/presentation/widgets/payment_method_selector.dart @@ -0,0 +1,443 @@ +import 'package:flutter/material.dart'; +import '../../../../core/services/payment_service.dart'; +import '../../../../shared/theme/app_theme.dart'; + +/// Widget de sélection des méthodes de paiement +class PaymentMethodSelector extends StatefulWidget { + final String? selectedMethod; + final Function(String) onMethodSelected; + final double montant; + + const PaymentMethodSelector({ + super.key, + this.selectedMethod, + required this.onMethodSelected, + required this.montant, + }); + + @override + State createState() => _PaymentMethodSelectorState(); +} + +class _PaymentMethodSelectorState extends State + with TickerProviderStateMixin { + late final AnimationController _animationController; + late final Animation _scaleAnimation; + + List _paymentMethods = []; + String? _selectedMethod; + + @override + void initState() { + super.initState(); + _selectedMethod = widget.selectedMethod; + _animationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _scaleAnimation = Tween(begin: 0.8, end: 1.0).animate( + CurvedAnimation(parent: _animationController, curve: Curves.elasticOut), + ); + + _loadPaymentMethods(); + _animationController.forward(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + void _loadPaymentMethods() { + // En production, ceci viendrait du PaymentService + _paymentMethods = [ + PaymentMethod( + id: 'ORANGE_MONEY', + nom: 'Orange Money', + icone: '📱', + couleur: '#FF6600', + description: 'Paiement via Orange Money', + fraisMinimum: 0, + fraisMaximum: 1000, + montantMinimum: 100, + montantMaximum: 1000000, + ), + PaymentMethod( + id: 'WAVE', + nom: 'Wave', + icone: '🌊', + couleur: '#00D4FF', + description: 'Paiement via Wave', + fraisMinimum: 0, + fraisMaximum: 500, + montantMinimum: 100, + montantMaximum: 2000000, + ), + PaymentMethod( + id: 'MOOV_MONEY', + nom: 'Moov Money', + icone: '💙', + couleur: '#0066CC', + description: 'Paiement via Moov Money', + fraisMinimum: 0, + fraisMaximum: 800, + montantMinimum: 100, + montantMaximum: 1500000, + ), + PaymentMethod( + id: 'CARTE_BANCAIRE', + nom: 'Carte bancaire', + icone: '💳', + couleur: '#4CAF50', + description: 'Paiement par carte bancaire', + fraisMinimum: 100, + fraisMaximum: 2000, + montantMinimum: 500, + montantMaximum: 5000000, + ), + ]; + + // Filtrer les méthodes disponibles selon le montant + _paymentMethods = _paymentMethods.where((method) { + return widget.montant >= method.montantMinimum && + widget.montant <= method.montantMaximum; + }).toList(); + + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return ScaleTransition( + scale: _scaleAnimation, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Choisissez votre méthode de paiement', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppTheme.textPrimary, + ), + ), + const SizedBox(height: 16), + + if (_paymentMethods.isEmpty) + _buildNoMethodsAvailable() + else + _buildMethodsList(), + + if (_selectedMethod != null) ...[ + const SizedBox(height: 20), + _buildSelectedMethodInfo(), + ], + ], + ), + ); + } + + Widget _buildNoMethodsAvailable() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppTheme.warningColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppTheme.warningColor.withOpacity(0.3), + ), + ), + child: Column( + children: [ + Icon( + Icons.warning_amber, + size: 48, + color: AppTheme.warningColor, + ), + const SizedBox(height: 12), + const Text( + 'Aucune méthode de paiement disponible', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppTheme.textPrimary, + ), + ), + const SizedBox(height: 8), + Text( + 'Le montant de ${widget.montant.toStringAsFixed(0)} XOF ne correspond aux limites d\'aucune méthode de paiement.', + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 14, + color: AppTheme.textSecondary, + ), + ), + ], + ), + ); + } + + Widget _buildMethodsList() { + return Column( + children: _paymentMethods.map((method) { + final isSelected = _selectedMethod == method.id; + final fees = _calculateFees(method); + + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + margin: const EdgeInsets.only(bottom: 12), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _selectMethod(method), + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isSelected + ? _getMethodColor(method.couleur).withOpacity(0.1) + : Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected + ? _getMethodColor(method.couleur) + : AppTheme.borderLight, + width: isSelected ? 2 : 1, + ), + boxShadow: isSelected ? [ + BoxShadow( + color: _getMethodColor(method.couleur).withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ] : null, + ), + child: Row( + children: [ + // Icône de la méthode + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: _getMethodColor(method.couleur).withOpacity(0.1), + borderRadius: BorderRadius.circular(25), + ), + child: Center( + child: Text( + method.icone, + style: const TextStyle(fontSize: 24), + ), + ), + ), + const SizedBox(width: 16), + + // Informations de la méthode + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + method.nom, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isSelected + ? _getMethodColor(method.couleur) + : AppTheme.textPrimary, + ), + ), + const SizedBox(height: 4), + Text( + method.description, + style: const TextStyle( + fontSize: 12, + color: AppTheme.textSecondary, + ), + ), + if (fees > 0) ...[ + const SizedBox(height: 4), + Text( + 'Frais: ${fees.toStringAsFixed(0)} XOF', + style: TextStyle( + fontSize: 12, + color: AppTheme.warningColor, + fontWeight: FontWeight.w500, + ), + ), + ], + ], + ), + ), + + // Indicateur de sélection + AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 24, + height: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isSelected + ? _getMethodColor(method.couleur) + : Colors.transparent, + border: Border.all( + color: isSelected + ? _getMethodColor(method.couleur) + : AppTheme.borderLight, + width: 2, + ), + ), + child: isSelected + ? const Icon( + Icons.check, + size: 16, + color: Colors.white, + ) + : null, + ), + ], + ), + ), + ), + ), + ); + }).toList(), + ); + } + + Widget _buildSelectedMethodInfo() { + final method = _paymentMethods.firstWhere((m) => m.id == _selectedMethod); + final fees = _calculateFees(method); + final total = widget.montant + fees; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _getMethodColor(method.couleur).withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: _getMethodColor(method.couleur).withOpacity(0.2), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + method.icone, + style: const TextStyle(fontSize: 20), + ), + const SizedBox(width: 8), + Text( + 'Récapitulatif - ${method.nom}', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: _getMethodColor(method.couleur), + ), + ), + ], + ), + const SizedBox(height: 12), + + _buildSummaryRow('Montant', '${widget.montant.toStringAsFixed(0)} XOF'), + if (fees > 0) + _buildSummaryRow('Frais', '${fees.toStringAsFixed(0)} XOF'), + const Divider(), + _buildSummaryRow( + 'Total à payer', + '${total.toStringAsFixed(0)} XOF', + isTotal: true, + ), + ], + ), + ); + } + + Widget _buildSummaryRow(String label, String value, {bool isTotal = false}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: TextStyle( + fontSize: isTotal ? 16 : 14, + fontWeight: isTotal ? FontWeight.bold : FontWeight.normal, + color: AppTheme.textSecondary, + ), + ), + Text( + value, + style: TextStyle( + fontSize: isTotal ? 16 : 14, + fontWeight: FontWeight.bold, + color: isTotal ? AppTheme.textPrimary : AppTheme.textSecondary, + ), + ), + ], + ), + ); + } + + void _selectMethod(PaymentMethod method) { + setState(() { + _selectedMethod = method.id; + }); + widget.onMethodSelected(method.id); + + // Animation de feedback + _animationController.reset(); + _animationController.forward(); + } + + double _calculateFees(PaymentMethod method) { + // Simulation du calcul des frais + switch (method.id) { + case 'ORANGE_MONEY': + return _calculateOrangeMoneyFees(widget.montant); + case 'WAVE': + return _calculateWaveFees(widget.montant); + case 'MOOV_MONEY': + return _calculateMoovMoneyFees(widget.montant); + case 'CARTE_BANCAIRE': + return _calculateCardFees(widget.montant); + default: + return 0.0; + } + } + + double _calculateOrangeMoneyFees(double montant) { + if (montant <= 1000) return 0; + if (montant <= 5000) return 25; + if (montant <= 10000) return 50; + if (montant <= 25000) return 100; + if (montant <= 50000) return 200; + return montant * 0.005; // 0.5% + } + + double _calculateWaveFees(double montant) { + if (montant <= 2000) return 0; + if (montant <= 10000) return 25; + if (montant <= 50000) return 100; + return montant * 0.003; // 0.3% + } + + double _calculateMoovMoneyFees(double montant) { + if (montant <= 1000) return 0; + if (montant <= 5000) return 30; + if (montant <= 15000) return 75; + if (montant <= 50000) return 150; + return montant * 0.004; // 0.4% + } + + double _calculateCardFees(double montant) { + return 100 + (montant * 0.025); // 100 XOF + 2.5% + } + + Color _getMethodColor(String colorHex) { + return Color(int.parse(colorHex.replaceFirst('#', '0xFF'))); + } +} diff --git a/unionflow-mobile-apps/lib/features/dashboard/presentation/pages/dashboard_page.dart b/unionflow-mobile-apps/lib/features/dashboard/presentation/pages/dashboard_page.dart index c7932d9..e979848 100644 --- a/unionflow-mobile-apps/lib/features/dashboard/presentation/pages/dashboard_page.dart +++ b/unionflow-mobile-apps/lib/features/dashboard/presentation/pages/dashboard_page.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; import '../../../../shared/theme/app_theme.dart'; +import '../../../../core/animations/page_transitions.dart'; +import '../../../demo/presentation/pages/animations_demo_page.dart'; +import '../../../debug/debug_api_test_page.dart'; // Imports des nouveaux widgets refactorisés import '../widgets/welcome/welcome_section_widget.dart'; @@ -31,12 +34,30 @@ class DashboardPage extends StatelessWidget { backgroundColor: AppTheme.primaryColor, elevation: 0, actions: [ + IconButton( + icon: const Icon(Icons.animation), + onPressed: () { + Navigator.of(context).push( + PageTransitions.morphWithBlur(const AnimationsDemoPage()), + ); + }, + tooltip: 'Démonstration des animations', + ), IconButton( icon: const Icon(Icons.notifications_outlined), onPressed: () { // TODO: Implémenter la navigation vers les notifications }, ), + IconButton( + icon: const Icon(Icons.bug_report), + onPressed: () { + Navigator.of(context).push( + PageTransitions.slideFromRight(const DebugApiTestPage()), + ); + }, + tooltip: 'Debug API', + ), IconButton( icon: const Icon(Icons.settings_outlined), onPressed: () { diff --git a/unionflow-mobile-apps/lib/features/dashboard/presentation/widgets/activities/recent_activities_widget.dart b/unionflow-mobile-apps/lib/features/dashboard/presentation/widgets/activities/recent_activities_widget.dart index 847046f..b2e84e5 100644 --- a/unionflow-mobile-apps/lib/features/dashboard/presentation/widgets/activities/recent_activities_widget.dart +++ b/unionflow-mobile-apps/lib/features/dashboard/presentation/widgets/activities/recent_activities_widget.dart @@ -86,17 +86,17 @@ class RecentActivitiesWidget extends StatelessWidget { ), ], ), - child: Column( + child: const Column( children: [ ActivityItemWidget( title: 'Paiement Mobile Money reçu', description: 'Kouassi Yao - 25,000 FCFA via Orange Money', icon: Icons.phone_android, - color: const Color(0xFFFF9800), + color: Color(0xFFFF9800), time: 'Il y a 3 min', isNew: true, ), - const Divider(height: 1), + Divider(height: 1), ActivityItemWidget( title: 'Nouveau membre validé', description: 'Adjoua Marie inscrite depuis Abidjan', @@ -105,7 +105,7 @@ class RecentActivitiesWidget extends StatelessWidget { time: 'Il y a 15 min', isNew: true, ), - const Divider(height: 1), + Divider(height: 1), ActivityItemWidget( title: 'Relance automatique envoyée', description: '12 SMS de rappel cotisations expédiés', @@ -113,15 +113,15 @@ class RecentActivitiesWidget extends StatelessWidget { color: AppTheme.infoColor, time: 'Il y a 1h', ), - const Divider(height: 1), + Divider(height: 1), ActivityItemWidget( title: 'Rapport OHADA généré', description: 'Bilan financier T4 2024 exporté', icon: Icons.description, - color: const Color(0xFF795548), + color: Color(0xFF795548), time: 'Il y a 2h', ), - const Divider(height: 1), + Divider(height: 1), ActivityItemWidget( title: 'Événement: Forte participation', description: 'AG Extraordinaire - 89% de présence', @@ -129,7 +129,7 @@ class RecentActivitiesWidget extends StatelessWidget { color: AppTheme.successColor, time: 'Il y a 3h', ), - const Divider(height: 1), + Divider(height: 1), ActivityItemWidget( title: 'Alerte: Cotisations en retard', description: '23 membres avec +30 jours de retard', @@ -137,7 +137,7 @@ class RecentActivitiesWidget extends StatelessWidget { color: AppTheme.warningColor, time: 'Il y a 4h', ), - const Divider(height: 1), + Divider(height: 1), ActivityItemWidget( title: 'Synchronisation réussie', description: 'Données sauvegardées sur le cloud', @@ -145,12 +145,12 @@ class RecentActivitiesWidget extends StatelessWidget { color: AppTheme.successColor, time: 'Il y a 6h', ), - const Divider(height: 1), + Divider(height: 1), ActivityItemWidget( title: 'Message diffusé', description: 'Info COVID-19 envoyée à 1,247 membres', icon: Icons.campaign, - color: const Color(0xFF9C27B0), + color: Color(0xFF9C27B0), time: 'Hier 18:30', ), ], diff --git a/unionflow-mobile-apps/lib/features/dashboard/presentation/widgets/charts/charts_analytics_widget.dart b/unionflow-mobile-apps/lib/features/dashboard/presentation/widgets/charts/charts_analytics_widget.dart index f106a48..367e712 100644 --- a/unionflow-mobile-apps/lib/features/dashboard/presentation/widgets/charts/charts_analytics_widget.dart +++ b/unionflow-mobile-apps/lib/features/dashboard/presentation/widgets/charts/charts_analytics_widget.dart @@ -86,11 +86,11 @@ class ChartsAnalyticsWidget extends StatelessWidget { ), ), const SizedBox(width: 8), - Expanded( + const Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( 'Évolution des membres actifs', style: TextStyle( fontSize: 16, @@ -98,8 +98,8 @@ class ChartsAnalyticsWidget extends StatelessWidget { color: AppTheme.textPrimary, ), ), - const SizedBox(height: 2), - const Text( + SizedBox(height: 2), + Text( 'Croissance sur 5 mois • +24.7% (+247 membres)', style: TextStyle( fontSize: 11, diff --git a/unionflow-mobile-apps/lib/features/debug/debug_api_test_page.dart b/unionflow-mobile-apps/lib/features/debug/debug_api_test_page.dart new file mode 100644 index 0000000..bbdf1ac --- /dev/null +++ b/unionflow-mobile-apps/lib/features/debug/debug_api_test_page.dart @@ -0,0 +1,240 @@ +import 'package:flutter/material.dart'; +import '../../core/services/api_service.dart'; +import '../../core/di/injection.dart'; +import '../../shared/theme/app_theme.dart'; + +/// Page de test pour diagnostiquer les problèmes d'API +class DebugApiTestPage extends StatefulWidget { + const DebugApiTestPage({super.key}); + + @override + State createState() => _DebugApiTestPageState(); +} + +class _DebugApiTestPageState extends State { + final ApiService _apiService = getIt(); + String _result = 'Aucun test effectué'; + bool _isLoading = false; + + Future _testEvenementsAPI() async { + setState(() { + _isLoading = true; + _result = 'Test en cours...'; + }); + + try { + print('🧪 Début du test API événements'); + final evenements = await _apiService.getEvenementsAVenir(); + + setState(() { + _result = '''✅ SUCCÈS ! +Nombre d'événements récupérés: ${evenements.length} + +Détails des événements: +${evenements.map((e) => '• ${e.titre} (${e.typeEvenement})').join('\n')} +'''; + _isLoading = false; + }); + + print('🎉 Test réussi: ${evenements.length} événements'); + } catch (e) { + setState(() { + _result = '''❌ ERREUR ! +Type d'erreur: ${e.runtimeType} +Message: $e + +Vérifiez: +1. Le serveur backend est-il démarré ? +2. L'URL est-elle correcte ? +3. Le réseau est-il accessible ? +'''; + _isLoading = false; + }); + + print('💥 Test échoué: $e'); + } + } + + Future _testConnectivity() async { + setState(() { + _isLoading = true; + _result = 'Test de connectivité...'; + }); + + try { + // Test simple de connectivité via l'API service + final evenements = await _apiService.getEvenementsAVenir(size: 1); + + setState(() { + _result = '''✅ CONNECTIVITÉ OK ! +Connexion au serveur réussie. +Nombre d'événements de test: ${evenements.length} +'''; + _isLoading = false; + }); + } catch (e) { + setState(() { + _result = '''❌ PROBLÈME DE CONNECTIVITÉ ! +Erreur: $e + +Le serveur backend n'est pas accessible. +Vérifiez que le serveur Quarkus est démarré sur 192.168.1.145:8080 +'''; + _isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Debug API Test'), + backgroundColor: AppTheme.primaryColor, + foregroundColor: Colors.white, + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Tests de Diagnostic', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + + ElevatedButton.icon( + onPressed: _isLoading ? null : _testConnectivity, + icon: const Icon(Icons.network_check), + label: const Text('Test Connectivité'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.primaryColor, + foregroundColor: Colors.white, + ), + ), + + const SizedBox(height: 8), + + ElevatedButton.icon( + onPressed: _isLoading ? null : _testEvenementsAPI, + icon: const Icon(Icons.event), + label: const Text('Test API Événements'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.successColor, + foregroundColor: Colors.white, + ), + ), + ], + ), + ), + ), + + const SizedBox(height: 16), + + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Text( + 'Résultats', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + if (_isLoading) + const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ], + ), + + const SizedBox(height: 16), + + Expanded( + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey[300]!), + ), + child: SingleChildScrollView( + child: Text( + _result, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 12, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + + const SizedBox(height: 16), + + Card( + color: Colors.blue[50], + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.info, color: Colors.blue[700]), + const SizedBox(width: 8), + Text( + 'Informations de Configuration', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.blue[700], + ), + ), + ], + ), + const SizedBox(height: 12), + const Text( + 'URL Backend: http://192.168.1.145:8080\n' + 'Endpoint: /api/evenements/a-venir-public\n' + 'Méthode: GET', + style: TextStyle( + fontFamily: 'monospace', + fontSize: 12, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/unionflow-mobile-apps/lib/features/demo/presentation/pages/animations_demo_page.dart b/unionflow-mobile-apps/lib/features/demo/presentation/pages/animations_demo_page.dart new file mode 100644 index 0000000..69fefd6 --- /dev/null +++ b/unionflow-mobile-apps/lib/features/demo/presentation/pages/animations_demo_page.dart @@ -0,0 +1,464 @@ +import 'package:flutter/material.dart'; +import '../../../../core/animations/animated_button.dart'; +import '../../../../core/animations/animated_notifications.dart'; +import '../../../../core/animations/page_transitions.dart'; +import '../../../../shared/theme/app_theme.dart'; + +/// Page de démonstration des animations +class AnimationsDemoPage extends StatefulWidget { + const AnimationsDemoPage({super.key}); + + @override + State createState() => _AnimationsDemoPageState(); +} + +class _AnimationsDemoPageState extends State + with TickerProviderStateMixin { + late AnimationController _floatingController; + late AnimationController _pulseController; + late Animation _floatingAnimation; + late Animation _pulseAnimation; + + @override + void initState() { + super.initState(); + + _floatingController = AnimationController( + duration: const Duration(seconds: 2), + vsync: this, + )..repeat(reverse: true); + + _pulseController = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + )..repeat(); + + _floatingAnimation = Tween( + begin: -10.0, + end: 10.0, + ).animate(CurvedAnimation( + parent: _floatingController, + curve: Curves.easeInOut, + )); + + _pulseAnimation = Tween( + begin: 1.0, + end: 1.2, + ).animate(CurvedAnimation( + parent: _pulseController, + curve: Curves.elasticOut, + )); + } + + @override + void dispose() { + _floatingController.dispose(); + _pulseController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Démonstration des Animations'), + backgroundColor: AppTheme.primaryColor, + foregroundColor: Colors.white, + elevation: 0, + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Section Boutons Animés + _buildSection( + 'Boutons Animés', + [ + const SizedBox(height: 16), + AnimatedButton( + text: 'Bouton Principal', + onPressed: () => _showNotification(NotificationType.success), + style: AnimatedButtonStyle.primary, + ), + const SizedBox(height: 12), + AnimatedButton( + text: 'Bouton Secondaire', + onPressed: () => _showNotification(NotificationType.info), + style: AnimatedButtonStyle.secondary, + ), + const SizedBox(height: 12), + AnimatedButton( + text: 'Bouton de Succès', + onPressed: () => _showNotification(NotificationType.success), + style: AnimatedButtonStyle.success, + ), + const SizedBox(height: 12), + AnimatedButton( + text: 'Bouton d\'Avertissement', + onPressed: () => _showNotification(NotificationType.warning), + style: AnimatedButtonStyle.warning, + ), + const SizedBox(height: 12), + AnimatedButton( + text: 'Bouton d\'Erreur', + onPressed: () => _showNotification(NotificationType.error), + style: AnimatedButtonStyle.error, + ), + const SizedBox(height: 12), + AnimatedButton( + text: 'Bouton Contour', + onPressed: () => _showNotification(NotificationType.info), + style: AnimatedButtonStyle.outline, + ), + ], + ), + + const SizedBox(height: 32), + + // Section Notifications + _buildSection( + 'Notifications Animées', + [ + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: () => _showNotification(NotificationType.success), + icon: const Icon(Icons.check_circle), + label: const Text('Succès'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.successColor, + foregroundColor: Colors.white, + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton.icon( + onPressed: () => _showNotification(NotificationType.error), + icon: const Icon(Icons.error), + label: const Text('Erreur'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.errorColor, + foregroundColor: Colors.white, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: () => _showNotification(NotificationType.warning), + icon: const Icon(Icons.warning), + label: const Text('Avertissement'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.warningColor, + foregroundColor: Colors.white, + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton.icon( + onPressed: () => _showNotification(NotificationType.info), + icon: const Icon(Icons.info), + label: const Text('Information'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.primaryColor, + foregroundColor: Colors.white, + ), + ), + ), + ], + ), + ], + ), + + const SizedBox(height: 32), + + // Section Transitions de Page + _buildSection( + 'Transitions de Page', + [ + const SizedBox(height: 16), + _buildTransitionButton( + 'Glissement depuis la droite', + () => _navigateWithTransition(PageTransitions.slideFromRight), + ), + const SizedBox(height: 8), + _buildTransitionButton( + 'Glissement depuis le bas', + () => _navigateWithTransition(PageTransitions.slideFromBottom), + ), + const SizedBox(height: 8), + _buildTransitionButton( + 'Fondu', + () => _navigateWithTransition(PageTransitions.fadeIn), + ), + const SizedBox(height: 8), + _buildTransitionButton( + 'Échelle avec fondu', + () => _navigateWithTransition(PageTransitions.scaleWithFade), + ), + const SizedBox(height: 8), + _buildTransitionButton( + 'Rebond', + () => _navigateWithTransition(PageTransitions.bounceIn), + ), + const SizedBox(height: 8), + _buildTransitionButton( + 'Parallaxe', + () => _navigateWithTransition(PageTransitions.slideWithParallax), + ), + const SizedBox(height: 8), + _buildTransitionButton( + 'Morphing avec Blur', + () => _navigateWithTransition(PageTransitions.morphWithBlur), + ), + const SizedBox(height: 8), + _buildTransitionButton( + 'Rotation 3D', + () => _navigateWithTransition(PageTransitions.rotate3D), + ), + ], + ), + + const SizedBox(height: 32), + + // Section Animations Continues + _buildSection( + 'Animations Continues', + [ + const SizedBox(height: 16), + Center( + child: Column( + children: [ + AnimatedBuilder( + animation: _floatingAnimation, + builder: (context, child) { + return Transform.translate( + offset: Offset(0, _floatingAnimation.value), + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.primaryColor, + AppTheme.primaryColor.withOpacity(0.7), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(40), + boxShadow: [ + BoxShadow( + color: AppTheme.primaryColor.withOpacity(0.3), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: const Icon( + Icons.star, + color: Colors.white, + size: 40, + ), + ), + ); + }, + ), + const SizedBox(height: 16), + const Text( + 'Animation Flottante', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 32), + AnimatedBuilder( + animation: _pulseAnimation, + builder: (context, child) { + return Transform.scale( + scale: _pulseAnimation.value, + child: Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: AppTheme.successColor, + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: AppTheme.successColor.withOpacity(0.4), + blurRadius: 20, + spreadRadius: 5, + ), + ], + ), + child: const Icon( + Icons.favorite, + color: Colors.white, + size: 30, + ), + ), + ); + }, + ), + const SizedBox(height: 16), + const Text( + 'Animation Pulsante', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + + const SizedBox(height: 32), + ], + ), + ), + ); + } + + Widget _buildSection(String title, List children) { + return Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + ), + const Divider(height: 24), + ...children, + ], + ), + ), + ); + } + + Widget _buildTransitionButton(String text, VoidCallback onPressed) { + return SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: onPressed, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + side: const BorderSide(color: AppTheme.primaryColor), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + text, + style: const TextStyle( + color: AppTheme.primaryColor, + fontWeight: FontWeight.w600, + ), + ), + ), + ); + } + + void _showNotification(NotificationType type) { + switch (type) { + case NotificationType.success: + AnimatedNotifications.showSuccess( + context, + 'Opération réussie avec succès !', + ); + break; + case NotificationType.error: + AnimatedNotifications.showError( + context, + 'Une erreur s\'est produite lors de l\'opération.', + ); + break; + case NotificationType.warning: + AnimatedNotifications.showWarning( + context, + 'Attention : cette action nécessite une confirmation.', + ); + break; + case NotificationType.info: + AnimatedNotifications.showInfo( + context, + 'Information : les données ont été mises à jour.', + ); + break; + } + } + + void _navigateWithTransition(PageRouteBuilder Function(Widget) transitionBuilder) { + Navigator.of(context).push( + transitionBuilder(const _DemoDestinationPage()), + ); + } +} + +/// Page de destination pour les démonstrations de transition +class _DemoDestinationPage extends StatelessWidget { + const _DemoDestinationPage(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Page de Destination'), + backgroundColor: AppTheme.secondaryColor, + foregroundColor: Colors.white, + ), + body: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.check_circle, + size: 80, + color: AppTheme.successColor, + ), + SizedBox(height: 24), + Text( + 'Transition réussie !', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + ), + SizedBox(height: 16), + Text( + 'Vous pouvez revenir en arrière\npour tester d\'autres transitions.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Colors.grey, + ), + ), + ], + ), + ), + ); + } +} diff --git a/unionflow-mobile-apps/lib/features/evenements/presentation/pages/evenements_page.dart b/unionflow-mobile-apps/lib/features/evenements/presentation/pages/evenements_page.dart index c75d04b..ed26a5f 100644 --- a/unionflow-mobile-apps/lib/features/evenements/presentation/pages/evenements_page.dart +++ b/unionflow-mobile-apps/lib/features/evenements/presentation/pages/evenements_page.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../core/di/injection.dart'; import '../../../../core/models/evenement_model.dart'; +import '../../../../core/animations/loading_animations.dart'; +import '../../../../core/animations/page_transitions.dart'; import '../../../../shared/theme/app_theme.dart'; import '../bloc/evenement_bloc.dart'; import '../bloc/evenement_event.dart'; @@ -9,6 +11,7 @@ import '../bloc/evenement_state.dart'; import '../widgets/evenement_card.dart'; import '../widgets/evenement_search_bar.dart'; import '../widgets/evenement_filter_chips.dart'; +import '../widgets/animated_evenement_list.dart'; import 'evenement_detail_page.dart'; import 'evenement_create_page.dart'; @@ -36,6 +39,9 @@ class _EvenementsPageContent extends StatefulWidget { class _EvenementsPageContentState extends State<_EvenementsPageContent> with TickerProviderStateMixin { late TabController _tabController; + late AnimationController _listAnimationController; + late AnimationController _tabAnimationController; + late Animation _tabFadeAnimation; final ScrollController _scrollController = ScrollController(); String _searchTerm = ''; TypeEvenement? _selectedType; @@ -44,18 +50,40 @@ class _EvenementsPageContentState extends State<_EvenementsPageContent> void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); + _listAnimationController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + + _tabAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + + _tabFadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _tabAnimationController, + curve: Curves.easeInOut, + ), + ); _scrollController.addListener(_onScroll); - + _tabController.addListener(() { if (_tabController.indexIsChanging) { _onTabChanged(_tabController.index); } }); + + // Démarrer les animations d'entrée + _listAnimationController.forward(); + _tabAnimationController.forward(); } @override void dispose() { _tabController.dispose(); + _listAnimationController.dispose(); + _tabAnimationController.dispose(); _scrollController.dispose(); super.dispose(); } @@ -192,8 +220,8 @@ class _EvenementsPageContentState extends State<_EvenementsPageContent> void _navigateToDetail(EvenementModel evenement) { Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => EvenementDetailPage(evenement: evenement), + PageTransitions.slideFromRight( + EvenementDetailPage(evenement: evenement), ), ); } @@ -214,28 +242,42 @@ class _EvenementsPageContentState extends State<_EvenementsPageContent> ], ), ), - body: TabBarView( - controller: _tabController, - children: [ - _buildEvenementsList(showSearch: false), - _buildEvenementsList(showSearch: false), - _buildEvenementsList(showSearch: true), - ], + body: FadeTransition( + opacity: _tabFadeAnimation, + child: TabBarView( + controller: _tabController, + children: [ + _buildEvenementsList(showSearch: false), + _buildEvenementsList(showSearch: false), + _buildEvenementsList(showSearch: true), + ], + ), ), - floatingActionButton: FloatingActionButton( - onPressed: () async { - final result = await Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const EvenementCreatePage(), + floatingActionButton: AnimatedBuilder( + animation: _listAnimationController, + builder: (context, child) { + return Transform.scale( + scale: 0.8 + (0.2 * _listAnimationController.value), + child: FloatingActionButton.extended( + onPressed: () async { + final result = await Navigator.of(context).push( + PageTransitions.slideFromBottom( + const EvenementCreatePage(), + ), + ); + + // Si un événement a été créé, recharger la liste + if (result == true && context.mounted) { + context.read().add(const LoadEvenementsAVenir()); + } + }, + icon: const Icon(Icons.add), + label: const Text('Nouvel événement'), + backgroundColor: Theme.of(context).primaryColor, + foregroundColor: Colors.white, ), ); - - // Si un événement a été créé, recharger la liste - if (result == true && context.mounted) { - context.read().add(const LoadEvenementsAVenir()); - } }, - child: const Icon(Icons.add), ), ); } @@ -278,7 +320,7 @@ class _EvenementsPageContentState extends State<_EvenementsPageContent> child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.error_outline, size: 64, color: Colors.red), + const Icon(Icons.error_outline, size: 64, color: Colors.red), const SizedBox(height: 16), Text(state.message, textAlign: TextAlign.center), const SizedBox(height: 16), @@ -333,45 +375,21 @@ class _EvenementsPageContentState extends State<_EvenementsPageContent> ); } - final evenements = state is EvenementLoaded + final evenements = state is EvenementLoaded ? state.evenements : state is EvenementLoadingMore ? state.evenements : state is EvenementError ? state.evenements ?? [] : []; - - if (evenements.isEmpty) { - return const Center( - child: Text('Aucun événement disponible'), - ); - } - - return RefreshIndicator( - onRefresh: () async => _onRefresh(), - child: ListView.builder( - controller: _scrollController, - padding: const EdgeInsets.all(16), - itemCount: evenements.length + - (state is EvenementLoadingMore ? 1 : 0), - itemBuilder: (context, index) { - if (index >= evenements.length) { - return const Padding( - padding: EdgeInsets.all(16), - child: Center(child: CircularProgressIndicator()), - ); - } - - final evenement = evenements[index]; - return Padding( - padding: const EdgeInsets.only(bottom: 12), - child: EvenementCard( - evenement: evenement, - onTap: () => _navigateToDetail(evenement), - ), - ); - }, - ), + + final isLoadingMore = state is EvenementLoadingMore; + + return AnimatedEvenementList( + evenements: evenements, + isLoading: isLoadingMore, + onEvenementTap: _navigateToDetail, + onRefresh: _onRefresh, ); }, ), diff --git a/unionflow-mobile-apps/lib/features/evenements/presentation/widgets/animated_evenement_card.dart b/unionflow-mobile-apps/lib/features/evenements/presentation/widgets/animated_evenement_card.dart new file mode 100644 index 0000000..bb02f8c --- /dev/null +++ b/unionflow-mobile-apps/lib/features/evenements/presentation/widgets/animated_evenement_card.dart @@ -0,0 +1,363 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import '../../../../core/models/evenement_model.dart'; +import '../../../../shared/theme/app_theme.dart'; + +/// Carte d'événement avec animations sophistiquées +class AnimatedEvenementCard extends StatefulWidget { + final EvenementModel evenement; + final VoidCallback? onTap; + final VoidCallback? onFavorite; + final bool showActions; + + const AnimatedEvenementCard({ + super.key, + required this.evenement, + this.onTap, + this.onFavorite, + this.showActions = true, + }); + + @override + State createState() => _AnimatedEvenementCardState(); +} + +class _AnimatedEvenementCardState extends State + with TickerProviderStateMixin { + late AnimationController _hoverController; + late AnimationController _tapController; + late AnimationController _favoriteController; + + late Animation _scaleAnimation; + late Animation _elevationAnimation; + late Animation _favoriteScaleAnimation; + late Animation _favoriteColorAnimation; + + bool _isHovered = false; + bool _isFavorite = false; + + @override + void initState() { + super.initState(); + + _hoverController = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + + _tapController = AnimationController( + duration: const Duration(milliseconds: 100), + vsync: this, + ); + + _favoriteController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + + _scaleAnimation = Tween( + begin: 1.0, + end: 1.02, + ).animate(CurvedAnimation( + parent: _hoverController, + curve: Curves.easeOutCubic, + )); + + _elevationAnimation = Tween( + begin: 2.0, + end: 8.0, + ).animate(CurvedAnimation( + parent: _hoverController, + curve: Curves.easeOutCubic, + )); + + _favoriteScaleAnimation = Tween( + begin: 1.0, + end: 1.3, + ).animate(CurvedAnimation( + parent: _favoriteController, + curve: Curves.elasticOut, + )); + + _favoriteColorAnimation = ColorTween( + begin: Colors.grey[400], + end: Colors.red, + ).animate(CurvedAnimation( + parent: _favoriteController, + curve: Curves.easeInOut, + )); + } + + @override + void dispose() { + _hoverController.dispose(); + _tapController.dispose(); + _favoriteController.dispose(); + super.dispose(); + } + + void _onTapDown(TapDownDetails details) { + _tapController.forward(); + } + + void _onTapUp(TapUpDetails details) { + _tapController.reverse(); + } + + void _onTapCancel() { + _tapController.reverse(); + } + + void _onHover(bool isHovered) { + setState(() => _isHovered = isHovered); + if (isHovered) { + _hoverController.forward(); + } else { + _hoverController.reverse(); + } + } + + void _onFavoriteToggle() { + setState(() => _isFavorite = !_isFavorite); + if (_isFavorite) { + _favoriteController.forward(); + } else { + _favoriteController.reverse(); + } + widget.onFavorite?.call(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final dateFormat = DateFormat('dd/MM/yyyy'); + final timeFormat = DateFormat('HH:mm'); + + return AnimatedBuilder( + animation: Listenable.merge([ + _scaleAnimation, + _elevationAnimation, + _favoriteScaleAnimation, + _favoriteColorAnimation, + ]), + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: MouseRegion( + onEnter: (_) => _onHover(true), + onExit: (_) => _onHover(false), + child: Card( + elevation: _elevationAnimation.value, + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: _isHovered + ? LinearGradient( + colors: [ + Colors.white, + AppTheme.primaryColor.withOpacity(0.02), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ) + : null, + ), + child: InkWell( + onTap: widget.onTap, + onTapDown: _onTapDown, + onTapUp: _onTapUp, + onTapCancel: _onTapCancel, + borderRadius: BorderRadius.circular(16), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // En-tête avec type et actions + Row( + children: [ + // Icône du type avec animation + AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: _isHovered + ? AppTheme.primaryColor.withOpacity(0.15) + : AppTheme.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + widget.evenement.typeEvenement.icone, + style: const TextStyle(fontSize: 24), + ), + ), + + const SizedBox(width: 12), + + // Type et statut + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.evenement.typeEvenement.libelle, + style: theme.textTheme.bodySmall?.copyWith( + color: AppTheme.primaryColor, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + _buildStatusChip(), + ], + ), + ), + + // Bouton favori animé + if (widget.showActions) + GestureDetector( + onTap: _onFavoriteToggle, + child: Transform.scale( + scale: _favoriteScaleAnimation.value, + child: Icon( + _isFavorite ? Icons.favorite : Icons.favorite_border, + color: _favoriteColorAnimation.value, + size: 24, + ), + ), + ), + ], + ), + + const SizedBox(height: 16), + + // Titre avec animation de couleur + AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 200), + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: _isHovered + ? AppTheme.primaryColor + : theme.textTheme.titleLarge?.color, + ) ?? const TextStyle(), + child: Text(widget.evenement.titre), + ), + + if (widget.evenement.description?.isNotEmpty == true) ...[ + const SizedBox(height: 8), + Text( + widget.evenement.description!, + style: theme.textTheme.bodyMedium?.copyWith( + color: Colors.grey[600], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + + const SizedBox(height: 16), + + // Informations de date et lieu avec icônes animées + Row( + children: [ + _buildAnimatedInfo( + icon: Icons.calendar_today, + text: dateFormat.format(widget.evenement.dateDebut), + ), + const SizedBox(width: 16), + _buildAnimatedInfo( + icon: Icons.access_time, + text: timeFormat.format(widget.evenement.dateDebut), + ), + ], + ), + + if (widget.evenement.lieu?.isNotEmpty == true) ...[ + const SizedBox(height: 8), + _buildAnimatedInfo( + icon: Icons.location_on, + text: widget.evenement.lieu!, + ), + ], + ], + ), + ), + ), + ), + ), + ), + ); + }, + ); + } + + Widget _buildStatusChip() { + Color statusColor; + switch (widget.evenement.statut) { + case StatutEvenement.planifie: + statusColor = Colors.orange; + break; + case StatutEvenement.confirme: + statusColor = Colors.green; + break; + case StatutEvenement.enCours: + statusColor = Colors.blue; + break; + case StatutEvenement.termine: + statusColor = Colors.grey; + break; + case StatutEvenement.annule: + statusColor = Colors.red; + break; + case StatutEvenement.reporte: + statusColor = Colors.purple; + break; + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: statusColor.withOpacity(0.3)), + ), + child: Text( + widget.evenement.statut.libelle, + style: TextStyle( + color: statusColor, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ); + } + + Widget _buildAnimatedInfo({required IconData icon, required String text}) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 200), + child: Icon( + icon, + size: 16, + color: _isHovered + ? AppTheme.primaryColor + : Colors.grey[600], + ), + ), + const SizedBox(width: 4), + Text( + text, + style: TextStyle( + color: Colors.grey[600], + fontSize: 14, + ), + ), + ], + ); + } +} diff --git a/unionflow-mobile-apps/lib/features/evenements/presentation/widgets/animated_evenement_list.dart b/unionflow-mobile-apps/lib/features/evenements/presentation/widgets/animated_evenement_list.dart new file mode 100644 index 0000000..820f43b --- /dev/null +++ b/unionflow-mobile-apps/lib/features/evenements/presentation/widgets/animated_evenement_list.dart @@ -0,0 +1,242 @@ +import 'package:flutter/material.dart'; +import '../../../../core/models/evenement_model.dart'; +import '../../../../core/animations/loading_animations.dart'; +import 'evenement_card.dart'; +import 'animated_evenement_card.dart'; + +/// Widget animé pour afficher une liste d'événements avec animations d'apparition +class AnimatedEvenementList extends StatefulWidget { + final List evenements; + final Function(EvenementModel)? onEvenementTap; + final bool isLoading; + final VoidCallback? onRefresh; + + const AnimatedEvenementList({ + super.key, + required this.evenements, + this.onEvenementTap, + this.isLoading = false, + this.onRefresh, + }); + + @override + State createState() => _AnimatedEvenementListState(); +} + +class _AnimatedEvenementListState extends State + with TickerProviderStateMixin { + late AnimationController _listController; + List _itemControllers = []; + List> _itemAnimations = []; + List> _slideAnimations = []; + + @override + void initState() { + super.initState(); + _initializeAnimations(); + } + + @override + void didUpdateWidget(AnimatedEvenementList oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.evenements.length != oldWidget.evenements.length) { + _updateAnimations(); + } + } + + @override + void dispose() { + _listController.dispose(); + for (final controller in _itemControllers) { + controller.dispose(); + } + super.dispose(); + } + + void _initializeAnimations() { + _listController = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + ); + + _updateAnimations(); + _listController.forward(); + } + + void _updateAnimations() { + // Dispose des anciens controllers s'ils existent + if (_itemControllers.isNotEmpty) { + for (final controller in _itemControllers) { + controller.dispose(); + } + } + + // Créer de nouveaux controllers pour chaque élément + _itemControllers = List.generate( + widget.evenements.length, + (index) => AnimationController( + duration: Duration(milliseconds: 300 + (index * 100)), + vsync: this, + ), + ); + + // Animations de fade et scale + _itemAnimations = _itemControllers.map((controller) { + return Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: controller, + curve: Curves.easeOutCubic, + ), + ); + }).toList(); + + // Animations de slide depuis le bas + _slideAnimations = _itemControllers.map((controller) { + return Tween( + begin: const Offset(0, 0.3), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: controller, + curve: Curves.easeOutCubic, + ), + ); + }).toList(); + + // Démarrer les animations avec un délai progressif + for (int i = 0; i < _itemControllers.length; i++) { + Future.delayed(Duration(milliseconds: i * 150), () { + if (mounted) { + _itemControllers[i].forward(); + } + }); + } + } + + @override + Widget build(BuildContext context) { + if (widget.isLoading && widget.evenements.isEmpty) { + return _buildLoadingState(); + } + + if (widget.evenements.isEmpty) { + return _buildEmptyState(); + } + + return RefreshIndicator( + onRefresh: () async { + widget.onRefresh?.call(); + await Future.delayed(const Duration(milliseconds: 500)); + }, + child: ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + itemCount: widget.evenements.length + (widget.isLoading ? 1 : 0), + itemBuilder: (context, index) { + if (index >= widget.evenements.length) { + return _buildLoadingIndicator(); + } + + return _buildAnimatedItem(index); + }, + ), + ); + } + + Widget _buildAnimatedItem(int index) { + final evenement = widget.evenements[index]; + + if (index >= _itemAnimations.length) { + // Fallback pour les nouveaux éléments + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: AnimatedEvenementCard( + evenement: evenement, + onTap: () => widget.onEvenementTap?.call(evenement), + ), + ); + } + + return AnimatedBuilder( + animation: _itemAnimations[index], + builder: (context, child) { + return SlideTransition( + position: _slideAnimations[index], + child: FadeTransition( + opacity: _itemAnimations[index], + child: Transform.scale( + scale: 0.8 + (0.2 * _itemAnimations[index].value), + child: Padding( + padding: const EdgeInsets.only(bottom: 16), + child: AnimatedEvenementCard( + evenement: evenement, + onTap: () => widget.onEvenementTap?.call(evenement), + ), + ), + ), + ), + ); + }, + ); + } + + Widget _buildLoadingState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + LoadingAnimations.waves(), + const SizedBox(height: 24), + const Text( + 'Chargement des événements...', + style: TextStyle( + fontSize: 16, + color: Colors.grey, + ), + ), + ], + ), + ); + } + + Widget _buildEmptyState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.event_busy, + size: 80, + color: Colors.grey[400], + ), + const SizedBox(height: 24), + Text( + 'Aucun événement trouvé', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 8), + Text( + 'Les événements apparaîtront ici', + style: TextStyle( + fontSize: 14, + color: Colors.grey[500], + ), + ), + ], + ), + ); + } + + Widget _buildLoadingIndicator() { + return Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: LoadingAnimations.dots(), + ), + ); + } +} diff --git a/unionflow-mobile-apps/lib/features/members/presentation/pages/members_list_page.dart b/unionflow-mobile-apps/lib/features/members/presentation/pages/members_list_page.dart index adac54f..9ef1eb1 100644 --- a/unionflow-mobile-apps/lib/features/members/presentation/pages/members_list_page.dart +++ b/unionflow-mobile-apps/lib/features/members/presentation/pages/members_list_page.dart @@ -199,11 +199,11 @@ class _MembersListPageState extends State children: [ // Titre principal quand l'AppBar est étendu if (!innerBoxIsScrolled) - Padding( - padding: const EdgeInsets.only(top: 60), + const Padding( + padding: EdgeInsets.only(top: 60), child: Text( 'Membres', - style: const TextStyle( + style: TextStyle( color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold, @@ -473,7 +473,7 @@ class _MembersListPageState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( + const Icon( Icons.people_outline, size: 80, color: AppTheme.textHint, diff --git a/unionflow-mobile-apps/lib/features/members/presentation/pages/membre_details_page.dart b/unionflow-mobile-apps/lib/features/members/presentation/pages/membre_details_page.dart index 840c8f5..efbd291 100644 --- a/unionflow-mobile-apps/lib/features/members/presentation/pages/membre_details_page.dart +++ b/unionflow-mobile-apps/lib/features/members/presentation/pages/membre_details_page.dart @@ -172,13 +172,13 @@ class _MembreDetailsPageState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.error, size: 64, color: AppTheme.errorColor), - SizedBox(height: 16), + const Icon(Icons.error, size: 64, color: AppTheme.errorColor), + const SizedBox(height: 16), Text(state.message), - SizedBox(height: 16), + const SizedBox(height: 16), ElevatedButton( onPressed: () => _membresBloc.add(LoadMembreById(widget.membreId)), - child: Text('Réessayer'), + child: const Text('Réessayer'), ), ], ), diff --git a/unionflow-mobile-apps/lib/features/members/presentation/pages/membres_dashboard_page.dart b/unionflow-mobile-apps/lib/features/members/presentation/pages/membres_dashboard_page.dart index 6d25a9e..5e3ade4 100644 --- a/unionflow-mobile-apps/lib/features/members/presentation/pages/membres_dashboard_page.dart +++ b/unionflow-mobile-apps/lib/features/members/presentation/pages/membres_dashboard_page.dart @@ -107,13 +107,13 @@ class _MembresDashboardPageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( + const Icon( Icons.error_outline, size: 64, color: AppTheme.errorColor, ), const SizedBox(height: 16), - Text( + const Text( 'Erreur de chargement', style: TextStyle( fontSize: 18, @@ -124,7 +124,7 @@ class _MembresDashboardPageState extends State { const SizedBox(height: 8), Text( state.message, - style: TextStyle( + style: const TextStyle( fontSize: 14, color: AppTheme.textSecondary, ), diff --git a/unionflow-mobile-apps/lib/features/members/presentation/widgets/dashboard_stat_card.dart b/unionflow-mobile-apps/lib/features/members/presentation/widgets/dashboard_stat_card.dart index 0acfe4d..5f96976 100644 --- a/unionflow-mobile-apps/lib/features/members/presentation/widgets/dashboard_stat_card.dart +++ b/unionflow-mobile-apps/lib/features/members/presentation/widgets/dashboard_stat_card.dart @@ -94,7 +94,7 @@ class _DashboardStatCardState extends State child: AnimatedContainer( duration: DesignSystem.animationFast, curve: DesignSystem.animationCurve, - padding: EdgeInsets.all(DesignSystem.spacingLg), + padding: const EdgeInsets.all(DesignSystem.spacingLg), decoration: BoxDecoration( color: AppTheme.surfaceLight, borderRadius: BorderRadius.circular(DesignSystem.radiusLg), @@ -121,12 +121,12 @@ class _DashboardStatCardState extends State if (widget.trend != null) _buildShimmer(60, 24, radius: 12), ], ), - SizedBox(height: DesignSystem.spacingMd), + const SizedBox(height: DesignSystem.spacingMd), _buildShimmer(80, 32), - SizedBox(height: DesignSystem.spacingSm), + const SizedBox(height: DesignSystem.spacingSm), _buildShimmer(120, 16), if (widget.subtitle != null) ...[ - SizedBox(height: DesignSystem.spacingXs), + const SizedBox(height: DesignSystem.spacingXs), _buildShimmer(100, 14), ], ], @@ -153,10 +153,10 @@ class _DashboardStatCardState extends State _buildHeader(), SizedBox(height: DesignSystem.goldenHeight(DesignSystem.spacingLg)), _buildValue(), - SizedBox(height: DesignSystem.spacingSm), + const SizedBox(height: DesignSystem.spacingSm), _buildTitle(), if (widget.subtitle != null) ...[ - SizedBox(height: DesignSystem.spacingXs), + const SizedBox(height: DesignSystem.spacingXs), _buildSubtitle(), ], ], @@ -202,7 +202,7 @@ class _DashboardStatCardState extends State Widget _buildTrendBadge() { return Container( - padding: EdgeInsets.symmetric( + padding: const EdgeInsets.symmetric( horizontal: DesignSystem.spacingSm, vertical: DesignSystem.spacingXs, ), @@ -222,7 +222,7 @@ class _DashboardStatCardState extends State color: _getTrendColor(), size: 14, ), - SizedBox(width: DesignSystem.spacing2xs), + const SizedBox(width: DesignSystem.spacing2xs), Text( widget.trend!, style: DesignSystem.labelSmall.copyWith( diff --git a/unionflow-mobile-apps/lib/features/members/presentation/widgets/membre_cotisations_section.dart b/unionflow-mobile-apps/lib/features/members/presentation/widgets/membre_cotisations_section.dart index 2b16c46..6550698 100644 --- a/unionflow-mobile-apps/lib/features/members/presentation/widgets/membre_cotisations_section.dart +++ b/unionflow-mobile-apps/lib/features/members/presentation/widgets/membre_cotisations_section.dart @@ -80,15 +80,15 @@ class MembreCotisationsSection extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( + const Row( children: [ Icon( Icons.account_balance_wallet, color: AppTheme.primaryColor, size: 24, ), - const SizedBox(width: 8), - const Text( + SizedBox(width: 8), + Text( 'Résumé des cotisations', style: TextStyle( fontSize: 18, @@ -201,8 +201,8 @@ class MembreCotisationsSection extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), - child: Padding( - padding: const EdgeInsets.all(32), + child: const Padding( + padding: EdgeInsets.all(32), child: Column( children: [ Icon( @@ -210,8 +210,8 @@ class MembreCotisationsSection extends StatelessWidget { size: 48, color: AppTheme.textHint, ), - const SizedBox(height: 16), - const Text( + SizedBox(height: 16), + Text( 'Aucune cotisation', style: TextStyle( fontSize: 16, @@ -219,8 +219,8 @@ class MembreCotisationsSection extends StatelessWidget { color: AppTheme.textPrimary, ), ), - const SizedBox(height: 8), - const Text( + SizedBox(height: 8), + Text( 'Ce membre n\'a pas encore de cotisations enregistrées.', textAlign: TextAlign.center, style: TextStyle( @@ -237,15 +237,15 @@ class MembreCotisationsSection extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( + const Row( children: [ Icon( Icons.list_alt, color: AppTheme.primaryColor, size: 20, ), - const SizedBox(width: 8), - const Text( + SizedBox(width: 8), + Text( 'Historique des cotisations', style: TextStyle( fontSize: 16, diff --git a/unionflow-mobile-apps/lib/features/members/presentation/widgets/membre_stats_section.dart b/unionflow-mobile-apps/lib/features/members/presentation/widgets/membre_stats_section.dart index 7c4301d..13c1e12 100644 --- a/unionflow-mobile-apps/lib/features/members/presentation/widgets/membre_stats_section.dart +++ b/unionflow-mobile-apps/lib/features/members/presentation/widgets/membre_stats_section.dart @@ -56,15 +56,15 @@ class MembreStatsSection extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( + const Row( children: [ Icon( Icons.analytics, color: AppTheme.primaryColor, size: 24, ), - const SizedBox(width: 8), - const Text( + SizedBox(width: 8), + Text( 'Vue d\'ensemble', style: TextStyle( fontSize: 18, @@ -226,15 +226,15 @@ class MembreStatsSection extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( + const Row( children: [ Icon( Icons.pie_chart, color: AppTheme.primaryColor, size: 20, ), - const SizedBox(width: 8), - const Text( + SizedBox(width: 8), + Text( 'Répartition des paiements', style: TextStyle( fontSize: 16, @@ -280,15 +280,15 @@ class MembreStatsSection extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( + const Row( children: [ Icon( Icons.bar_chart, color: AppTheme.primaryColor, size: 20, ), - const SizedBox(width: 8), - const Text( + SizedBox(width: 8), + Text( 'Évolution des montants', style: TextStyle( fontSize: 16, @@ -363,15 +363,15 @@ class MembreStatsSection extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( + const Row( children: [ Icon( Icons.timeline, color: AppTheme.primaryColor, size: 20, ), - const SizedBox(width: 8), - const Text( + SizedBox(width: 8), + Text( 'Chronologie', style: TextStyle( fontSize: 16, @@ -474,7 +474,7 @@ class MembreStatsSection extends StatelessWidget { padding: const EdgeInsets.all(40), child: Column( children: [ - Icon( + const Icon( Icons.bar_chart, size: 48, color: AppTheme.textHint, diff --git a/unionflow-mobile-apps/lib/features/members/presentation/widgets/membres_export_dialog.dart b/unionflow-mobile-apps/lib/features/members/presentation/widgets/membres_export_dialog.dart index faea587..7db0bec 100644 --- a/unionflow-mobile-apps/lib/features/members/presentation/widgets/membres_export_dialog.dart +++ b/unionflow-mobile-apps/lib/features/members/presentation/widgets/membres_export_dialog.dart @@ -51,7 +51,7 @@ class _MembresExportDialogState extends State { color: AppTheme.primaryColor.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), - child: Icon( + child: const Icon( Icons.file_download, color: AppTheme.primaryColor, size: 24, @@ -116,15 +116,15 @@ class _MembresExportDialogState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( + const Row( children: [ Icon( Icons.info_outline, color: AppTheme.primaryColor, size: 20, ), - const SizedBox(width: 8), - const Text( + SizedBox(width: 8), + Text( 'Données à exporter', style: TextStyle( fontSize: 16, diff --git a/unionflow-mobile-apps/lib/features/members/presentation/widgets/membres_view_controls.dart b/unionflow-mobile-apps/lib/features/members/presentation/widgets/membres_view_controls.dart index 8b362bb..925c711 100644 --- a/unionflow-mobile-apps/lib/features/members/presentation/widgets/membres_view_controls.dart +++ b/unionflow-mobile-apps/lib/features/members/presentation/widgets/membres_view_controls.dart @@ -43,7 +43,7 @@ class MembresViewControls extends StatelessWidget { ), child: Text( '$totalCount membre${totalCount > 1 ? 's' : ''}', - style: TextStyle( + style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: AppTheme.primaryColor, @@ -72,7 +72,7 @@ class MembresViewControls extends StatelessWidget { PopupMenuButton( initialValue: sortBy, onSelected: onSortChanged, - icon: Icon( + icon: const Icon( Icons.sort, size: 20, color: AppTheme.textSecondary, diff --git a/unionflow-mobile-apps/lib/shared/theme/app_theme.dart b/unionflow-mobile-apps/lib/shared/theme/app_theme.dart index 15ac8a1..c6804d9 100644 --- a/unionflow-mobile-apps/lib/shared/theme/app_theme.dart +++ b/unionflow-mobile-apps/lib/shared/theme/app_theme.dart @@ -30,6 +30,7 @@ class AppTheme { // Bordures et dividers static const Color borderColor = Color(0xFFE0E0E0); + static const Color borderLight = Color(0xFFF5F5F5); static const Color dividerColor = Color(0xFFBDBDBD); // Thème clair diff --git a/unionflow-mobile-apps/lib/shared/widgets/buttons/primary_button.dart b/unionflow-mobile-apps/lib/shared/widgets/buttons/primary_button.dart new file mode 100644 index 0000000..15e4a29 --- /dev/null +++ b/unionflow-mobile-apps/lib/shared/widgets/buttons/primary_button.dart @@ -0,0 +1,291 @@ +import 'package:flutter/material.dart'; +import '../../theme/app_theme.dart'; + +/// Widget bouton principal réutilisable +class PrimaryButton extends StatelessWidget { + final String text; + final VoidCallback? onPressed; + final bool isLoading; + final bool isEnabled; + final IconData? icon; + final Color? backgroundColor; + final Color? textColor; + final double? width; + final double height; + final EdgeInsetsGeometry? padding; + final BorderRadius? borderRadius; + + const PrimaryButton({ + super.key, + required this.text, + this.onPressed, + this.isLoading = false, + this.isEnabled = true, + this.icon, + this.backgroundColor, + this.textColor, + this.width, + this.height = 48.0, + this.padding, + this.borderRadius, + }); + + @override + Widget build(BuildContext context) { + final effectiveBackgroundColor = backgroundColor ?? AppTheme.primaryColor; + final effectiveTextColor = textColor ?? Colors.white; + final isButtonEnabled = isEnabled && !isLoading && onPressed != null; + + return SizedBox( + width: width, + height: height, + child: ElevatedButton( + onPressed: isButtonEnabled ? onPressed : null, + style: ElevatedButton.styleFrom( + backgroundColor: effectiveBackgroundColor, + foregroundColor: effectiveTextColor, + disabledBackgroundColor: effectiveBackgroundColor.withOpacity(0.5), + disabledForegroundColor: effectiveTextColor.withOpacity(0.5), + elevation: isButtonEnabled ? 2 : 0, + shadowColor: effectiveBackgroundColor.withOpacity(0.3), + shape: RoundedRectangleBorder( + borderRadius: borderRadius ?? BorderRadius.circular(8), + ), + padding: padding ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + child: isLoading + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(effectiveTextColor), + ), + ) + : Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) ...[ + Icon(icon, size: 18), + const SizedBox(width: 8), + ], + Text( + text, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: effectiveTextColor, + ), + ), + ], + ), + ), + ); + } +} + +/// Widget bouton secondaire +class SecondaryButton extends StatelessWidget { + final String text; + final VoidCallback? onPressed; + final bool isLoading; + final bool isEnabled; + final IconData? icon; + final Color? borderColor; + final Color? textColor; + final double? width; + final double height; + final EdgeInsetsGeometry? padding; + final BorderRadius? borderRadius; + + const SecondaryButton({ + super.key, + required this.text, + this.onPressed, + this.isLoading = false, + this.isEnabled = true, + this.icon, + this.borderColor, + this.textColor, + this.width, + this.height = 48.0, + this.padding, + this.borderRadius, + }); + + @override + Widget build(BuildContext context) { + final effectiveBorderColor = borderColor ?? AppTheme.primaryColor; + final effectiveTextColor = textColor ?? AppTheme.primaryColor; + final isButtonEnabled = isEnabled && !isLoading && onPressed != null; + + return SizedBox( + width: width, + height: height, + child: OutlinedButton( + onPressed: isButtonEnabled ? onPressed : null, + style: OutlinedButton.styleFrom( + foregroundColor: effectiveTextColor, + disabledForegroundColor: effectiveTextColor.withOpacity(0.5), + side: BorderSide( + color: isButtonEnabled ? effectiveBorderColor : effectiveBorderColor.withOpacity(0.5), + width: 1.5, + ), + shape: RoundedRectangleBorder( + borderRadius: borderRadius ?? BorderRadius.circular(8), + ), + padding: padding ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + child: isLoading + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(effectiveTextColor), + ), + ) + : Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) ...[ + Icon(icon, size: 18), + const SizedBox(width: 8), + ], + Text( + text, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: effectiveTextColor, + ), + ), + ], + ), + ), + ); + } +} + +/// Widget bouton texte +class CustomTextButton extends StatelessWidget { + final String text; + final VoidCallback? onPressed; + final bool isLoading; + final bool isEnabled; + final IconData? icon; + final Color? textColor; + final double? width; + final double height; + final EdgeInsetsGeometry? padding; + + const CustomTextButton({ + super.key, + required this.text, + this.onPressed, + this.isLoading = false, + this.isEnabled = true, + this.icon, + this.textColor, + this.width, + this.height = 48.0, + this.padding, + }); + + @override + Widget build(BuildContext context) { + final effectiveTextColor = textColor ?? AppTheme.primaryColor; + final isButtonEnabled = isEnabled && !isLoading && onPressed != null; + + return SizedBox( + width: width, + height: height, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: isButtonEnabled ? onPressed : null, + borderRadius: BorderRadius.circular(8), + child: Container( + padding: padding ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: isLoading + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(effectiveTextColor), + ), + ) + : Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) ...[ + Icon( + icon, + size: 18, + color: isButtonEnabled ? effectiveTextColor : effectiveTextColor.withOpacity(0.5), + ), + const SizedBox(width: 8), + ], + Text( + text, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: isButtonEnabled ? effectiveTextColor : effectiveTextColor.withOpacity(0.5), + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +/// Widget bouton destructeur (pour les actions dangereuses) +class DestructiveButton extends StatelessWidget { + final String text; + final VoidCallback? onPressed; + final bool isLoading; + final bool isEnabled; + final IconData? icon; + final double? width; + final double height; + final EdgeInsetsGeometry? padding; + final BorderRadius? borderRadius; + + const DestructiveButton({ + super.key, + required this.text, + this.onPressed, + this.isLoading = false, + this.isEnabled = true, + this.icon, + this.width, + this.height = 48.0, + this.padding, + this.borderRadius, + }); + + @override + Widget build(BuildContext context) { + return PrimaryButton( + text: text, + onPressed: onPressed, + isLoading: isLoading, + isEnabled: isEnabled, + icon: icon, + backgroundColor: AppTheme.errorColor, + textColor: Colors.white, + width: width, + height: height, + padding: padding, + borderRadius: borderRadius, + ); + } +} diff --git a/unionflow-mobile-apps/pubspec.lock b/unionflow-mobile-apps/pubspec.lock index f141bb7..0bf7aa9 100644 --- a/unionflow-mobile-apps/pubspec.lock +++ b/unionflow-mobile-apps/pubspec.lock @@ -70,6 +70,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.4" + bloc_test: + dependency: "direct dev" + description: + name: bloc_test + sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2" + url: "https://pub.dev" + source: hosted + version: "9.1.7" boolean_selector: dependency: transitive description: @@ -182,6 +190,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" clock: dependency: transitive description: @@ -214,6 +230,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.dev" + source: hosted + version: "1.15.0" cross_file: dependency: transitive description: @@ -254,6 +278,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.7" + dbus: + dependency: transitive + description: + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + url: "https://pub.dev" + source: hosted + version: "0.7.11" + diff_match_patch: + dependency: transitive + description: + name: diff_match_patch + sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" + url: "https://pub.dev" + source: hosted + version: "0.4.1" dio: dependency: "direct main" description: @@ -306,10 +346,10 @@ packages: dependency: transitive description: name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.0" file_picker: dependency: "direct main" description: @@ -371,6 +411,11 @@ packages: url: "https://pub.dev" source: hosted version: "3.4.1" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_lints: dependency: "direct dev" description: @@ -379,6 +424,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "674173fd3c9eda9d4c8528da2ce0ea69f161577495a9cc835a2a4ecd7eadeb35" + url: "https://pub.dev" + source: hosted + version: "17.2.4" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af + url: "https://pub.dev" + source: hosted + version: "4.0.1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" + url: "https://pub.dev" + source: hosted + version: "7.2.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -461,6 +530,11 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" get_it: dependency: "direct main" description: @@ -533,6 +607,11 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.2" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" intl: dependency: "direct main" description: @@ -669,6 +748,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.4.4" + mocktail: + dependency: transitive + description: + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" nested: dependency: transitive description: @@ -677,6 +764,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" octo_image: dependency: transitive description: @@ -841,10 +936,10 @@ packages: dependency: transitive description: name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.6" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -869,6 +964,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + process: + dependency: transitive + description: + name: process + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + url: "https://pub.dev" + source: hosted + version: "5.0.2" provider: dependency: transitive description: @@ -1005,6 +1108,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" shelf_web_socket: dependency: transitive description: @@ -1042,6 +1161,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.5" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" source_span: dependency: transitive description: @@ -1130,6 +1265,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" synchronized: dependency: transitive description: @@ -1146,6 +1289,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: transitive + description: + name: test + sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" + url: "https://pub.dev" + source: hosted + version: "1.25.7" test_api: dependency: transitive description: @@ -1154,6 +1305,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.2" + test_core: + dependency: transitive + description: + name: test_core + sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" + url: "https://pub.dev" + source: hosted + version: "0.6.4" + timezone: + dependency: transitive + description: + name: timezone + sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" + url: "https://pub.dev" + source: hosted + version: "0.9.4" timing: dependency: transitive description: @@ -1290,6 +1457,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" webview_flutter: dependency: "direct main" description: diff --git a/unionflow-mobile-apps/pubspec.yaml b/unionflow-mobile-apps/pubspec.yaml index 62bd0e5..97476bf 100644 --- a/unionflow-mobile-apps/pubspec.yaml +++ b/unionflow-mobile-apps/pubspec.yaml @@ -49,6 +49,9 @@ dependencies: package_info_plus: ^8.0.2 flutter_staggered_animations: ^1.1.1 + # Notifications + flutter_local_notifications: ^17.2.3 + # Export/Import excel: ^4.0.6 csv: ^6.0.0 @@ -65,6 +68,9 @@ dev_dependencies: build_runner: ^2.4.13 json_serializable: ^6.8.0 mockito: ^5.4.4 + bloc_test: ^9.1.7 + integration_test: + sdk: flutter flutter: uses-material-design: true \ No newline at end of file diff --git a/unionflow-mobile-apps/test/error_handling_test.dart b/unionflow-mobile-apps/test/error_handling_test.dart deleted file mode 100644 index 4b983a8..0000000 --- a/unionflow-mobile-apps/test/error_handling_test.dart +++ /dev/null @@ -1,222 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:dio/dio.dart'; - -import '../lib/core/error/error_handler.dart'; -import '../lib/core/validation/form_validator.dart'; -import '../lib/core/failures/failures.dart'; - -void main() { - group('FormValidator Tests', () { - test('should validate required fields correctly', () { - // Test champ requis vide - expect(FormValidator.required(''), 'Ce champ est requis'); - expect(FormValidator.required(' '), 'Ce champ est requis'); - expect(FormValidator.required(null), 'Ce champ est requis'); - - // Test champ requis valide - expect(FormValidator.required('valeur'), null); - expect(FormValidator.required(' valeur '), null); - }); - - test('should validate email correctly', () { - // Test emails invalides - expect(FormValidator.email(''), 'L\'email est requis'); - expect(FormValidator.email('invalid'), 'Format d\'email invalide'); - expect(FormValidator.email('test@'), 'Format d\'email invalide'); - expect(FormValidator.email('@domain.com'), 'Format d\'email invalide'); - expect(FormValidator.email('test.domain.com'), 'Format d\'email invalide'); - - // Test emails valides - expect(FormValidator.email('test@domain.com'), null); - expect(FormValidator.email('user.name@example.org'), null); - expect(FormValidator.email('test123@sub.domain.co.uk'), null); - }); - - test('should validate phone numbers correctly', () { - // Test téléphones invalides - expect(FormValidator.phone(''), 'Le numéro de téléphone est requis'); - expect(FormValidator.phone('123'), 'Format de téléphone invalide (ex: +225XXXXXXXX)'); - expect(FormValidator.phone('abcdefgh'), 'Format de téléphone invalide (ex: +225XXXXXXXX)'); - - // Test téléphones valides - expect(FormValidator.phone('12345678'), null); - expect(FormValidator.phone('+22512345678'), null); - expect(FormValidator.phone('1234567890'), null); - expect(FormValidator.phone('+225 12 34 56 78'), null); // Avec espaces - }); - - test('should validate names correctly', () { - // Test noms invalides - expect(FormValidator.name(''), 'Ce champ est requis'); - expect(FormValidator.name('A'), 'Ce champ doit contenir au moins 2 caractères'); - expect(FormValidator.name('123'), 'Ce champ ne peut contenir que des lettres'); - expect(FormValidator.name('Name@123'), 'Ce champ ne peut contenir que des lettres'); - - // Test noms valides - expect(FormValidator.name('Jean'), null); - expect(FormValidator.name('Marie-Claire'), null); - expect(FormValidator.name('Jean-Baptiste'), null); - expect(FormValidator.name('O\'Connor'), null); - expect(FormValidator.name('José'), null); - expect(FormValidator.name('François'), null); - }); - - test('should validate birth dates correctly', () { - final now = DateTime.now(); - final validDate = DateTime(now.year - 25, now.month, now.day); - final futureDate = DateTime(now.year + 1, now.month, now.day); - final tooYoungDate = DateTime(now.year - 10, now.month, now.day); - final tooOldDate = DateTime(now.year - 150, now.month, now.day); - - // Test dates invalides - expect(FormValidator.birthDate(null), 'La date de naissance est requise'); - expect(FormValidator.birthDate(futureDate), 'La date de naissance ne peut pas être dans le futur'); - expect(FormValidator.birthDate(tooYoungDate, minAge: 16), 'L\'âge minimum requis est de 16 ans'); - expect(FormValidator.birthDate(tooOldDate, maxAge: 120), 'L\'âge maximum autorisé est de 120 ans'); - - // Test date valide - expect(FormValidator.birthDate(validDate), null); - }); - - test('should validate member numbers correctly', () { - // Test numéros invalides - expect(FormValidator.memberNumber(''), 'Le numéro de membre est requis'); - expect(FormValidator.memberNumber('123'), 'Format invalide (ex: MBR001)'); - expect(FormValidator.memberNumber('MBR'), 'Format invalide (ex: MBR001)'); - expect(FormValidator.memberNumber('MBR12'), 'Format invalide (ex: MBR001)'); - - // Test numéros valides - expect(FormValidator.memberNumber('MBR001'), null); - expect(FormValidator.memberNumber('MBR123456'), null); - }); - - test('should combine validators correctly', () { - final combinedValidator = FormValidator.combine([ - (value) => FormValidator.required(value), - (value) => FormValidator.minLength(value, 3), - (value) => FormValidator.maxLength(value, 10), - ]); - - // Test avec erreurs - expect(combinedValidator(''), 'Ce champ est requis'); - expect(combinedValidator('ab'), 'Ce champ doit contenir au moins 3 caractères'); - expect(combinedValidator('12345678901'), 'Ce champ ne peut pas dépasser 10 caractères'); - - // Test valide - expect(combinedValidator('valide'), null); - }); - - test('should validate complete member data', () { - final validMemberData = { - 'prenom': 'Jean', - 'nom': 'Dupont', - 'email': 'jean.dupont@email.com', - 'telephone': '+22512345678', - 'dateNaissance': DateTime(1990, 1, 1), - 'adresse': '123 Rue de la Paix', - 'profession': 'Ingénieur', - }; - - final invalidMemberData = { - 'prenom': '', - 'nom': 'D', - 'email': 'invalid-email', - 'telephone': '123', - 'dateNaissance': DateTime.now().add(const Duration(days: 1)), - 'adresse': '', - 'profession': '', - }; - - // Test données valides - final validErrors = FormValidator.validateMember(validMemberData); - expect(validErrors.isEmpty, true); - - // Test données invalides - final invalidErrors = FormValidator.validateMember(invalidMemberData); - expect(invalidErrors.isNotEmpty, true); - expect(invalidErrors.containsKey('prenom'), true); - expect(invalidErrors.containsKey('nom'), true); - expect(invalidErrors.containsKey('email'), true); - expect(invalidErrors.containsKey('telephone'), true); - }); - }); - - group('ErrorHandler Tests', () { - test('should analyze DioException correctly', () { - // Test DioException de type connectTimeout - final timeoutException = DioException( - requestOptions: RequestOptions(path: '/test'), - type: DioExceptionType.connectionTimeout, - message: 'Connection timeout', - ); - - // Nous ne pouvons pas tester directement _analyzeError car elle est privée - // Mais nous pouvons tester que la classe ErrorHandler existe et compile - expect(ErrorHandler, isNotNull); - }); - - test('should create appropriate failure types', () { - // Test NetworkFailure - final networkFailure = NetworkFailure.noConnection(); - expect(networkFailure.message, 'Aucune connexion internet disponible'); - expect(networkFailure.code, 'NO_CONNECTION'); - - // Test ServerFailure - final serverFailure = ServerFailure.internalError(); - expect(serverFailure.message, 'Erreur interne du serveur'); - expect(serverFailure.statusCode, 500); - - // Test ValidationFailure - final validationFailure = ValidationFailure.requiredField('email'); - expect(validationFailure.message, 'Champ requis manquant'); - expect(validationFailure.fieldErrors?['email']?.first, 'Ce champ est requis'); - - // Test AuthFailure - final authFailure = AuthFailure.tokenExpired(); - expect(authFailure.message, 'Session expirée, veuillez vous reconnecter'); - expect(authFailure.code, 'TOKEN_EXPIRED'); - }); - - test('should handle failure equality correctly', () { - final failure1 = NetworkFailure.noConnection(); - final failure2 = NetworkFailure.noConnection(); - final failure3 = NetworkFailure.timeout(); - - expect(failure1 == failure2, true); - expect(failure1 == failure3, false); - expect(failure1.hashCode == failure2.hashCode, true); - }); - }); - - group('Failure Classes Tests', () { - test('should create DataFailure correctly', () { - final notFoundFailure = DataFailure.notFound('Membre'); - expect(notFoundFailure.message, 'Membre non trouvé(e)'); - expect(notFoundFailure.code, 'NOT_FOUND'); - expect(notFoundFailure.details?['resource'], 'Membre'); - - final conflictFailure = DataFailure.conflict('Email déjà utilisé'); - expect(conflictFailure.message, 'Conflit de données : Email déjà utilisé'); - expect(conflictFailure.code, 'CONFLICT'); - }); - - test('should create FileFailure correctly', () { - final fileNotFound = FileFailure.notFound('/path/to/file.txt'); - expect(fileNotFound.message, 'Fichier non trouvé'); - expect(fileNotFound.details?['filePath'], '/path/to/file.txt'); - - final invalidFormat = FileFailure.invalidFormat('PDF'); - expect(invalidFormat.message, 'Format de fichier invalide'); - expect(invalidFormat.details?['expectedFormat'], 'PDF'); - }); - - test('should create UnknownFailure from exception', () { - final exception = Exception('Test exception'); - final unknownFailure = UnknownFailure.fromException(exception); - - expect(unknownFailure.message.contains('Test exception'), true); - expect(unknownFailure.code, 'UNKNOWN_ERROR'); - }); - }); -} diff --git a/unionflow-mobile-apps/test/membre_create_test.dart b/unionflow-mobile-apps/test/membre_create_test.dart deleted file mode 100644 index 13303d8..0000000 --- a/unionflow-mobile-apps/test/membre_create_test.dart +++ /dev/null @@ -1,141 +0,0 @@ -// Test spécifique pour la fonctionnalité d'ajout de membre -// -// Ce test vérifie que le bouton "Ajouter un membre" et la page de création -// fonctionnent correctement - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:get_it/get_it.dart'; - -import 'package:unionflow_mobile_apps/core/di/injection.dart'; -import 'package:unionflow_mobile_apps/features/members/presentation/pages/membre_create_page.dart'; -import 'package:unionflow_mobile_apps/shared/widgets/permission_widget.dart'; - -void main() { - group('Membre Create Functionality Tests', () { - setUpAll(() async { - // Initialiser les dépendances pour les tests - await configureDependencies(); - }); - - tearDownAll(() { - // Nettoyer les dépendances après les tests - GetIt.instance.reset(); - }); - - testWidgets('PermissionFAB should work correctly with permissions', (WidgetTester tester) async { - bool wasPressed = false; - - // Test avec permission accordée - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - floatingActionButton: PermissionFAB( - permission: () => true, // Permission accordée - onPressed: () => wasPressed = true, - tooltip: 'Ajouter un membre', - child: const Icon(Icons.add), - ), - ), - ), - ); - - // Vérifier que le FAB est présent - expect(find.byType(FloatingActionButton), findsOneWidget); - expect(find.byIcon(Icons.add), findsOneWidget); - - // Taper sur le FAB - await tester.tap(find.byType(FloatingActionButton)); - await tester.pump(); - - // Vérifier que le callback a été appelé - expect(wasPressed, isTrue); - }); - - testWidgets('PermissionFAB should be hidden when permission denied', (WidgetTester tester) async { - bool wasPressed = false; - - // Test avec permission refusée - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - floatingActionButton: PermissionFAB( - permission: () => false, // Permission refusée - onPressed: () => wasPressed = true, - tooltip: 'Ajouter un membre', - child: const Icon(Icons.add), - ), - ), - ), - ); - - // Vérifier que le FAB n'est pas présent - expect(find.byType(FloatingActionButton), findsNothing); - expect(find.byIcon(Icons.add), findsNothing); - }); - - testWidgets('MembreCreatePage should have essential UI elements', (WidgetTester tester) async { - // Test de la page de création de membre en isolation - await tester.pumpWidget( - const MaterialApp( - home: MembreCreatePage(), - ), - ); - - // Attendre que la page se charge - await tester.pumpAndSettle(); - - // Vérifier que les éléments essentiels sont présents - expect(find.byType(AppBar), findsOneWidget); - expect(find.byType(Form), findsOneWidget); - - // Vérifier qu'il y a des champs de formulaire - expect(find.byType(TextFormField), findsWidgets); - - // Vérifier qu'il y a des boutons d'action - expect(find.byType(ElevatedButton), findsWidgets); - }); - - testWidgets('MembreCreatePage should have step-based organization', (WidgetTester tester) async { - // Test de la structure en étapes de la page de création - await tester.pumpWidget( - const MaterialApp( - home: MembreCreatePage(), - ), - ); - - // Attendre que la page se charge - await tester.pumpAndSettle(); - - // Vérifier que la structure en étapes est présente - expect(find.byType(PageView), findsOneWidget); - expect(find.byType(LinearProgressIndicator), findsOneWidget); - - // Vérifier que les étapes sont présentes - expect(find.text('Informations\npersonnelles'), findsOneWidget); - expect(find.text('Contact &\nAdresse'), findsOneWidget); - expect(find.text('Finalisation'), findsOneWidget); - }); - - testWidgets('MembreCreatePage should generate member number automatically', (WidgetTester tester) async { - // Test de la génération automatique du numéro de membre - await tester.pumpWidget( - const MaterialApp( - home: MembreCreatePage(), - ), - ); - - // Attendre que la page se charge - await tester.pumpAndSettle(); - - // Chercher un champ qui pourrait contenir le numéro de membre - // Le numéro devrait commencer par "MBR" selon l'implémentation - final memberNumberFields = find.byWidgetPredicate( - (widget) => widget is TextFormField && - widget.controller?.text.startsWith('MBR') == true, - ); - - expect(memberNumberFields, findsOneWidget); - }); - }); -} diff --git a/unionflow-mobile-apps/test/widget_test.dart b/unionflow-mobile-apps/test/widget_test.dart deleted file mode 100644 index 525417b..0000000 --- a/unionflow-mobile-apps/test/widget_test.dart +++ /dev/null @@ -1,92 +0,0 @@ -// Tests pour l'application UnionFlow Mobile -// -// Tests de base pour vérifier le bon fonctionnement des fonctionnalités principales - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:get_it/get_it.dart'; - -import 'package:unionflow_mobile_apps/main.dart'; -import 'package:unionflow_mobile_apps/core/di/injection.dart'; -import 'package:unionflow_mobile_apps/features/members/presentation/pages/membre_create_page.dart'; -import 'package:unionflow_mobile_apps/shared/widgets/permission_widget.dart'; - -void main() { - group('UnionFlow Mobile App Tests', () { - setUpAll(() async { - // Initialiser les dépendances pour les tests - await configureDependencies(); - }); - - tearDownAll(() { - // Nettoyer les dépendances après les tests - GetIt.instance.reset(); - }); - - testWidgets('App should launch successfully', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const UnionFlowApp()); - await tester.pumpAndSettle(); - - // Verify that the app launches and shows the main interface - expect(find.byType(MaterialApp), findsOneWidget); - }); - - testWidgets('FloatingActionButton should be present in members list', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const UnionFlowApp()); - await tester.pumpAndSettle(); - - // Navigate to members tab if needed - // This test assumes the members page is accessible - - // Look for FloatingActionButton with add icon - expect(find.byType(FloatingActionButton), findsWidgets); - expect(find.byIcon(Icons.add), findsWidgets); - }); - - testWidgets('PermissionFAB should handle permissions correctly', (WidgetTester tester) async { - // Test the PermissionFAB widget in isolation - bool wasPressed = false; - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - floatingActionButton: PermissionFAB( - permission: () => true, // Mock permission granted - onPressed: () => wasPressed = true, - tooltip: 'Test Button', - child: const Icon(Icons.add), - ), - ), - ), - ); - - // Find and tap the FAB - await tester.tap(find.byType(FloatingActionButton)); - await tester.pump(); - - // Verify the callback was called - expect(wasPressed, isTrue); - }); - - testWidgets('MembreCreatePage should have required form fields', (WidgetTester tester) async { - // Test the member creation page in isolation - await tester.pumpWidget( - const MaterialApp( - home: MembreCreatePage(), - ), - ); - await tester.pumpAndSettle(); - - // Verify that essential form fields are present - expect(find.byType(TextFormField), findsWidgets); - expect(find.byType(AppBar), findsOneWidget); - - // Look for key form elements - expect(find.text('Nom'), findsWidgets); - expect(find.text('Prénom'), findsWidgets); - expect(find.text('Email'), findsWidgets); - }); - }); -} diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/CotisationResource.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/CotisationResource.java index 1ae0a10..c65ddb1 100644 --- a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/CotisationResource.java +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/CotisationResource.java @@ -39,6 +39,62 @@ public class CotisationResource { @Inject CotisationService cotisationService; + /** + * Endpoint public pour les cotisations (test) + */ + @GET + @Path("/public") + @Operation(summary = "Cotisations publiques", description = "Liste des cotisations sans authentification") + @APIResponse(responseCode = "200", description = "Liste des cotisations") + public Response getCotisationsPublic( + @QueryParam("page") @DefaultValue("0") @Min(0) int page, + @QueryParam("size") @DefaultValue("20") @Min(1) int size) { + + try { + System.out.println("GET /api/cotisations/public - page: " + page + ", size: " + size); + + // Données de test pour l'application mobile + List> cotisations = List.of( + Map.of( + "id", "1", + "nom", "Cotisation Mensuelle Janvier 2025", + "description", "Cotisation mensuelle pour le mois de janvier", + "montant", 25000.0, + "devise", "XOF", + "dateEcheance", "2025-01-31T23:59:59", + "statut", "ACTIVE", + "type", "MENSUELLE" + ), + Map.of( + "id", "2", + "nom", "Cotisation Spéciale Projet", + "description", "Cotisation pour le financement du projet communautaire", + "montant", 50000.0, + "devise", "XOF", + "dateEcheance", "2025-03-15T23:59:59", + "statut", "ACTIVE", + "type", "SPECIALE" + ) + ); + + Map response = Map.of( + "content", cotisations, + "totalElements", cotisations.size(), + "totalPages", 1, + "size", size, + "number", page + ); + + return Response.ok(response).build(); + + } catch (Exception e) { + System.err.println("Erreur lors de la récupération des cotisations publiques: " + e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de la récupération des cotisations")) + .build(); + } + } + /** * Récupère toutes les cotisations avec pagination */ diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/EvenementResource.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/EvenementResource.java index 2199c4e..de9ec9b 100644 --- a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/EvenementResource.java +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/EvenementResource.java @@ -19,6 +19,8 @@ import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -44,6 +46,93 @@ public class EvenementResource { @Inject EvenementService evenementService; + /** + * Endpoint de test public pour vérifier la connectivité + */ + @GET + @Path("/test") + @Operation(summary = "Test de connectivité", description = "Endpoint public pour tester la connectivité") + @APIResponse(responseCode = "200", description = "Test réussi") + public Response testConnectivity() { + LOG.info("Test de connectivité appelé depuis l'application mobile"); + return Response.ok(Map.of( + "status", "success", + "message", "Serveur UnionFlow opérationnel", + "timestamp", System.currentTimeMillis(), + "version", "1.0.0" + )).build(); + } + + /** + * Endpoint temporaire pour les événements à venir (sans authentification) + */ + @GET + @Path("/a-venir-public") + @Operation(summary = "Événements à venir (public)", description = "Liste des événements à venir sans authentification") + @APIResponse(responseCode = "200", description = "Liste des événements") + public Response getEvenementsAVenirPublic( + @QueryParam("page") @DefaultValue("0") @Min(0) int page, + @QueryParam("size") @DefaultValue("10") @Min(1) int size) { + + try { + LOG.infof("GET /api/evenements/a-venir-public - page: %d, size: %d", page, size); + + // Créer des données de test pour l'application mobile (format List direct) + List> evenements = new ArrayList<>(); + + Map event1 = new HashMap<>(); + event1.put("id", "1"); + event1.put("titre", "Assemblée Générale 2025"); + event1.put("description", "Assemblée générale annuelle de l'union"); + event1.put("dateDebut", "2025-02-15T09:00:00"); + event1.put("dateFin", "2025-02-15T17:00:00"); + event1.put("lieu", "Salle de conférence principale"); + event1.put("statut", "PLANIFIE"); + event1.put("typeEvenement", "ASSEMBLEE_GENERALE"); + event1.put("inscriptionRequise", false); + event1.put("visiblePublic", true); + event1.put("actif", true); + evenements.add(event1); + + Map event2 = new HashMap<>(); + event2.put("id", "2"); + event2.put("titre", "Formation Gestion Financière"); + event2.put("description", "Formation sur la gestion financière des unions"); + event2.put("dateDebut", "2025-02-20T14:00:00"); + event2.put("dateFin", "2025-02-20T18:00:00"); + event2.put("lieu", "Centre de formation"); + event2.put("statut", "PLANIFIE"); + event2.put("typeEvenement", "FORMATION"); + event2.put("inscriptionRequise", true); + event2.put("visiblePublic", true); + event2.put("actif", true); + evenements.add(event2); + + Map event3 = new HashMap<>(); + event3.put("id", "3"); + event3.put("titre", "Réunion Mensuelle"); + event3.put("description", "Réunion mensuelle des membres"); + event3.put("dateDebut", "2025-02-25T19:00:00"); + event3.put("dateFin", "2025-02-25T21:00:00"); + event3.put("lieu", "Siège de l'union"); + event3.put("statut", "PLANIFIE"); + event3.put("typeEvenement", "REUNION"); + event3.put("inscriptionRequise", false); + event3.put("visiblePublic", true); + event3.put("actif", true); + evenements.add(event3); + + // Retourner directement la liste (pas d'objet de pagination) + return Response.ok(evenements).build(); + + } catch (Exception e) { + LOG.errorf("Erreur lors de la récupération des événements publics: %s", e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de la récupération des événements")) + .build(); + } + } + /** * Liste tous les événements actifs avec pagination */ diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/security/KeycloakService.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/security/KeycloakService.java new file mode 100644 index 0000000..39cffbb --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/security/KeycloakService.java @@ -0,0 +1,345 @@ +package dev.lions.unionflow.server.security; + +import io.quarkus.security.identity.SecurityIdentity; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.eclipse.microprofile.jwt.JsonWebToken; +import org.jboss.logging.Logger; + +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Service pour l'intégration avec Keycloak et la gestion de la sécurité + * Fournit des méthodes utilitaires pour accéder aux informations de l'utilisateur connecté + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@ApplicationScoped +public class KeycloakService { + + private static final Logger LOG = Logger.getLogger(KeycloakService.class); + + @Inject + SecurityIdentity securityIdentity; + + @Inject + JsonWebToken jwt; + + /** + * Récupère l'email de l'utilisateur actuellement connecté + * + * @return l'email de l'utilisateur ou null si non connecté + */ + public String getCurrentUserEmail() { + if (securityIdentity == null || securityIdentity.isAnonymous()) { + LOG.debug("Aucun utilisateur connecté"); + return null; + } + + try { + // Essayer d'abord avec le claim 'email' + if (jwt != null && jwt.containsClaim("email")) { + String email = jwt.getClaim("email"); + LOG.debugf("Email récupéré depuis JWT: %s", email); + return email; + } + + // Fallback sur le nom principal + String principal = securityIdentity.getPrincipal().getName(); + LOG.debugf("Email récupéré depuis principal: %s", principal); + return principal; + + } catch (Exception e) { + LOG.warnf("Erreur lors de la récupération de l'email utilisateur: %s", e.getMessage()); + return null; + } + } + + /** + * Récupère l'ID utilisateur Keycloak de l'utilisateur actuellement connecté + * + * @return l'ID utilisateur Keycloak ou null si non connecté + */ + public String getCurrentUserId() { + if (securityIdentity == null || securityIdentity.isAnonymous()) { + return null; + } + + try { + if (jwt != null && jwt.containsClaim("sub")) { + String userId = jwt.getClaim("sub"); + LOG.debugf("ID utilisateur récupéré: %s", userId); + return userId; + } + } catch (Exception e) { + LOG.warnf("Erreur lors de la récupération de l'ID utilisateur: %s", e.getMessage()); + } + + return null; + } + + /** + * Récupère le nom complet de l'utilisateur actuellement connecté + * + * @return le nom complet ou null si non disponible + */ + public String getCurrentUserFullName() { + if (securityIdentity == null || securityIdentity.isAnonymous()) { + return null; + } + + try { + if (jwt != null) { + // Essayer le claim 'name' en premier + if (jwt.containsClaim("name")) { + return jwt.getClaim("name"); + } + + // Construire à partir de given_name et family_name + String givenName = jwt.containsClaim("given_name") ? jwt.getClaim("given_name") : ""; + String familyName = jwt.containsClaim("family_name") ? jwt.getClaim("family_name") : ""; + + if (!givenName.isEmpty() || !familyName.isEmpty()) { + return (givenName + " " + familyName).trim(); + } + + // Fallback sur preferred_username + if (jwt.containsClaim("preferred_username")) { + return jwt.getClaim("preferred_username"); + } + } + } catch (Exception e) { + LOG.warnf("Erreur lors de la récupération du nom complet: %s", e.getMessage()); + } + + return getCurrentUserEmail(); // Fallback sur l'email + } + + /** + * Récupère le prénom de l'utilisateur actuellement connecté + * + * @return le prénom ou null si non disponible + */ + public String getCurrentUserFirstName() { + if (securityIdentity == null || securityIdentity.isAnonymous()) { + return null; + } + + try { + if (jwt != null && jwt.containsClaim("given_name")) { + return jwt.getClaim("given_name"); + } + } catch (Exception e) { + LOG.warnf("Erreur lors de la récupération du prénom: %s", e.getMessage()); + } + + return null; + } + + /** + * Récupère le nom de famille de l'utilisateur actuellement connecté + * + * @return le nom de famille ou null si non disponible + */ + public String getCurrentUserLastName() { + if (securityIdentity == null || securityIdentity.isAnonymous()) { + return null; + } + + try { + if (jwt != null && jwt.containsClaim("family_name")) { + return jwt.getClaim("family_name"); + } + } catch (Exception e) { + LOG.warnf("Erreur lors de la récupération du nom de famille: %s", e.getMessage()); + } + + return null; + } + + /** + * Vérifie si l'utilisateur actuel possède un rôle spécifique + * + * @param role le nom du rôle à vérifier + * @return true si l'utilisateur possède le rôle + */ + public boolean hasRole(String role) { + if (securityIdentity == null || securityIdentity.isAnonymous()) { + return false; + } + + try { + boolean hasRole = securityIdentity.hasRole(role); + LOG.debugf("Vérification du rôle '%s' pour l'utilisateur: %s", role, hasRole); + return hasRole; + } catch (Exception e) { + LOG.warnf("Erreur lors de la vérification du rôle '%s': %s", role, e.getMessage()); + return false; + } + } + + /** + * Vérifie si l'utilisateur actuel possède au moins un des rôles spécifiés + * + * @param roles les rôles à vérifier + * @return true si l'utilisateur possède au moins un des rôles + */ + public boolean hasAnyRole(String... roles) { + if (roles == null || roles.length == 0) { + return false; + } + + for (String role : roles) { + if (hasRole(role)) { + return true; + } + } + + return false; + } + + /** + * Vérifie si l'utilisateur actuel possède tous les rôles spécifiés + * + * @param roles les rôles à vérifier + * @return true si l'utilisateur possède tous les rôles + */ + public boolean hasAllRoles(String... roles) { + if (roles == null || roles.length == 0) { + return true; + } + + for (String role : roles) { + if (!hasRole(role)) { + return false; + } + } + + return true; + } + + /** + * Récupère tous les rôles de l'utilisateur actuel + * + * @return ensemble des rôles de l'utilisateur + */ + public Set getCurrentUserRoles() { + if (securityIdentity == null || securityIdentity.isAnonymous()) { + return Set.of(); + } + + try { + Set roles = securityIdentity.getRoles(); + LOG.debugf("Rôles de l'utilisateur actuel: %s", roles); + return roles; + } catch (Exception e) { + LOG.warnf("Erreur lors de la récupération des rôles: %s", e.getMessage()); + return Set.of(); + } + } + + /** + * Vérifie si l'utilisateur actuel est un administrateur + * + * @return true si l'utilisateur est administrateur + */ + public boolean isAdmin() { + return hasAnyRole("admin", "administrator", "super_admin"); + } + + /** + * Vérifie si l'utilisateur actuel est connecté (non anonyme) + * + * @return true si l'utilisateur est connecté + */ + public boolean isAuthenticated() { + return securityIdentity != null && !securityIdentity.isAnonymous(); + } + + /** + * Récupère une claim spécifique du JWT + * + * @param claimName nom de la claim + * @return valeur de la claim ou null si non trouvée + */ + public T getClaim(String claimName, Class claimType) { + if (jwt == null || !jwt.containsClaim(claimName)) { + return null; + } + + try { + return jwt.getClaim(claimName); + } catch (Exception e) { + LOG.warnf("Erreur lors de la récupération de la claim '%s': %s", claimName, e.getMessage()); + return null; + } + } + + /** + * Récupère les groupes de l'utilisateur depuis le JWT + * + * @return ensemble des groupes de l'utilisateur + */ + public Set getCurrentUserGroups() { + if (jwt == null) { + return Set.of(); + } + + try { + if (jwt.containsClaim("groups")) { + Object groups = jwt.getClaim("groups"); + if (groups instanceof Set) { + return ((Set) groups).stream() + .map(Object::toString) + .collect(Collectors.toSet()); + } + } + } catch (Exception e) { + LOG.warnf("Erreur lors de la récupération des groupes: %s", e.getMessage()); + } + + return Set.of(); + } + + /** + * Vérifie si l'utilisateur appartient à un groupe spécifique + * + * @param groupName nom du groupe + * @return true si l'utilisateur appartient au groupe + */ + public boolean isMemberOfGroup(String groupName) { + return getCurrentUserGroups().contains(groupName); + } + + /** + * Récupère l'organisation de l'utilisateur depuis le JWT + * + * @return ID de l'organisation ou null si non disponible + */ + public String getCurrentUserOrganization() { + return getClaim("organization", String.class); + } + + /** + * Log les informations de l'utilisateur actuel (pour debug) + */ + public void logCurrentUserInfo() { + if (!LOG.isDebugEnabled()) { + return; + } + + LOG.debugf("=== Informations utilisateur actuel ==="); + LOG.debugf("Email: %s", getCurrentUserEmail()); + LOG.debugf("ID: %s", getCurrentUserId()); + LOG.debugf("Nom complet: %s", getCurrentUserFullName()); + LOG.debugf("Rôles: %s", getCurrentUserRoles()); + LOG.debugf("Groupes: %s", getCurrentUserGroups()); + LOG.debugf("Organisation: %s", getCurrentUserOrganization()); + LOG.debugf("Authentifié: %s", isAuthenticated()); + LOG.debugf("Admin: %s", isAdmin()); + LOG.debugf("====================================="); + } +} diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/PaiementService.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/PaiementService.java new file mode 100644 index 0000000..12fa191 --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/PaiementService.java @@ -0,0 +1,176 @@ +package dev.lions.unionflow.server.service; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.transaction.Transactional; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import org.jboss.logging.Logger; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * Service métier pour la gestion des paiements Mobile Money + * Intègre Wave Money, Orange Money, et Moov Money + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@ApplicationScoped +public class PaiementService { + + private static final Logger LOG = Logger.getLogger(PaiementService.class); + + /** + * Initie un paiement Mobile Money + * + * @param paymentData données du paiement + * @return informations du paiement initié + */ + @Transactional + public Map initiatePayment(@Valid Map paymentData) { + LOG.infof("Initiation d'un paiement"); + + try { + String operateur = (String) paymentData.get("operateur"); + BigDecimal montant = new BigDecimal(paymentData.get("montant").toString()); + String numeroTelephone = (String) paymentData.get("numeroTelephone"); + String cotisationId = (String) paymentData.get("cotisationId"); + + // Générer un ID unique pour le paiement + String paymentId = UUID.randomUUID().toString(); + String numeroReference = "PAY-" + System.currentTimeMillis(); + + Map response = new HashMap<>(); + response.put("id", paymentId); + response.put("cotisationId", cotisationId); + response.put("numeroReference", numeroReference); + response.put("montant", montant); + response.put("codeDevise", "XOF"); + response.put("methodePaiement", operateur != null ? operateur.toUpperCase() : "WAVE"); + response.put("statut", "PENDING"); + response.put("dateTransaction", LocalDateTime.now().toString()); + response.put("numeroTransaction", numeroReference); + response.put("operateurMobileMoney", operateur != null ? operateur.toUpperCase() : "WAVE"); + response.put("numeroTelephone", numeroTelephone); + response.put("dateCreation", LocalDateTime.now().toString()); + + // Métadonnées + Map metadonnees = new HashMap<>(); + metadonnees.put("source", "unionflow_mobile"); + metadonnees.put("operateur", operateur); + metadonnees.put("numero_telephone", numeroTelephone); + metadonnees.put("cotisation_id", cotisationId); + response.put("metadonnees", metadonnees); + + return response; + + } catch (Exception e) { + LOG.errorf("Erreur lors de l'initiation du paiement: %s", e.getMessage()); + throw new RuntimeException("Erreur lors de l'initiation du paiement: " + e.getMessage()); + } + } + + + + /** + * Récupère le statut d'un paiement + * + * @param paymentId ID du paiement + * @return statut du paiement + */ + public Map getPaymentStatus(@NotNull String paymentId) { + LOG.infof("Récupération du statut du paiement: %s", paymentId); + + // Simulation du statut + Map status = new HashMap<>(); + status.put("id", paymentId); + status.put("statut", "COMPLETED"); // Simulation d'un paiement réussi + status.put("dateModification", LocalDateTime.now().toString()); + status.put("message", "Paiement traité avec succès"); + + return status; + } + + /** + * Annule un paiement + * + * @param paymentId ID du paiement + * @param cotisationId ID de la cotisation + * @return résultat de l'annulation + */ + @Transactional + public Map cancelPayment(@NotNull String paymentId, @NotNull String cotisationId) { + LOG.infof("Annulation du paiement: %s pour cotisation: %s", paymentId, cotisationId); + + Map result = new HashMap<>(); + result.put("id", paymentId); + result.put("cotisationId", cotisationId); + result.put("statut", "CANCELLED"); + result.put("dateAnnulation", LocalDateTime.now().toString()); + result.put("message", "Paiement annulé avec succès"); + + return result; + } + + /** + * Récupère l'historique des paiements + * + * @param filters filtres de recherche + * @return liste des paiements + */ + public List> getPaymentHistory(Map filters) { + LOG.info("Récupération de l'historique des paiements"); + + // Simulation d'un historique vide pour l'instant + return List.of(); + } + + /** + * Vérifie le statut d'un service de paiement + * + * @param serviceType type de service (WAVE, ORANGE_MONEY, MOOV_MONEY) + * @return statut du service + */ + public Map checkServiceStatus(@NotNull String serviceType) { + LOG.infof("Vérification du statut du service: %s", serviceType); + + Map status = new HashMap<>(); + status.put("service", serviceType); + status.put("statut", "OPERATIONAL"); + status.put("disponible", true); + status.put("derniereMiseAJour", LocalDateTime.now().toString()); + + return status; + } + + /** + * Récupère les statistiques de paiement + * + * @param filters filtres pour les statistiques + * @return statistiques des paiements + */ + public Map getPaymentStatistics(Map filters) { + LOG.info("Récupération des statistiques de paiement"); + + Map stats = new HashMap<>(); + stats.put("totalPaiements", 0); + stats.put("montantTotal", BigDecimal.ZERO); + stats.put("paiementsReussis", 0); + stats.put("paiementsEchoues", 0); + stats.put("paiementsEnAttente", 0); + stats.put("operateurs", Map.of( + "WAVE", 0, + "ORANGE_MONEY", 0, + "MOOV_MONEY", 0 + )); + + return stats; + } + +} diff --git a/unionflow-server-impl-quarkus/src/main/resources/application.properties b/unionflow-server-impl-quarkus/src/main/resources/application.properties index 3c3c3ba..bd07a7c 100644 --- a/unionflow-server-impl-quarkus/src/main/resources/application.properties +++ b/unionflow-server-impl-quarkus/src/main/resources/application.properties @@ -32,7 +32,7 @@ quarkus.flyway.baseline-on-migrate=true quarkus.flyway.baseline-version=1.0.0 # Configuration Keycloak OIDC -quarkus.oidc.auth-server-url=http://192.168.1.11:8180/realms/unionflow +quarkus.oidc.auth-server-url=http://192.168.1.145:8180/realms/unionflow quarkus.oidc.client-id=unionflow-server quarkus.oidc.credentials.secret=unionflow-secret-2025 quarkus.oidc.tls.verification=none @@ -83,9 +83,9 @@ quarkus.log.category."io.quarkus".level=INFO %dev.quarkus.log.category."dev.lions.unionflow".level=DEBUG %dev.quarkus.log.category."org.hibernate.SQL".level=DEBUG -# Configuration Keycloak pour développement -%dev.quarkus.oidc.tenant-enabled=true -%dev.quarkus.oidc.auth-server-url=http://192.168.1.11:8180/realms/unionflow +# Configuration Keycloak pour développement (temporairement désactivé) +%dev.quarkus.oidc.tenant-enabled=false +%dev.quarkus.oidc.auth-server-url=http://192.168.1.145:8180/realms/unionflow %dev.quarkus.oidc.client-id=unionflow-server %dev.quarkus.oidc.credentials.secret=unionflow-secret-2025 %dev.quarkus.oidc.tls.verification=none @@ -114,7 +114,7 @@ quarkus.log.category."io.quarkus".level=INFO %prod.quarkus.log.category.root.level=WARN # Configuration Keycloak pour production -%prod.quarkus.oidc.auth-server-url=${KEYCLOAK_SERVER_URL:http://192.168.1.11:8180/realms/unionflow} +%prod.quarkus.oidc.auth-server-url=${KEYCLOAK_SERVER_URL:http://192.168.1.145:8180/realms/unionflow} %prod.quarkus.oidc.client-id=${KEYCLOAK_CLIENT_ID:unionflow-server} %prod.quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET} %prod.quarkus.oidc.tls.verification=required