diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..c1b9cf1d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,21 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build Commands +- Flutter: `cd flutt && flutter run` - run the Flutter app +- Flutter tests: `cd flutt && flutter test [test_file_path]` - run specific test +- Flutter linting: `cd flutt && flutter analyze` - analyze Dart code +- Flutter build: `cd flutt && flutter build [platform]` - build for specific platform +- Web: `cd web && npm run dev` - run Svelte dev server +- Web build: `cd web && npm run build` - build web app for production + +## Code Style Guidelines +- Flutter/Dart: Follow Flutter lint rules in analysis_options.yaml +- Naming: camelCase for variables/methods, PascalCase for classes/enums +- Imports: Group imports by type (dart, flutter, third-party, project) +- Error handling: Use try/catch with specific error types +- Documentation: Add documentation comments for public APIs +- UI/Layout: Use responsive design principles (DashboardLayout widget) +- Types: Use strong typing, avoid dynamic when possible +- Architecture: Follow repository pattern for data access \ No newline at end of file diff --git a/CONTEXT-AI.md b/CONTEXT-AI.md new file mode 100644 index 00000000..25ccd3a0 --- /dev/null +++ b/CONTEXT-AI.md @@ -0,0 +1,224 @@ +# AI_CONTEXT.md - GEOSECTOR + +## Informations Générales + +- **Nom du Projet**: GEOSECTOR +- **Type de Projet**: Projet hybride avec un site web en Svelte 5, une application Web et mobile Flutter et une API PHP8.3 avec base de données centrale MariaDB +- **Description**: Gestion de la distribution des calendriers pour les amicales de pompiers +- **Date de Début**: 01/03/2025 +- **Statut Actuel**: [En développement] +- **Lien GitLab**: [http://51.68.36.203:35788/d6soft/geosector] + +## Structure du Projet + +### Architecture Générale + +- Un dossier /web pour le site web en Svelte 5 + Vite et TailwindCSS sans SvelteKit +- Un dossier /app pour l'application Flutter avec Hive pour stocker en local les données de l'utilisateur +- Un dossier /api pour l'API PHP 8.3 avec une base de données centrale MariaDB 11 + +### Structure des Dossiers + +#### Application Flutter (/app) + +- **/lib/core/data/models/** : Modèles de données avec annotations Hive pour le stockage local +- **/lib/core/repositories/** : Gestion des données et communication avec l'API +- **/lib/core/services/** : Services d'API, de synchronisation et utilitaires +- **/lib/core/constants/** : Constantes de l'application, clés API et endpoints +- **/lib/presentation/** : Interface utilisateur, pages et widgets +- **/lib/chat/** : Fonctionnalités de messagerie + +#### API PHP (/api) + +- **/src/Controllers/** : Contrôleurs pour les différentes fonctionnalités +- **/src/Core/** : Classes de base pour la gestion des requêtes, sessions, etc. +- **/src/Services/** : Services pour l'authentification, le chiffrement, etc. + +### Composants Principaux + +#### Gestion des Utilisateurs et Authentification + +- **UserRepository** : Gère les utilisateurs, l'authentification et les sessions +- **ApiService** : Communication avec l'API, gestion des tokens de session +- **Session** (PHP) : Gestion des sessions côté serveur +- **LoginController** (PHP) : Authentification et déconnexion + +#### Stockage Local avec Hive + +- **UserModel** : Modèle utilisateur avec annotations Hive (typeId: 0) +- **AmicaleModel** : Modèle pour les amicales (typeId: 11) +- **OperationModel**, **SectorModel**, **PassageModel** : Modèles pour les opérations de terrain + +#### Interface Utilisateur + +- **LoginPage** : Page de connexion avec détection du type d'utilisateur +- **SplashPage** : Page de démarrage et initialisation + +## Technologies et Frameworks + +### Langages de Programmation + +- **Dart** : Pour l'application Flutter +- **PHP 8.3** : Pour l'API backend +- **JavaScript/TypeScript** : Pour le site web Svelte + +### Frameworks et Bibliothèques + +- **Flutter** : Framework UI cross-platform +- **Hive** : Base de données NoSQL locale pour Flutter +- **Dio** : Client HTTP pour les requêtes API +- **GoRouter** : Navigation et routage dans Flutter +- **Svelte 5** : Framework UI pour le site web + +### Base de Données + +- **Type**: MariaDB +- **Version**: 11 +- **Schéma**: Structure principale dans `docs/geo_app.dump` avec tables pour utilisateurs, opérations, secteurs, passages +- **Stockage Local**: Hive pour le stockage local dans l'application Flutter + +### Outils de Développement + +- **Gestionnaire de Paquets**: [Composer] +- **Outils de Build**: [Webpack / Vite / Flutter CLI] +- **Outils de Test**: [PHPUnit / Jest / Flutter Test] +- **Linters/Formatters**: [PHP_CodeSniffer / ESLint / Dart Analyzer] + +## Conventions de Code + +### Style de Code + +- [PSR-12 pour PHP] + +### Conventions de Nommage + +- **Classes**: [PascalCase] +- **Méthodes/Fonctions**: [camelCase] +- **Variables**: [camelCase] +- **Constantes**: [UPPER_SNAKE_CASE] +- **Fichiers**: [kebab-case.ext / PascalCase.ext] + +### Pratiques Spécifiques au Projet + +[Toute convention ou pratique spécifique à ce projet] + +## Flux de Travail et Processus de Développement + +### Branches GitLab + +- **main/master**: [Production-ready code] +- **develop**: [Integration branch for features] +- **feature/[feature-name]**: [Feature development] +- **bugfix/[bug-name]**: [Bug fixes] +- **release/[version]**: [Release preparation] + +### Processus de Merge Request + +1. [Créer une branche à partir de develop] +2. [Développer la fonctionnalité/correction] +3. [Soumettre une MR vers develop] +4. [Code review] +5. [CI/CD validation] +6. [Merge] + +### CI/CD Pipeline + +[Description de votre pipeline CI/CD dans GitLab] + +## Intégration avec GitLab + +### Issues et Kanban + +- **Labels**: [Liste des labels principaux et leur signification] +- **Milestones**: [Comment les milestones sont utilisées] +- **Boards**: [Description des tableaux Kanban] + +### Automatisations + +[Description des automatisations GitLab utilisées] + +## Déploiement + +### Environnements + +- Un environnement DEV dans un container Incus Alpine distant dva-geo +- Un environnement RECETTE dans un container Incus Alpine distant rca-geo +- Un environnement PROD dans un container Incus Alpine distant pra-geo + +### Processus de Déploiement + +- Un script /web/deploy-web.sh pour déployer le site Web sur l'environnement DEV +- Un script /app/deploy-app.sh pour déployer l'application Flutter Web sur l'environnement DEV +- Un script /api/deploy-api.sh pour déployer l'API PHP sur l'environnement DEV + +- Un script /web/livre-web.sh $0 $1 pour livrer le site web d'un environnement $0 à l'autre $1 +- Un script /app/livre-app.sh $0 $1 pour livrer l'application Flutter Web d'un environnement $0 à l'autre $1 +- Un script /api/livre-api.sh $0 $1 pour livrer l'API PHP d'un environnement $0 à l'autre $1 + +## Ressources et Documentation + +### Documentation Interne + +- [Liens vers la documentation interne] + +### API Documentation + +- [Liens vers la documentation API (Swagger/OpenAPI)] + +### Ressources Externes + +- [Liens vers des ressources externes pertinentes] + +## Contacts + +### Équipe Principale + +- **[Nom]**: [Rôle] - [Email/GitLab username] + +### Parties Prenantes + +- **[Nom]**: [Rôle/Organisation] - [Contact] + +## Historique des Versions + +| Version | Date | Description | +| ------- | ------ | ------------- | +| 1.0.0 | [Date] | [Description] | + +## Processus d'Authentification et Gestion des Sessions + +### Flux de Connexion + +1. L'utilisateur entre ses identifiants dans la page de login (username/password) +2. L'application envoie une requête POST à `/api/login` avec les identifiants et le type de connexion (user/admin) +3. Le serveur vérifie les identifiants, crée une session PHP et renvoie: + - Un `session_id` (utilisé comme token Bearer) + - Une date d'expiration de session + - Les données de l'utilisateur et les données associées (opérations, secteurs, passages) +4. L'application stocke ces données dans des boîtes Hive locales +5. Le `session_id` est utilisé pour toutes les requêtes API suivantes + +### Flux de Déconnexion + +1. L'utilisateur demande une déconnexion +2. L'application envoie une requête POST à `/api/logout` avec le `session_id` dans l'en-tête +3. Le serveur détruit la session PHP avec `session_unset()` et `session_destroy()` +4. L'application: + - Vide toutes les boîtes Hive sauf la boîte utilisateur + - Conserve uniquement le username et le rôle de l'utilisateur pour faciliter la reconnexion + - Réinitialise le `session_id` à null + +### Particularités + +- La page de login vérifie le rôle de l'utilisateur avant de pré-remplir le champ username +- Le type de connexion (user/admin) détermine les données chargées et les droits d'accès +- Les utilisateurs avec rôle=1 sont des utilisateurs standards, ceux avec rôle>1 sont des administrateurs +- Les sessions expirent après 24 heures par défaut + +## Notes Spécifiques pour les Assistants IA + +- Toujours vérifier les issues GitLab avant de proposer des solutions +- Respecter strictement les conventions de code mentionnées ci-dessus +- Lors de modifications des modèles Hive, s'assurer que les typeId sont uniques pour éviter les conflits +- Vérifier la compatibilité des modifications avec les trois plateformes (web, iOS, Android) +- Pour les modifications de l'API, s'assurer que la réponse reste compatible avec le format attendu par l'application diff --git a/api b/api new file mode 160000 index 00000000..f1dc7122 --- /dev/null +++ b/api @@ -0,0 +1 @@ +Subproject commit f1dc712215ba609c3f9a39f1b4e021e00d0247fb diff --git a/flutt/.cline b/app/.cline similarity index 100% rename from flutt/.cline rename to app/.cline diff --git a/flutt/.env-backup b/app/.env-backup similarity index 100% rename from flutt/.env-backup rename to app/.env-backup diff --git a/flutt/.env-deploy-dev b/app/.env-deploy-dev similarity index 62% rename from flutt/.env-deploy-dev rename to app/.env-deploy-dev index 65d89532..1bcbd50e 100644 --- a/flutt/.env-deploy-dev +++ b/app/.env-deploy-dev @@ -1,15 +1,15 @@ # Paramètres de connexion au host Debian 12 -HOST_SSH_USER=debian -HOST_SSH_HOST=145.239.9.105 +HOST_SSH_USER=pierre +HOST_SSH_HOST=195.154.80.116 HOST_SSH_PORT=22 HOST_SSH_KEY=/Users/pierre/.ssh/id_rsa_mbpi # Paramètres du container Incus -INCUS_PROJECT=DEV -INCUS_CONTAINER=d-apps +INCUS_PROJECT=default +INCUS_CONTAINER=dva-geo CONTAINER_USER=root USE_SUDO=true # Paramètres de déploiement -DEPLOY_TARGET_DIR=/var/www/geosector +DEPLOY_TARGET_DIR=/var/www/geosector/app FLUTTER_BUILD_DIR=build/web diff --git a/flutt/.gitignore b/app/.gitignore similarity index 100% rename from flutt/.gitignore rename to app/.gitignore diff --git a/flutt/.metadata b/app/.metadata similarity index 100% rename from flutt/.metadata rename to app/.metadata diff --git a/app/.vscode/settings.json b/app/.vscode/settings.json new file mode 100644 index 00000000..02f95d2d --- /dev/null +++ b/app/.vscode/settings.json @@ -0,0 +1,130 @@ +{ + "window.zoomLevel": 1, // Permet de zoomer, pratique si vous faites une présentation + // Apparence + // -- Editeur + "workbench.startupEditor": "none", // On ne veut pas une page d'accueil chargée + "editor.minimap.enabled": true, // On veut voir la minimap + "editor.minimap.showSlider": "always", // On veut voir la minimap + "editor.minimap.size": "fill", // On veut voir la minimap + "editor.minimap.scale": 2, + "editor.tokenColorCustomizations": { + "textMateRules": [ + { + "scope": [ + "storage.type.function", + "storage.type.class" + ], + "settings": { + "fontStyle": "bold", + "foreground": "#4B9CD3" + } + } + ] + }, + "editor.minimap.renderCharacters": true, + "editor.minimap.maxColumn": 120, + "breadcrumbs.enabled": false, + // -- Tabs + "workbench.editor.wrapTabs": true, // On veut voir les tabs + "workbench.editor.tabSizing": "shrink", // On veut voir les tabs + "workbench.editor.pinnedTabSizing": "compact", + "workbench.editor.enablePreview": false, // Un clic sur un fichier l'ouvre + // -- Sidebar + "workbench.tree.indent": 15, // Indente plus pour plus de clarté dans la sidebar + "workbench.tree.renderIndentGuides": "always", + // -- Code + "editor.occurrencesHighlight": "singleFile", // On veut voir les occurences d'une variable + "editor.renderWhitespace": "trailing", // On ne veut pas laisser d'espace en fin de ligne + "editor.renderControlCharacters": true, // On veut voir les caractères de contrôle + // Thème + "editor.fontFamily": "'JetBrains Mono', 'Fira Code', 'Operator Mono Lig', monospace", + "editor.fontLigatures": false, + "editor.fontSize": 13, + "editor.lineHeight": 22, + "editor.guides.bracketPairs": "active", + // Ergonomie + "editor.wordWrap": "off", + "editor.rulers": [], + "editor.suggest.insertMode": "replace", // L'autocomplétion remplace le mot en cours + "editor.acceptSuggestionOnCommitCharacter": false, // Evite que l'autocomplétion soit accepté lors d'un . par exemple + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "editor.linkedEditing": true, // Quand on change un élément HTML, change la balise fermante + "editor.tabSize": 2, + "editor.unicodeHighlight.nonBasicASCII": false, + "[php]": { + "editor.defaultFormatter": "bmewburn.vscode-intelephense-client", + "editor.formatOnSave": true, + "editor.formatOnPaste": true + }, + "intelephense.format.braces": "k&r", + "intelephense.format.enable": true, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.formatOnPaste": true + }, + "[dart]": { + "editor.formatOnSave": true, + "editor.formatOnType": true, + "editor.rulers": [ + 80 + ], + "editor.selectionHighlight": false, + "editor.tabCompletion": "onlySnippets", + "editor.wordBasedSuggestions": "off" + }, + "prettier.printWidth": 360, + "prettier.semi": true, + "prettier.singleQuote": true, + "prettier.tabWidth": 2, + "prettier.trailingComma": "es5", + "explorer.autoReveal": false, + "explorer.confirmDragAndDrop": false, + "emmet.triggerExpansionOnTab": true, + // Fichiers + "files.defaultLanguage": "markdown", + "files.autoSaveWorkspaceFilesOnly": true, + "files.exclude": { + "**/.idea": true + }, + // Languages + "javascript.preferences.importModuleSpecifierEnding": "js", + "typescript.preferences.importModuleSpecifierEnding": "js", + // Extensions + "tailwindCSS.experimental.configFile": "frontend/tailwind.config.js", + "editor.quickSuggestions": { + "strings": true + }, + "[svelte]": { + "editor.defaultFormatter": "svelte.svelte-vscode", + "editor.formatOnSave": true + }, + "prettier.documentSelectors": [ + "**/*.svelte" + ], + "svelte.plugin.svelte.diagnostics.enable": false, + "problems.decorations.enabled": false, + "js/ts.implicitProjectConfig.checkJs": false, + "svelte.enable-ts-plugin": false, + "workbench.colorCustomizations": { + "activityBar.activeBackground": "#405978", + "activityBar.background": "#405978", + "activityBar.foreground": "#e7e7e7", + "activityBar.inactiveForeground": "#e7e7e799", + "activityBarBadge.background": "#bc829c", + "activityBarBadge.foreground": "#15202b", + "commandCenter.border": "#e7e7e799", + "sash.hoverBorder": "#405978", + "statusBar.background": "#2e4057", + "statusBar.foreground": "#e7e7e7", + "statusBarItem.hoverBackground": "#405978", + "statusBarItem.remoteBackground": "#2e4057", + "statusBarItem.remoteForeground": "#e7e7e7", + "titleBar.activeBackground": "#2e4057", + "titleBar.activeForeground": "#e7e7e7", + "titleBar.inactiveBackground": "#2e405799", + "titleBar.inactiveForeground": "#e7e7e799" + }, + "peacock.color": "#2E4057", +} \ No newline at end of file diff --git a/flutt/.windsurfrules b/app/.windsurfrules similarity index 100% rename from flutt/.windsurfrules rename to app/.windsurfrules diff --git a/app/README-icons.md b/app/README-icons.md new file mode 100644 index 00000000..e2900ea5 --- /dev/null +++ b/app/README-icons.md @@ -0,0 +1,90 @@ +# Génération des icônes pour GEOSECTOR + +Ce document explique comment générer les icônes pour toutes les plateformes (Android, iOS, Web) à partir du fichier SVG source. + +## Prérequis + +- Flutter SDK installé et configuré +- ImageMagick installé (`brew install imagemagick`) +- Le fichier SVG source doit être présent dans `assets/images/icon-geosector.svg` + +## Utilisation du script de génération + +Pour générer toutes les icônes, exécutez simplement: + +```bash +# Rendre le script exécutable +chmod +x generate_icons.sh + +# Exécuter le script +./generate_icons.sh +``` + +Ce script effectuera les actions suivantes: + +1. Vérifier les dépendances nécessaires +2. Mettre à jour les dépendances Flutter +3. Générer les icônes principales avec `flutter_launcher_icons` +4. Générer les icônes supplémentaires pour le web (favicon et iOS) avec ImageMagick +5. Copier les icônes vers l'application web Svelte si elle existe + +## Configuration + +La configuration de génération des icônes est définie dans `pubspec.yaml` sous la section `flutter_launcher_icons`: + +```yaml +flutter_launcher_icons: + android: true + ios: true + image_path: 'assets/images/icon-geosector.svg' + min_sdk_android: 21 + adaptive_icon_background: '#FFFFFF' + adaptive_icon_foreground: 'assets/images/icon-geosector.svg' + remove_alpha_ios: true + web: + generate: true + image_path: 'assets/images/icon-geosector.svg' + background_color: '#FFFFFF' + theme_color: '#4B77BE' + windows: + generate: true + image_path: 'assets/images/icon-geosector.svg' + icon_size: 48 +``` + +## Icônes générées + +Le processus génère les fichiers suivants: + +### Android + +- `android/app/src/main/res/mipmap-*` - Icônes adaptatives pour diverses densités d'écran + +### iOS + +- `ios/Runner/Assets.xcassets/AppIcon.appiconset/` - Icônes pour diverses tailles d'appareils + +### Web + +- `web/icons/Icon-*.png` - Icônes PWA pour diverses tailles (192, 512, etc.) +- `web/favicon.png` et `web/favicon-*.png` - Favicons pour navigateurs +- `web/manifest.json` - Configuration PWA mise à jour + +### Windows (si applicable) + +- `windows/runner/resources/app_icon.ico` - Icône Windows + +## Personnalisation + +Pour personnaliser davantage le processus de génération: + +1. Modifiez `pubspec.yaml` pour changer les couleurs ou paramètres de base +2. Modifiez `generate_icons.sh` pour ajouter d'autres tailles ou formats d'icônes + +## Dépannage + +Si vous rencontrez des problèmes: + +1. Vérifiez que le fichier SVG source existe et est valide +2. Assurez-vous qu'ImageMagick est correctement installé +3. Vérifiez les droits d'accès aux répertoires cibles diff --git a/flutt/README.md b/app/README.md similarity index 100% rename from flutt/README.md rename to app/README.md diff --git a/flutt/add_framework_paths.rb b/app/add_framework_paths.rb similarity index 100% rename from flutt/add_framework_paths.rb rename to app/add_framework_paths.rb diff --git a/flutt/analysis_options.yaml b/app/analysis_options.yaml similarity index 100% rename from flutt/analysis_options.yaml rename to app/analysis_options.yaml diff --git a/flutt/android/.gitignore b/app/android/.gitignore similarity index 100% rename from flutt/android/.gitignore rename to app/android/.gitignore diff --git a/flutt/android/app/build.gradle.kts b/app/android/app/build.gradle.kts similarity index 100% rename from flutt/android/app/build.gradle.kts rename to app/android/app/build.gradle.kts diff --git a/flutt/android/app/src/debug/AndroidManifest.xml b/app/android/app/src/debug/AndroidManifest.xml similarity index 100% rename from flutt/android/app/src/debug/AndroidManifest.xml rename to app/android/app/src/debug/AndroidManifest.xml diff --git a/flutt/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml similarity index 100% rename from flutt/android/app/src/main/AndroidManifest.xml rename to app/android/app/src/main/AndroidManifest.xml diff --git a/flutt/android/app/src/main/kotlin/fr/geosector/app2/geosector_app/MainActivity.kt b/app/android/app/src/main/kotlin/fr/geosector/app2/geosector_app/MainActivity.kt similarity index 100% rename from flutt/android/app/src/main/kotlin/fr/geosector/app2/geosector_app/MainActivity.kt rename to app/android/app/src/main/kotlin/fr/geosector/app2/geosector_app/MainActivity.kt diff --git a/flutt/android/app/src/main/res/drawable-v21/launch_background.xml b/app/android/app/src/main/res/drawable-v21/launch_background.xml similarity index 100% rename from flutt/android/app/src/main/res/drawable-v21/launch_background.xml rename to app/android/app/src/main/res/drawable-v21/launch_background.xml diff --git a/flutt/android/app/src/main/res/drawable/launch_background.xml b/app/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from flutt/android/app/src/main/res/drawable/launch_background.xml rename to app/android/app/src/main/res/drawable/launch_background.xml diff --git a/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..ee1085f6 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/app/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png new file mode 100644 index 00000000..ce9f89d4 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png differ diff --git a/app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..0592ad3b Binary files /dev/null and b/app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/flutt/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/app/android/app/src/main/res/mipmap-hdpi/launcher_icon.png similarity index 100% rename from flutt/android/app/src/main/res/mipmap-hdpi/launcher_icon.png rename to app/android/app/src/main/res/mipmap-hdpi/launcher_icon.png diff --git a/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..b4830450 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/app/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png new file mode 100644 index 00000000..be2c2087 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png differ diff --git a/app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..86a119c5 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/flutt/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/app/android/app/src/main/res/mipmap-mdpi/launcher_icon.png similarity index 100% rename from flutt/android/app/src/main/res/mipmap-mdpi/launcher_icon.png rename to app/android/app/src/main/res/mipmap-mdpi/launcher_icon.png diff --git a/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..a11636d5 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png new file mode 100644 index 00000000..81c69d23 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png differ diff --git a/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..e7586d35 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/flutt/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/app/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png similarity index 100% rename from flutt/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png rename to app/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png diff --git a/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..37cd721a Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png new file mode 100644 index 00000000..ad257209 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png differ diff --git a/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..fb1357bb Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/flutt/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/app/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png similarity index 100% rename from flutt/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png rename to app/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png diff --git a/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..2d2b0b01 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png new file mode 100644 index 00000000..ea17ba9a Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png differ diff --git a/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..aa707e80 Binary files /dev/null and b/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/flutt/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/app/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png similarity index 100% rename from flutt/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png rename to app/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png diff --git a/flutt/android/app/src/main/res/values-night/styles.xml b/app/android/app/src/main/res/values-night/styles.xml similarity index 100% rename from flutt/android/app/src/main/res/values-night/styles.xml rename to app/android/app/src/main/res/values-night/styles.xml diff --git a/flutt/android/app/src/main/res/values/styles.xml b/app/android/app/src/main/res/values/styles.xml similarity index 100% rename from flutt/android/app/src/main/res/values/styles.xml rename to app/android/app/src/main/res/values/styles.xml diff --git a/flutt/android/app/src/profile/AndroidManifest.xml b/app/android/app/src/profile/AndroidManifest.xml similarity index 100% rename from flutt/android/app/src/profile/AndroidManifest.xml rename to app/android/app/src/profile/AndroidManifest.xml diff --git a/flutt/android/build.gradle.kts b/app/android/build.gradle.kts similarity index 100% rename from flutt/android/build.gradle.kts rename to app/android/build.gradle.kts diff --git a/flutt/android/gradle.properties b/app/android/gradle.properties similarity index 100% rename from flutt/android/gradle.properties rename to app/android/gradle.properties diff --git a/flutt/android/gradle/wrapper/gradle-wrapper.properties b/app/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from flutt/android/gradle/wrapper/gradle-wrapper.properties rename to app/android/gradle/wrapper/gradle-wrapper.properties diff --git a/flutt/android/settings.gradle.kts b/app/android/settings.gradle.kts similarity index 100% rename from flutt/android/settings.gradle.kts rename to app/android/settings.gradle.kts diff --git a/flutt/assets/animations/geo_main.json b/app/assets/animations/geo_main.json similarity index 100% rename from flutt/assets/animations/geo_main.json rename to app/assets/animations/geo_main.json diff --git a/app/assets/fonts/Figtree-VariableFont_wght.ttf b/app/assets/fonts/Figtree-VariableFont_wght.ttf new file mode 100644 index 00000000..06f9fe57 Binary files /dev/null and b/app/assets/fonts/Figtree-VariableFont_wght.ttf differ diff --git a/app/assets/images/geosector-logo.png b/app/assets/images/geosector-logo.png new file mode 100644 index 00000000..ba42d618 Binary files /dev/null and b/app/assets/images/geosector-logo.png differ diff --git a/app/assets/images/icon-geosector.svg b/app/assets/images/icon-geosector.svg new file mode 100644 index 00000000..1fbeeabb --- /dev/null +++ b/app/assets/images/icon-geosector.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/images/icons/icon-1024.png b/app/assets/images/icons/icon-1024.png new file mode 100644 index 00000000..532e85c9 Binary files /dev/null and b/app/assets/images/icons/icon-1024.png differ diff --git a/app/assets/images/logo-geosector-1024.png b/app/assets/images/logo-geosector-1024.png new file mode 100644 index 00000000..532e85c9 Binary files /dev/null and b/app/assets/images/logo-geosector-1024.png differ diff --git a/flutt/backup.sh b/app/backup.sh similarity index 100% rename from flutt/backup.sh rename to app/backup.sh diff --git a/flutt/clean_flutter.sh b/app/clean_flutter.sh similarity index 100% rename from flutt/clean_flutter.sh rename to app/clean_flutter.sh diff --git a/app/copy-web-images.sh b/app/copy-web-images.sh new file mode 100755 index 00000000..855dbfc9 --- /dev/null +++ b/app/copy-web-images.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# Script pour copier les images dans le bon dossier pour l'application web +echo "🔍 Copying images to correct web directory..." + +# Vérifier si le dossier build/web existe +if [ ! -d "build/web" ]; then + echo "❌ Error: build/web directory does not exist. Run 'flutter build web' first." + exit 1 +fi + +# Création du dossier assets/images si inexistant +mkdir -p build/web/assets/images + +# Copie des images depuis le répertoire source +cp -r assets/images/* build/web/assets/images/ + +# S'assurer que le logo est disponible avec les deux noms pour la compatibilité +echo "🔄 Création d'un lien symbolique pour le logo..." +if [ -f "build/web/assets/images/logo-geosector-1024.png" ]; then + cp "build/web/assets/images/logo-geosector-1024.png" "build/web/assets/images/geosector-logo.png" + echo "✅ Logo copié avec les deux noms pour assurer la compatibilité" +fi + +echo "✅ Images copied successfully!" + +# Si besoin de redéployer sans reconstruire l'application +if [ "$1" == "--deploy" ]; then + # Définition des variables + REMOTE_USER="root" + REMOTE_HOST="87.98.163.161" + SSH_KEY="/Users/pierre/.ssh/id_rsa_mbpi" + REMOTE_PATH="/var/www/geosector" + + echo "📤 Deploying fixed assets to server..." + rsync -rltz \ + -e "ssh -i ${SSH_KEY}" \ + build/web/assets/ \ + ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH}/assets/ + + if [ $? -ne 0 ]; then + echo "❌ Deployment failed" + exit 1 + fi + + echo "✅ Assets deployed successfully!" +fi diff --git a/flutt/deploy-dev.sh b/app/deploy-app.sh similarity index 93% rename from flutt/deploy-dev.sh rename to app/deploy-app.sh index 04708433..8f15eea4 100755 --- a/flutt/deploy-dev.sh +++ b/app/deploy-app.sh @@ -1,5 +1,5 @@ #!/bin/bash -cd /Users/pierre/dev/geosector/flutt +cd /Users/pierre/dev/geosector/app # Charger les variables d'environnement if [ ! -f .env-deploy-dev ]; then @@ -32,6 +32,9 @@ dart pub run build_runner build --delete-conflicting-outputs || error_exit "Code echo "🏗️ Building Flutter web application..." flutter build web --release || error_exit "Flutter build failed" +echo "🖼️ Fixing web assets structure..." +./copy-web-images.sh || error_exit "Failed to fix web assets" + echo "✅ Build completed successfully!" # Préparation de la commande SSH pour le host diff --git a/flutt/docs/chat.md b/app/docs/chat.md similarity index 100% rename from flutt/docs/chat.md rename to app/docs/chat.md diff --git a/flutt/fix-web-assets.sh b/app/fix-web-assets.sh similarity index 100% rename from flutt/fix-web-assets.sh rename to app/fix-web-assets.sh diff --git a/flutt/fix_ios_build.sh b/app/fix_ios_build.sh similarity index 100% rename from flutt/fix_ios_build.sh rename to app/fix_ios_build.sh diff --git a/app/flutter_launcher_icons.yaml b/app/flutter_launcher_icons.yaml new file mode 100644 index 00000000..200564f5 --- /dev/null +++ b/app/flutter_launcher_icons.yaml @@ -0,0 +1,22 @@ +flutter_launcher_icons: + # Configuration générale + image_path: "assets/images/icon-geosector.svg" + image_path_android: "assets/images/icon-geosector.svg" + image_path_ios: "assets/images/icon-geosector.svg" + + # Configuration Android + android: true + min_sdk_android: 21 + adaptive_icon_background: "#FFFFFF" + adaptive_icon_foreground: "assets/images/icon-geosector.svg" + + # Configuration iOS + ios: true + remove_alpha_ios: true + + # Configuration Web + web: + generate: true + image_path: "assets/images/icon-geosector.svg" + background_color: "#FFFFFF" + theme_color: "#4B77BE" diff --git a/flutt/git-create-branch.sh b/app/git-create-branch.sh similarity index 100% rename from flutt/git-create-branch.sh rename to app/git-create-branch.sh diff --git a/flutt/git-merge.sh b/app/git-merge.sh similarity index 100% rename from flutt/git-merge.sh rename to app/git-merge.sh diff --git a/flutt/ios/.gitignore b/app/ios/.gitignore similarity index 100% rename from flutt/ios/.gitignore rename to app/ios/.gitignore diff --git a/flutt/ios/Flutter/AppFrameworkInfo.plist b/app/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from flutt/ios/Flutter/AppFrameworkInfo.plist rename to app/ios/Flutter/AppFrameworkInfo.plist diff --git a/flutt/ios/Flutter/Debug.xcconfig b/app/ios/Flutter/Debug.xcconfig similarity index 100% rename from flutt/ios/Flutter/Debug.xcconfig rename to app/ios/Flutter/Debug.xcconfig diff --git a/flutt/ios/Flutter/Release.xcconfig b/app/ios/Flutter/Release.xcconfig similarity index 100% rename from flutt/ios/Flutter/Release.xcconfig rename to app/ios/Flutter/Release.xcconfig diff --git a/flutt/ios/Podfile b/app/ios/Podfile similarity index 100% rename from flutt/ios/Podfile rename to app/ios/Podfile diff --git a/flutt/ios/Podfile.lock b/app/ios/Podfile.lock similarity index 100% rename from flutt/ios/Podfile.lock rename to app/ios/Podfile.lock diff --git a/flutt/ios/Runner.xcodeproj/project.pbxproj b/app/ios/Runner.xcodeproj/project.pbxproj similarity index 100% rename from flutt/ios/Runner.xcodeproj/project.pbxproj rename to app/ios/Runner.xcodeproj/project.pbxproj diff --git a/flutt/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from flutt/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/flutt/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from flutt/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/flutt/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from flutt/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/flutt/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from flutt/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/flutt/ios/Runner.xcworkspace/contents.xcworkspacedata b/app/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from flutt/ios/Runner.xcworkspace/contents.xcworkspacedata rename to app/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/flutt/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from flutt/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/flutt/ios/Runner/AppDelegate.swift b/app/ios/Runner/AppDelegate.swift similarity index 100% rename from flutt/ios/Runner/AppDelegate.swift rename to app/ios/Runner/AppDelegate.swift diff --git a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 00000000..20a06ba4 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 00000000..6d29a7da Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 00000000..192bc9e7 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 00000000..f9442e8c Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 00000000..accf6835 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 00000000..67f4b105 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 00000000..5d98064a Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 00000000..192bc9e7 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 00000000..d96803b9 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 00000000..18f45966 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 00000000..18f45966 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 00000000..e3a9ce22 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 00000000..975e23f1 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 00000000..b713aca0 Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 00000000..dd1d082d Binary files /dev/null and b/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/flutt/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from flutt/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/flutt/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from flutt/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/flutt/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from flutt/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/flutt/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from flutt/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/flutt/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from flutt/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/flutt/ios/Runner/Base.lproj/LaunchScreen.storyboard b/app/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from flutt/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to app/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/flutt/ios/Runner/Base.lproj/Main.storyboard b/app/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from flutt/ios/Runner/Base.lproj/Main.storyboard rename to app/ios/Runner/Base.lproj/Main.storyboard diff --git a/flutt/ios/Runner/Info.plist b/app/ios/Runner/Info.plist similarity index 100% rename from flutt/ios/Runner/Info.plist rename to app/ios/Runner/Info.plist diff --git a/flutt/ios/Runner/Runner-Bridging-Header.h b/app/ios/Runner/Runner-Bridging-Header.h similarity index 100% rename from flutt/ios/Runner/Runner-Bridging-Header.h rename to app/ios/Runner/Runner-Bridging-Header.h diff --git a/flutt/ios/RunnerTests/RunnerTests.swift b/app/ios/RunnerTests/RunnerTests.swift similarity index 100% rename from flutt/ios/RunnerTests/RunnerTests.swift rename to app/ios/RunnerTests/RunnerTests.swift diff --git a/flutt/ios_reset.sh b/app/ios_reset.sh similarity index 100% rename from flutt/ios_reset.sh rename to app/ios_reset.sh diff --git a/app/lib/app.dart b/app/lib/app.dart new file mode 100644 index 00000000..1e3804f2 --- /dev/null +++ b/app/lib/app.dart @@ -0,0 +1,266 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:geosector_app/core/theme/app_theme.dart'; +import 'package:go_router/go_router.dart'; +import 'package:geosector_app/core/services/api_service.dart'; +import 'package:geosector_app/core/repositories/user_repository.dart'; +import 'package:geosector_app/core/repositories/operation_repository.dart'; +import 'package:geosector_app/core/repositories/passage_repository.dart'; +import 'package:geosector_app/core/repositories/sector_repository.dart'; +import 'package:geosector_app/core/repositories/membre_repository.dart'; +import 'package:geosector_app/core/repositories/amicale_repository.dart'; +import 'package:geosector_app/core/services/sync_service.dart'; +import 'package:geosector_app/core/services/connectivity_service.dart'; +import 'package:geosector_app/presentation/auth/splash_page.dart'; +import 'package:geosector_app/presentation/auth/login_page.dart'; +import 'package:geosector_app/presentation/auth/register_page.dart'; +import 'package:geosector_app/presentation/admin/admin_dashboard_page.dart'; +import 'package:geosector_app/presentation/user/user_dashboard_page.dart'; + +// Instances globales des services et repositories +final apiService = ApiService(); +final operationRepository = OperationRepository(apiService); +final passageRepository = PassageRepository(apiService); +final userRepository = UserRepository(apiService); +final sectorRepository = SectorRepository(apiService); +final membreRepository = MembreRepository(apiService); +final amicaleRepository = AmicaleRepository(apiService); +final syncService = SyncService(userRepository: userRepository); +final connectivityService = ConnectivityService(); + +class GeoSectorApp extends StatelessWidget { + const GeoSectorApp({super.key}); + + @override + Widget build(BuildContext context) { + // Utiliser directement le router sans provider + final router = GoRouter( + initialLocation: '/', + debugLogDiagnostics: true, + refreshListenable: + userRepository, // Écouter les changements d'état d'authentification + // Gestionnaire de redirection global - intercepte toutes les navigations + redirect: (context, state) { + // Détection manuelle des paramètres d'URL pour le Web + if (kIsWeb && state.uri.path == '/login') { + try { + // Obtenir le paramètre 'type' de l'URL actuelle + final typeParam = state.uri.queryParameters['type']; + + // Obtenir l'URL brute du navigateur pour comparer + final rawUri = Uri.parse(Uri.base.toString()); + final rawTypeParam = rawUri.queryParameters['type']; + + print('APP ROUTER: state.uri = ${state.uri}, type = $typeParam'); + print('APP ROUTER: rawUri = $rawUri, type = $rawTypeParam'); + + // Pas de redirection si on a déjà le paramètre type + if (typeParam != null) { + print('APP ROUTER: Param type déjà présent, pas de redirection'); + return null; // Pas de redirection + } + + // Si un paramètre type=user est présent dans l'URL brute mais pas dans l'état + if (rawTypeParam == 'user' && typeParam == null) { + print( + 'APP ROUTER: Paramètre détecté dans l\'URL brute, redirection vers /login?type=user'); + return '/login?type=user'; + } + } catch (e) { + print('Erreur lors de la récupération des paramètres d\'URL: $e'); + } + } + // Sauvegarder le chemin actuel pour l'utilisateur connecté, sauf pour la page de splash + if (state.uri.toString() != '/' && userRepository.isLoggedIn) { + // Ne pas sauvegarder les chemins de login/register + if (!state.uri.toString().startsWith('/login') && + !state.uri.toString().startsWith('/register')) { + userRepository.updateLastPath(state.uri.toString()); + } + } + + // Vérifier si l'utilisateur est sur la page de splash + if (state.uri.toString() == '/') { + // Laisser l'utilisateur sur la page de splash, la redirection sera gérée par SplashPage + return null; + } + + // Vérifier si l'utilisateur est sur une page d'authentification + final isLoggedIn = userRepository.isLoggedIn; + final isOnLoginPage = state.uri.toString().startsWith('/login'); + final isOnRegisterPage = state.uri.toString() == '/register'; + final isOnAdminRegisterPage = state.uri.toString() == '/admin-register'; + + // Si l'utilisateur n'est pas connecté et n'est pas sur une page d'authentification, rediriger vers la page de connexion + if (!isLoggedIn && + !isOnLoginPage && + !isOnRegisterPage && + !isOnAdminRegisterPage) { + return '/login'; + } + + // Si l'utilisateur est connecté et se trouve sur une page d'authentification, rediriger vers le tableau de bord approprié + if (isLoggedIn && + (isOnLoginPage || isOnRegisterPage || isOnAdminRegisterPage)) { + // Récupérer le rôle de l'utilisateur directement + final user = userRepository.getCurrentUser(); + if (user != null) { + // Convertir le rôle en int si nécessaire + int roleValue; + if (user.role is String) { + roleValue = int.tryParse(user.role as String) ?? 1; + } else { + roleValue = user.role as int; + } + + // Redirection simple basée sur le rôle + if (roleValue > 1) { + debugPrint( + 'Router: Redirection vers /admin (rôle $roleValue > 1)'); + return '/admin'; + } else { + debugPrint( + 'Router: Redirection vers /user (rôle $roleValue = 1)'); + return '/user'; + } + } + } + + // Si l'utilisateur est connecté mais essaie d'accéder à la mauvaise page selon son rôle + if (isLoggedIn) { + final user = userRepository.getCurrentUser(); + if (user != null) { + // Convertir le rôle en int si nécessaire + int roleValue; + if (user.role is String) { + roleValue = int.tryParse(user.role as String) ?? 1; + } else { + roleValue = user.role as int; + } + + // Vérifier si l'utilisateur est sur la bonne page en fonction de son rôle + final isOnUserPage = state.uri.toString().startsWith('/user'); + final isOnAdminPage = state.uri.toString().startsWith('/admin'); + + // Admin (rôle > 1) essayant d'accéder à une page utilisateur + if (roleValue > 1 && isOnUserPage) { + debugPrint( + 'Router: Redirection d\'admin (rôle $roleValue) vers /admin'); + return '/admin'; + } + + // Utilisateur standard (rôle = 1) essayant d'accéder à une page admin + if (roleValue == 1 && isOnAdminPage) { + debugPrint( + 'Router: Redirection d\'utilisateur (rôle $roleValue) vers /user'); + return '/user'; + } + } + } + + return null; + }, + routes: [ + // Splash screen + GoRoute( + path: '/', + builder: (context, state) => const SplashPage(), + ), + + // Page de connexion utilisateur dédiée + GoRoute( + path: '/login/user', + builder: (context, state) { + print('ROUTER: Accès direct à la route login user'); + return const LoginPage( + key: Key('login_page_user'), + loginType: 'user', + ); + }, + ), + + // Pages d'authentification standard + GoRoute( + path: '/login', + builder: (context, state) { + // Ajouter des logs de débogage détaillés pour comprendre les paramètres + print('ROUTER DEBUG: Uri complète = ${state.uri}'); + print('ROUTER DEBUG: Path = ${state.uri.path}'); + print('ROUTER DEBUG: Query params = ${state.uri.queryParameters}'); + print( + 'ROUTER DEBUG: Has type? ${state.uri.queryParameters.containsKey("type")}'); + + // Donner la priorité aux paramètres d'URL puis aux extras + String? loginType; + + // 1. Essayer d'abord les paramètres d'URL (pour les liens externes) + final queryParams = state.uri.queryParameters; + loginType = queryParams['type']; + print('ROUTER DEBUG: Type from query params = $loginType'); + + // 2. Si aucun type dans les paramètres d'URL, vérifier les extras (pour la navigation interne) + if (loginType == null && + state.extra != null && + state.extra is Map) { + final extras = state.extra as Map; + loginType = extras['type']?.toString(); + print('ROUTER DEBUG: Type from extras = $loginType'); + } + + // 3. Normaliser et valider le type + if (loginType != null) { + loginType = loginType.trim().toLowerCase(); + // Vérifier explicitement que c'est 'user', sinon mettre 'admin' + if (loginType != 'user') { + loginType = 'admin'; + } + } else { + // Si aucun type n'est spécifié, retourner la page de splash + print( + 'ROUTER: Aucun type spécifié, utilisation de la page splash'); + return const SplashPage(); + } + + print('ROUTER: Type de connexion final: $loginType'); + + return LoginPage( + key: Key('login_page_${loginType}'), + loginType: loginType, + ); + }, + ), + GoRoute( + path: '/register', + builder: (context, state) => const RegisterPage(), + ), + + // Pages administrateur + GoRoute( + path: '/admin', + builder: (context, state) => const AdminDashboardPage(), + routes: [ + // Ajouter d'autres routes admin ici + ], + ), + + // Pages utilisateur + GoRoute( + path: '/user', + builder: (context, state) => const UserDashboardPage(), + routes: [ + // Ajouter d'autres routes utilisateur ici + ], + ), + ], + ); + + return MaterialApp.router( + debugShowCheckedModeBanner: false, + title: 'GEOSECTOR', + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + themeMode: ThemeMode.system, + routerConfig: router, + ); + } +} diff --git a/flutt/lib/chat/README.md b/app/lib/chat/README.md similarity index 100% rename from flutt/lib/chat/README.md rename to app/lib/chat/README.md diff --git a/flutt/lib/chat/chat.dart b/app/lib/chat/chat.dart similarity index 100% rename from flutt/lib/chat/chat.dart rename to app/lib/chat/chat.dart diff --git a/flutt/lib/chat/chat_updated.md b/app/lib/chat/chat_updated.md similarity index 100% rename from flutt/lib/chat/chat_updated.md rename to app/lib/chat/chat_updated.md diff --git a/flutt/lib/chat/constants/chat_constants.dart b/app/lib/chat/constants/chat_constants.dart similarity index 100% rename from flutt/lib/chat/constants/chat_constants.dart rename to app/lib/chat/constants/chat_constants.dart diff --git a/flutt/lib/chat/example_integration/mqtt_integration_example.dart b/app/lib/chat/example_integration/mqtt_integration_example.dart similarity index 100% rename from flutt/lib/chat/example_integration/mqtt_integration_example.dart rename to app/lib/chat/example_integration/mqtt_integration_example.dart diff --git a/flutt/lib/chat/models/anonymous_user_model.dart b/app/lib/chat/models/anonymous_user_model.dart similarity index 100% rename from flutt/lib/chat/models/anonymous_user_model.dart rename to app/lib/chat/models/anonymous_user_model.dart diff --git a/flutt/lib/chat/models/anonymous_user_model.g.dart b/app/lib/chat/models/anonymous_user_model.g.dart similarity index 100% rename from flutt/lib/chat/models/anonymous_user_model.g.dart rename to app/lib/chat/models/anonymous_user_model.g.dart diff --git a/flutt/lib/chat/models/audience_target_model.dart b/app/lib/chat/models/audience_target_model.dart similarity index 100% rename from flutt/lib/chat/models/audience_target_model.dart rename to app/lib/chat/models/audience_target_model.dart diff --git a/flutt/lib/chat/models/audience_target_model.g.dart b/app/lib/chat/models/audience_target_model.g.dart similarity index 100% rename from flutt/lib/chat/models/audience_target_model.g.dart rename to app/lib/chat/models/audience_target_model.g.dart diff --git a/flutt/lib/chat/models/chat_adapters.dart b/app/lib/chat/models/chat_adapters.dart similarity index 100% rename from flutt/lib/chat/models/chat_adapters.dart rename to app/lib/chat/models/chat_adapters.dart diff --git a/flutt/lib/chat/models/chat_config.dart b/app/lib/chat/models/chat_config.dart similarity index 100% rename from flutt/lib/chat/models/chat_config.dart rename to app/lib/chat/models/chat_config.dart diff --git a/flutt/lib/chat/models/conversation_model.dart b/app/lib/chat/models/conversation_model.dart similarity index 100% rename from flutt/lib/chat/models/conversation_model.dart rename to app/lib/chat/models/conversation_model.dart diff --git a/flutt/lib/chat/models/conversation_model.g.dart b/app/lib/chat/models/conversation_model.g.dart similarity index 100% rename from flutt/lib/chat/models/conversation_model.g.dart rename to app/lib/chat/models/conversation_model.g.dart diff --git a/flutt/lib/chat/models/message_model.dart b/app/lib/chat/models/message_model.dart similarity index 100% rename from flutt/lib/chat/models/message_model.dart rename to app/lib/chat/models/message_model.dart diff --git a/flutt/lib/chat/models/message_model.g.dart b/app/lib/chat/models/message_model.g.dart similarity index 100% rename from flutt/lib/chat/models/message_model.g.dart rename to app/lib/chat/models/message_model.g.dart diff --git a/flutt/lib/chat/models/notification_settings.dart b/app/lib/chat/models/notification_settings.dart similarity index 100% rename from flutt/lib/chat/models/notification_settings.dart rename to app/lib/chat/models/notification_settings.dart diff --git a/flutt/lib/chat/models/notification_settings.g.dart b/app/lib/chat/models/notification_settings.g.dart similarity index 100% rename from flutt/lib/chat/models/notification_settings.g.dart rename to app/lib/chat/models/notification_settings.g.dart diff --git a/flutt/lib/chat/models/participant_model.dart b/app/lib/chat/models/participant_model.dart similarity index 100% rename from flutt/lib/chat/models/participant_model.dart rename to app/lib/chat/models/participant_model.dart diff --git a/flutt/lib/chat/models/participant_model.g.dart b/app/lib/chat/models/participant_model.g.dart similarity index 100% rename from flutt/lib/chat/models/participant_model.g.dart rename to app/lib/chat/models/participant_model.g.dart diff --git a/flutt/lib/chat/pages/chat_page.dart b/app/lib/chat/pages/chat_page.dart similarity index 100% rename from flutt/lib/chat/pages/chat_page.dart rename to app/lib/chat/pages/chat_page.dart diff --git a/flutt/lib/chat/repositories/chat_repository.dart b/app/lib/chat/repositories/chat_repository.dart similarity index 100% rename from flutt/lib/chat/repositories/chat_repository.dart rename to app/lib/chat/repositories/chat_repository.dart diff --git a/flutt/lib/chat/scripts/chat_tables.sql b/app/lib/chat/scripts/chat_tables.sql similarity index 100% rename from flutt/lib/chat/scripts/chat_tables.sql rename to app/lib/chat/scripts/chat_tables.sql diff --git a/flutt/lib/chat/scripts/mqtt_notification_sender.php b/app/lib/chat/scripts/mqtt_notification_sender.php similarity index 100% rename from flutt/lib/chat/scripts/mqtt_notification_sender.php rename to app/lib/chat/scripts/mqtt_notification_sender.php diff --git a/flutt/lib/chat/scripts/send_notification.php b/app/lib/chat/scripts/send_notification.php similarity index 100% rename from flutt/lib/chat/scripts/send_notification.php rename to app/lib/chat/scripts/send_notification.php diff --git a/flutt/lib/chat/services/chat_api_service.dart b/app/lib/chat/services/chat_api_service.dart similarity index 100% rename from flutt/lib/chat/services/chat_api_service.dart rename to app/lib/chat/services/chat_api_service.dart diff --git a/flutt/lib/chat/services/notifications/README_MQTT.md b/app/lib/chat/services/notifications/README_MQTT.md similarity index 100% rename from flutt/lib/chat/services/notifications/README_MQTT.md rename to app/lib/chat/services/notifications/README_MQTT.md diff --git a/flutt/lib/chat/services/notifications/chat_notification_service.dart b/app/lib/chat/services/notifications/chat_notification_service.dart similarity index 100% rename from flutt/lib/chat/services/notifications/chat_notification_service.dart rename to app/lib/chat/services/notifications/chat_notification_service.dart diff --git a/flutt/lib/chat/services/notifications/mqtt_config.dart b/app/lib/chat/services/notifications/mqtt_config.dart similarity index 100% rename from flutt/lib/chat/services/notifications/mqtt_config.dart rename to app/lib/chat/services/notifications/mqtt_config.dart diff --git a/flutt/lib/chat/services/notifications/mqtt_notification_service.dart b/app/lib/chat/services/notifications/mqtt_notification_service.dart similarity index 100% rename from flutt/lib/chat/services/notifications/mqtt_notification_service.dart rename to app/lib/chat/services/notifications/mqtt_notification_service.dart diff --git a/flutt/lib/chat/services/offline_queue_service.dart b/app/lib/chat/services/offline_queue_service.dart similarity index 100% rename from flutt/lib/chat/services/offline_queue_service.dart rename to app/lib/chat/services/offline_queue_service.dart diff --git a/flutt/lib/chat/widgets/chat_input.dart b/app/lib/chat/widgets/chat_input.dart similarity index 100% rename from flutt/lib/chat/widgets/chat_input.dart rename to app/lib/chat/widgets/chat_input.dart diff --git a/flutt/lib/chat/widgets/chat_screen.dart b/app/lib/chat/widgets/chat_screen.dart similarity index 100% rename from flutt/lib/chat/widgets/chat_screen.dart rename to app/lib/chat/widgets/chat_screen.dart diff --git a/flutt/lib/chat/widgets/conversations_list.dart b/app/lib/chat/widgets/conversations_list.dart similarity index 100% rename from flutt/lib/chat/widgets/conversations_list.dart rename to app/lib/chat/widgets/conversations_list.dart diff --git a/flutt/lib/chat/widgets/message_bubble.dart b/app/lib/chat/widgets/message_bubble.dart similarity index 100% rename from flutt/lib/chat/widgets/message_bubble.dart rename to app/lib/chat/widgets/message_bubble.dart diff --git a/flutt/lib/chat/widgets/notification_settings_widget.dart b/app/lib/chat/widgets/notification_settings_widget.dart similarity index 100% rename from flutt/lib/chat/widgets/notification_settings_widget.dart rename to app/lib/chat/widgets/notification_settings_widget.dart diff --git a/app/lib/core/constants/app_keys.dart b/app/lib/core/constants/app_keys.dart new file mode 100644 index 00000000..95feb4e6 --- /dev/null +++ b/app/lib/core/constants/app_keys.dart @@ -0,0 +1,192 @@ +/// Fichier contenant toutes les constantes utilisées dans l'application +/// Centralise les clés, noms de boîtes Hive, et autres constantes +/// pour faciliter la maintenance et éviter les erreurs de frappe + +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/material.dart'; + +class AppKeys { + // Noms des boîtes Hive + static const String usersBoxName = 'users'; + static const String amicaleBoxName = 'amicale'; + static const String clientsBoxName = 'clients'; + static const String operationsBoxName = 'operations'; + static const String sectorsBoxName = 'sectors'; + static const String passagesBoxName = 'passages'; + static const String settingsBoxName = 'settings'; + static const String membresBoxName = 'membres'; + static const String userSectorBoxName = 'user_sector'; + static const String chatConversationsBoxName = 'chat_conversations'; + static const String chatMessagesBoxName = 'chat_messages'; + static const String regionsBoxName = 'regions'; + + // Rôles utilisateurs + static const int roleUser = 1; + static const int roleAdmin1 = 2; + static const int roleAdmin2 = 4; + static const int roleAdmin3 = 9; + + // URLs API pour les différents environnements + static const String baseApiUrlDev = 'https://dapp.geosector.fr/api'; + static const String baseApiUrlRec = 'https://rapp.geosector.fr/api'; + static const String baseApiUrlProd = 'https://app.geosector.fr/api'; + + // Identifiants d'application pour les différents environnements + static const String appIdentifierDev = 'dapp.geosector.fr'; + static const String appIdentifierRec = 'rapp.geosector.fr'; + static const String appIdentifierProd = 'app.geosector.fr'; + + // Endpoints API + static const String loginEndpoint = '/login'; + static const String logoutEndpoint = '/logout'; + static const String registerEndpoint = '/register'; + static const String syncDataEndpoint = '/data/sync'; + static const String sectorsEndpoint = '/sectors'; + + // Durées + static const Duration connectionTimeout = Duration(seconds: 5); + static const Duration receiveTimeout = Duration(seconds: 30); + static const Duration sessionDefaultExpiry = Duration(days: 7); + + // Clés API externes + static const String mapboxApiKeyDev = + 'pk.eyJ1IjoicHZkNnNvZnQiLCJhIjoiY21hanVmNjN5MTM5djJtczdsMW92cjQ0ciJ9.pUCMuvWPB3cuBaPh4ywTAw'; + static const String mapboxApiKeyRec = + 'pk.eyJ1IjoicHZkNnNvZnQiLCJhIjoiY21hanVlZ3FiMGx0NDJpc2k4YnkxaWZ2dSJ9.OqGJtjlWRgB4fIjECCB8WA'; + static const String mapboxApiKeyProd = + 'pk.eyJ1IjoicHZkNnNvZnQiLCJhIjoiY204dTNhNmd0MGV1ZzJqc2pnNnB0NjYxdSJ9.TA5Mvliyn91Oi01F_2Yuxw'; + + // Méthode pour obtenir la clé API Mapbox en fonction de l'environnement actuel + static String getMapboxApiKey(String environment) { + // Utiliser l'environnement passé en paramètre pour déterminer quelle clé retourner + switch (environment) { + case 'DEV': + return mapboxApiKeyDev; + case 'REC': + return mapboxApiKeyRec; + case 'PROD': + default: + return mapboxApiKeyProd; + } + } + + // Pour la compatibilité avec le code existant, on garde un getter qui utilise + // l'environnement actuel (à utiliser uniquement si l'ApiService n'est pas disponible) + static String get mapboxApiKey { + // Note: Cette implémentation est une solution de secours et devrait être évitée + // Il est préférable d'utiliser getMapboxApiKey(apiService.getCurrentEnvironment()) + + // Détection basique de l'environnement basée sur l'URL en mode web + if (kIsWeb) { + // Essayer d'accéder à l'URL actuelle (fonctionne uniquement en mode web) + try { + final String currentUrl = Uri.base.toString().toLowerCase(); + + if (currentUrl.contains('dapp.geosector.fr')) { + return mapboxApiKeyDev; + } else if (currentUrl.contains('rapp.geosector.fr')) { + return mapboxApiKeyRec; + } + } catch (e) { + // En cas d'erreur, utiliser la clé de production par défaut + print('Erreur lors de la détection de l\'environnement: $e'); + } + } + + // Par défaut, retourner la clé de production + return mapboxApiKeyProd; + } + + // Headers + static const String sessionHeader = 'Authorization'; + + // En-têtes par défaut pour les requêtes API + // Note: Ces en-têtes seront complétés dynamiquement dans ApiService + static const Map defaultHeaders = { + 'Content-Type': 'application/json', + 'X-Client-Type': kIsWeb ? 'web' : 'mobile', + 'Accept': 'application/json', + }; + + // Civilités + static const Map civilites = { + 1: 'M.', + 2: 'Mme', + }; + + // Types de règlements (basés sur la maquette Figma) + static const Map> typesReglements = { + 0: { + 'titre': 'Pas de règlement', + 'couleur': 0xFF757575, // Gris foncé + 'icon_data': Icons.money_off, + }, + 1: { + 'titre': 'Espèce', + 'couleur': 0xFFB87333, // Couleur cuivrée + 'icon_data': Icons.payments_outlined, + }, + 2: { + 'titre': 'Chèque', + 'couleur': 0xFFD8D5EC, // Violet clair (Figma) + 'icon_data': Icons.account_balance_wallet_outlined, + }, + 3: { + 'titre': 'CB', + 'couleur': 0xFF0099FF, // Bleu flashy + 'icon_data': Icons.credit_card, + }, + }; + + // Types de passages (basés sur la maquette Figma) + static const Map> typesPassages = { + 1: { + 'titres': 'Effectués', + 'titre': 'Effectué', + 'couleur1': 0xFF00E09D, // Vert (Figma) + 'couleur2': 0xFF00E09D, // Vert (Figma) + 'couleur3': 0xFF00E09D, // Vert (Figma) + 'icon_data': Icons.task_alt, + }, + 2: { + 'titres': 'À finaliser', + 'titre': 'À finaliser', + 'couleur1': 0xFFFFFFFF, // Blanc + 'couleur2': 0xFFF7A278, // Orange (Figma) + 'couleur3': 0xFFE65100, // Orange foncé + 'icon_data': Icons.refresh, + }, + 3: { + 'titres': 'Refusés', + 'titre': 'Refusé', + 'couleur1': 0xFFE41B13, // Rouge (Figma) + 'couleur2': 0xFFE41B13, // Rouge (Figma) + 'couleur3': 0xFFE41B13, // Rouge (Figma) + 'icon_data': Icons.block, + }, + 4: { + 'titres': 'Dons', + 'titre': 'Don', + 'couleur1': 0xFF395AA7, // Bleu (Figma) + 'couleur2': 0xFF395AA7, // Bleu (Figma) + 'couleur3': 0xFF395AA7, // Bleu (Figma) + 'icon_data': Icons.volunteer_activism, + }, + 5: { + 'titres': 'Lots', + 'titre': 'Lot', + 'couleur1': 0xFF20335E, // Bleu foncé (Figma) + 'couleur2': 0xFF20335E, // Bleu foncé (Figma) + 'couleur3': 0xFF20335E, // Bleu foncé (Figma) + 'icon_data': Icons.layers, + }, + 6: { + 'titres': 'Maisons vides', + 'titre': 'Maison vide', + 'couleur1': 0xFFB8B8B8, // Gris (Figma) + 'couleur2': 0xFFB8B8B8, // Gris (Figma) + 'couleur3': 0xFFB8B8B8, // Gris (Figma) + 'icon_data': Icons.home_outlined, + }, + }; +} diff --git a/app/lib/core/constants/reponse-login.json b/app/lib/core/constants/reponse-login.json new file mode 100644 index 00000000..76d61aa2 --- /dev/null +++ b/app/lib/core/constants/reponse-login.json @@ -0,0 +1,14 @@ +{"status":"success","message":"Connexion réussie","session_id":"4d3a0615ae61f833c08b2bfc26ba24c9","session_expiry":"2025-05-15T18:18:42+00:00", + +"user":{"id":9999980,"username":"pv_admin","name":"VALERY ADM","first_name":"Pierre","fk_role":2,"entite_id":5,"sect_name":"","entite_name":"AMICALE TEST DEV PIERRE","entite_adresse":"17 place hoche","entite_code_postal":"35000","entite_ville":"RENNES","entite_gps_lat":"48.13537","entite_gps_lng":"-1.54272"}, + +"amicales":[{"id":5,"name":"AMICALE TEST DEV PIERRE","adresse1":"17 place hoche","adresse2":"","code_postal":"35000","ville":"RENNES","fk_region":5,"lib_region":"Bretagne","fk_type":1,"phone":"","mobile":"0645622426","email":"pierre.vaissaire@d6soft.fr","gps_lat":"48.13537","gps_lng":"-1.54272","stripe_id":"","chk_demo":0,"chk_copie_mail_recu":1,"chk_accept_sms":0,"chk_active":1}], + +"membres":[{"id":9999979,"fk_role":1,"fk_titre":1,"first_name":"Pierre","sect_name":"","date_naissance":"1966-04-24","date_embauche":"2017-12-01","chk_active":0,"name":"VAISSAIRE","username":"pv_mobile","email":"pierre.vaissaire@d6soft.fr"},{"id":9999980,"fk_role":2,"fk_titre":1,"first_name":"Pierre","sect_name":"","date_naissance":"1966-04-24","date_embauche":"2017-12-01","chk_active":1,"name":"VALERY ADM","username":"pv_admin","email":"pierre.vaissaire@d6soft.fr"},{"id":9999985,"fk_role":1,"fk_titre":1,"first_name":"Clément","sect_name":"clem tournée","date_naissance":null,"date_embauche":null,"chk_active":1,"name":"VAISSAIRE","username":"cv_mobile","mobile":"06 45 62 24 26","email":"pierre@d6mail.fr"},{"id":10011253,"fk_role":1,"fk_titre":1,"first_name":"Pierre","sect_name":"","date_naissance":null,"date_embauche":null,"chk_active":1,"name":"TEST1","username":"pierre.test1","email":"test1@d6mail.fr"},{"id":10016609,"fk_role":1,"fk_titre":1,"first_name":"","sect_name":"Tournée test","date_naissance":"1989-04-24","date_embauche":"2015-04-24","chk_active":1,"name":"ANDREZIEUX","username":"pa_mobile","mobile":"06 45 62 24 26","email":"pierre@d6mail.fr"},{"id":10018304,"fk_role":1,"fk_titre":1,"first_name":"Alban","sect_name":"Albin new turn","date_naissance":"2005-10-22","date_embauche":"0000-00-00","chk_active":1,"name":"VAISSAIRE","username":"albanquise_mobile","email":"pierre.vaissaire@gmail.com"},{"id":10018305,"fk_role":1,"fk_titre":1,"first_name":"Aubin","sect_name":"Albin turn","date_naissance":"2005-10-22","date_embauche":"0000-00-00","chk_active":1,"name":"VAISSAIRE","username":"alban_mobile","email":"pierre@d6mail.fr"},{"id":10021649,"fk_role":1,"fk_titre":1,"first_name":"Alb","sect_name":"Alban","date_naissance":null,"date_embauche":null,"chk_active":0,"name":"VAISSAIRE","username":"VAISSAIREALB"},{"id":10021968,"fk_role":1,"fk_titre":1,"first_name":"Jean","sect_name":"","date_naissance":"0000-00-00","date_embauche":"0000-00-00","chk_active":0,"name":"GRÉGORIEN","username":"5gregorien@2024","email":"pierre.vaissaire@orange.fr"},{"id":10021969,"fk_role":1,"fk_titre":1,"first_name":"Kris","sect_name":"","date_naissance":"0000-00-00","date_embauche":"0000-00-00","chk_active":0,"name":"MORTIZ","username":"5@kMortiz","email":"pierre.vaissaire@gmail.com"},{"id":10021970,"fk_role":1,"fk_titre":1,"first_name":"GréGoire","sect_name":"","date_naissance":"0000-00-00","date_embauche":"0000-00-00","chk_active":0,"name":"SARZINET","username":"5GSarzinet","email":"test1@d6mail.fr"},{"id":10021972,"fk_role":1,"fk_titre":1,"first_name":"Greg","sect_name":"","date_naissance":"0000-00-00","date_embauche":"0000-00-00","chk_active":1,"name":"POULAVER","username":"5GregPoulaver","email":"pierre.vaissaire@gmail.com"},{"id":10022230,"fk_role":1,"fk_titre":1,"first_name":"Paul","sect_name":"","date_naissance":"0000-00-00","date_embauche":"0000-00-00","chk_active":0,"name":"TEST","username":"TEST_PIERRE_35"},{"id":10022231,"fk_role":1,"fk_titre":1,"first_name":"Paul","sect_name":"","date_naissance":"0000-00-00","date_embauche":"0000-00-00","chk_active":0,"name":"TEST0724","username":"TEST0724_PAUL","email":"acigne@d6mail.fr"},{"id":10022232,"fk_role":1,"fk_titre":1,"first_name":"Patrick","sect_name":"","date_naissance":"0000-00-00","date_embauche":"0000-00-00","chk_active":1,"name":"TEST2407","username":"TEST2407_Pierre","email":"acigne@d6mail.fr"},{"id":10022233,"fk_role":1,"fk_titre":1,"first_name":"Pierre","sect_name":"","date_naissance":"0000-00-00","date_embauche":"0000-00-00","chk_active":1,"name":"ESSAIPIERRE","username":"ESSAIPIERRE","email":"acigne@d6mail.fr"},{"id":10022234,"fk_role":1,"fk_titre":1,"first_name":"","sect_name":"","date_naissance":"0000-00-00","date_embauche":"0000-00-00","chk_active":1,"name":"ESSAIPIERRE2","username":"ESSAIPIERRE2","email":"test1@d6mail.fr"},{"id":10022235,"fk_role":1,"fk_titre":1,"first_name":"","sect_name":"","date_naissance":"0000-00-00","date_embauche":"0000-00-00","chk_active":1,"name":"ESSAIPIERRE3","username":"ESSAIPIERRE3","email":"test2@d6mail.fr"},{"id":10023147,"fk_role":1,"fk_titre":1,"first_name":"HERVE","sect_name":"EQUIPE N°1","date_naissance":null,"date_embauche":null,"chk_active":1,"name":"BLAUZET","username":"1534BLAUZET","mobile":"0000000000","email":"contact@d6soft.fr"},{"id":10023148,"fk_role":1,"fk_titre":1,"first_name":"CLEMENT","sect_name":"EQUIPE N°3","date_naissance":null,"date_embauche":null,"chk_active":1,"name":"ELCOUFFE","username":"3623ELCOUFFE","mobile":"0000000000","email":"support@unikoffice.com"},{"id":10023664,"fk_role":1,"fk_titre":1,"first_name":"AuréLien","sect_name":"","date_naissance":"0000-00-00","date_embauche":"0000-00-00","chk_active":1,"name":"JESSIEN","username":"JESSIEN5.Aur"}], + +"operations":[{"id":2644,"name":"OPE 2024-25","date_deb":"2024-09-22","date_fin":"2025-05-30"},{"id":2021,"name":"OPéRATION TEST 2023-2024","date_deb":"2023-09-18","date_fin":"2024-03-31"},{"id":1525,"name":"OPERATION TEST 2022 PVA","date_deb":"2022-06-01","date_fin":"2022-12-31"}], + +"sectors":[{"id":3,"libelle":"Secteur 1","color":"#4B77BE","sector":"48.116432720272/-1.6741597652435#48.117893905675/-1.6733872890472#48.118180407744/-1.6724002361298#48.120844800462/-1.6719710826874#48.122334508201/-1.6694819927216#48.119269101034/-1.6674220561981#48.116289464564/-1.6662204265594#48.116432720272/-1.6741597652435#"},{"id":6,"libelle":"Secteur Guehenno 1","color":"#4B77BE","sector":"48.117287948711/-1.678032875061#48.114914189448/-1.6778075695038#48.114856885519/-1.6772925853729#48.113663516855/-1.677303314209#48.113538877519/-1.6751039028168#48.114684973349/-1.6742885112762#48.116461371365/-1.6740739345551#48.117922555954/-1.6733872890472#48.118190435462/-1.6723895072937#48.12074023299/-1.672089099884#48.121069691978/-1.6761445999146#48.118921008363/-1.6774749755859#48.117287948711/-1.678032875061#"},{"id":7,"libelle":"Secteur Campus","color":"#0fe33f","sector":"48.119187/-1.64988#48.116724/-1.648507#48.11403/-1.648078#48.114145/-1.633658#48.117755/-1.632113#48.120333/-1.631341#48.12188/-1.628852#48.125032/-1.627736#48.127037/-1.628766#48.124459/-1.633916#48.122396/-1.638122#"},{"id":13,"libelle":"Secteur Guehenno 2","color":"#1a29e1","sector":"48.120799439802/-1.6719388961792#48.118220995116/-1.6722822189331#48.118335595406/-1.6693639755249#48.11925238852/-1.6673040390015#48.122403740071/-1.6692781448364#48.120799439802/-1.6719388961792#"},{"id":16,"libelle":"Secteur Acigné Nord","color":"#4B77BE","sector":"48.140148207985/-1.5367126464844#48.138859487463/-1.5367555618286#48.137771209385/-1.5381073951721#48.136997945152/-1.5383219718933#48.136897706343/-1.5372061729431#48.136926346023/-1.5363907814026#48.137227061694/-1.535210609436#48.13742753783/-1.5343308448792#48.137384578724/-1.5337514877319#48.137298660404/-1.5329146385193#48.138243754015/-1.532506942749#48.139876147457/-1.5320777893066#48.141379621813/-1.5317130088806#48.142152820049/-1.5309405326843#48.142539414801/-1.5336441993713#48.140277078257/-1.5342020988464#48.140148207985/-1.5367126464844#"},{"id":19,"libelle":"Secteur Le Landry","color":"#1cd7a0","sector":"48.104913686431/-1.6441833972931#48.100872816645/-1.6435825824738#48.098952575695/-1.6451275348663#48.098264710743/-1.6437542438507#48.095369844872/-1.6381752490997#48.095713797864/-1.6374456882477#48.096487683684/-1.6368877887726#48.096545008097/-1.6379606723785#48.098551322258/-1.6377031803131#48.100442918186/-1.637316942215#48.102219808535/-1.6362869739532#48.102449080236/-1.6359865665436#48.102936279203/-1.6363728046417#48.103967979422/-1.636244058609#48.104168585393/-1.6395056247711#48.105056973854/-1.6419088840485#48.104913686431/-1.6441833972931#"},{"id":448,"libelle":"Secteur Marc","color":"#000000","sector":"48.109312189476/-1.6737788915634#48.109326517011/-1.672083735466#48.108495513433/-1.6716116666794#48.107664496418/-1.6698306798935#48.107435247981/-1.6669338941574#48.107363607634/-1.6660112142563#48.104884790144/-1.6663330793381#48.104168346635/-1.6714614629745#48.103896095483/-1.6732639074326#48.103982069687/-1.6734570264816#48.104612542787/-1.6724914312363#48.10707704525/-1.6732639074326#48.108896689252/-1.6737359762192#48.109312189476/-1.6737788915634#"},{"id":7999,"libelle":"Secteur Longs Champs","color":"#21e379","sector":"48.12750231304/-1.6519489293569#48.132744067381/-1.633066177892#48.131311947185/-1.6325511937612#48.129335555724/-1.6303195958608#48.127015346953/-1.628731728124#48.125267960042/-1.6328945165151#48.123176746746/-1.6363706593984#48.122202727929/-1.6390743260854#48.121142745291/-1.6426362996572#48.119137312899/-1.6499748235219#48.120970854154/-1.6509189610952#48.12750231304/-1.6519489293569#"},{"id":8581,"libelle":"Pledeliac","color":"#4B77BE","sector":"48.458289272328/-2.3204755783081#48.455443196677/-2.3201322555542#48.45384932462/-2.3228788375854#48.45384932462/-2.3273420333862#48.451230711901/-2.3263120651245#48.447359471811/-2.3261404037476#48.445309872285/-2.3294019699097#48.441552058324/-2.3352384567261#48.436313429526/-2.3493146896362#48.431675083242/-2.3589491844177#48.435490573371/-2.3828959465027#48.438021737405/-2.3996114730835#48.449864425568/-2.3994398117065#48.464891553376/-2.4037313461304#48.491860916212/-2.4248456954956#48.497776326794/-2.3982381820679#48.491747151553/-2.3405599594116#48.481962435927/-2.353949546814#48.472403449296/-2.3385000228882#48.477183167752/-2.3096609115601#48.475248574049/-2.3088026046753#48.472175832424/-2.3144674301147#48.460566017971/-2.3125791549683#48.460793686919/-2.3182439804077#48.463980944977/-2.3199605941772#48.461362854822/-2.3232221603394#48.458289272328/-2.3204755783081#"},{"id":8871,"libelle":"Secteur Haut Acigné","color":"#4b77be","sector":"48.136974/-1.538055#48.13522/-1.538301#48.133824/-1.538043#48.134117/-1.535017#48.134497/-1.533022#48.134848/-1.53167#48.137833/-1.531187#48.137869/-1.530147#48.13798/-1.529127#48.139545/-1.529117#48.139876/-1.532078#48.137299/-1.532915#48.137428/-1.534331#48.136926/-1.536391#48.136898/-1.537206#"},{"id":21752,"libelle":"Secteur Chevré","color":"#fefb00","sector":"48.135943/-1.545188#48.135557/-1.54344#48.134246/-1.541487#48.133573/-1.542549#48.133165/-1.544931#48.1329/-1.546379#48.134024/-1.546766#48.134175/-1.545221#48.134869/-1.545446#"},{"id":21802,"libelle":"Secteur St Ex","color":"#932092","sector":"48.135663/-1.543922#48.136042/-1.543214#48.13668/-1.540854#48.136458/-1.539277#48.135555/-1.53918#48.135184/-1.538901#48.134646/-1.539513#48.133736/-1.539588#48.134246/-1.541487#48.135557/-1.54344#"},{"id":21806,"libelle":"Secteur Andrezieux","color":"#ffff00","sector":"48.141208/-1.524855#48.140033/-1.522666#48.139374/-1.524018#48.138871/-1.52416#48.138642/-1.525211#48.1388/-1.526134#48.139047/-1.527797#48.139688/-1.527593#48.13999/-1.527409#48.140463/-1.526894#48.140922/-1.526143#"},{"id":21808,"libelle":"Secteur externe","color":"#bdcee6","sector":"48.138786/-1.521071#48.140991/-1.519913#48.141506/-1.516394#48.138471/-1.51618#48.13595/-1.51648#48.134175/-1.51781#48.134719/-1.519612#"},{"id":21988,"libelle":"Secteur Rennes 1","color":"#bdcee6","sector":"48.114546/-1.657047#48.114727/-1.65297#48.114555/-1.650138#48.11403/-1.648078#48.111675/-1.646276#48.110228/-1.652563#48.110142/-1.655567#"},{"id":22813,"libelle":"Secteur Monthelon","color":"#3b12ce","sector":"48.148794/-1.561217#48.141029/-1.569371#48.139024/-1.566582#48.137534/-1.559715#48.136073/-1.555939#48.135701/-1.550875#48.138308/-1.549416#48.148165/-1.550145#48.149481/-1.551476#48.149911/-1.555853#"},{"id":23006,"libelle":"Chez Thierry","color":"#bdcee6","sector":"48.212391/-1.519632#48.210504/-1.51474#48.205928/-1.521349#48.20404/-1.52416#48.205099/-1.525297#48.209846/-1.522121#"}], + +"passages":[{"id":18027475,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Allée Henri Fabre","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.118935","gps_lng":"-1.638332","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"Michel Vabre","email":"","phone":""},{"id":18027476,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Allée Henri Fabre","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.118935","gps_lng":"-1.638332","nom_recu":null,"remarque":"hors co repasser à effectué ","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"Jean Vabre","email":"","phone":""},{"id":18027477,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Allée Henri Fabre","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.118935","gps_lng":"-1.638332","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"Laurent Vabre","email":"","phone":""},{"id":18027478,"fk_operation":2644,"fk_sector":7,"fk_user":10011253,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-13 17:14:00","numero":"6","rue":"Allée Henri Fabre","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.118935","gps_lng":"-1.638332","nom_recu":null,"remarque":"Fait par Pierre.test1","montant":"6.00","fk_type_reglement":3,"email_erreur":"","nb_passages":1,"name":"Gilles Vabre","email":"","phone":""},{"id":18027479,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-11 11:43:24","numero":"1","rue":"Allée Alfred Kastler","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.942348","gps_lng":"2.540949","nom_recu":"recu_1196843761.pdf","remarque":"","montant":"5.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"k1","email":"pierre@d6mail.fr","phone":""},{"id":18027480,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-11 11:11:41","numero":"1","rue":"Avenue Pierre Donzelot","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.124726","gps_lng":"-1.640065","nom_recu":null,"remarque":"","montant":"11.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Dun","email":"","phone":""},{"id":18027481,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":5,"fk_adresse":"","passed_at":"2024-11-11 09:44:07","numero":"4","rue":"Allée Blaise Pascal","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.117273","gps_lng":"-1.638439","nom_recu":null,"remarque":"","montant":"50.00","fk_type_reglement":3,"email_erreur":"","nb_passages":6,"name":"","email":"","phone":""},{"id":18027482,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"14","rue":"Avenue Pierre Donzelot","rue_bis":"","ville":"Cesson-Sévigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.113683","gps_lng":"-1.608247","nom_recu":null,"remarque":"ok top ios","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M Donzel","email":"pierre@d6mail.fr","phone":""},{"id":18027483,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"15","rue":"Avenue Pierre Donzelot","rue_bis":"","ville":"Cesson-Sévigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.112516","gps_lng":"-1.607011","nom_recu":null,"remarque":"ok depuis chrome test","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M. Donza","email":"","phone":""},{"id":18027484,"fk_operation":2644,"fk_sector":13,"fk_user":9999985,"fk_type":3,"fk_adresse":"","passed_at":"2024-11-11 07:31:41","numero":"14","rue":"Boulevard de la Duchesse Anne","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.11457","gps_lng":"-1.665744","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M Dimarie","email":"pierre.Vaissaire@gmail.com","phone":""},{"id":18027485,"fk_operation":2644,"fk_sector":13,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"90","rue":"Rue de Fougères","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.120169","gps_lng":"-1.665373","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M. BECHRIR ","email":"","phone":""},{"id":18027486,"fk_operation":2644,"fk_sector":13,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-10 18:31:39","numero":"91","rue":"Rue de Fougères","rue_bis":"","ville":"RENNES","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.127323","gps_lng":"-1.649515","nom_recu":"recu_1373689758.pdf","remarque":"","montant":"10.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"M. Martin","email":"pierre.vaissaire@gmail.com","phone":""},{"id":18027487,"fk_operation":2644,"fk_sector":13,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"92","rue":"Rue de Fougères","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.120362","gps_lng":"-1.66513","nom_recu":null,"remarque":"paiement par chèque 12 euros","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M. BECHRIR ","email":"pierre@d6mail.fr","phone":""},{"id":18027488,"fk_operation":2644,"fk_sector":13,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"18","rue":"Rue de Fougères","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.127323","gps_lng":"-1.649515","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M. morine","email":"","phone":""},{"id":18027489,"fk_operation":2644,"fk_sector":13,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"19","rue":"Rue de Fougères","rue_bis":"","ville":"RENNES","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.127323","gps_lng":"-1.649515","nom_recu":null,"remarque":"Ok 21:50","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"Pierre Vaissaire","email":"pierre.vaissaire@d6soft.fr","phone":""},{"id":18027490,"fk_operation":2644,"fk_sector":13,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"20","rue":"Rue de Fougères","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.127323","gps_lng":"-1.649515","nom_recu":null,"remarque":"par CB","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M. Armand","email":"","phone":""},{"id":18027491,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-14 18:42:06","numero":"7","rue":"Avenue Général Leclerc","rue_bis":"","ville":"Cesson-Sévigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"45.514119","gps_lng":"4.86698","nom_recu":null,"remarque":"","montant":"7.00","fk_type_reglement":2,"email_erreur":"","nb_passages":1,"name":"M.Sept","email":"","phone":""},{"id":18027492,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"10","rue":"Avenue Général Leclerc","rue_bis":"","ville":"Cesson-Sévigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"45.51559","gps_lng":"4.8681","nom_recu":null,"remarque":"hors co","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M. Dix","email":"","phone":""},{"id":18027493,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"9","rue":"Avenue Général Leclerc","rue_bis":"","ville":"Cesson-Sévigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"45.513904","gps_lng":"4.866746","nom_recu":null,"remarque":"hors co","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M.Neuf","email":"","phone":""},{"id":18027494,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Avenue Général Leclerc","rue_bis":"","ville":"Cesson-Sévigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"45.515576","gps_lng":"4.868086","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M.Huit hors co","email":"","phone":""},{"id":18027495,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"14","rue":"Avenue Général Leclerc","rue_bis":"","ville":"Cesson-Sévigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"45.515144","gps_lng":"4.867803","nom_recu":null,"remarque":"hors co le 10","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M.TreizeBis","email":"","phone":""},{"id":18027496,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"12","rue":"Avenue Général Leclerc","rue_bis":"","ville":"Cesson-Sévigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"45.515461","gps_lng":"4.867876","nom_recu":null,"remarque":"hors co le 10","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M.Douze","email":"","phone":""},{"id":18027497,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"11","rue":"Avenue Général Leclerc","rue_bis":"","ville":"Cesson-Sévigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"45.513811","gps_lng":"4.866642","nom_recu":null,"remarque":"hors co le 10","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M.Onze","email":"","phone":""},{"id":18027498,"fk_operation":2644,"fk_sector":8581,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"17","rue":"Saint-Maleu du Val","rue_bis":"","ville":"Plédéliac","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.447755","gps_lng":"-2.3276444","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M. Onze","email":"","phone":""},{"id":18027499,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"23","rue":"Promenade Marcel-Henri Lebouc","rue_bis":"","ville":"RENNES","residence":"","fk_habitat":2,"appt":"101","niveau":"1","gps_lat":"48.127669","gps_lng":"-1.637316","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"Test","email":"","phone":""},{"id":18027500,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"23","rue":"Promenade Marcel-Henri Lebouc","rue_bis":"","ville":"RENNES","residence":"","fk_habitat":2,"appt":"102","niveau":"1","gps_lat":"48.127669","gps_lng":"-1.637316","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"Pierre","email":"","phone":""},{"id":18027501,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"22","rue":"Square Marcel Bouget","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":2,"appt":"21","niveau":"2","gps_lat":"48.127459","gps_lng":"-1.630427","nom_recu":null,"remarque":"Test remarque modif","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"Louis","email":"","phone":""},{"id":18027502,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"24","rue":"Square Marcel Bouget","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":2,"appt":"22","niveau":"2","gps_lat":"48.127324","gps_lng":"-1.63","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"Lopir","email":"","phone":""},{"id":18027503,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-11 07:33:18","numero":"8","rue":"Allée des Mésanges","rue_bis":"","ville":"ACIGNE","residence":"","fk_habitat":2,"appt":"11","niveau":"1","gps_lat":"47.736944","gps_lng":"-0.236197","nom_recu":null,"remarque":"","montant":"10.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Boris","email":"","phone":""},{"id":18027504,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Allée des Mésanges","rue_bis":"","ville":"ACIGNE","residence":"","fk_habitat":2,"appt":"12","niveau":"1","gps_lat":"47.736664","gps_lng":"-0.236316","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M. Doris","email":"","phone":""},{"id":18027505,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-09 10:36:30","numero":"7","rue":"Allée des Fauvettes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13305","gps_lng":"-1.543094","nom_recu":null,"remarque":"","montant":"17.00","fk_type_reglement":3,"email_erreur":"","nb_passages":1,"name":"Franck Depuis","email":"","phone":""},{"id":18027506,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Allée des Fauvettes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133012","gps_lng":"-1.543254","nom_recu":null,"remarque":"test","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"Fred Vargas","email":"","phone":""},{"id":18027507,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"9","rue":"Allée des Fauvettes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133832","gps_lng":"-1.546352","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M. Fred","email":"","phone":""},{"id":18027508,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-09 13:02:09","numero":"10","rue":"Allée des Fauvettes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":2,"appt":"11","niveau":"1","gps_lat":"48.133886","gps_lng":"-1.546185","nom_recu":null,"remarque":"x2","montant":"10.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"M. MERLUETTE","email":"","phone":""},{"id":18027509,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-14 18:45:53","numero":"16","rue":"Allée Alfred Kastler","rue_bis":"","ville":"Cesson-Sévigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.94228","gps_lng":"2.54106","nom_recu":null,"remarque":"","montant":"10.60","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"M. Kramer","email":"","phone":""},{"id":18027510,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"17","rue":"Allée Alfred Kastler","rue_bis":"","ville":"Cesson-Sévigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.94228","gps_lng":"2.54106","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M. Kerin","email":"","phone":""},{"id":18027511,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"18","rue":"Allée Alfred Kastler","rue_bis":"","ville":"Cesson-Sévigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.94228","gps_lng":"2.54106","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M. Kaiser","email":"","phone":""},{"id":18027512,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"19","rue":"Allée Alfred Kastler","rue_bis":"","ville":"Cesson-Sévigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.94228","gps_lng":"2.54106","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M. Kozinsky","email":"","phone":""},{"id":18027513,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"20","rue":"Allée Alfred Kastler","rue_bis":"","ville":"Cesson-Sévigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.94228","gps_lng":"2.54106","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M. Krevin","email":"","phone":""},{"id":18027514,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-08 12:35:43","numero":"5","rue":"Allée Jean Leray","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.119617","gps_lng":"-1.64302","nom_recu":null,"remarque":"","montant":"5.50","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Jean Leroy","email":"","phone":""},{"id":18027515,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Allée Jean Leray","rue_bis":"","ville":"Cesson-Sévigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.1378508","gps_lng":"-1.5342292","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"Jacques Leray","email":"","phone":""},{"id":18027516,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-09 13:32:15","numero":"5","rue":"Allée Jean Leray","rue_bis":"","ville":"Cesson-Sévigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.1396333","gps_lng":"-1.5360518","nom_recu":null,"remarque":"","montant":"5.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Gilles Lerinot","email":"","phone":""},{"id":18027517,"fk_operation":2644,"fk_sector":8581,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-08 13:51:18","numero":"11","rue":"Impasse des Genêts","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133319","gps_lng":"-1.535478","nom_recu":null,"remarque":"","montant":"11.00","fk_type_reglement":3,"email_erreur":"","nb_passages":1,"name":"Judd Gin","email":"","phone":""},{"id":18027518,"fk_operation":2644,"fk_sector":8581,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-08 14:23:52","numero":"14","rue":"Impasse des Genêts","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133319","gps_lng":"-1.535478","nom_recu":null,"remarque":"2x","montant":"14.14","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Julie Ginit","email":"","phone":""},{"id":18027519,"fk_operation":2644,"fk_sector":8581,"fk_user":9999985,"fk_type":3,"fk_adresse":"","passed_at":"2024-11-09 08:24:59","numero":"15","rue":"Impasse des Genêts","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133319","gps_lng":"-1.535478","nom_recu":null,"remarque":"Menaçant","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"Jesus Ginola","email":"","phone":""},{"id":18027520,"fk_operation":2644,"fk_sector":8581,"fk_user":9999985,"fk_type":3,"fk_adresse":"","passed_at":"2024-11-09 12:51:24","numero":"15","rue":"Impasse des Genêts","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133319","gps_lng":"-1.535478","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"Jean Ginelier","email":"","phone":""},{"id":18027521,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"24","rue":"Allée Doyen Charles Bodin","rue_bis":"","ville":"Cesson-Sévigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.127004","gps_lng":"-1.632351","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M. Tillon","email":"pierre@d6mail.fr","phone":""},{"id":18027522,"fk_operation":2644,"fk_sector":8581,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-08 15:12:45","numero":"1","rue":"Impasse des Genêts","rue_bis":"","ville":"ACIGNE","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133319","gps_lng":"-1.535478","nom_recu":"recu_641799064.pdf","remarque":"au 1B depuis utilisateur","montant":"2.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Pierre VAISSAIRE","email":"pierre@d6mail.fr","phone":""},{"id":18027523,"fk_operation":2644,"fk_sector":8581,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-09 12:50:19","numero":"2","rue":"Impasse des Genêts","rue_bis":"","ville":"ACIGNE","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13342","gps_lng":"-1.535295","nom_recu":null,"remarque":"","montant":"12.10","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Paul Ricot","email":"","phone":""},{"id":18027524,"fk_operation":2644,"fk_sector":13,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Rue de Fougères","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.127323","gps_lng":"-1.649515","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M. Villain","email":"","phone":""},{"id":18027525,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Allée Jules Noël","rue_bis":"","ville":"Cesson-Sévigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.1378464","gps_lng":"-1.5341469","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M. Paul Noel","email":"","phone":""},{"id":18027526,"fk_operation":2644,"fk_sector":6,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-09 13:13:47","numero":"1","rue":"Andre Yves Gueguen","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.1396333","gps_lng":"-1.5360518","nom_recu":null,"remarque":"","montant":"11.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Jean Guerlan","email":"","phone":""},{"id":18027527,"fk_operation":2644,"fk_sector":6,"fk_user":10011253,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Andre Yves Gueguen","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.1396333","gps_lng":"-1.5360518","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"Louis Murat","email":"","phone":""},{"id":18027528,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-10-22 13:15:00","numero":"1","rue":"Allée Alfred Kastler","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":2,"appt":"58","niveau":"5","gps_lat":"48.94228","gps_lng":"2.54106","nom_recu":"recu_667793738.pdf","remarque":"Test email","montant":"15.00","fk_type_reglement":2,"email_erreur":"","nb_passages":1,"name":"M. Klein","email":"test1@d6mail.fr","phone":""},{"id":18027529,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-10-25 13:48:00","numero":"3","rue":"Allée Alfred Kastler","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.942322","gps_lng":"2.541188","nom_recu":"recu_327001413.pdf","remarque":"revenir demain ","montant":"13.35","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Mme Klaus","email":"pierre@d6mail.fr","phone":""},{"id":18027530,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-08 13:57:20","numero":"2","rue":"Allée Alfred Kastler","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.942292","gps_lng":"2.540665","nom_recu":null,"remarque":"","montant":"2.20","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"M. Kloos","email":"","phone":""},{"id":18027531,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-09 19:30:40","numero":"3","rue":"Lycée Chateaubriand","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.110267","gps_lng":"-1.67754","nom_recu":"recu_1278453379.pdf","remarque":"","montant":"13.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"M. FREMONT","email":"pierre@d6mail.fr","phone":""},{"id":18027532,"fk_operation":2644,"fk_sector":6,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-09 19:30:04","numero":"6","rue":"Rue Martenot","rue_bis":"","ville":"RENNES","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.112262","gps_lng":"-1.672994","nom_recu":"recu_1714002942.pdf","remarque":"","montant":"16.00","fk_type_reglement":2,"email_erreur":"","nb_passages":1,"name":"Pierre Vaissaire","email":"pierre.vaissaire@d6soft.fr","phone":""},{"id":18027533,"fk_operation":2644,"fk_sector":6,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-09 13:38:27","numero":"7","rue":"Rue Martenot","rue_bis":"","ville":"RENNES","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.112991","gps_lng":"-1.673722","nom_recu":"recu_838455233.pdf","remarque":"","montant":"7.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"HENRYET","email":"pierre@d6mail.fr","phone":""},{"id":18027534,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-11 08:45:50","numero":"6","rue":"Rue Benjamin Franklin","rue_bis":"","ville":"RENNES","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.125516","gps_lng":"-1.635318","nom_recu":"recu_1381800324.pdf","remarque":"","montant":"11.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Francky","email":"pierre.vaissaire@d6soft.fr","phone":""},{"id":18027535,"fk_operation":2644,"fk_sector":8581,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-09 09:55:33","numero":"15C","rue":"Le Clos Denais","rue_bis":"","ville":"PLEDELIAC","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.4537914","gps_lng":"-2.3420160","nom_recu":null,"remarque":"","montant":"15.56","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Boris","email":"","phone":""},{"id":18027536,"fk_operation":2644,"fk_sector":6,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-10 16:36:27","numero":"2","rue":"Allée Rimbaud","rue_bis":"","ville":"RENNES","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.118429","gps_lng":"-1.673879","nom_recu":"recu_1753034004.pdf","remarque":"","montant":"10.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Paul MACUSE","email":"pierre@d6mail.fr","phone":""},{"id":18027537,"fk_operation":2644,"fk_sector":8581,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-09 08:25:55","numero":"13","rue":"Saint-Maleu du Val","rue_bis":"","ville":"PLEDELIAC","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.4501469","gps_lng":"-2.3286151","nom_recu":"recu_1595213733.pdf","remarque":"","montant":"15.00","fk_type_reglement":2,"email_erreur":"","nb_passages":1,"name":"Manu le Melon","email":"pierre@d6mail.fr","phone":""},{"id":18027538,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Place Jean Zay","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136246","gps_lng":"-1.539338","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027539,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Place Jean Zay","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135969","gps_lng":"-1.539665","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027540,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Place Jean Zay","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135671","gps_lng":"-1.53968","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027541,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Rue de Rennes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133908","gps_lng":"-1.539844","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027542,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Square Françoise Dolto","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13468","gps_lng":"-1.539856","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027543,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Rue de Rennes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133977","gps_lng":"-1.540115","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027544,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Rue de Rennes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134083","gps_lng":"-1.540564","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027545,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"10","rue":"Rue de Rennes","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134621","gps_lng":"-1.540753","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027546,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Rue de Rennes","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134398","gps_lng":"-1.540759","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027547,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"11","rue":"Rue Saint-Exupéry","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136261","gps_lng":"-1.540768","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027548,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"10","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135651","gps_lng":"-1.540868","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027549,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"9","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135536","gps_lng":"-1.540887","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027550,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135403","gps_lng":"-1.540908","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027551,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"7","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135285","gps_lng":"-1.540924","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027552,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"12","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136339","gps_lng":"-1.540945","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027553,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135141","gps_lng":"-1.540948","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027554,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135038","gps_lng":"-1.541006","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027555,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"12","rue":"Rue de Rennes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134739","gps_lng":"-1.541009","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027556,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Rue de Rennes","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134208","gps_lng":"-1.541054","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027557,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"11","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135967","gps_lng":"-1.541069","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027558,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"31","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135625","gps_lng":"-1.541104","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027559,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13495","gps_lng":"-1.541105","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027560,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"10","rue":"Rue de Rennes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134484","gps_lng":"-1.541125","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027561,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"32","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13548","gps_lng":"-1.541125","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027562,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"33","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135302","gps_lng":"-1.541152","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027563,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"13","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136271","gps_lng":"-1.541179","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027564,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"12","rue":"Rue de Rennes","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134718","gps_lng":"-1.54118","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027565,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"34","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135074","gps_lng":"-1.541261","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027566,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"13","rue":"Rue Saint-Exupéry","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136181","gps_lng":"-1.541271","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027567,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Rue de Rennes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134272","gps_lng":"-1.541308","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027568,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"12","rue":"Rue de Rennes","rue_bis":"t","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134555","gps_lng":"-1.541365","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027569,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134902","gps_lng":"-1.54137","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027570,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"14","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136073","gps_lng":"-1.541475","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027571,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"35","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135042","gps_lng":"-1.541489","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027572,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"30","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135861","gps_lng":"-1.541562","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027573,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"14","rue":"Rue de Rennes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134477","gps_lng":"-1.541597","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027574,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134869","gps_lng":"-1.541603","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027575,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"29","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135652","gps_lng":"-1.541694","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027576,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"15","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136011","gps_lng":"-1.541701","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027577,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134818","gps_lng":"-1.54182","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027578,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"36","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13499","gps_lng":"-1.541827","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027579,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"16","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135956","gps_lng":"-1.541901","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027580,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"28","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135787","gps_lng":"-1.54192","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027581,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"25","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135451","gps_lng":"-1.541923","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027582,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"24","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135259","gps_lng":"-1.541989","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027583,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"26","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135559","gps_lng":"-1.542059","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027584,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"17","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135897","gps_lng":"-1.542114","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027585,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"23","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135379","gps_lng":"-1.542145","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027586,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"27","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135671","gps_lng":"-1.542206","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027587,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"22","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135496","gps_lng":"-1.542298","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027588,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"16","rue":"Rue de Rennes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134928","gps_lng":"-1.542309","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027589,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"18","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13584","gps_lng":"-1.542323","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027590,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"21","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135604","gps_lng":"-1.542437","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027591,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"20","rue":"Rue de Rennes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135045","gps_lng":"-1.542468","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027592,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"19","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135779","gps_lng":"-1.542545","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027593,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"20","rue":"Rue de Rennes","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135182","gps_lng":"-1.542644","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027594,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"20","rue":"Rue Saint-Exupéry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135731","gps_lng":"-1.542719","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027595,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"22","rue":"Rue de Rennes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135382","gps_lng":"-1.542867","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027596,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"24","rue":"Rue de Rennes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135592","gps_lng":"-1.543225","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027597,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"26","rue":"Rue de Rennes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135656","gps_lng":"-1.543303","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027598,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"28","rue":"Rue de Rennes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135668","gps_lng":"-1.543384","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027599,"fk_operation":2644,"fk_sector":21802,"fk_user":9999980,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"28","rue":"Rue de Rennes","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135702","gps_lng":"-1.543544","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027600,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":1,"fk_adresse":"","passed_at":"2024-10-24 11:12:00","numero":"1","rue":"Rue Robert Doisneau","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139501","gps_lng":"-1.524021","nom_recu":"recu_1696474785.pdf","remarque":"","montant":"11.54","fk_type_reglement":3,"email_erreur":"","nb_passages":1,"name":"Gilles Touchet","email":"test2@d6mail.fr","phone":""},{"id":18027601,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Rue Tristan Corbière","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139506","gps_lng":"-1.524392","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027602,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Rue Tristan Corbière","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139526","gps_lng":"-1.524555","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027603,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Rue Tristan Corbière","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139539","gps_lng":"-1.52474","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027604,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Rue Tristan Corbière","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139564","gps_lng":"-1.525009","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027605,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"10","rue":"Rue Tristan Corbière","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13958","gps_lng":"-1.525184","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027606,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Rue Robert Doisneau","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139586","gps_lng":"-1.523916","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027607,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"12","rue":"Rue Tristan Corbière","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139594","gps_lng":"-1.525344","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027608,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"14","rue":"Rue Tristan Corbière","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139619","gps_lng":"-1.525508","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027609,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Rue Robert Doisneau","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139648","gps_lng":"-1.52382","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027610,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"7","rue":"Rue Robert Doisneau","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139714","gps_lng":"-1.523727","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027611,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"16","rue":"Rue Jean Guéhenno","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139752","gps_lng":"-1.526346","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027612,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"9","rue":"Rue Robert Doisneau","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139769","gps_lng":"-1.523616","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027613,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"11","rue":"Rue Robert Doisneau","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139797","gps_lng":"-1.52347","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027614,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Rue Alfred Jarry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139802","gps_lng":"-1.524573","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027615,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Rue Jean Guéhenno","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139812","gps_lng":"-1.526636","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027616,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Rue Alfred Jarry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139837","gps_lng":"-1.524915","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027617,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"18","rue":"Rue Jean Guéhenno","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139842","gps_lng":"-1.526261","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027618,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"13","rue":"Rue Robert Doisneau","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139869","gps_lng":"-1.523375","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027619,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Rue Jean Guéhenno","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139878","gps_lng":"-1.526579","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027620,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Rue Alfred Jarry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139904","gps_lng":"-1.524544","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027621,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"20","rue":"Rue Jean Guéhenno","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139924","gps_lng":"-1.526166","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027622,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Rue Alfred Jarry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13994","gps_lng":"-1.524828","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027623,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"14","rue":"Rue du Botrel","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139947","gps_lng":"-1.527225","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027624,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Rue Jean Guéhenno","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13995","gps_lng":"-1.526469","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027625,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"15","rue":"Rue Robert Doisneau","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139954","gps_lng":"-1.523127","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027626,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Rue Alfred Jarry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140014","gps_lng":"-1.524522","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027627,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"17","rue":"Rue Robert Doisneau","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140015","gps_lng":"-1.523002","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027628,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"16","rue":"Rue du Botrel","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140016","gps_lng":"-1.527171","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027629,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"22","rue":"Rue Jean Guéhenno","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140018","gps_lng":"-1.526065","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027630,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"7","rue":"Rue Jean Guéhenno","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140039","gps_lng":"-1.526348","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027631,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Rue Alfred Jarry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140044","gps_lng":"-1.524735","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027632,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"18","rue":"Rue du Botrel","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140053","gps_lng":"-1.527107","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027633,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"19","rue":"Rue Robert Doisneau","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.14007","gps_lng":"-1.522882","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027634,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"20","rue":"Rue du Botrel","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140102","gps_lng":"-1.527053","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027635,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"24","rue":"Rue Jean Guéhenno","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140103","gps_lng":"-1.52598","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027636,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Rue Alfred Jarry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140115","gps_lng":"-1.524416","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027637,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"9","rue":"Rue Jean Guéhenno","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140122","gps_lng":"-1.526258","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027638,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"7","rue":"Rue Alfred Jarry","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140147","gps_lng":"-1.524663","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027639,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"22","rue":"Rue du Botrel","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140152","gps_lng":"-1.527004","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027640,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"26","rue":"Rue Jean Guéhenno","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140193","gps_lng":"-1.525881","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027641,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"11","rue":"Rue Jean Guéhenno","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140199","gps_lng":"-1.526172","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027642,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"24","rue":"Rue du Botrel","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140201","gps_lng":"-1.526943","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027643,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"26","rue":"Rue du Botrel","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140248","gps_lng":"-1.52689","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027644,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"28","rue":"Rue Jean Guéhenno","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140277","gps_lng":"-1.525849","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027645,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"28","rue":"Rue du Botrel","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140296","gps_lng":"-1.526823","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027646,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"13","rue":"Rue Jean Guéhenno","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140299","gps_lng":"-1.526047","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027647,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"30","rue":"Rue du Botrel","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140344","gps_lng":"-1.526757","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027648,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"26","rue":"Rue Louis Guilloux","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140356","gps_lng":"-1.524494","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027649,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"32","rue":"Rue du Botrel","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140392","gps_lng":"-1.526692","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027650,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"24","rue":"Rue Louis Guilloux","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140439","gps_lng":"-1.523511","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027651,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Rue Paul Féval","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140473","gps_lng":"-1.525179","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027652,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"22","rue":"Rue Louis Guilloux","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140486","gps_lng":"-1.5236","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027653,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Rue Paul Féval","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.1405","gps_lng":"-1.525108","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027654,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Rue Paul Féval","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140516","gps_lng":"-1.525222","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027655,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"20","rue":"Rue Louis Guilloux","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140532","gps_lng":"-1.523685","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027656,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Rue Paul Féval","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140546","gps_lng":"-1.525154","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027657,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Rue Paul Féval","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140569","gps_lng":"-1.525269","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027658,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"18","rue":"Rue Louis Guilloux","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140582","gps_lng":"-1.523769","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027659,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Rue Paul Féval","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140596","gps_lng":"-1.525199","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027660,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"7","rue":"Rue Paul Féval","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140602","gps_lng":"-1.52498","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027661,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"16","rue":"Rue Louis Guilloux","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140624","gps_lng":"-1.523856","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027662,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"9","rue":"Rue Paul Féval","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140629","gps_lng":"-1.524908","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027663,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"34","rue":"Rue du Botrel","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140643","gps_lng":"-1.526286","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027664,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"11","rue":"Rue Paul Féval","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.14066","gps_lng":"-1.524833","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027665,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"14","rue":"Rue Louis Guilloux","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140666","gps_lng":"-1.523936","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027666,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Rue Paul Féval","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.14067","gps_lng":"-1.524947","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027667,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"10","rue":"Rue Paul Féval","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140701","gps_lng":"-1.524875","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027668,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"36","rue":"Rue du Botrel","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140705","gps_lng":"-1.526137","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027669,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"12","rue":"Rue Louis Guilloux","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140707","gps_lng":"-1.524007","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027670,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"12","rue":"Rue Paul Féval","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140727","gps_lng":"-1.524807","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027671,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"38","rue":"Rue du Botrel","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140796","gps_lng":"-1.525902","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027672,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"40","rue":"Rue du Botrel","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140842","gps_lng":"-1.525755","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027673,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"10","rue":"Rue Louis Guilloux","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140881","gps_lng":"-1.524335","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027674,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"42","rue":"Rue du Botrel","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140891","gps_lng":"-1.525603","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027675,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"44","rue":"Rue du Botrel","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140926","gps_lng":"-1.525448","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027676,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Rue Louis Guilloux","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140934","gps_lng":"-1.524433","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027677,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"46","rue":"Rue du Botrel","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140961","gps_lng":"-1.525295","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027678,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Rue Louis Guilloux","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140978","gps_lng":"-1.524516","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027679,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"48","rue":"Rue du Botrel","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140996","gps_lng":"-1.525128","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027680,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"50","rue":"Rue du Botrel","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.141026","gps_lng":"-1.524986","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027681,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Rue Louis Guilloux","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.141036","gps_lng":"-1.524639","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027682,"fk_operation":2644,"fk_sector":21806,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Rue Louis Guilloux","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.141078","gps_lng":"-1.524715","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027683,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Rue de Joval","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137739","gps_lng":"-1.516728","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"Jean Tournil","email":"pierre.vaissaire@gmail.com","phone":""},{"id":18027684,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Rue de Joval","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137983","gps_lng":"-1.516943","nom_recu":null,"remarque":"M. Lorin a demandé un reçu papier pour ses impôts","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"Jacques Lorin","email":"pierre@d6mail.fr","phone":""},{"id":18027685,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Rue Jules Verne","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13991","gps_lng":"-1.5171","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"Gérard Vernier","email":"pierre.vaissaire@orange.fr","phone":""},{"id":18027686,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"13","rue":"Rue Jules Verne","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140893","gps_lng":"-1.517103","nom_recu":null,"remarque":"Pas ouvert","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"Gilles Hublon","email":"","phone":""},{"id":18027687,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"11","rue":"Rue Jules Verne","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140673","gps_lng":"-1.517114","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027688,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"15","rue":"Rue Jules Verne","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.141129","gps_lng":"-1.517117","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027689,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"7","rue":"Rue Jules Verne","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140159","gps_lng":"-1.517119","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027690,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"9","rue":"Rue Jules Verne","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140421","gps_lng":"-1.517132","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027691,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Rue Jules Verne","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139595","gps_lng":"-1.51728","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027692,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Rue Jules Verne","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139037","gps_lng":"-1.517301","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027693,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"14","rue":"Rue Jules Verne","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.141202","gps_lng":"-1.51745","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027694,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Rue Jules Verne","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139386","gps_lng":"-1.51753","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027695,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Rue Jules Verne","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139886","gps_lng":"-1.517554","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027696,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"10","rue":"Rue Jules Verne","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140652","gps_lng":"-1.517581","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027697,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"12","rue":"Rue Jules Verne","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140877","gps_lng":"-1.517591","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027698,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Rue Jules Verne","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139241","gps_lng":"-1.517592","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027699,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"7","rue":"Rue de Joval","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136956","gps_lng":"-1.517603","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027700,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Rue de Joval","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13733","gps_lng":"-1.517628","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027701,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Rue Marguerite Yourcenar","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140378","gps_lng":"-1.51766","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027702,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Rue Jules Verne","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138936","gps_lng":"-1.517692","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027703,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"9","rue":"Rue de Joval","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136177","gps_lng":"-1.517699","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027704,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Rue de Joval","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137876","gps_lng":"-1.51777","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027705,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Rue Marguerite Yourcenar","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140144","gps_lng":"-1.517776","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027706,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Rue de Joval","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136928","gps_lng":"-1.517867","nom_recu":null,"remarque":"Toujours pas là","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"M. Jobard","email":"","phone":""},{"id":18027707,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Rue de Joval","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136311","gps_lng":"-1.517939","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027708,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Rue de Joval","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135969","gps_lng":"-1.518024","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027709,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"11","rue":"Rue de Joval","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135413","gps_lng":"-1.518024","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027710,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Rue Marguerite Yourcenar","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140356","gps_lng":"-1.518216","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027711,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Rue Marguerite Yourcenar","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140088","gps_lng":"-1.518342","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027712,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"21","rue":"Rue des Vignerons","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138828","gps_lng":"-1.518395","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027713,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Rue Marguerite Yourcenar","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140267","gps_lng":"-1.518724","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027714,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Rue Marguerite Yourcenar","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.14003","gps_lng":"-1.518809","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027715,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"7","rue":"Rue Marguerite Yourcenar","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140183","gps_lng":"-1.519175","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027716,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"19","rue":"Rue des Vignerons","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138876","gps_lng":"-1.519228","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027717,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Rue Marguerite Yourcenar","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139938","gps_lng":"-1.519286","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027718,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"28","rue":"Rue des Vignerons","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138121","gps_lng":"-1.519363","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027719,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"9","rue":"Rue Marguerite Yourcenar","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140098","gps_lng":"-1.519683","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027720,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"26","rue":"Rue des Vignerons","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138166","gps_lng":"-1.519701","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027721,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"10","rue":"Rue Marguerite Yourcenar","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139925","gps_lng":"-1.519731","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027722,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"17","rue":"Rue des Vignerons","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138876","gps_lng":"-1.519903","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027723,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"15","rue":"Rue des Vignerons","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13929","gps_lng":"-1.519987","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027724,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"15","rue":"Rue des Vignerons","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139285","gps_lng":"-1.520101","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027725,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"11","rue":"Rue des Vignerons","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13945","gps_lng":"-1.520431","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027726,"fk_operation":2644,"fk_sector":21808,"fk_user":10016609,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"13","rue":"Rue des Vignerons","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139309","gps_lng":"-1.520462","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027727,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Rue de la Perrière","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48,13537","gps_lng":"-1,54272","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"Jean Pierre","email":"pierre@d6mail.fr","phone":""},{"id":18027728,"fk_operation":2644,"fk_sector":6,"fk_user":9999985,"fk_type":3,"fk_adresse":"","passed_at":"2024-11-09 13:12:47","numero":"1","rue":"Carré Duguesclin","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48,13537","gps_lng":"-1,54272","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"J. GUER","email":"pierre@d6mail.fr","phone":""},{"id":18027729,"fk_operation":2644,"fk_sector":6,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-08 12:32:42","numero":"4","rue":"Carré Duguesclin","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48,13537","gps_lng":"-1,54272","nom_recu":"recu_2025202737.pdf","remarque":"","montant":"14.00","fk_type_reglement":2,"email_erreur":"","nb_passages":1,"name":"M. KELLERMANN","email":"pierre.vaissaire@gmail.com","phone":""},{"id":18027730,"fk_operation":2644,"fk_sector":21988,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-04 11:19:27","numero":"1","rue":"Avenue François Château","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.110442","gps_lng":"-1.655193","nom_recu":"recu_1419471850.pdf","remarque":"","montant":"5.65","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"klipper","email":"pierre.vaissaire@gmail.com","phone":""},{"id":18027731,"fk_operation":2644,"fk_sector":21988,"fk_user":9999985,"fk_type":5,"fk_adresse":"","passed_at":"2024-11-08 10:32:00","numero":"3","rue":"Avenue François Château","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.110442","gps_lng":"-1.654904","nom_recu":null,"remarque":"","montant":"55.00","fk_type_reglement":3,"email_erreur":"","nb_passages":6,"name":"Jalot","email":"","phone":""},{"id":18027732,"fk_operation":2644,"fk_sector":21988,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-07 12:55:30","numero":"4","rue":"Boulevard de Strasbourg","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.111336","gps_lng":"-1.655722","nom_recu":"recu_1928725831.pdf","remarque":"","montant":"4.40","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Lermer","email":"pierre@d6mail.fr","phone":""},{"id":18027733,"fk_operation":2644,"fk_sector":21988,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-08 12:03:11","numero":"2","rue":"Boulevard de Strasbourg","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.111488","gps_lng":"-1.653928","nom_recu":null,"remarque":"","montant":"21.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Lomer","email":"","phone":""},{"id":18027734,"fk_operation":2644,"fk_sector":21988,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-09-24 10:59:40","numero":"4","rue":"Boulevard de Strasbourg","rue_bis":"B","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.1117582","gps_lng":"-1.6558736","nom_recu":null,"remarque":"","montant":"4.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Jacques Hausmann","email":"","phone":""},{"id":18027735,"fk_operation":2644,"fk_sector":21988,"fk_user":9999985,"fk_type":3,"fk_adresse":"","passed_at":"2024-11-04 15:16:28","numero":"6","rue":"Boulevard de Strasbourg","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.113127","gps_lng":"-1.656273","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"Schiller","email":"","phone":""},{"id":18027736,"fk_operation":2644,"fk_sector":21988,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-04 11:14:04","numero":"8","rue":"Boulevard de Strasbourg","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.113478","gps_lng":"-1.656452","nom_recu":"recu_1901453252.pdf","remarque":"","montant":"5.75","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"kipler","email":"pierre@d6mail.fr","phone":""},{"id":18027737,"fk_operation":2644,"fk_sector":21988,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-02 13:05:56","numero":"10","rue":"Boulevard de Strasbourg","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.114203","gps_lng":"-1.656482","nom_recu":null,"remarque":"","montant":"10.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"M. Keller","email":"","phone":""},{"id":18027738,"fk_operation":2644,"fk_sector":21988,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-04 07:44:17","numero":"104","rue":"Avenue Général Leclerc","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.114475","gps_lng":"-1.655775","nom_recu":"recu_50117359.pdf","remarque":"","montant":"6.50","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"kessel","email":"pierre@d6mail.fr","phone":""},{"id":18027739,"fk_operation":2644,"fk_sector":7,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-10-31 08:15:03","numero":"108","rue":"Avenue Général Leclerc","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.114603","gps_lng":"-1.651699","nom_recu":"recu_1700296437.pdf","remarque":"","montant":"10.80","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"pierre","email":"pierre.vaissaire@hotmail.com","phone":""},{"id":18027740,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Rue de la Lande","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13422","gps_lng":"-1.541694","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027741,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Rue de la Lande","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133827","gps_lng":"-1.542311","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027742,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Rue de la Lande","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133729","gps_lng":"-1.542488","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027743,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Rue de la Lande","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133662","gps_lng":"-1.54265","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027744,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Rue de la Lande","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13363","gps_lng":"-1.542805","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027745,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"19","rue":"Rue de Rennes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135127","gps_lng":"-1.54293","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027746,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Rue des Perrets","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134245","gps_lng":"-1.542932","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027747,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"10","rue":"Rue de la Lande","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133604","gps_lng":"-1.542949","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027748,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"10","rue":"Rue de la Lande","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13358","gps_lng":"-1.543107","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027749,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Rue des Perrets","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134009","gps_lng":"-1.543237","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027750,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"12","rue":"Rue de la Lande","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13355","gps_lng":"-1.543304","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027751,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Rue des Perrets","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134121","gps_lng":"-1.543633","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027752,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Rue des Perrets","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134062","gps_lng":"-1.544","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027753,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"14","rue":"Rue de la Lande","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133449","gps_lng":"-1.544101","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027754,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Rue des Perrets","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133854","gps_lng":"-1.5442","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027755,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Rue des Perrets","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134013","gps_lng":"-1.544304","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027756,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"16","rue":"Rue de la Lande","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133321","gps_lng":"-1.5447","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027757,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Avenue du Chevré","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133667","gps_lng":"-1.544926","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027758,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Avenue du Chevré","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.1341","gps_lng":"-1.54506","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027759,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Allée des Chênes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133347","gps_lng":"-1.545204","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027760,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Avenue du Chevré","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134721","gps_lng":"-1.54522","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027761,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"10","rue":"Avenue du Chevré","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133814","gps_lng":"-1.545235","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027762,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Avenue du Chevré","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133973","gps_lng":"-1.545283","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027763,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"16","rue":"Allée des Chênes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133505","gps_lng":"-1.545286","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027764,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Avenue du Chevré","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134115","gps_lng":"-1.545324","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027765,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Allée des Chênes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.1333","gps_lng":"-1.545411","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027766,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"15","rue":"Allée des Chênes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133446","gps_lng":"-1.545549","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027767,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Allée des Chênes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133255","gps_lng":"-1.545614","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027768,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Allée des Chênes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133209","gps_lng":"-1.545813","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027769,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"14","rue":"Allée des Chênes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133372","gps_lng":"-1.545873","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027770,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"12","rue":"Allée des Chênes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133823","gps_lng":"-1.545909","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027771,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"11","rue":"Allée des Chênes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133894","gps_lng":"-1.545982","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027772,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Allée des Chênes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133234","gps_lng":"-1.54606","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027773,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"13","rue":"Allée des Chênes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133648","gps_lng":"-1.546072","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027774,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"10","rue":"Allée des Chênes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133886","gps_lng":"-1.546185","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027775,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Allée des Chênes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133361","gps_lng":"-1.546186","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027776,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"7","rue":"Allée des Chênes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.1335","gps_lng":"-1.54629","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027777,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"9","rue":"Allée des Chênes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133832","gps_lng":"-1.546352","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027778,"fk_operation":2644,"fk_sector":21752,"fk_user":10021972,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Allée des Chênes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.133637","gps_lng":"-1.546394","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027779,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Rue Seica Mare","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13806","gps_lng":"-1.529383","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027780,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"18","rue":"Rue Seica Mare","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139066","gps_lng":"-1.529398","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027781,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"16","rue":"Rue Seica Mare","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138982","gps_lng":"-1.529427","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027782,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"14","rue":"Rue Seica Mare","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138824","gps_lng":"-1.529458","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027783,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"12","rue":"Rue Seica Mare","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138693","gps_lng":"-1.529477","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027784,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"10","rue":"Rue Seica Mare","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138402","gps_lng":"-1.529486","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027785,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Rue Seica Mare","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138297","gps_lng":"-1.5295","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027786,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"20","rue":"Rue Seica Mare","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139164","gps_lng":"-1.529524","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027787,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Rue Seica Mare","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137945","gps_lng":"-1.529597","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027788,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"19","rue":"Rue Seica Mare","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13917","gps_lng":"-1.529614","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027789,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Rue Seica Mare","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13811","gps_lng":"-1.529706","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027790,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"13","rue":"Rue Seica Mare","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138806","gps_lng":"-1.52971","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027791,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"11","rue":"Rue Seica Mare","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138677","gps_lng":"-1.52973","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027792,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"9","rue":"Rue Seica Mare","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138473","gps_lng":"-1.529752","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027793,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"7","rue":"Rue Seica Mare","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138242","gps_lng":"-1.529757","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027794,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"17","rue":"Rue Seica Mare","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139097","gps_lng":"-1.529784","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027795,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"15","rue":"Rue Seica Mare","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139006","gps_lng":"-1.529794","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027796,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"18","rue":"Rue du Champ Janaie","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138414","gps_lng":"-1.530236","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027797,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"9","rue":"Rue du Champ Janaie","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138595","gps_lng":"-1.530261","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027798,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"52","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139242","gps_lng":"-1.530267","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027799,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"50","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13909","gps_lng":"-1.530307","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027800,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"10","rue":"Rue du Champ Janaie","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138029","gps_lng":"-1.530327","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027801,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"12","rue":"Rue du Champ Janaie","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138148","gps_lng":"-1.530349","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027802,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"46","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138915","gps_lng":"-1.530367","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027803,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"44","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138837","gps_lng":"-1.530393","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027804,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"54","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139235","gps_lng":"-1.530463","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027805,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"16","rue":"Rue du Champ Janaie","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138375","gps_lng":"-1.530471","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027806,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"48","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13904","gps_lng":"-1.530545","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027807,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Rue du Champ Janaie","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13806","gps_lng":"-1.530613","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027808,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"14","rue":"Rue du Champ Janaie","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138338","gps_lng":"-1.530649","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027809,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"56","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139256","gps_lng":"-1.530651","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027810,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"7","rue":"Rue du Champ Janaie","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138496","gps_lng":"-1.530788","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027811,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Rue du Champ Janaie","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138115","gps_lng":"-1.530864","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027812,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"58","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139278","gps_lng":"-1.530869","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027813,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"65","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138994","gps_lng":"-1.530892","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027814,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"42","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138805","gps_lng":"-1.530929","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027815,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Rue du Champ Janaie","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138441","gps_lng":"-1.531006","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027816,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Rue du Champ Janaie","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138228","gps_lng":"-1.531067","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027817,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"60","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139316","gps_lng":"-1.531071","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027818,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"63","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138961","gps_lng":"-1.531112","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027819,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"67","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139128","gps_lng":"-1.531116","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027820,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"40","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138788","gps_lng":"-1.531157","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027821,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Rue du Champ Janaie","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138383","gps_lng":"-1.531204","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027822,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"62","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139308","gps_lng":"-1.531227","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027823,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"64","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139179","gps_lng":"-1.531249","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027824,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"38","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138696","gps_lng":"-1.531289","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027825,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Rue du Champ Janaie","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138139","gps_lng":"-1.531331","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027826,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"61","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138933","gps_lng":"-1.531367","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027827,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Rue du Champ Janaie","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138326","gps_lng":"-1.53138","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027828,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"38","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139448","gps_lng":"-1.531514","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027829,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"36","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137954","gps_lng":"-1.531546","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027830,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"59","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139112","gps_lng":"-1.531562","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027831,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"28","rue":"Rue du Clos des Vignes","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.1371","gps_lng":"-1.5316","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027832,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"28","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136924","gps_lng":"-1.531661","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027833,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"30","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137487","gps_lng":"-1.53168","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027834,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"26","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136759","gps_lng":"-1.531696","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027835,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"57","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139015","gps_lng":"-1.531699","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027836,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"34","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137801","gps_lng":"-1.531707","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027837,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"55","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138906","gps_lng":"-1.531717","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027838,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"49","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138464","gps_lng":"-1.531723","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027839,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"24","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136609","gps_lng":"-1.531729","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027840,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"36","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139479","gps_lng":"-1.531732","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027841,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"47","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13834","gps_lng":"-1.531739","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027842,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"53","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138735","gps_lng":"-1.531758","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027843,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"32","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137649","gps_lng":"-1.53176","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027844,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Rue des Vignerons","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134985","gps_lng":"-1.53176","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027845,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"45","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137999","gps_lng":"-1.531761","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027846,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"22","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136461","gps_lng":"-1.531762","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027847,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"51","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13865","gps_lng":"-1.531768","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027848,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"20","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136306","gps_lng":"-1.531795","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027849,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"16","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135905","gps_lng":"-1.531818","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027850,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"25","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13729","gps_lng":"-1.531829","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027851,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"18","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13614","gps_lng":"-1.531852","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027852,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"23","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137094","gps_lng":"-1.531876","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027853,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"10","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135467","gps_lng":"-1.531882","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027854,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135421","gps_lng":"-1.531885","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027855,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"21","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13691","gps_lng":"-1.531918","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027856,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"43","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137829","gps_lng":"-1.531942","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027857,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"19","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136736","gps_lng":"-1.531957","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027858,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"34","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139511","gps_lng":"-1.531978","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027859,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"27","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137553","gps_lng":"-1.531991","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027860,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"17","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136558","gps_lng":"-1.531995","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027861,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"15","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136373","gps_lng":"-1.532036","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027862,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"13","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136173","gps_lng":"-1.532103","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027863,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"41","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137984","gps_lng":"-1.532111","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027864,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Rue des Vignerons","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134868","gps_lng":"-1.532177","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027865,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"14","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135815","gps_lng":"-1.532187","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027866,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"29","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137577","gps_lng":"-1.532215","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027867,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"32","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138839","gps_lng":"-1.532229","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027868,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134958","gps_lng":"-1.532276","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027869,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"12","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135607","gps_lng":"-1.532278","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027870,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"39","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137981","gps_lng":"-1.532302","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027871,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135317","gps_lng":"-1.532303","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027872,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"30","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138531","gps_lng":"-1.532312","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027873,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135144","gps_lng":"-1.532313","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027874,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"37","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137869","gps_lng":"-1.532349","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027875,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"28","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138355","gps_lng":"-1.53236","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027876,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"26","rue":"Rue des Verdaudais","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137272","gps_lng":"-1.532373","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027877,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"35","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137814","gps_lng":"-1.532377","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027878,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"31","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137611","gps_lng":"-1.532381","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027879,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"11","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135896","gps_lng":"-1.532413","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027880,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"33","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137696","gps_lng":"-1.532423","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027881,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"20","rue":"Rue des Verdaudais","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136642","gps_lng":"-1.532511","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027882,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"9","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135688","gps_lng":"-1.532522","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027883,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134968","gps_lng":"-1.532545","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027884,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"16","rue":"Rue des Verdaudais","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136434","gps_lng":"-1.532552","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027885,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"7","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135325","gps_lng":"-1.532558","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027886,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Rue du Clos des Vignes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135154","gps_lng":"-1.532569","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027887,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"22","rue":"Rue des Verdaudais","rue_bis":"t","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136922","gps_lng":"-1.532611","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027888,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"16","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136252","gps_lng":"-1.532693","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027889,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Rue des Vignerons","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134651","gps_lng":"-1.53273","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027890,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"26","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13736","gps_lng":"-1.532748","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027891,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"14","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136166","gps_lng":"-1.532753","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027892,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"24","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137161","gps_lng":"-1.53279","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027893,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"22","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136977","gps_lng":"-1.532887","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027894,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Rue du Courtil","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135573","gps_lng":"-1.532938","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027895,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Rue du Courtil","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13541","gps_lng":"-1.532942","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027896,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"22","rue":"Rue des Verdaudais","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136819","gps_lng":"-1.53298","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027897,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"20","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136719","gps_lng":"-1.532997","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027898,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"18","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13656","gps_lng":"-1.533061","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027899,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"21","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136971","gps_lng":"-1.533143","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027900,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Rue des Rosiers","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137165","gps_lng":"-1.533155","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027901,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"18","rue":"Rue des Verdaudais","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136404","gps_lng":"-1.533158","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027902,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Rue du Courtil","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135565","gps_lng":"-1.533175","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027903,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Rue du Courtil","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135402","gps_lng":"-1.533207","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027904,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134797","gps_lng":"-1.533253","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027905,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"12","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136133","gps_lng":"-1.533258","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027906,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134791","gps_lng":"-1.533355","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027907,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Impasse des Acacias","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136661","gps_lng":"-1.533367","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027908,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"10","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135991","gps_lng":"-1.533373","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027909,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"19","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136386","gps_lng":"-1.533391","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027910,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Rue des Rosiers","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137301","gps_lng":"-1.533402","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027911,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Rue du Courtil","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135557","gps_lng":"-1.53341","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027912,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Impasse des Acacias","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136882","gps_lng":"-1.533433","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027913,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Rue du Courtil","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135392","gps_lng":"-1.533448","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027914,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Rue des Verdaudais","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13479","gps_lng":"-1.533459","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027915,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"17","rue":"Rue des Verdaudais","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136244","gps_lng":"-1.53346","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027916,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"17","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136132","gps_lng":"-1.533515","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027917,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135056","gps_lng":"-1.533554","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027918,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Rue des Rosiers","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137324","gps_lng":"-1.533613","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027919,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134556","gps_lng":"-1.533658","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027920,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"15","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135877","gps_lng":"-1.533665","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027921,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Impasse des Acacias","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136932","gps_lng":"-1.533708","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027922,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134709","gps_lng":"-1.53371","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027923,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134811","gps_lng":"-1.533728","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027924,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"7","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134955","gps_lng":"-1.533764","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027925,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Impasse des Acacias","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136731","gps_lng":"-1.533775","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027926,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"13","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135671","gps_lng":"-1.533775","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027927,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"9","rue":"Rue des Verdaudais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135139","gps_lng":"-1.533801","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027928,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"7","rue":"Rue des Rosiers","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137344","gps_lng":"-1.533846","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027929,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Impasse des Acacias","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136679","gps_lng":"-1.533888","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027930,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13551","gps_lng":"-1.533916","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027931,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Impasse des Acacias","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136895","gps_lng":"-1.534015","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027932,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"9","rue":"Rue des Rosiers","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137369","gps_lng":"-1.534061","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027933,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"7","rue":"Impasse des Acacias","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136702","gps_lng":"-1.534089","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027934,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Impasse des Acacias","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136768","gps_lng":"-1.534157","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027935,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Avenue Jeanne-Marie Martin","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135503","gps_lng":"-1.534178","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027936,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"11","rue":"Rue des Rosiers","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137386","gps_lng":"-1.534235","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027937,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"13","rue":"Rue Prosper Chubert","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134822","gps_lng":"-1.53433","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027938,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135307","gps_lng":"-1.534368","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027939,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Avenue de l'Abbé Barbedet","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134317","gps_lng":"-1.534405","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027940,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135495","gps_lng":"-1.53442","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027941,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"12","rue":"Rue Prosper Chubert","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134667","gps_lng":"-1.53442","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027942,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Impasse du Verger","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135649","gps_lng":"-1.534513","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027943,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"13","rue":"Rue des Rosiers","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137363","gps_lng":"-1.534585","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027944,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Allée des Camélias","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137117","gps_lng":"-1.534615","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027945,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Impasse du Verger","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135801","gps_lng":"-1.534632","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027946,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Allée des Camélias","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136957","gps_lng":"-1.53466","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027947,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Impasse du Verger","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135798","gps_lng":"-1.534699","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027948,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"9","rue":"Allée des Camélias","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136314","gps_lng":"-1.534702","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027949,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135293","gps_lng":"-1.534717","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027950,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"11","rue":"Allée des Camélias","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136172","gps_lng":"-1.534745","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027951,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Allée des Camélias","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136789","gps_lng":"-1.534791","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027952,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135479","gps_lng":"-1.534825","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027953,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Allée des Camélias","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136628","gps_lng":"-1.534886","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027954,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"7","rue":"Allée des Camélias","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136438","gps_lng":"-1.534894","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027955,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"10","rue":"Allée des Camélias","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136114","gps_lng":"-1.534928","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027956,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Avenue de l'Abbé Barbedet","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134261","gps_lng":"-1.535005","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027957,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135282","gps_lng":"-1.535023","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027958,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135468","gps_lng":"-1.535061","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027959,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Allée des Camélias","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13617","gps_lng":"-1.535082","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027960,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"10","rue":"Rue Prosper Chubert","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134605","gps_lng":"-1.535108","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027961,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Allée des Camélias","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136288","gps_lng":"-1.535158","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027962,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Allée des Camélias","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136573","gps_lng":"-1.535174","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027963,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Allée des Camélias","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136418","gps_lng":"-1.535192","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027964,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"11","rue":"Rue Prosper Chubert","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134743","gps_lng":"-1.535193","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027965,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"7","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135274","gps_lng":"-1.535258","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027966,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"10","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135458","gps_lng":"-1.535284","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027967,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"10","rue":"Avenue de l'Abbé Barbedet","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134233","gps_lng":"-1.53529","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027968,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Allée des Lilas","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136935","gps_lng":"-1.535341","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027969,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Rue Prosper Chubert","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134578","gps_lng":"-1.535345","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027970,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"9","rue":"Rue Prosper Chubert","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134724","gps_lng":"-1.535396","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027971,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Allée des Lilas","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137076","gps_lng":"-1.535403","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027972,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Allée des Lilas","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136747","gps_lng":"-1.535437","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027973,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"12","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135448","gps_lng":"-1.535495","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027974,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Allée des Lilas","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136409","gps_lng":"-1.535528","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027975,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"12","rue":"Avenue de l'Abbé Barbedet","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134209","gps_lng":"-1.535535","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027976,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Rue Prosper Chubert","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134552","gps_lng":"-1.535595","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027977,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Allée des Lilas","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136569","gps_lng":"-1.535659","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027978,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"14","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135439","gps_lng":"-1.53571","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027979,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"7","rue":"Rue Prosper Chubert","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134796","gps_lng":"-1.535716","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027980,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Allée des Lilas","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13599","gps_lng":"-1.535746","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027981,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Allée des Lilas","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136152","gps_lng":"-1.535771","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027982,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Allée de la Noë","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135761","gps_lng":"-1.53578","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027983,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"9","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135255","gps_lng":"-1.535783","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027984,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"15","rue":"Rue des Tertres","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13705","gps_lng":"-1.535827","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027985,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"14","rue":"Avenue de l'Abbé Barbedet","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13418","gps_lng":"-1.53583","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027986,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Rue Prosper Chubert","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134434","gps_lng":"-1.535876","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027987,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Rue Prosper Chubert","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134674","gps_lng":"-1.535917","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027988,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"17","rue":"Rue des Tertres","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137003","gps_lng":"-1.535998","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027989,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"16","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135434","gps_lng":"-1.53607","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027990,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Rue Prosper Chubert","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134465","gps_lng":"-1.536131","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027991,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"19","rue":"Rue des Tertres","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136967","gps_lng":"-1.536136","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027992,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Rue Louise de Kermarrec","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135165","gps_lng":"-1.536144","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027993,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Rue Prosper Chubert","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134649","gps_lng":"-1.536226","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027994,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"18","rue":"Avenue de l'Abbé Barbedet","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134139","gps_lng":"-1.536233","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027995,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Allée de la Noë","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135961","gps_lng":"-1.536245","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027996,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Rue Prosper Chubert","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13444","gps_lng":"-1.536249","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027997,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Allée de la Noë","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136025","gps_lng":"-1.536253","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027998,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"33","rue":"Allée de la Noë","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136589","gps_lng":"-1.536305","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18027999,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"35","rue":"Allée de la Noë","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136457","gps_lng":"-1.536306","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028000,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"37","rue":"Allée de la Noë","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136344","gps_lng":"-1.536308","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028001,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"39","rue":"Allée de la Noë","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13626","gps_lng":"-1.536308","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028002,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Allée de la Noë","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135858","gps_lng":"-1.536318","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028003,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"31","rue":"Allée de la Noë","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136712","gps_lng":"-1.536324","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028004,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"21","rue":"Rue des Tertres","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136919","gps_lng":"-1.536338","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028005,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"3","rue":"Rue Louise de Kermarrec","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13507","gps_lng":"-1.536373","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028006,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"18","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135633","gps_lng":"-1.536383","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028007,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"20","rue":"Avenue de l'Abbé Barbedet","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134124","gps_lng":"-1.53639","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028008,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"7","rue":"Allée de la Noë","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135857","gps_lng":"-1.536453","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028009,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"17","rue":"Allée de la Noë","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136228","gps_lng":"-1.536525","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028010,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"15","rue":"Allée de la Noë","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136142","gps_lng":"-1.536527","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028011,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"29","rue":"Allée de la Noë","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136726","gps_lng":"-1.536527","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028012,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"19","rue":"Allée de la Noë","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136364","gps_lng":"-1.536553","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028013,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"13","rue":"Allée de la Noë","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136046","gps_lng":"-1.536564","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028014,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"5","rue":"Rue Louise de Kermarrec","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134974","gps_lng":"-1.53658","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028015,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"9","rue":"Allée de la Noë","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135871","gps_lng":"-1.536583","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028016,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"20","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135408","gps_lng":"-1.536593","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028017,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"22","rue":"Avenue de l'Abbé Barbedet","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134102","gps_lng":"-1.536614","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028018,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"1","rue":"Rue Prosper Chubert","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134543","gps_lng":"-1.536619","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028019,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"21","rue":"Allée de la Noë","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136469","gps_lng":"-1.536636","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028020,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"11","rue":"Allée de la Noë","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135894","gps_lng":"-1.536643","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028021,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"27","rue":"Allée de la Noë","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136674","gps_lng":"-1.536672","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028022,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"23","rue":"Allée de la Noë","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136546","gps_lng":"-1.5367","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028023,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"25","rue":"Allée de la Noë","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136598","gps_lng":"-1.536743","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028024,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"7","rue":"Rue Louise de Kermarrec","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134763","gps_lng":"-1.536747","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028025,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"11","rue":"Rue Louise de Kermarrec","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134267","gps_lng":"-1.536752","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028026,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"22","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135389","gps_lng":"-1.536845","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028027,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"11","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135203","gps_lng":"-1.536911","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028028,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"24","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135369","gps_lng":"-1.537091","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028029,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"51","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136681","gps_lng":"-1.537095","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028030,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"37","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135969","gps_lng":"-1.537132","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028031,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"13","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135186","gps_lng":"-1.537162","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028032,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"35","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135816","gps_lng":"-1.537183","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028033,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"39","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136092","gps_lng":"-1.53725","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028034,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"53","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136761","gps_lng":"-1.537258","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028035,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"41","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136179","gps_lng":"-1.537272","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028036,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"33","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135811","gps_lng":"-1.537285","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028037,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"43","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136298","gps_lng":"-1.537306","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028038,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"26","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13535","gps_lng":"-1.537348","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028039,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"45","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13638","gps_lng":"-1.537365","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028040,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"15","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135169","gps_lng":"-1.537388","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028041,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"31","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135805","gps_lng":"-1.537437","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028042,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"47","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136448","gps_lng":"-1.537443","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028043,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"22","rue":"Avenue de l'Abbé Barbedet","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134273","gps_lng":"-1.537478","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028044,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"17","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136177","gps_lng":"-1.537496","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028045,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"19","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136091","gps_lng":"-1.537525","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028046,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"49","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136516","gps_lng":"-1.537538","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028047,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"29","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135795","gps_lng":"-1.537543","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028048,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"14","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136814","gps_lng":"-1.537544","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028049,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"28","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135335","gps_lng":"-1.53755","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028050,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"15","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136334","gps_lng":"-1.537581","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028051,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"21","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135999","gps_lng":"-1.537617","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028052,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"17","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135151","gps_lng":"-1.53762","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028053,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"12","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136719","gps_lng":"-1.537649","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028054,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"27","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135799","gps_lng":"-1.537657","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028055,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"28","rue":"Avenue Jeanne-Marie Martin","rue_bis":"b","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135325","gps_lng":"-1.53767","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028056,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"10","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136633","gps_lng":"-1.537741","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028057,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"25","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135873","gps_lng":"-1.537747","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028058,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"23","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135929","gps_lng":"-1.537771","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028059,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"8","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136543","gps_lng":"-1.537824","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028060,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"19","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135135","gps_lng":"-1.537846","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028061,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"30","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135312","gps_lng":"-1.537849","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028062,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"6","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136456","gps_lng":"-1.53789","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028063,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"13","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.134265","gps_lng":"-1.537962","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028064,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"4","rue":"Rue du Stade","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136324","gps_lng":"-1.53798","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028065,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"21","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135117","gps_lng":"-1.538074","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028066,"fk_operation":2644,"fk_sector":8871,"fk_user":10022234,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"32","rue":"Avenue Jeanne-Marie Martin","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.135294","gps_lng":"-1.538106","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028067,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"2","rue":"Le Courtillon","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209527","gps_lng":"-1.518115","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028068,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"100","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.20987","gps_lng":"-1.515835","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028069,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"102","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.208809","gps_lng":"-1.517383","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028070,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"104","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.208549","gps_lng":"-1.518814","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028071,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"106","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.208301","gps_lng":"-1.5181","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028072,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"108","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.208244","gps_lng":"-1.518188","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028073,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"110","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.208039","gps_lng":"-1.518483","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028074,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"112","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.207905","gps_lng":"-1.518665","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028075,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"114","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.207864","gps_lng":"-1.518724","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028076,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"116","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.207793","gps_lng":"-1.518844","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028077,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"118","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.207643","gps_lng":"-1.519049","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028078,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"120","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.207514","gps_lng":"-1.519224","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028079,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"122","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.207211","gps_lng":"-1.51962","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028080,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"124","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.206962","gps_lng":"-1.520361","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028081,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"126","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.207247","gps_lng":"-1.520523","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028082,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"128","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.207052","gps_lng":"-1.520603","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028083,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"130","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.20698","gps_lng":"-1.520644","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028084,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"132","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.206931","gps_lng":"-1.520563","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028085,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"134","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.206628","gps_lng":"-1.520421","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028086,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"136","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.20647","gps_lng":"-1.520722","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028087,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"138","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.206143","gps_lng":"-1.521165","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028088,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"142","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.205717","gps_lng":"-1.52181","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028089,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"144","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.205602","gps_lng":"-1.521986","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028090,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"146","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.205476","gps_lng":"-1.522167","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028091,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"148","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.205272","gps_lng":"-1.52247","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028092,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"150","rue":"Rue de Rennes","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.204659","gps_lng":"-1.523351","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028093,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"108","rue":"Rue de Rennes","rue_bis":"e","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.208341","gps_lng":"-1.518611","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028094,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:25:59","numero":"136","rue":"Rue de Rennes","rue_bis":"b","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.206462","gps_lng":"-1.520703","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028095,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":1,"fk_adresse":"","passed_at":"2024-12-10 08:58:00","numero":"108","rue":"Rue de Rennes","rue_bis":"c","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.208341","gps_lng":"-1.518611","nom_recu":"recu_1837025894.pdf","remarque":"Test montant négatif","montant":"10.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Delmer","email":"test1@d6mail.fr","phone":""},{"id":18028096,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"108","rue":"Rue de Rennes","rue_bis":"b","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.208341","gps_lng":"-1.518611","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028097,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"150","rue":"Rue de Rennes","rue_bis":"b","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.20417","gps_lng":"-1.524059","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028098,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"108","rue":"Rue de Rennes","rue_bis":"a","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.208341","gps_lng":"-1.518611","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028099,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"100","rue":"Rue de Rennes","rue_bis":"b","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209684","gps_lng":"-1.515998","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028100,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"108","rue":"Rue de Rennes","rue_bis":"d","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.208341","gps_lng":"-1.518611","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028101,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"1","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209498","gps_lng":"-1.517509","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028102,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"2","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209474","gps_lng":"-1.51733","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028103,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"3","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209487","gps_lng":"-1.517579","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028104,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"4","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209456","gps_lng":"-1.517273","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028105,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"5","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209553","gps_lng":"-1.517499","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028106,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"6","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209419","gps_lng":"-1.517156","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028107,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"7","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209648","gps_lng":"-1.517398","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028108,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"8","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209391","gps_lng":"-1.517009","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028109,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"9","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209738","gps_lng":"-1.517284","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028110,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"10","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209395","gps_lng":"-1.516952","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028111,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"11","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209572","gps_lng":"-1.517337","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028112,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"12","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209491","gps_lng":"-1.51675","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028113,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"13","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209509","gps_lng":"-1.516894","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028114,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"14","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209491","gps_lng":"-1.51675","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028115,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"15","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209595","gps_lng":"-1.516777","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028116,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"16","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209621","gps_lng":"-1.518228","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028117,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"17","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209717","gps_lng":"-1.516598","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028118,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"18","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209674","gps_lng":"-1.516513","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028119,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"19","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209711","gps_lng":"-1.516545","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028120,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"20","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209824","gps_lng":"-1.516289","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028121,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"21","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209805","gps_lng":"-1.516484","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028122,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"22","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209852","gps_lng":"-1.516264","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028123,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"23","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210018","gps_lng":"-1.51659","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028124,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"24","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210003","gps_lng":"-1.516389","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028125,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"25","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210147","gps_lng":"-1.516789","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028126,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"26","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210058","gps_lng":"-1.516481","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028127,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"27","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210195","gps_lng":"-1.516829","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028128,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"28","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210076","gps_lng":"-1.516506","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028129,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"30","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210176","gps_lng":"-1.516663","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028130,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"32","rue":"Rue du Chene Micault","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210264","gps_lng":"-1.516794","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028131,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"2","rue":"Impasse Philippe Cattiau","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210516","gps_lng":"-1.520151","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028132,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"4","rue":"Impasse Philippe Cattiau","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210487","gps_lng":"-1.520002","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028133,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"6","rue":"Impasse Philippe Cattiau","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210457","gps_lng":"-1.519849","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028134,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"8","rue":"Impasse Philippe Cattiau","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210316","gps_lng":"-1.519641","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028135,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"10","rue":"Impasse Philippe Cattiau","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210314","gps_lng":"-1.519639","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028136,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"12","rue":"Impasse Philippe Cattiau","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210356","gps_lng":"-1.519536","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028137,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"14","rue":"Impasse Philippe Cattiau","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210415","gps_lng":"-1.519589","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028138,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"16","rue":"Impasse Philippe Cattiau","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.2105","gps_lng":"-1.519666","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028139,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"18","rue":"Impasse Philippe Cattiau","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210563","gps_lng":"-1.519891","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028140,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"20","rue":"Impasse Philippe Cattiau","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210554","gps_lng":"-1.519911","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028141,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"22","rue":"Impasse Philippe Cattiau","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210581","gps_lng":"-1.520052","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028142,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"1","rue":"Rue Marcel Cerdan","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209652","gps_lng":"-1.518914","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028143,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"2","rue":"Rue Marcel Cerdan","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209708","gps_lng":"-1.51884","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028144,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"3","rue":"Rue Marcel Cerdan","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209712","gps_lng":"-1.519089","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028145,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"4","rue":"Rue Marcel Cerdan","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209803","gps_lng":"-1.519049","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028146,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"5","rue":"Rue Marcel Cerdan","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209784","gps_lng":"-1.519396","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028147,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"6","rue":"Rue Marcel Cerdan","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209849","gps_lng":"-1.519293","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028148,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"7","rue":"Rue Marcel Cerdan","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209836","gps_lng":"-1.519646","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028149,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"8","rue":"Rue Marcel Cerdan","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209895","gps_lng":"-1.519536","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028150,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"9","rue":"Rue Marcel Cerdan","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210121","gps_lng":"-1.52037","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028151,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"10","rue":"Rue Marcel Cerdan","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.21013","gps_lng":"-1.52025","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028152,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"11","rue":"Rue Marcel Cerdan","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210202","gps_lng":"-1.520383","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028153,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"12","rue":"Rue Marcel Cerdan","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210215","gps_lng":"-1.520264","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028154,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"13","rue":"Rue Marcel Cerdan","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209942","gps_lng":"-1.520012","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028155,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"14","rue":"Rue Marcel Cerdan","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210011","gps_lng":"-1.519949","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028156,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"15","rue":"Rue Marcel Cerdan","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209986","gps_lng":"-1.520153","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028157,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"16","rue":"Rue Marcel Cerdan","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210061","gps_lng":"-1.520107","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028158,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"17","rue":"Rue Marcel Cerdan","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210032","gps_lng":"-1.520285","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028159,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"18","rue":"Rue Marcel Cerdan","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210197","gps_lng":"-1.52029","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028160,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"1","rue":"Allee des Meliades","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209369","gps_lng":"-1.518253","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028161,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"2","rue":"Allee des Meliades","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209427","gps_lng":"-1.518339","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028162,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"3","rue":"Allee des Meliades","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209369","gps_lng":"-1.518253","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028163,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"4","rue":"Allee des Meliades","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209402","gps_lng":"-1.518374","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028164,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"5","rue":"Allee des Meliades","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209369","gps_lng":"-1.518253","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028165,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"6","rue":"Allee des Meliades","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209378","gps_lng":"-1.518409","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028166,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"8","rue":"Allee des Meliades","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209354","gps_lng":"-1.518444","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028167,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"9","rue":"Allee des Meliades","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209369","gps_lng":"-1.518253","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028168,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"10","rue":"Allee des Meliades","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209329","gps_lng":"-1.518479","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028169,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"11","rue":"Allee des Meliades","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209369","gps_lng":"-1.518253","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028170,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"12","rue":"Allee des Meliades","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209305","gps_lng":"-1.518514","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028171,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"13","rue":"Allee des Meliades","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209369","gps_lng":"-1.518253","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028172,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"14","rue":"Allee des Meliades","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209281","gps_lng":"-1.518549","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028173,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"15","rue":"Allee des Meliades","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209369","gps_lng":"-1.518253","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028174,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"16","rue":"Allee des Meliades","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209253","gps_lng":"-1.518577","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028175,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"17","rue":"Allee des Meliades","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209369","gps_lng":"-1.518253","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028176,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"18","rue":"Allee des Meliades","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209223","gps_lng":"-1.5186","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028177,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"19","rue":"Allee des Meliades","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209369","gps_lng":"-1.518253","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028178,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"20","rue":"Allee des Meliades","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209191","gps_lng":"-1.518619","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028179,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"1","rue":"Rue de la Bretonniere","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210495","gps_lng":"-1.515954","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028180,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"3","rue":"Rue de la Bretonniere","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210623","gps_lng":"-1.516261","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028181,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"5","rue":"Rue de la Bretonniere","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210488","gps_lng":"-1.516778","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028182,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"7","rue":"Rue de la Bretonniere","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210786","gps_lng":"-1.516707","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028183,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"9","rue":"Rue de la Bretonniere","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210914","gps_lng":"-1.516934","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028184,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"11","rue":"Rue de la Bretonniere","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.211018","gps_lng":"-1.517165","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028185,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"13","rue":"Rue de la Bretonniere","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.2111","gps_lng":"-1.517343","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028186,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"15","rue":"Rue de la Bretonniere","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.211186","gps_lng":"-1.517527","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028187,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"16","rue":"Rue de la Bretonniere","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.211263","gps_lng":"-1.517399","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028188,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"17","rue":"Rue de la Bretonniere","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.211292","gps_lng":"-1.517778","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028189,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"18","rue":"Rue de la Bretonniere","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.211371","gps_lng":"-1.517683","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028190,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"19","rue":"Rue de la Bretonniere","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.211366","gps_lng":"-1.518001","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028191,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"20","rue":"Rue de la Bretonniere","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.21144","gps_lng":"-1.517871","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028192,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"21","rue":"Rue de la Bretonniere","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.211432","gps_lng":"-1.518235","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028193,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"22","rue":"Rue de la Bretonniere","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.21153","gps_lng":"-1.518119","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028194,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"23","rue":"Rue de la Bretonniere","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.211503","gps_lng":"-1.518463","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028195,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"24","rue":"Rue de la Bretonniere","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.211602","gps_lng":"-1.518317","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028196,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"26","rue":"Rue de la Bretonniere","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.211921","gps_lng":"-1.519133","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028197,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"28","rue":"Rue de la Bretonniere","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.211798","gps_lng":"-1.518927","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028198,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"29","rue":"Rue de la Bretonniere","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210267","gps_lng":"-1.515166","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028199,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"30","rue":"Rue de la Bretonniere","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.211837","gps_lng":"-1.519126","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028200,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"1","rue":"Rue Camille Muffat","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210606","gps_lng":"-1.518749","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028201,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"2","rue":"Rue Camille Muffat","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210693","gps_lng":"-1.518766","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028202,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"3","rue":"Rue Camille Muffat","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210606","gps_lng":"-1.518749","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028203,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"4","rue":"Rue Camille Muffat","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210693","gps_lng":"-1.518766","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028204,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"5","rue":"Rue Camille Muffat","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210709","gps_lng":"-1.519086","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028205,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"6","rue":"Rue Camille Muffat","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210747","gps_lng":"-1.518957","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028206,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"7","rue":"Rue Camille Muffat","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210772","gps_lng":"-1.51919","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028207,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"8","rue":"Rue Camille Muffat","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.2109","gps_lng":"-1.51922","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028208,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"9","rue":"Rue Camille Muffat","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210867","gps_lng":"-1.519369","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028209,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"10","rue":"Rue Camille Muffat","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.211012","gps_lng":"-1.519525","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028210,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"11","rue":"Rue Camille Muffat","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210918","gps_lng":"-1.519506","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028211,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"12","rue":"Rue Camille Muffat","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.211087","gps_lng":"-1.519855","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028212,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"13","rue":"Rue Camille Muffat","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210965","gps_lng":"-1.519694","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028213,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"15","rue":"Rue Camille Muffat","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210998","gps_lng":"-1.51984","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028214,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"2","rue":"Impasse Giovanni Pellegrini","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210082","gps_lng":"-1.518471","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028215,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"4","rue":"Impasse Giovanni Pellegrini","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210159","gps_lng":"-1.518648","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028216,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"6","rue":"Impasse Giovanni Pellegrini","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210235","gps_lng":"-1.518827","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028217,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"8","rue":"Impasse Giovanni Pellegrini","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210281","gps_lng":"-1.519027","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028218,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"10","rue":"Impasse Giovanni Pellegrini","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210214","gps_lng":"-1.519122","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028219,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"12","rue":"Impasse Giovanni Pellegrini","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.21009","gps_lng":"-1.519176","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028220,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"14","rue":"Impasse Giovanni Pellegrini","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.21019","gps_lng":"-1.519006","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028221,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"16","rue":"Impasse Giovanni Pellegrini","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.210013","gps_lng":"-1.518536","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028222,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"1","rue":"Rue du Courtillon","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.20921","gps_lng":"-1.51754","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028223,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"3","rue":"Rue du Courtillon","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209347","gps_lng":"-1.517959","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028224,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"16","rue":"Rue du Courtillon","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.209621","gps_lng":"-1.518228","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18028225,"fk_operation":2644,"fk_sector":23006,"fk_user":10022233,"fk_type":2,"fk_adresse":"","passed_at":"2024-09-24 09:26:00","numero":"1","rue":"Impasse Pierre Jonquères d’Oriola","rue_bis":"","ville":"Liffré","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.21054","gps_lng":"-1.518028","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18114203,"fk_operation":2644,"fk_sector":21988,"fk_user":9999985,"fk_type":5,"fk_adresse":"","passed_at":"2024-10-04 15:45:00","numero":"10","rue":"Boulevard de Strasbourg","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48,13537","gps_lng":"-1,54272","nom_recu":null,"remarque":"","montant":"123.00","fk_type_reglement":1,"email_erreur":"","nb_passages":10,"name":"Koller","email":"","phone":""},{"id":18953746,"fk_operation":2644,"fk_sector":21988,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-10 16:38:12","numero":"22","rue":"Rue de Brizeux","rue_bis":"","ville":"RENNES","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.1190514","gps_lng":"-1.6724301","nom_recu":null,"remarque":"","montant":"12.00","fk_type_reglement":2,"email_erreur":"","nb_passages":1,"name":"molder","email":"","phone":""},{"id":18953789,"fk_operation":2644,"fk_sector":21988,"fk_user":9999985,"fk_type":4,"fk_adresse":"","passed_at":"2024-11-10 16:41:55","numero":"22","rue":"Rue de Brizeux","rue_bis":"B","ville":"RENNES","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.4477662","gps_lng":"-2.3277249","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"Miller","email":"","phone":""},{"id":18957123,"fk_operation":2644,"fk_sector":21988,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-11 12:08:06","numero":"6","rue":"Allée Blaise Pascal","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.4477400","gps_lng":"-2.3277220","nom_recu":null,"remarque":"","montant":"6.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"juillet ","email":"","phone":""},{"id":18957755,"fk_operation":2644,"fk_sector":21988,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-11 11:12:39","numero":"3","rue":"Avenue Pierre Donzelot","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13537","gps_lng":"-1.54272","nom_recu":null,"remarque":"","montant":"13.00","fk_type_reglement":2,"email_erreur":"","nb_passages":1,"name":"Climer","email":"","phone":""},{"id":18957823,"fk_operation":2644,"fk_sector":21988,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-11 11:26:51","numero":"5","rue":"Avenue Pierre Donzelot","rue_bis":"","ville":"Rennes","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13537","gps_lng":"-1.54272","nom_recu":null,"remarque":"","montant":"5.50","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":18957881,"fk_operation":2644,"fk_sector":21988,"fk_user":9999985,"fk_type":1,"fk_adresse":"","passed_at":"2024-11-11 11:44:10","numero":"","rue":"Allée Alfred Kastler","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13537","gps_lng":"-1.54272","nom_recu":null,"remarque":"","montant":"5.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Mozer","email":"","phone":""},{"id":19031501,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":1,"fk_adresse":"35001_p9gefo_00038","passed_at":"2024-11-15 17:01:00","numero":"38","rue":"la Bégaudière","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.142109","gps_lng":"-1.561885","nom_recu":"recu_1595825982.pdf","remarque":"","montant":"3.80","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Olinda","email":"test1@d6mail.fr","phone":""},{"id":19031502,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":1,"fk_adresse":"35001_p9gefo_00040","passed_at":"2024-11-15 10:31:30","numero":"40","rue":"la Bégaudière","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.142132","gps_lng":"-1.562039","nom_recu":null,"remarque":"","montant":"4.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Nyvel","email":"","phone":""},{"id":19031503,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":1,"fk_adresse":"35001_p9gefo_00042","passed_at":"2024-11-15 09:59:38","numero":"42","rue":"la Bégaudière","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.142439","gps_lng":"-1.563064","nom_recu":null,"remarque":"","montant":"4.20","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Nilmer","email":"","phone":""},{"id":19031504,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":1,"fk_adresse":"35001_p9gefo_00044","passed_at":"2024-11-15 09:20:00","numero":"44","rue":"la Bégaudière","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.142256","gps_lng":"-1.563055","nom_recu":null,"remarque":"","montant":"4.40","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Noller","email":"","phone":""},{"id":19031505,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":1,"fk_adresse":"35001_qwag0j_00001","passed_at":"2024-11-15 13:50:10","numero":"1","rue":"le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13952","gps_lng":"-1.557953","nom_recu":null,"remarque":"","montant":"11.00","fk_type_reglement":2,"email_erreur":"","nb_passages":1,"name":"Booler","email":"","phone":""},{"id":19031506,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":1,"fk_adresse":"35001_qwag0j_00002","passed_at":"2024-11-17 11:38:42","numero":"2","rue":"le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138924","gps_lng":"-1.557702","nom_recu":null,"remarque":"","montant":"21.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Qater","email":"","phone":""},{"id":19031507,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":4,"fk_adresse":"35001_qwag0j_00003","passed_at":"2024-11-16 10:30:55","numero":"3","rue":"le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139711","gps_lng":"-1.557888","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"pieter","email":"","phone":""},{"id":19031508,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":4,"fk_adresse":"35001_qwag0j_00004","passed_at":"2024-11-16 10:37:53","numero":"4","rue":"le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139214","gps_lng":"-1.557699","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"Polder","email":"","phone":""},{"id":19031509,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":1,"fk_adresse":"35001_qwag0j_00005","passed_at":"2024-12-06 06:51:41","numero":"5","rue":"le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139806","gps_lng":"-1.558061","nom_recu":"recu_1849062964.pdf","remarque":"","montant":"5.00","fk_type_reglement":3,"email_erreur":"","nb_passages":1,"name":"CLOMER","email":"pierre@d6mail.fr","phone":""},{"id":19031510,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":1,"fk_adresse":"35001_qwag0j_00006","passed_at":"2024-12-05 20:07:37","numero":"6","rue":"le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139492","gps_lng":"-1.557674","nom_recu":"recu_501382916.pdf","remarque":"","montant":"6.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"blamer ","email":"pievais@proton.me","phone":""},{"id":19031511,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":1,"fk_adresse":"35001_qwag0j_00007","passed_at":"2024-12-10 09:02:27","numero":"7","rue":"le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139833","gps_lng":"-1.557848","nom_recu":"recu_1096678729.pdf","remarque":"Test montant négatif mob2","montant":"7.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Dermer","email":"test1@d6mail.fr","phone":""},{"id":19031512,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_qwag0j_00008","passed_at":"2024-11-15 08:12:39","numero":"8","rue":"le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139842","gps_lng":"-1.557576","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031513,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":1,"fk_adresse":"35001_qwag0j_00009","passed_at":"2024-11-29 10:36:17","numero":"9","rue":"le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140105","gps_lng":"-1.557797","nom_recu":null,"remarque":"","montant":"9.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"butler","email":"","phone":""},{"id":19031514,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":1,"fk_adresse":"35001_qwag0j_00010","passed_at":"2024-11-28 16:57:00","numero":"10","rue":"le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140089","gps_lng":"-1.557553","nom_recu":"recu_166362165.pdf","remarque":"","montant":"10.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"ayer","email":"pierre.vaissaire@gmail.com","phone":""},{"id":19031515,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":1,"fk_adresse":"35001_qwag0j_00011","passed_at":"2024-11-28 16:58:33","numero":"11","rue":"le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140372","gps_lng":"-1.557861","nom_recu":"recu_140745245.pdf","remarque":"","montant":"11.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Azier","email":"pievais@proton.me","phone":""},{"id":19031516,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":1,"fk_adresse":"35001_qwag0j_00012","passed_at":"2024-11-28 16:45:51","numero":"12","rue":"le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.14033","gps_lng":"-1.557316","nom_recu":null,"remarque":"","montant":"12.00","fk_type_reglement":3,"email_erreur":"","nb_passages":1,"name":"Azher","email":"","phone":""},{"id":19031517,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":1,"fk_adresse":"35001_qwag0j_00013","passed_at":"2024-11-28 16:28:50","numero":"13","rue":"le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140432","gps_lng":"-1.557862","nom_recu":null,"remarque":"","montant":"13.00","fk_type_reglement":2,"email_erreur":"","nb_passages":1,"name":"Aulner","email":"","phone":""},{"id":19031518,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":1,"fk_adresse":"35001_qwag0j_00014","passed_at":"2024-11-28 21:10:47","numero":"14","rue":"le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140704","gps_lng":"-1.557655","nom_recu":"recu_660143382.pdf","remarque":"","montant":"14.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Auler","email":"pievais@proton.me","phone":""},{"id":19031519,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":1,"fk_adresse":"35001_qwag0j_00015","passed_at":"2024-11-27 11:32:12","numero":"15","rue":"le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140521","gps_lng":"-1.557859","nom_recu":"recu_1601122659.pdf","remarque":"","montant":"15.00","fk_type_reglement":3,"email_erreur":"","nb_passages":1,"name":"Zummer","email":"test1@d6mail.fr","phone":""},{"id":19031520,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":1,"fk_adresse":"35001_qwag0j_00016","passed_at":"2024-11-27 10:29:42","numero":"16","rue":"le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140835","gps_lng":"-1.55764","nom_recu":"recu_282486416.pdf","remarque":"","montant":"16.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Zuker","email":"test1@d6mail.fr","phone":""},{"id":19031521,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":1,"fk_adresse":"35001_qwag0j_00017","passed_at":"2024-11-27 10:18:40","numero":"17","rue":"le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140789","gps_lng":"-1.557865","nom_recu":"recu_31943308.pdf","remarque":"","montant":"17.00","fk_type_reglement":2,"email_erreur":"","nb_passages":1,"name":"Zimmer","email":"d6soft@gmail.com","phone":""},{"id":19031522,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_qwag0j_00018","passed_at":"2024-11-15 08:12:39","numero":"18","rue":"le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.141184","gps_lng":"-1.557256","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031523,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_qwag0j_00019","passed_at":"2024-11-15 08:12:39","numero":"19","rue":"le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.141357","gps_lng":"-1.558061","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031524,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_qwag0j_00020","passed_at":"2024-11-15 08:12:39","numero":"20","rue":"le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.14116","gps_lng":"-1.557484","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031525,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":1,"fk_adresse":"35001_qwag0j_00021","passed_at":"2024-11-25 09:33:14","numero":"21","rue":"le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.141202","gps_lng":"-1.558379","nom_recu":"recu_718104764.pdf","remarque":"","montant":"12.00","fk_type_reglement":2,"email_erreur":"","nb_passages":1,"name":"Wilfred ","email":"test1@d6mail.fr","phone":""},{"id":19031526,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":1,"fk_adresse":"35001_o933qz_00001","passed_at":"2024-12-05 19:58:00","numero":"1","rue":"Ifer","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.142717","gps_lng":"-1.554733","nom_recu":"recu_159520101.pdf","remarque":"","montant":"10.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Clément Test","email":"pievais@proton.me","phone":""},{"id":19031527,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":1,"fk_adresse":"35001_bw846s_00034","passed_at":"2024-12-05 19:59:00","numero":"34","rue":"la Janaie","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.144898","gps_lng":"-1.561975","nom_recu":"recu_682737091.pdf","remarque":"","montant":"3.40","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Baker","email":"pievais@proton.me","phone":""},{"id":19031528,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":1,"fk_adresse":"35001_bw846s_00036","passed_at":"2024-11-22 09:56:56","numero":"36","rue":"la Janaie","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.144536","gps_lng":"-1.561947","nom_recu":null,"remarque":"","montant":"3.60","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"ulmer ","email":"","phone":""},{"id":19031529,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":1,"fk_adresse":"35001_g6b9j3_00003","passed_at":"2024-12-05 20:08:00","numero":"3","rue":"la Perlais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.148014","gps_lng":"-1.552113","nom_recu":"recu_2143342911.pdf","remarque":"","montant":"3.00","fk_type_reglement":2,"email_erreur":"","nb_passages":1,"name":"Bottier","email":"pievais@proton.me","phone":""},{"id":19031530,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":1,"fk_adresse":"35001_g6b9j3_00005","passed_at":"2024-12-06 06:46:00","numero":"5","rue":"la Perlais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.147966","gps_lng":"-1.55306","nom_recu":"recu_1799797432.pdf","remarque":"","montant":"5.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"CREMER","email":"pierre@d6mail.fr","phone":""},{"id":19031531,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":1,"fk_adresse":"35001_g6b9j3_00007","passed_at":"2024-12-10 08:59:00","numero":"7","rue":"la Perlais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.147827","gps_lng":"-1.553481","nom_recu":"recu_1567060765.pdf","remarque":"Test montant négatif mob","montant":"5.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Dolmer","email":"test1@d6mail.fr","phone":""},{"id":19031532,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":1,"fk_adresse":"35001_g6b9j3_00009","passed_at":"2024-12-19 16:58:00","numero":"9","rue":"la Perlais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.147679","gps_lng":"-1.55432","nom_recu":"recu_746672579.pdf","remarque":"","montant":"9.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Josselin","email":"p.josselin@aoutlouc.com","phone":""},{"id":19031533,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_g6b9j3_00010","passed_at":"2024-11-15 08:12:39","numero":"10","rue":"la Perlais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.148378","gps_lng":"-1.553634","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031534,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_g6b9j3_00012","passed_at":"2024-11-15 08:12:39","numero":"12","rue":"la Perlais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.148556","gps_lng":"-1.553684","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031535,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_g6b9j3_00014","passed_at":"2024-11-15 08:12:39","numero":"14","rue":"la Perlais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.148636","gps_lng":"-1.553567","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031536,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_g6b9j3_00016","passed_at":"2024-11-15 08:12:39","numero":"16","rue":"la Perlais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.148776","gps_lng":"-1.553675","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031537,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_g6b9j3_00018","passed_at":"2024-11-15 08:12:39","numero":"18","rue":"la Perlais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.148787","gps_lng":"-1.55416","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031538,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_g6b9j3_00020","passed_at":"2024-11-15 08:12:39","numero":"20","rue":"la Perlais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.148008","gps_lng":"-1.555485","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031539,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_g6b9j3_00022","passed_at":"2024-11-15 08:12:39","numero":"22","rue":"la Perlais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.148057","gps_lng":"-1.555632","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031540,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_g6b9j3_00024","passed_at":"2024-11-15 08:12:39","numero":"24","rue":"la Perlais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.148205","gps_lng":"-1.555356","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031541,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_g6b9j3_00026","passed_at":"2024-11-15 08:12:39","numero":"26","rue":"la Perlais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.148299","gps_lng":"-1.555301","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031542,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_g6b9j3_00028","passed_at":"2024-11-15 08:12:39","numero":"28","rue":"la Perlais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.148571","gps_lng":"-1.555271","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031543,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_g6b9j3_00030","passed_at":"2024-11-15 08:12:39","numero":"30","rue":"la Perlais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.147586","gps_lng":"-1.556823","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031544,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_8yzt63_00011","passed_at":"2024-11-15 08:12:39","numero":"11","rue":"la Perrière","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.14592","gps_lng":"-1.558019","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031545,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_8yzt63_00013","passed_at":"2024-11-15 08:12:39","numero":"13","rue":"la Perrière","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.145629","gps_lng":"-1.557877","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031546,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":1,"fk_adresse":"35001_8yzt63_00015","passed_at":"2024-11-26 18:36:39","numero":"15","rue":"la Perrière","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.145509","gps_lng":"-1.558698","nom_recu":"recu_773582324.pdf","remarque":"","montant":"12.15","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Yuller","email":"test2@d6mail.fr","phone":""},{"id":19031547,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":1,"fk_adresse":"35001_8yzt63_00032","passed_at":"2024-11-26 13:10:20","numero":"32","rue":"la Perrière","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.146826","gps_lng":"-1.558266","nom_recu":"recu_1755334428.pdf","remarque":"","montant":"3.20","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Ylomer","email":"pierre.vaissaire@gmail.com","phone":""},{"id":19031548,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":1,"fk_adresse":"35001_2x9yjx_00002","passed_at":"2024-11-26 12:52:50","numero":"2","rue":"les Landeriots","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.142226","gps_lng":"-1.567873","nom_recu":"recu_275851002.pdf","remarque":"","montant":"2.00","fk_type_reglement":2,"email_erreur":"","nb_passages":1,"name":"Ylmer","email":"d6soft@gmail.com","phone":""},{"id":19031549,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_0123_00021","passed_at":"2024-11-15 08:12:39","numero":"21","rue":"Rue de Rennes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137282","gps_lng":"-1.551389","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031550,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_0123_00034","passed_at":"2024-11-15 08:12:39","numero":"34","rue":"Rue de Rennes","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138039","gps_lng":"-1.550509","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031551,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":1,"fk_adresse":"35001_b749_00001","passed_at":"2024-11-23 17:03:00","numero":"1","rue":"le Petit Monthélon","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137755","gps_lng":"-1.553593","nom_recu":"recu_1543322377.pdf","remarque":"","montant":"11.00","fk_type_reglement":2,"email_erreur":"","nb_passages":1,"name":"Vuller","email":"test1@d6mail.fr","phone":""},{"id":19031552,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":1,"fk_adresse":"35001_b749_00002","passed_at":"2024-11-23 17:04:00","numero":"2","rue":"le Petit Monthélon","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136835","gps_lng":"-1.551971","nom_recu":"recu_135658121.pdf","remarque":"","montant":"12.00","fk_type_reglement":2,"email_erreur":"","nb_passages":1,"name":"Vuyller","email":"test2@d6mail.fr","phone":""},{"id":19031553,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_b749_00003","passed_at":"2024-11-15 08:12:39","numero":"3","rue":"le Petit Monthélon","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13801","gps_lng":"-1.553423","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031554,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_b749_00004","passed_at":"2024-11-15 08:12:39","numero":"4","rue":"le Petit Monthélon","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136546","gps_lng":"-1.552027","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031555,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_b749_00005","passed_at":"2024-11-15 08:12:39","numero":"5","rue":"le Petit Monthélon","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138327","gps_lng":"-1.553307","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031556,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_b749_00006","passed_at":"2024-11-15 08:12:39","numero":"6","rue":"le Petit Monthélon","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136348","gps_lng":"-1.55205","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031557,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_b749_00007","passed_at":"2024-11-15 08:12:39","numero":"7","rue":"le Petit Monthélon","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138297","gps_lng":"-1.553509","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031558,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_b749_00008","passed_at":"2024-11-15 08:12:39","numero":"8","rue":"le Petit Monthélon","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136564","gps_lng":"-1.552306","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031559,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_b749_00009","passed_at":"2024-11-15 08:12:39","numero":"9","rue":"le Petit Monthélon","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.138271","gps_lng":"-1.553671","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031560,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_b749_00010","passed_at":"2024-11-15 08:12:39","numero":"10","rue":"le Petit Monthélon","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136997","gps_lng":"-1.552211","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031561,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_b749_00011","passed_at":"2024-11-15 08:12:39","numero":"11","rue":"le Petit Monthélon","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137784","gps_lng":"-1.553876","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031562,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_b749_00012","passed_at":"2024-11-15 08:12:39","numero":"12","rue":"le Petit Monthélon","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137225","gps_lng":"-1.552184","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031563,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_b749_00013","passed_at":"2024-11-15 08:12:39","numero":"13","rue":"le Petit Monthélon","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137853","gps_lng":"-1.554464","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031564,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_b749_00014","passed_at":"2024-11-15 08:12:39","numero":"14","rue":"le Petit Monthélon","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137381","gps_lng":"-1.552582","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031565,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_b749_00016","passed_at":"2024-11-15 08:12:39","numero":"16","rue":"le Petit Monthélon","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.137093","gps_lng":"-1.553462","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031566,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_b749_00018","passed_at":"2024-11-15 08:12:39","numero":"18","rue":"le Petit Monthélon","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.136806","gps_lng":"-1.55362","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031567,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_a032_00001","passed_at":"2024-11-15 08:12:39","numero":"1","rue":"Zone d'activités le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140508","gps_lng":"-1.561377","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031568,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_a032_00002","passed_at":"2024-11-15 08:12:39","numero":"2","rue":"Zone d'activités le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140317","gps_lng":"-1.561414","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031569,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_a032_00003","passed_at":"2024-11-15 08:12:39","numero":"3","rue":"Zone d'activités le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140408","gps_lng":"-1.560925","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031570,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_a032_00004","passed_at":"2024-11-15 08:12:39","numero":"4","rue":"Zone d'activités le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140193","gps_lng":"-1.560805","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031571,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_a032_00005","passed_at":"2024-11-15 08:12:39","numero":"5","rue":"Zone d'activités le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140305","gps_lng":"-1.560468","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031572,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":5,"fk_adresse":"35001_a032_00006","passed_at":"2024-11-17 19:36:04","numero":"6","rue":"Zone d'activités le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140078","gps_lng":"-1.560242","nom_recu":null,"remarque":"","montant":"65.00","fk_type_reglement":2,"email_erreur":"","nb_passages":6,"name":"rullier ","email":"","phone":""},{"id":19031573,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_a032_00007","passed_at":"2024-11-15 08:12:39","numero":"7","rue":"Zone d'activités le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.141229","gps_lng":"-1.560215","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031574,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":1,"fk_adresse":"35001_a032_00008","passed_at":"2024-11-17 19:35:09","numero":"8","rue":"Zone d'activités le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13997","gps_lng":"-1.559631","nom_recu":null,"remarque":"","montant":"15.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"rohmer ","email":"","phone":""},{"id":19031575,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_a032_00009","passed_at":"2024-11-15 08:12:39","numero":"9","rue":"Zone d'activités le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.141261","gps_lng":"-1.561332","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031576,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":4,"fk_adresse":"35001_a032_00010","passed_at":"2024-11-17 19:34:36","numero":"10","rue":"Zone d'activités le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139912","gps_lng":"-1.559291","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":4,"email_erreur":"","nb_passages":1,"name":"reiner ","email":"","phone":""},{"id":19031577,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_a032_00011","passed_at":"2024-11-15 08:12:39","numero":"11","rue":"Zone d'activités le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.141695","gps_lng":"-1.561351","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031578,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_a032_00012","passed_at":"2024-11-15 08:12:39","numero":"12","rue":"Zone d'activités le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140413","gps_lng":"-1.559816","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031579,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_a032_00014","passed_at":"2024-11-15 08:12:39","numero":"14","rue":"Zone d'activités le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.141036","gps_lng":"-1.559891","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031580,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_a032_00016","passed_at":"2024-11-15 08:12:39","numero":"16","rue":"Zone d'activités le Boulais","rue_bis":"","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.141399","gps_lng":"-1.559949","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031581,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35001_a032_00009_bis","passed_at":"2024-11-15 08:12:39","numero":"9","rue":"Zone d'activités le Boulais","rue_bis":"B","ville":"Acigné","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.141428","gps_lng":"-1.561364","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031582,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35334_c8bao1_00002","passed_at":"2024-11-15 08:12:39","numero":"2","rue":"le Bas de l'avenue de Monthélon","rue_bis":"","ville":"Thorigné-Fouillard","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140768","gps_lng":"-1.568326","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031583,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35334_c8bao1_00004","passed_at":"2024-11-15 08:12:39","numero":"4","rue":"le Bas de l'avenue de Monthélon","rue_bis":"","ville":"Thorigné-Fouillard","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140704","gps_lng":"-1.568371","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031584,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35334_c8bao1_00006","passed_at":"2024-11-15 08:12:39","numero":"6","rue":"le Bas de l'avenue de Monthélon","rue_bis":"","ville":"Thorigné-Fouillard","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140748","gps_lng":"-1.568258","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031585,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":1,"fk_adresse":"35334_c8bao1_00008","passed_at":"2024-11-15 10:37:34","numero":"8","rue":"le Bas de l'avenue de Monthélon","rue_bis":"","ville":"Thorigné-Fouillard","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140684","gps_lng":"-1.568304","nom_recu":null,"remarque":"","montant":"8.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Nailer","email":"","phone":""},{"id":19031586,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":1,"fk_adresse":"35334_b355_00010","passed_at":"2024-11-15 10:34:25","numero":"10","rue":"les Grands Champs","rue_bis":"","ville":"Thorigné-Fouillard","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139932","gps_lng":"-1.565698","nom_recu":null,"remarque":"","montant":"10.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Nauler","email":"","phone":""},{"id":19031587,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35334_b355_00012","passed_at":"2024-11-15 08:12:39","numero":"12","rue":"les Grands Champs","rue_bis":"","ville":"Thorigné-Fouillard","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140131","gps_lng":"-1.565373","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031588,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35334_b355_00014","passed_at":"2024-11-15 08:12:39","numero":"14","rue":"les Grands Champs","rue_bis":"","ville":"Thorigné-Fouillard","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.13999","gps_lng":"-1.565252","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""},{"id":19031589,"fk_operation":2644,"fk_sector":22813,"fk_user":10011253,"fk_type":1,"fk_adresse":"35334_b355_00016","passed_at":"2024-11-15 13:05:18","numero":"16","rue":"les Grands Champs","rue_bis":"","ville":"Thorigné-Fouillard","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.140171","gps_lng":"-1.564804","nom_recu":null,"remarque":"","montant":"16.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"Ollier","email":"","phone":""},{"id":19031590,"fk_operation":2644,"fk_sector":22813,"fk_user":9999985,"fk_type":2,"fk_adresse":"35334_b355_00018","passed_at":"2024-11-15 08:12:39","numero":"18","rue":"les Grands Champs","rue_bis":"","ville":"Thorigné-Fouillard","residence":"","fk_habitat":1,"appt":"","niveau":"","gps_lat":"48.139729","gps_lng":"-1.565082","nom_recu":null,"remarque":"","montant":"0.00","fk_type_reglement":1,"email_erreur":"","nb_passages":1,"name":"","email":"","phone":""}], +"users_sectors":[{"id":9999985,"first_name":"Clément","sect_name":"clem tournée","fk_sector":6,"name":"VAISSAIRE"},{"id":9999985,"first_name":"Clément","sect_name":"clem tournée","fk_sector":13,"name":"VAISSAIRE"},{"id":9999985,"first_name":"Clément","sect_name":"clem tournée","fk_sector":8581,"name":"VAISSAIRE"},{"id":10011253,"first_name":"Pierre","sect_name":"","fk_sector":6,"name":"TEST1"},{"id":10016609,"first_name":"","sect_name":"Tournée test","fk_sector":21806,"name":"ANDREZIEUX"},{"id":10016609,"first_name":"","sect_name":"Tournée test","fk_sector":21808,"name":"ANDREZIEUX"},{"id":9999985,"first_name":"Clément","sect_name":"clem tournée","fk_sector":21988,"name":"VAISSAIRE"},{"id":10021972,"first_name":"Greg","sect_name":"","fk_sector":21752,"name":"POULAVER"},{"id":10022234,"first_name":"","sect_name":"","fk_sector":8871,"name":"ESSAIPIERRE2"},{"id":10022233,"first_name":"Pierre","sect_name":"","fk_sector":23006,"name":"ESSAIPIERRE"},{"id":10011253,"first_name":"Pierre","sect_name":"","fk_sector":23006,"name":"TEST1"},{"id":10011253,"first_name":"Pierre","sect_name":"","fk_sector":7,"name":"TEST1"},{"id":9999985,"first_name":"Clément","sect_name":"clem tournée","fk_sector":7,"name":"VAISSAIRE"},{"id":10021972,"first_name":"Greg","sect_name":"","fk_sector":22813,"name":"POULAVER"},{"id":10011253,"first_name":"Pierre","sect_name":"","fk_sector":22813,"name":"TEST1"},{"id":9999985,"first_name":"Clément","sect_name":"clem tournée","fk_sector":22813,"name":"VAISSAIRE"}]} \ No newline at end of file diff --git a/app/lib/core/data/models/amicale_model.dart b/app/lib/core/data/models/amicale_model.dart new file mode 100644 index 00000000..f6b1adfa --- /dev/null +++ b/app/lib/core/data/models/amicale_model.dart @@ -0,0 +1,249 @@ +import 'package:hive/hive.dart'; + +part 'amicale_model.g.dart'; + +@HiveType(typeId: 11) +class AmicaleModel extends HiveObject { + @HiveField(0) + final int id; + + @HiveField(1) + final String name; + + @HiveField(2) + final String adresse1; + + @HiveField(3) + final String adresse2; + + @HiveField(4) + final String codePostal; + + @HiveField(5) + final String ville; + + @HiveField(6) + final int? fkRegion; + + @HiveField(7) + final String? libRegion; + + @HiveField(8) + final int? fkType; + + @HiveField(9) + final String phone; + + @HiveField(10) + final String mobile; + + @HiveField(11) + final String email; + + @HiveField(12) + final String gpsLat; + + @HiveField(13) + final String gpsLng; + + @HiveField(14) + final String stripeId; + + @HiveField(15) + final bool chkDemo; + + @HiveField(16) + final bool chkCopieMailRecu; + + @HiveField(17) + final bool chkAcceptSms; + + @HiveField(18) + final bool chkActive; + + @HiveField(19) + final bool chkStripe; + + @HiveField(20) + final DateTime? createdAt; + + @HiveField(21) + final DateTime? updatedAt; + + AmicaleModel({ + required this.id, + required this.name, + this.adresse1 = '', + this.adresse2 = '', + this.codePostal = '', + this.ville = '', + this.fkRegion, + this.libRegion, + this.fkType, + this.phone = '', + this.mobile = '', + this.email = '', + this.gpsLat = '', + this.gpsLng = '', + this.stripeId = '', + this.chkDemo = false, + this.chkCopieMailRecu = false, + this.chkAcceptSms = false, + this.chkActive = true, + this.chkStripe = false, + this.createdAt, + this.updatedAt, + }); + + // Factory pour convertir depuis JSON (API) + factory AmicaleModel.fromJson(Map json) { + // Convertir l'ID en int, qu'il soit déjà int ou string + final dynamic rawId = json['id']; + final int id = rawId is String ? int.parse(rawId) : rawId as int; + + // Convertir fk_region en int si présent + final dynamic rawFkRegion = json['fk_region']; + final int? fkRegion = rawFkRegion != null + ? (rawFkRegion is String ? int.parse(rawFkRegion) : rawFkRegion as int) + : null; + + // Convertir fk_type en int si présent + final dynamic rawFkType = json['fk_type']; + final int? fkType = rawFkType != null + ? (rawFkType is String ? int.parse(rawFkType) : rawFkType as int) + : null; + + // Convertir les booléens + final bool chkDemo = json['chk_demo'] == 1 || json['chk_demo'] == true; + final bool chkCopieMailRecu = + json['chk_copie_mail_recu'] == 1 || json['chk_copie_mail_recu'] == true; + final bool chkAcceptSms = + json['chk_accept_sms'] == 1 || json['chk_accept_sms'] == true; + final bool chkActive = + json['chk_active'] == 1 || json['chk_active'] == true; + final bool chkStripe = + json['chk_stripe'] == 1 || json['chk_stripe'] == true; + + // Traiter les dates si présentes + DateTime? createdAt; + if (json['created_at'] != null && json['created_at'] != '') { + try { + createdAt = DateTime.parse(json['created_at']); + } catch (e) { + createdAt = null; + } + } + + DateTime? updatedAt; + if (json['updated_at'] != null && json['updated_at'] != '') { + try { + updatedAt = DateTime.parse(json['updated_at']); + } catch (e) { + updatedAt = null; + } + } + + return AmicaleModel( + id: id, + name: json['name'] ?? '', + adresse1: json['adresse1'] ?? '', + adresse2: json['adresse2'] ?? '', + codePostal: json['code_postal'] ?? '', + ville: json['ville'] ?? '', + fkRegion: fkRegion, + libRegion: json['lib_region'], + fkType: fkType, + phone: json['phone'] ?? '', + mobile: json['mobile'] ?? '', + email: json['email'] ?? '', + gpsLat: json['gps_lat'] ?? '', + gpsLng: json['gps_lng'] ?? '', + stripeId: json['stripe_id'] ?? '', + chkDemo: chkDemo, + chkCopieMailRecu: chkCopieMailRecu, + chkAcceptSms: chkAcceptSms, + chkActive: chkActive, + chkStripe: chkStripe, + createdAt: createdAt, + updatedAt: updatedAt, + ); + } + + // Convertir en JSON pour l'API + Map toJson() { + return { + 'id': id, + 'name': name, + 'adresse1': adresse1, + 'adresse2': adresse2, + 'code_postal': codePostal, + 'ville': ville, + 'fk_region': fkRegion, + 'lib_region': libRegion, + 'fk_type': fkType, + 'phone': phone, + 'mobile': mobile, + 'email': email, + 'gps_lat': gpsLat, + 'gps_lng': gpsLng, + 'stripe_id': stripeId, + 'chk_demo': chkDemo ? 1 : 0, + 'chk_copie_mail_recu': chkCopieMailRecu ? 1 : 0, + 'chk_accept_sms': chkAcceptSms ? 1 : 0, + 'chk_active': chkActive ? 1 : 0, + 'chk_stripe': chkStripe ? 1 : 0, + 'created_at': createdAt?.toIso8601String(), + 'updated_at': updatedAt?.toIso8601String(), + }; + } + + // Copier avec de nouvelles valeurs + AmicaleModel copyWith({ + String? name, + String? adresse1, + String? adresse2, + String? codePostal, + String? ville, + int? fkRegion, + String? libRegion, + int? fkType, + String? phone, + String? mobile, + String? email, + String? gpsLat, + String? gpsLng, + String? stripeId, + bool? chkDemo, + bool? chkCopieMailRecu, + bool? chkAcceptSms, + bool? chkActive, + bool? chkStripe, + DateTime? createdAt, + DateTime? updatedAt, + }) { + return AmicaleModel( + id: this.id, + name: name ?? this.name, + adresse1: adresse1 ?? this.adresse1, + adresse2: adresse2 ?? this.adresse2, + codePostal: codePostal ?? this.codePostal, + ville: ville ?? this.ville, + fkRegion: fkRegion ?? this.fkRegion, + libRegion: libRegion ?? this.libRegion, + fkType: fkType ?? this.fkType, + phone: phone ?? this.phone, + mobile: mobile ?? this.mobile, + email: email ?? this.email, + gpsLat: gpsLat ?? this.gpsLat, + gpsLng: gpsLng ?? this.gpsLng, + stripeId: stripeId ?? this.stripeId, + chkDemo: chkDemo ?? this.chkDemo, + chkCopieMailRecu: chkCopieMailRecu ?? this.chkCopieMailRecu, + chkAcceptSms: chkAcceptSms ?? this.chkAcceptSms, + chkActive: chkActive ?? this.chkActive, + chkStripe: chkStripe ?? this.chkStripe, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + } +} diff --git a/app/lib/core/data/models/amicale_model.g.dart b/app/lib/core/data/models/amicale_model.g.dart new file mode 100644 index 00000000..b13c340b --- /dev/null +++ b/app/lib/core/data/models/amicale_model.g.dart @@ -0,0 +1,104 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'amicale_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class AmicaleModelAdapter extends TypeAdapter { + @override + final int typeId = 11; + + @override + AmicaleModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return AmicaleModel( + id: fields[0] as int, + name: fields[1] as String, + adresse1: fields[2] as String, + adresse2: fields[3] as String, + codePostal: fields[4] as String, + ville: fields[5] as String, + fkRegion: fields[6] as int?, + libRegion: fields[7] as String?, + fkType: fields[8] as int?, + phone: fields[9] as String, + mobile: fields[10] as String, + email: fields[11] as String, + gpsLat: fields[12] as String, + gpsLng: fields[13] as String, + stripeId: fields[14] as String, + chkDemo: fields[15] as bool, + chkCopieMailRecu: fields[16] as bool, + chkAcceptSms: fields[17] as bool, + chkActive: fields[18] as bool, + chkStripe: fields[19] as bool, + createdAt: fields[20] as DateTime?, + updatedAt: fields[21] as DateTime?, + ); + } + + @override + void write(BinaryWriter writer, AmicaleModel obj) { + writer + ..writeByte(22) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.name) + ..writeByte(2) + ..write(obj.adresse1) + ..writeByte(3) + ..write(obj.adresse2) + ..writeByte(4) + ..write(obj.codePostal) + ..writeByte(5) + ..write(obj.ville) + ..writeByte(6) + ..write(obj.fkRegion) + ..writeByte(7) + ..write(obj.libRegion) + ..writeByte(8) + ..write(obj.fkType) + ..writeByte(9) + ..write(obj.phone) + ..writeByte(10) + ..write(obj.mobile) + ..writeByte(11) + ..write(obj.email) + ..writeByte(12) + ..write(obj.gpsLat) + ..writeByte(13) + ..write(obj.gpsLng) + ..writeByte(14) + ..write(obj.stripeId) + ..writeByte(15) + ..write(obj.chkDemo) + ..writeByte(16) + ..write(obj.chkCopieMailRecu) + ..writeByte(17) + ..write(obj.chkAcceptSms) + ..writeByte(18) + ..write(obj.chkActive) + ..writeByte(19) + ..write(obj.chkStripe) + ..writeByte(20) + ..write(obj.createdAt) + ..writeByte(21) + ..write(obj.updatedAt); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AmicaleModelAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/app/lib/core/data/models/client_model.dart b/app/lib/core/data/models/client_model.dart new file mode 100644 index 00000000..754b5be0 --- /dev/null +++ b/app/lib/core/data/models/client_model.dart @@ -0,0 +1,200 @@ +import 'package:hive/hive.dart'; + +part 'client_model.g.dart'; + +@HiveType(typeId: 10) +class ClientModel extends HiveObject { + @HiveField(0) + final int id; + + @HiveField(1) + final String name; + + @HiveField(2) + final String? adresse1; + + @HiveField(3) + final String? adresse2; + + @HiveField(4) + final String? codePostal; + + @HiveField(5) + final String? ville; + + @HiveField(6) + final int? fkRegion; + + @HiveField(7) + final String? libRegion; + + @HiveField(8) + final int? fkType; + + @HiveField(9) + final String? phone; + + @HiveField(10) + final String? mobile; + + @HiveField(11) + final String? email; + + @HiveField(12) + final String? gpsLat; + + @HiveField(13) + final String? gpsLng; + + @HiveField(14) + final String? stripeId; + + @HiveField(15) + final bool? chkDemo; + + @HiveField(16) + final bool? chkCopieMailRecu; + + @HiveField(17) + final bool? chkAcceptSms; + + @HiveField(18) + final bool? chkActive; + + ClientModel({ + required this.id, + required this.name, + this.adresse1, + this.adresse2, + this.codePostal, + this.ville, + this.fkRegion, + this.libRegion, + this.fkType, + this.phone, + this.mobile, + this.email, + this.gpsLat, + this.gpsLng, + this.stripeId, + this.chkDemo, + this.chkCopieMailRecu, + this.chkAcceptSms, + this.chkActive, + }); + + // Factory pour convertir depuis JSON (API) + factory ClientModel.fromJson(Map json) { + // Convertir l'ID en int, qu'il soit déjà int ou string + final dynamic rawId = json['id']; + final int id = rawId is String ? int.parse(rawId) : rawId as int; + + // Convertir fk_region en int si présent + int? fkRegion; + if (json['fk_region'] != null) { + final dynamic rawFkRegion = json['fk_region']; + fkRegion = + rawFkRegion is String ? int.parse(rawFkRegion) : rawFkRegion as int; + } + + // Convertir fk_type en int si présent + int? fkType; + if (json['fk_type'] != null) { + final dynamic rawFkType = json['fk_type']; + fkType = rawFkType is String ? int.parse(rawFkType) : rawFkType as int; + } + + return ClientModel( + id: id, + name: json['name'] ?? '', + adresse1: json['adresse1'], + adresse2: json['adresse2'], + codePostal: json['code_postal'], + ville: json['ville'], + fkRegion: fkRegion, + libRegion: json['lib_region'], + fkType: fkType, + phone: json['phone'], + mobile: json['mobile'], + email: json['email'], + gpsLat: json['gps_lat'], + gpsLng: json['gps_lng'], + stripeId: json['stripe_id'], + chkDemo: json['chk_demo'] == 1 || json['chk_demo'] == true, + chkCopieMailRecu: json['chk_copie_mail_recu'] == 1 || + json['chk_copie_mail_recu'] == true, + chkAcceptSms: + json['chk_accept_sms'] == 1 || json['chk_accept_sms'] == true, + chkActive: json['chk_active'] == 1 || json['chk_active'] == true, + ); + } + + // Convertir en JSON pour l'API + Map toJson() { + return { + 'id': id, + 'name': name, + 'adresse1': adresse1, + 'adresse2': adresse2, + 'code_postal': codePostal, + 'ville': ville, + 'fk_region': fkRegion, + 'lib_region': libRegion, + 'fk_type': fkType, + 'phone': phone, + 'mobile': mobile, + 'email': email, + 'gps_lat': gpsLat, + 'gps_lng': gpsLng, + 'stripe_id': stripeId, + 'chk_demo': chkDemo, + 'chk_copie_mail_recu': chkCopieMailRecu, + 'chk_accept_sms': chkAcceptSms, + 'chk_active': chkActive, + }; + } + + // Copier avec de nouvelles valeurs + ClientModel copyWith({ + String? name, + String? adresse1, + String? adresse2, + String? codePostal, + String? ville, + int? fkRegion, + String? libRegion, + int? fkType, + String? phone, + String? mobile, + String? email, + String? gpsLat, + String? gpsLng, + String? stripeId, + bool? chkDemo, + bool? chkCopieMailRecu, + bool? chkAcceptSms, + bool? chkActive, + }) { + return ClientModel( + id: this.id, + name: name ?? this.name, + adresse1: adresse1 ?? this.adresse1, + adresse2: adresse2 ?? this.adresse2, + codePostal: codePostal ?? this.codePostal, + ville: ville ?? this.ville, + fkRegion: fkRegion ?? this.fkRegion, + libRegion: libRegion ?? this.libRegion, + fkType: fkType ?? this.fkType, + phone: phone ?? this.phone, + mobile: mobile ?? this.mobile, + email: email ?? this.email, + gpsLat: gpsLat ?? this.gpsLat, + gpsLng: gpsLng ?? this.gpsLng, + stripeId: stripeId ?? this.stripeId, + chkDemo: chkDemo ?? this.chkDemo, + chkCopieMailRecu: chkCopieMailRecu ?? this.chkCopieMailRecu, + chkAcceptSms: chkAcceptSms ?? this.chkAcceptSms, + chkActive: chkActive ?? this.chkActive, + ); + } +} diff --git a/app/lib/core/data/models/client_model.g.dart b/app/lib/core/data/models/client_model.g.dart new file mode 100644 index 00000000..82b1b76d --- /dev/null +++ b/app/lib/core/data/models/client_model.g.dart @@ -0,0 +1,95 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'client_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class ClientModelAdapter extends TypeAdapter { + @override + final int typeId = 10; + + @override + ClientModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return ClientModel( + id: fields[0] as int, + name: fields[1] as String, + adresse1: fields[2] as String?, + adresse2: fields[3] as String?, + codePostal: fields[4] as String?, + ville: fields[5] as String?, + fkRegion: fields[6] as int?, + libRegion: fields[7] as String?, + fkType: fields[8] as int?, + phone: fields[9] as String?, + mobile: fields[10] as String?, + email: fields[11] as String?, + gpsLat: fields[12] as String?, + gpsLng: fields[13] as String?, + stripeId: fields[14] as String?, + chkDemo: fields[15] as bool?, + chkCopieMailRecu: fields[16] as bool?, + chkAcceptSms: fields[17] as bool?, + chkActive: fields[18] as bool?, + ); + } + + @override + void write(BinaryWriter writer, ClientModel obj) { + writer + ..writeByte(19) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.name) + ..writeByte(2) + ..write(obj.adresse1) + ..writeByte(3) + ..write(obj.adresse2) + ..writeByte(4) + ..write(obj.codePostal) + ..writeByte(5) + ..write(obj.ville) + ..writeByte(6) + ..write(obj.fkRegion) + ..writeByte(7) + ..write(obj.libRegion) + ..writeByte(8) + ..write(obj.fkType) + ..writeByte(9) + ..write(obj.phone) + ..writeByte(10) + ..write(obj.mobile) + ..writeByte(11) + ..write(obj.email) + ..writeByte(12) + ..write(obj.gpsLat) + ..writeByte(13) + ..write(obj.gpsLng) + ..writeByte(14) + ..write(obj.stripeId) + ..writeByte(15) + ..write(obj.chkDemo) + ..writeByte(16) + ..write(obj.chkCopieMailRecu) + ..writeByte(17) + ..write(obj.chkAcceptSms) + ..writeByte(18) + ..write(obj.chkActive); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ClientModelAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/flutt/lib/core/data/models/membre_model.dart b/app/lib/core/data/models/membre_model.dart similarity index 100% rename from flutt/lib/core/data/models/membre_model.dart rename to app/lib/core/data/models/membre_model.dart diff --git a/flutt/lib/core/data/models/membre_model.g.dart b/app/lib/core/data/models/membre_model.g.dart similarity index 100% rename from flutt/lib/core/data/models/membre_model.g.dart rename to app/lib/core/data/models/membre_model.g.dart diff --git a/flutt/lib/core/data/models/operation_model.dart b/app/lib/core/data/models/operation_model.dart similarity index 100% rename from flutt/lib/core/data/models/operation_model.dart rename to app/lib/core/data/models/operation_model.dart diff --git a/flutt/lib/core/data/models/operation_model.g.dart b/app/lib/core/data/models/operation_model.g.dart similarity index 100% rename from flutt/lib/core/data/models/operation_model.g.dart rename to app/lib/core/data/models/operation_model.g.dart diff --git a/flutt/lib/core/data/models/passage_model.dart b/app/lib/core/data/models/passage_model.dart similarity index 100% rename from flutt/lib/core/data/models/passage_model.dart rename to app/lib/core/data/models/passage_model.dart diff --git a/flutt/lib/core/data/models/passage_model.g.dart b/app/lib/core/data/models/passage_model.g.dart similarity index 100% rename from flutt/lib/core/data/models/passage_model.g.dart rename to app/lib/core/data/models/passage_model.g.dart diff --git a/app/lib/core/data/models/region_model.dart b/app/lib/core/data/models/region_model.dart new file mode 100644 index 00000000..25306f4e --- /dev/null +++ b/app/lib/core/data/models/region_model.dart @@ -0,0 +1,89 @@ +import 'package:hive/hive.dart'; + +part 'region_model.g.dart'; + +@HiveType(typeId: 7) // Assurez-vous que cet ID est unique +class RegionModel extends HiveObject { + @HiveField(0) + final int id; + + @HiveField(1) + final int fkPays; + + @HiveField(2) + final String libelle; + + @HiveField(3) + final String? libelleLong; + + @HiveField(4) + final String? tableOsm; + + @HiveField(5) + final String? departements; + + @HiveField(6) + final bool chkActive; + + RegionModel({ + required this.id, + required this.fkPays, + required this.libelle, + this.libelleLong, + this.tableOsm, + this.departements, + this.chkActive = true, + }); + + // Constructeur de copie + RegionModel copyWith({ + int? id, + int? fkPays, + String? libelle, + String? libelleLong, + String? tableOsm, + String? departements, + bool? chkActive, + }) { + return RegionModel( + id: id ?? this.id, + fkPays: fkPays ?? this.fkPays, + libelle: libelle ?? this.libelle, + libelleLong: libelleLong ?? this.libelleLong, + tableOsm: tableOsm ?? this.tableOsm, + departements: departements ?? this.departements, + chkActive: chkActive ?? this.chkActive, + ); + } + + // Conversion depuis JSON + factory RegionModel.fromJson(Map json) { + return RegionModel( + id: json['id'] as int, + fkPays: json['fk_pays'] as int, + libelle: json['libelle'] as String, + libelleLong: json['libelle_long'] as String?, + tableOsm: json['table_osm'] as String?, + departements: json['departements'] as String?, + chkActive: json['chk_active'] == 1 || json['chk_active'] == true, + ); + } + + // Conversion vers JSON + Map toJson() { + return { + 'id': id, + 'fk_pays': fkPays, + 'libelle': libelle, + 'libelle_long': libelleLong, + 'table_osm': tableOsm, + 'departements': departements, + 'chk_active': chkActive ? 1 : 0, + }; + } + + @override + String toString() { + return 'RegionModel(id: $id, libelle: $libelle)'; + } +} diff --git a/app/lib/core/data/models/region_model.g.dart b/app/lib/core/data/models/region_model.g.dart new file mode 100644 index 00000000..1736d95c --- /dev/null +++ b/app/lib/core/data/models/region_model.g.dart @@ -0,0 +1,59 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'region_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class RegionModelAdapter extends TypeAdapter { + @override + final int typeId = 7; + + @override + RegionModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return RegionModel( + id: fields[0] as int, + fkPays: fields[1] as int, + libelle: fields[2] as String, + libelleLong: fields[3] as String?, + tableOsm: fields[4] as String?, + departements: fields[5] as String?, + chkActive: fields[6] as bool, + ); + } + + @override + void write(BinaryWriter writer, RegionModel obj) { + writer + ..writeByte(7) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.fkPays) + ..writeByte(2) + ..write(obj.libelle) + ..writeByte(3) + ..write(obj.libelleLong) + ..writeByte(4) + ..write(obj.tableOsm) + ..writeByte(5) + ..write(obj.departements) + ..writeByte(6) + ..write(obj.chkActive); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is RegionModelAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/flutt/lib/core/data/models/sector_model.dart b/app/lib/core/data/models/sector_model.dart similarity index 100% rename from flutt/lib/core/data/models/sector_model.dart rename to app/lib/core/data/models/sector_model.dart diff --git a/flutt/lib/core/data/models/sector_model.g.dart b/app/lib/core/data/models/sector_model.g.dart similarity index 100% rename from flutt/lib/core/data/models/sector_model.g.dart rename to app/lib/core/data/models/sector_model.g.dart diff --git a/flutt/lib/core/data/models/user_model.dart b/app/lib/core/data/models/user_model.dart similarity index 58% rename from flutt/lib/core/data/models/user_model.dart rename to app/lib/core/data/models/user_model.dart index fc8bf77b..f9389672 100644 --- a/flutt/lib/core/data/models/user_model.dart +++ b/app/lib/core/data/models/user_model.dart @@ -12,7 +12,7 @@ class UserModel extends HiveObject { @HiveField(2) String? name; - + @HiveField(11) String? username; @@ -33,21 +33,36 @@ class UserModel extends HiveObject { @HiveField(7) bool isSynced; - + @HiveField(8) String? sessionId; - + @HiveField(9) DateTime? sessionExpiry; - + @HiveField(12) String? lastPath; - + @HiveField(13) String? sectName; - + @HiveField(14) - String? interface; + int? fkEntite; + + @HiveField(15) + int? fkTitre; + + @HiveField(16) + String? phone; + + @HiveField(17) + String? mobile; + + @HiveField(18) + DateTime? dateNaissance; + + @HiveField(19) + DateTime? dateEmbauche; UserModel({ required this.id, @@ -64,7 +79,12 @@ class UserModel extends HiveObject { this.sessionExpiry, this.lastPath, this.sectName, - this.interface, + this.fkEntite, + this.fkTitre, + this.phone, + this.mobile, + this.dateNaissance, + this.dateEmbauche, }); // Factory pour convertir depuis JSON (API) @@ -72,28 +92,66 @@ class UserModel extends HiveObject { // Convertir l'ID en int, qu'il soit déjà int ou string final dynamic rawId = json['id']; final int id = rawId is String ? int.parse(rawId) : rawId as int; - + // Convertir le rôle en int, qu'il soit déjà int ou string - final dynamic rawRole = json['role']; + final dynamic rawRole = json['role'] ?? json['fk_role']; final int role = rawRole is String ? int.parse(rawRole) : rawRole as int; - + + // Convertir fk_entite en int si présent + final dynamic rawFkEntite = json['fk_entite']; + final int? fkEntite = rawFkEntite != null + ? (rawFkEntite is String ? int.parse(rawFkEntite) : rawFkEntite as int) + : null; + + // Convertir fk_titre en int si présent + final dynamic rawFkTitre = json['fk_titre']; + final int? fkTitre = rawFkTitre != null + ? (rawFkTitre is String ? int.parse(rawFkTitre) : rawFkTitre as int) + : null; + + // Traiter les dates si présentes + DateTime? dateNaissance; + if (json['date_naissance'] != null && json['date_naissance'] != '') { + try { + dateNaissance = DateTime.parse(json['date_naissance']); + } catch (e) { + dateNaissance = null; + } + } + + DateTime? dateEmbauche; + if (json['date_embauche'] != null && json['date_embauche'] != '') { + try { + dateEmbauche = DateTime.parse(json['date_embauche']); + } catch (e) { + dateEmbauche = null; + } + } + return UserModel( id: id, - email: json['email'], + email: json['email'] ?? '', name: json['name'], username: json['username'], firstName: json['first_name'], role: role, - createdAt: DateTime.parse(json['created_at']), + createdAt: json['created_at'] != null + ? DateTime.parse(json['created_at']) + : DateTime.now(), lastSyncedAt: DateTime.now(), isActive: json['is_active'] ?? true, isSynced: true, sessionId: json['session_id'], - sessionExpiry: json['session_expiry'] != null - ? DateTime.parse(json['session_expiry']) + sessionExpiry: json['session_expiry'] != null + ? DateTime.parse(json['session_expiry']) : null, sectName: json['sect_name'], - interface: json['interface'], + fkEntite: fkEntite, + fkTitre: fkTitre, + phone: json['phone'], + mobile: json['mobile'], + dateNaissance: dateNaissance, + dateEmbauche: dateEmbauche, ); } @@ -112,7 +170,12 @@ class UserModel extends HiveObject { 'session_expiry': sessionExpiry?.toIso8601String(), 'last_path': lastPath, 'sect_name': sectName, - 'interface': interface, + 'fk_entite': fkEntite, + 'fk_titre': fkTitre, + 'phone': phone, + 'mobile': mobile, + 'date_naissance': dateNaissance?.toIso8601String(), + 'date_embauche': dateEmbauche?.toIso8601String(), }; } @@ -130,7 +193,12 @@ class UserModel extends HiveObject { DateTime? sessionExpiry, String? lastPath, String? sectName, - String? interface, + int? fkEntite, + int? fkTitre, + String? phone, + String? mobile, + DateTime? dateNaissance, + DateTime? dateEmbauche, }) { return UserModel( id: this.id, @@ -147,10 +215,15 @@ class UserModel extends HiveObject { sessionExpiry: sessionExpiry ?? this.sessionExpiry, lastPath: lastPath ?? this.lastPath, sectName: sectName ?? this.sectName, - interface: interface ?? this.interface, + fkEntite: fkEntite ?? this.fkEntite, + fkTitre: fkTitre ?? this.fkTitre, + phone: phone ?? this.phone, + mobile: mobile ?? this.mobile, + dateNaissance: dateNaissance ?? this.dateNaissance, + dateEmbauche: dateEmbauche ?? this.dateEmbauche, ); } - + // Vérifier si la session est valide bool get hasValidSession { if (sessionId == null || sessionExpiry == null) { @@ -158,7 +231,7 @@ class UserModel extends HiveObject { } return sessionExpiry!.isAfter(DateTime.now()); } - + // Effacer les données de session UserModel clearSession() { return copyWith( @@ -166,4 +239,4 @@ class UserModel extends HiveObject { sessionExpiry: null, ); } -} \ No newline at end of file +} diff --git a/flutt/lib/core/data/models/user_model.g.dart b/app/lib/core/data/models/user_model.g.dart similarity index 80% rename from flutt/lib/core/data/models/user_model.g.dart rename to app/lib/core/data/models/user_model.g.dart index 5fdb9392..adafe54f 100644 --- a/flutt/lib/core/data/models/user_model.g.dart +++ b/app/lib/core/data/models/user_model.g.dart @@ -31,14 +31,19 @@ class UserModelAdapter extends TypeAdapter { sessionExpiry: fields[9] as DateTime?, lastPath: fields[12] as String?, sectName: fields[13] as String?, - interface: fields[14] as String?, + fkEntite: fields[14] as int?, + fkTitre: fields[15] as int?, + phone: fields[16] as String?, + mobile: fields[17] as String?, + dateNaissance: fields[18] as DateTime?, + dateEmbauche: fields[19] as DateTime?, ); } @override void write(BinaryWriter writer, UserModel obj) { writer - ..writeByte(15) + ..writeByte(20) ..writeByte(0) ..write(obj.id) ..writeByte(1) @@ -68,7 +73,17 @@ class UserModelAdapter extends TypeAdapter { ..writeByte(13) ..write(obj.sectName) ..writeByte(14) - ..write(obj.interface); + ..write(obj.fkEntite) + ..writeByte(15) + ..write(obj.fkTitre) + ..writeByte(16) + ..write(obj.phone) + ..writeByte(17) + ..write(obj.mobile) + ..writeByte(18) + ..write(obj.dateNaissance) + ..writeByte(19) + ..write(obj.dateEmbauche); } @override diff --git a/app/lib/core/data/models/user_sector_model.dart b/app/lib/core/data/models/user_sector_model.dart new file mode 100644 index 00000000..792d338c --- /dev/null +++ b/app/lib/core/data/models/user_sector_model.dart @@ -0,0 +1,80 @@ +import 'package:hive/hive.dart'; + +part 'user_sector_model.g.dart'; + +/// Modèle pour stocker les associations entre utilisateurs et secteurs +/// +/// Cette classe représente l'association entre un utilisateur et un secteur, +/// telle que reçue de l'API dans la réponse users_sectors. +@HiveType( + typeId: 7) // Assurez-vous que cet ID est unique parmi vos modèles Hive +class UserSectorModel extends HiveObject { + @HiveField(0) + final int id; // ID de l'utilisateur + + @HiveField(1) + final String? firstName; + + @HiveField(2) + final String? sectName; + + @HiveField(3) + final int fkSector; // ID du secteur + + @HiveField(4) + final String? name; + + UserSectorModel({ + required this.id, + this.firstName, + this.sectName, + required this.fkSector, + this.name, + }); + + /// Crée un modèle UserSectorModel à partir d'un objet JSON + factory UserSectorModel.fromJson(Map json) { + return UserSectorModel( + id: json['id'] is String ? int.parse(json['id']) : json['id'], + firstName: json['first_name'], + sectName: json['sect_name'], + fkSector: json['fk_sector'] is String + ? int.parse(json['fk_sector']) + : json['fk_sector'], + name: json['name'], + ); + } + + /// Convertit le modèle en objet JSON + Map toJson() { + return { + 'id': id, + 'first_name': firstName, + 'sect_name': sectName, + 'fk_sector': fkSector, + 'name': name, + }; + } + + /// Crée une copie du modèle avec des valeurs potentiellement modifiées + UserSectorModel copyWith({ + int? id, + String? firstName, + String? sectName, + int? fkSector, + String? name, + }) { + return UserSectorModel( + id: id ?? this.id, + firstName: firstName ?? this.firstName, + sectName: sectName ?? this.sectName, + fkSector: fkSector ?? this.fkSector, + name: name ?? this.name, + ); + } + + @override + String toString() { + return 'UserSectorModel(id: $id, firstName: $firstName, sectName: $sectName, fkSector: $fkSector, name: $name)'; + } +} diff --git a/app/lib/core/data/models/user_sector_model.g.dart b/app/lib/core/data/models/user_sector_model.g.dart new file mode 100644 index 00000000..cb732753 --- /dev/null +++ b/app/lib/core/data/models/user_sector_model.g.dart @@ -0,0 +1,53 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_sector_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class UserSectorModelAdapter extends TypeAdapter { + @override + final int typeId = 7; + + @override + UserSectorModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return UserSectorModel( + id: fields[0] as int, + firstName: fields[1] as String?, + sectName: fields[2] as String?, + fkSector: fields[3] as int, + name: fields[4] as String?, + ); + } + + @override + void write(BinaryWriter writer, UserSectorModel obj) { + writer + ..writeByte(5) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.firstName) + ..writeByte(2) + ..write(obj.sectName) + ..writeByte(3) + ..write(obj.fkSector) + ..writeByte(4) + ..write(obj.name); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is UserSectorModelAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/app/lib/core/models/loading_state.dart b/app/lib/core/models/loading_state.dart new file mode 100644 index 00000000..9b4858f4 --- /dev/null +++ b/app/lib/core/models/loading_state.dart @@ -0,0 +1,75 @@ +/// Modèle pour suivre l'état du chargement des données +class LoadingState { + /// Progression globale (0.0 à 1.0) + final double progress; + + /// Description de l'étape en cours + final String? stepDescription; + + /// Message principal + final String? message; + + /// Indique si le chargement est terminé + final bool isCompleted; + + /// Indique si une erreur s'est produite + final bool hasError; + + /// Message d'erreur éventuel + final String? errorMessage; + + const LoadingState({ + this.progress = 0.0, + this.stepDescription, + this.message, + this.isCompleted = false, + this.hasError = false, + this.errorMessage, + }); + + /// Crée un nouvel état de chargement avec les valeurs mises à jour + LoadingState copyWith({ + double? progress, + String? stepDescription, + String? message, + bool? isCompleted, + bool? hasError, + String? errorMessage, + }) { + return LoadingState( + progress: progress ?? this.progress, + stepDescription: stepDescription ?? this.stepDescription, + message: message ?? this.message, + isCompleted: isCompleted ?? this.isCompleted, + hasError: hasError ?? this.hasError, + errorMessage: errorMessage ?? this.errorMessage, + ); + } + + /// État initial du chargement + static const initial = LoadingState( + progress: 0.0, + message: 'Chargement en cours...', + isCompleted: false, + hasError: false, + ); + + /// État de chargement terminé avec succès + static const completed = LoadingState( + progress: 1.0, + message: 'Chargement terminé', + isCompleted: true, + hasError: false, + ); + + /// Crée un état d'erreur + static LoadingState error(String message) { + return LoadingState( + progress: 0.0, + message: 'Erreur de chargement', + errorMessage: message, + isCompleted: true, + hasError: true, + ); + } +} diff --git a/app/lib/core/repositories/amicale_repository.dart b/app/lib/core/repositories/amicale_repository.dart new file mode 100644 index 00000000..cb68d092 --- /dev/null +++ b/app/lib/core/repositories/amicale_repository.dart @@ -0,0 +1,295 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:geosector_app/core/constants/app_keys.dart'; +import 'package:geosector_app/core/services/api_service.dart'; +import 'package:geosector_app/core/data/models/amicale_model.dart'; + +class AmicaleRepository extends ChangeNotifier { + // Utilisation de getters lazy pour n'accéder à la boîte que lorsque nécessaire + Box get _amicaleBox => + Hive.box(AppKeys.amicaleBoxName); + + final ApiService _apiService; + bool _isLoading = false; + + AmicaleRepository(this._apiService); + + // Getters + bool get isLoading => _isLoading; + + // Méthode pour vérifier si une boîte est ouverte et l'ouvrir si nécessaire + Future _ensureBoxIsOpen() async { + try { + if (!Hive.isBoxOpen(AppKeys.amicaleBoxName)) { + debugPrint('Ouverture de la boîte amicale...'); + await Hive.openBox(AppKeys.amicaleBoxName); + } + } catch (e) { + debugPrint('Erreur lors de l\'ouverture de la boîte amicale: $e'); + throw Exception('Impossible d\'ouvrir la boîte amicale: $e'); + } + } + + // Récupérer toutes les amicales + List getAllAmicales() { + try { + _ensureBoxIsOpen(); + return _amicaleBox.values.toList(); + } catch (e) { + debugPrint('Erreur lors de la récupération des amicales: $e'); + return []; + } + } + + // Récupérer une amicale par son ID + AmicaleModel? getAmicaleById(int id) { + try { + _ensureBoxIsOpen(); + return _amicaleBox.get(id); + } catch (e) { + debugPrint('Erreur lors de la récupération de l\'amicale: $e'); + return null; + } + } + + // Récupérer l'amicale de l'utilisateur connecté (basé sur fkEntite) + AmicaleModel? getAmicaleByUserId(int userId, int fkEntite) { + try { + _ensureBoxIsOpen(); + return _amicaleBox.get(fkEntite); + } catch (e) { + debugPrint( + 'Erreur lors de la récupération de l\'amicale de l\'utilisateur: $e'); + return null; + } + } + + // Créer ou mettre à jour une amicale localement + Future saveAmicale(AmicaleModel amicale) async { + await _ensureBoxIsOpen(); + await _amicaleBox.put(amicale.id, amicale); + notifyListeners(); // Notifier les changements pour mettre à jour l'UI + return amicale; + } + + // Supprimer une amicale localement + Future deleteAmicale(int id) async { + await _ensureBoxIsOpen(); + await _amicaleBox.delete(id); + notifyListeners(); + } + + // Vider la boîte des amicales + Future clearAmicales() async { + await _ensureBoxIsOpen(); + await _amicaleBox.clear(); + notifyListeners(); + } + + // Traiter les données des amicales reçues de l'API + Future processAmicalesData(dynamic amicalesData) async { + try { + debugPrint('Traitement des données des amicales...'); + debugPrint('Détails amicale: $amicalesData'); + + // Vérifier que les données sont au bon format + if (amicalesData == null) { + debugPrint('Aucune donnée d\'amicale à traiter'); + return; + } + + // Vider la boîte avant d'ajouter les nouvelles données + await _ensureBoxIsOpen(); + await _amicaleBox.clear(); + + int count = 0; + + // Cas 1: Les données sont une liste d'amicales + if (amicalesData is List) { + for (final amicaleData in amicalesData) { + try { + final amicale = AmicaleModel.fromJson(amicaleData); + await _amicaleBox.put(amicale.id, amicale); + count++; + debugPrint('Amicale traitée: ${amicale.name} (ID: ${amicale.id})'); + } catch (e) { + debugPrint('Erreur lors du traitement d\'une amicale: $e'); + } + } + } + // Cas 2: Les données sont un objet avec une clé 'data' contenant une liste + else if (amicalesData is Map && amicalesData.containsKey('data')) { + final amicalesList = amicalesData['data'] as List; + for (final amicaleData in amicalesList) { + try { + final amicale = AmicaleModel.fromJson(amicaleData); + await _amicaleBox.put(amicale.id, amicale); + count++; + debugPrint('Amicale traitée: ${amicale.name} (ID: ${amicale.id})'); + } catch (e) { + debugPrint('Erreur lors du traitement d\'une amicale: $e'); + } + } + } + // Cas 3: Les données sont un objet amicale unique (pas une liste) + else if (amicalesData is Map) { + try { + // Convertir Map en Map + final Map amicaleMap = {}; + amicalesData.forEach((key, value) { + if (key is String) { + amicaleMap[key] = value; + } + }); + + final amicale = AmicaleModel.fromJson(amicaleMap); + await _amicaleBox.put(amicale.id, amicale); + count++; + debugPrint( + 'Amicale unique traitée: ${amicale.name} (ID: ${amicale.id})'); + } catch (e) { + debugPrint('Erreur lors du traitement de l\'amicale unique: $e'); + debugPrint('Exception détaillée: $e'); + } + } else { + debugPrint('Format de données d\'amicale non reconnu'); + return; + } + + debugPrint('$count amicales traitées et stockées'); + notifyListeners(); + } catch (e) { + debugPrint('Erreur lors du traitement des amicales: $e'); + } + } + + // Récupérer les amicales depuis l'API + Future> fetchAmicalesFromApi() async { + _isLoading = true; + notifyListeners(); + + try { + final response = await _apiService.get('/amicales'); + + if (response.statusCode == 200) { + final amicalesData = response.data; + await processAmicalesData(amicalesData); + return getAllAmicales(); + } else { + debugPrint( + 'Erreur lors de la récupération des amicales: ${response.statusCode}'); + return []; + } + } catch (e) { + debugPrint('Erreur lors de la récupération des amicales: $e'); + return []; + } finally { + _isLoading = false; + notifyListeners(); + } + } + + // Récupérer une amicale spécifique depuis l'API + Future fetchAmicaleByIdFromApi(int id) async { + _isLoading = true; + notifyListeners(); + + try { + final response = await _apiService.get('/amicales/$id'); + + if (response.statusCode == 200) { + final amicaleData = response.data; + final amicale = AmicaleModel.fromJson(amicaleData); + await saveAmicale(amicale); + return amicale; + } else { + debugPrint( + 'Erreur lors de la récupération de l\'amicale: ${response.statusCode}'); + return null; + } + } catch (e) { + debugPrint('Erreur lors de la récupération de l\'amicale: $e'); + return null; + } finally { + _isLoading = false; + notifyListeners(); + } + } + + // Mettre à jour une amicale via l'API + Future updateAmicaleViaApi(AmicaleModel amicale) async { + _isLoading = true; + notifyListeners(); + + try { + final response = await _apiService.put( + '/amicales/${amicale.id}', + data: amicale.toJson(), + ); + + if (response.statusCode == 200) { + final updatedAmicaleData = response.data; + final updatedAmicale = AmicaleModel.fromJson(updatedAmicaleData); + await saveAmicale(updatedAmicale); + return updatedAmicale; + } else { + debugPrint( + 'Erreur lors de la mise à jour de l\'amicale: ${response.statusCode}'); + return null; + } + } catch (e) { + debugPrint('Erreur lors de la mise à jour de l\'amicale: $e'); + return null; + } finally { + _isLoading = false; + notifyListeners(); + } + } + + // Filtrer les amicales par nom + List searchAmicalesByName(String query) { + if (query.isEmpty) { + return getAllAmicales(); + } + + final lowercaseQuery = query.toLowerCase(); + return _amicaleBox.values + .where((amicale) => amicale.name.toLowerCase().contains(lowercaseQuery)) + .toList(); + } + + // Filtrer les amicales par type + List getAmicalesByType(int type) { + return _amicaleBox.values + .where((amicale) => amicale.fkType == type) + .toList(); + } + + // Filtrer les amicales par région + List getAmicalesByRegion(int regionId) { + return _amicaleBox.values + .where((amicale) => amicale.fkRegion == regionId) + .toList(); + } + + // Filtrer les amicales actives + List getActiveAmicales() { + return _amicaleBox.values.where((amicale) => amicale.chkActive).toList(); + } + + // Filtrer les amicales par code postal + List getAmicalesByPostalCode(String postalCode) { + return _amicaleBox.values + .where((amicale) => amicale.codePostal == postalCode) + .toList(); + } + + // Filtrer les amicales par ville + List getAmicalesByCity(String city) { + final lowercaseCity = city.toLowerCase(); + return _amicaleBox.values + .where((amicale) => amicale.ville.toLowerCase().contains(lowercaseCity)) + .toList(); + } +} diff --git a/app/lib/core/repositories/client_repository.dart b/app/lib/core/repositories/client_repository.dart new file mode 100644 index 00000000..9cb858b1 --- /dev/null +++ b/app/lib/core/repositories/client_repository.dart @@ -0,0 +1,179 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:geosector_app/core/constants/app_keys.dart'; +import 'package:geosector_app/core/services/api_service.dart'; +import 'package:geosector_app/core/data/models/client_model.dart'; + +class ClientRepository extends ChangeNotifier { + // Utilisation de getters lazy pour n'accéder à la boîte que lorsque nécessaire + Box get _clientBox => + Hive.box(AppKeys.clientsBoxName); + + final ApiService _apiService; + bool _isLoading = false; + + ClientRepository(this._apiService); + + // Getters + bool get isLoading => _isLoading; + + // Méthode pour vérifier si une boîte est ouverte et l'ouvrir si nécessaire + Future _ensureBoxIsOpen() async { + try { + if (!Hive.isBoxOpen(AppKeys.clientsBoxName)) { + debugPrint('Ouverture de la boîte clients...'); + await Hive.openBox(AppKeys.clientsBoxName); + } + } catch (e) { + debugPrint('Erreur lors de l\'ouverture de la boîte clients: $e'); + throw Exception('Impossible d\'ouvrir la boîte clients: $e'); + } + } + + // Récupérer tous les clients + List getAllClients() { + try { + _ensureBoxIsOpen(); + return _clientBox.values.toList(); + } catch (e) { + debugPrint('Erreur lors de la récupération des clients: $e'); + return []; + } + } + + // Récupérer un client par son ID + ClientModel? getClientById(int id) { + try { + _ensureBoxIsOpen(); + return _clientBox.get(id); + } catch (e) { + debugPrint('Erreur lors de la récupération du client: $e'); + return null; + } + } + + // Créer ou mettre à jour un client localement + Future saveClient(ClientModel client) async { + await _ensureBoxIsOpen(); + await _clientBox.put(client.id, client); + notifyListeners(); // Notifier les changements pour mettre à jour l'UI + return client; + } + + // Supprimer un client localement + Future deleteClient(int id) async { + await _ensureBoxIsOpen(); + await _clientBox.delete(id); + notifyListeners(); + } + + // Vider la boîte des clients + Future clearClients() async { + await _ensureBoxIsOpen(); + await _clientBox.clear(); + notifyListeners(); + } + + // Traiter les données des clients reçues de l'API + Future processClientsData(dynamic clientsData) async { + try { + debugPrint('Traitement des données des clients...'); + + // Vérifier que les données sont au bon format + if (clientsData == null) { + debugPrint('Aucune donnée de client à traiter'); + return; + } + + List clientsList; + if (clientsData is List) { + clientsList = clientsData; + } else if (clientsData is Map && clientsData.containsKey('data')) { + clientsList = clientsData['data'] as List; + } else { + debugPrint('Format de données de clients non reconnu'); + return; + } + + // Vider la boîte avant d'ajouter les nouvelles données + await _ensureBoxIsOpen(); + await _clientBox.clear(); + + // Traiter chaque client + int count = 0; + for (final clientData in clientsList) { + try { + final client = ClientModel.fromJson(clientData); + await _clientBox.put(client.id, client); + count++; + debugPrint('Client traité: ${client.name} (ID: ${client.id})'); + } catch (e) { + debugPrint('Erreur lors du traitement d\'un client: $e'); + } + } + + debugPrint('$count clients traités et stockés'); + notifyListeners(); + } catch (e) { + debugPrint('Erreur lors du traitement des clients: $e'); + } + } + + // Récupérer les clients depuis l'API + Future> fetchClientsFromApi() async { + _isLoading = true; + notifyListeners(); + + try { + final response = await _apiService.get('/clients'); + + if (response.statusCode == 200) { + final clientsData = response.data; + await processClientsData(clientsData); + return getAllClients(); + } else { + debugPrint( + 'Erreur lors de la récupération des clients: ${response.statusCode}'); + return []; + } + } catch (e) { + debugPrint('Erreur lors de la récupération des clients: $e'); + return []; + } finally { + _isLoading = false; + notifyListeners(); + } + } + + // Filtrer les clients par nom + List searchClientsByName(String query) { + if (query.isEmpty) { + return getAllClients(); + } + + final lowercaseQuery = query.toLowerCase(); + return _clientBox.values + .where((client) => client.name.toLowerCase().contains(lowercaseQuery)) + .toList(); + } + + // Filtrer les clients par type + List getClientsByType(int type) { + return _clientBox.values.where((client) => client.fkType == type).toList(); + } + + // Filtrer les clients par région + List getClientsByRegion(int regionId) { + return _clientBox.values + .where((client) => client.fkRegion == regionId) + .toList(); + } + + // Filtrer les clients actifs + List getActiveClients() { + return _clientBox.values + .where((client) => client.chkActive == true) + .toList(); + } +} diff --git a/flutt/lib/core/repositories/membre_repository.dart b/app/lib/core/repositories/membre_repository.dart similarity index 100% rename from flutt/lib/core/repositories/membre_repository.dart rename to app/lib/core/repositories/membre_repository.dart diff --git a/flutt/lib/core/repositories/operation_repository.dart b/app/lib/core/repositories/operation_repository.dart similarity index 100% rename from flutt/lib/core/repositories/operation_repository.dart rename to app/lib/core/repositories/operation_repository.dart diff --git a/flutt/lib/core/repositories/passage_repository.dart b/app/lib/core/repositories/passage_repository.dart similarity index 64% rename from flutt/lib/core/repositories/passage_repository.dart rename to app/lib/core/repositories/passage_repository.dart index 6d20b27f..31e90bab 100644 --- a/flutt/lib/core/repositories/passage_repository.dart +++ b/app/lib/core/repositories/passage_repository.dart @@ -8,19 +8,51 @@ import 'package:geosector_app/core/constants/app_keys.dart'; class PassageRepository extends ChangeNotifier { // Utiliser un getter lazy pour n'accéder à la boîte que lorsque nécessaire // et vérifier qu'elle est ouverte avant accès + Box? _box; + Box get _passageBox { - _ensureBoxIsOpen(); - return Hive.box(AppKeys.passagesBoxName); + if (_box != null && _box!.isOpen) { + return _box!; + } + + if (!Hive.isBoxOpen(AppKeys.passagesBoxName)) { + throw StateError( + 'La boîte ${AppKeys.passagesBoxName} n\'est pas ouverte. Appelez _ensureBoxIsOpen() avant d\'accéder à la boîte.'); + } + + _box = Hive.box(AppKeys.passagesBoxName); + return _box!; } - + // Méthode pour vérifier si la boîte est ouverte et l'ouvrir si nécessaire Future _ensureBoxIsOpen() async { final boxName = AppKeys.passagesBoxName; - if (!Hive.isBoxOpen(boxName)) { - debugPrint('Ouverture de la boîte $boxName dans PassageRepository...'); - await Hive.openBox(boxName); + + // Si nous avons déjà une référence à la boîte et qu'elle est ouverte, retourner + if (_box != null && _box!.isOpen) { + return; + } + + // Si la boîte est déjà ouverte, récupérer la référence + if (Hive.isBoxOpen(boxName)) { + _box = Hive.box(boxName); + debugPrint( + 'PassageRepository: Boîte $boxName déjà ouverte, référence récupérée'); + return; + } + + // Sinon, ouvrir la boîte + try { + debugPrint('PassageRepository: Ouverture de la boîte $boxName...'); + _box = await Hive.openBox(boxName); + debugPrint('PassageRepository: Boîte $boxName ouverte avec succès'); + } catch (e) { + debugPrint( + 'PassageRepository: ERREUR lors de l\'ouverture de la boîte $boxName: $e'); + rethrow; // Propager l'erreur pour permettre une gestion appropriée } } + final ApiService _apiService; bool _isLoading = false; @@ -33,40 +65,124 @@ class PassageRepository extends ChangeNotifier { // Récupérer tous les passages List getAllPassages() { - return _passageBox.values.toList(); + try { + // S'assurer que la boîte est ouverte avant d'y accéder + _ensureBoxIsOpen().then((_) { + debugPrint( + 'PassageRepository: Boîte ouverte avec succès pour getAllPassages'); + }).catchError((e) { + debugPrint( + 'PassageRepository: Erreur lors de l\'ouverture de la boîte pour getAllPassages: $e'); + }); + + return _passageBox.values.toList(); + } catch (e) { + debugPrint('PassageRepository: Erreur dans getAllPassages: $e'); + return []; // Retourner une liste vide en cas d'erreur + } } // Récupérer un passage par son ID PassageModel? getPassageById(int id) { - return _passageBox.get(id); + try { + // S'assurer que la boîte est ouverte avant d'y accéder + _ensureBoxIsOpen().then((_) { + debugPrint( + 'PassageRepository: Boîte ouverte avec succès pour getPassageById'); + }).catchError((e) { + debugPrint( + 'PassageRepository: Erreur lors de l\'ouverture de la boîte pour getPassageById: $e'); + }); + + return _passageBox.get(id); + } catch (e) { + debugPrint('PassageRepository: Erreur dans getPassageById: $e'); + return null; + } } // Récupérer les passages par secteur List getPassagesBySector(int sectorId) { - return _passageBox.values - .where((passage) => passage.fkSector == sectorId) - .toList(); + try { + // S'assurer que la boîte est ouverte avant d'y accéder + _ensureBoxIsOpen().then((_) { + debugPrint( + 'PassageRepository: Boîte ouverte avec succès pour getPassagesBySector'); + }).catchError((e) { + debugPrint( + 'PassageRepository: Erreur lors de l\'ouverture de la boîte pour getPassagesBySector: $e'); + }); + + return _passageBox.values + .where((passage) => passage.fkSector == sectorId) + .toList(); + } catch (e) { + debugPrint('PassageRepository: Erreur dans getPassagesBySector: $e'); + return []; + } } // Récupérer les passages par opération List getPassagesByOperation(int operationId) { - return _passageBox.values - .where((passage) => passage.fkOperation == operationId) - .toList(); + try { + // S'assurer que la boîte est ouverte avant d'y accéder + _ensureBoxIsOpen().then((_) { + debugPrint( + 'PassageRepository: Boîte ouverte avec succès pour getPassagesByOperation'); + }).catchError((e) { + debugPrint( + 'PassageRepository: Erreur lors de l\'ouverture de la boîte pour getPassagesByOperation: $e'); + }); + + return _passageBox.values + .where((passage) => passage.fkOperation == operationId) + .toList(); + } catch (e) { + debugPrint('PassageRepository: Erreur dans getPassagesByOperation: $e'); + return []; + } } // Récupérer les passages par type List getPassagesByType(int typeId) { - return _passageBox.values - .where((passage) => passage.fkType == typeId) - .toList(); + try { + // S'assurer que la boîte est ouverte avant d'y accéder + _ensureBoxIsOpen().then((_) { + debugPrint( + 'PassageRepository: Boîte ouverte avec succès pour getPassagesByType'); + }).catchError((e) { + debugPrint( + 'PassageRepository: Erreur lors de l\'ouverture de la boîte pour getPassagesByType: $e'); + }); + + return _passageBox.values + .where((passage) => passage.fkType == typeId) + .toList(); + } catch (e) { + debugPrint('PassageRepository: Erreur dans getPassagesByType: $e'); + return []; + } } // Récupérer les passages par type de règlement List getPassagesByPaymentType(int paymentTypeId) { - return _passageBox.values - .where((passage) => passage.fkTypeReglement == paymentTypeId) - .toList(); + try { + // S'assurer que la boîte est ouverte avant d'y accéder + _ensureBoxIsOpen().then((_) { + debugPrint( + 'PassageRepository: Boîte ouverte avec succès pour getPassagesByPaymentType'); + }).catchError((e) { + debugPrint( + 'PassageRepository: Erreur lors de l\'ouverture de la boîte pour getPassagesByPaymentType: $e'); + }); + + return _passageBox.values + .where((passage) => passage.fkTypeReglement == paymentTypeId) + .toList(); + } catch (e) { + debugPrint('PassageRepository: Erreur dans getPassagesByPaymentType: $e'); + return []; + } } // Sauvegarder un passage @@ -89,13 +205,13 @@ class PassageRepository extends ChangeNotifier { try { for (var passageData in passagesData) { final passageJson = passageData as Map; - final passageId = passageJson['id'] is String - ? int.parse(passageJson['id']) + final passageId = passageJson['id'] is String + ? int.parse(passageJson['id']) : passageJson['id'] as int; - + // Vérifier si le passage existe déjà PassageModel? existingPassage = getPassageById(passageId); - + if (existingPassage == null) { // Créer un nouveau passage final newPassage = PassageModel.fromJson(passageJson); @@ -177,13 +293,13 @@ class PassageRepository extends ChangeNotifier { // Appeler l'API pour créer le passage final response = await _apiService.post('/passages', data: data); - + if (response.statusCode == 201 || response.statusCode == 200) { // Récupérer l'ID du nouveau passage - final passageId = response.data['id'] is String - ? int.parse(response.data['id']) + final passageId = response.data['id'] is String + ? int.parse(response.data['id']) : response.data['id'] as int; - + // Créer le modèle local final newPassage = PassageModel( id: passageId, @@ -215,11 +331,12 @@ class PassageRepository extends ChangeNotifier { isActive: true, isSynced: true, ); - + await savePassage(newPassage); return true; } else { - debugPrint('Erreur lors de la création du passage: ${response.statusMessage}'); + debugPrint( + 'Erreur lors de la création du passage: ${response.statusMessage}'); return false; } } catch (e) { @@ -241,38 +358,40 @@ class PassageRepository extends ChangeNotifier { final Map data = passage.toJson(); // Appeler l'API pour mettre à jour le passage - final response = await _apiService.put('/passages/${passage.id}', data: data); - + final response = + await _apiService.put('/passages/${passage.id}', data: data); + if (response.statusCode == 200) { // Mettre à jour le modèle local final updatedPassage = passage.copyWith( lastSyncedAt: DateTime.now(), isSynced: true, ); - + await savePassage(updatedPassage); return true; } else { - debugPrint('Erreur lors de la mise à jour du passage: ${response.statusMessage}'); - + debugPrint( + 'Erreur lors de la mise à jour du passage: ${response.statusMessage}'); + // Marquer comme non synchronisé mais sauvegarder localement final updatedPassage = passage.copyWith( lastSyncedAt: DateTime.now(), isSynced: false, ); - + await savePassage(updatedPassage); return false; } } catch (e) { debugPrint('Erreur lors de la mise à jour du passage: $e'); - + // Marquer comme non synchronisé mais sauvegarder localement final updatedPassage = passage.copyWith( lastSyncedAt: DateTime.now(), isSynced: false, ); - + await savePassage(updatedPassage); return false; } finally { @@ -290,7 +409,8 @@ class PassageRepository extends ChangeNotifier { return; } - final unsyncedPassages = _passageBox.values.where((passage) => !passage.isSynced).toList(); + final unsyncedPassages = + _passageBox.values.where((passage) => !passage.isSynced).toList(); if (unsyncedPassages.isEmpty) { return; @@ -328,7 +448,7 @@ class PassageRepository extends ChangeNotifier { email: passage.email, phone: passage.phone, ); - + // Supprimer l'ancien passage avec ID temporaire await deletePassage(passage.id); } else { @@ -336,7 +456,8 @@ class PassageRepository extends ChangeNotifier { await updatePassage(passage); } } catch (e) { - debugPrint('Erreur lors de la synchronisation du passage ${passage.id}: $e'); + debugPrint( + 'Erreur lors de la synchronisation du passage ${passage.id}: $e'); } } } catch (e) { @@ -360,7 +481,7 @@ class PassageRepository extends ChangeNotifier { notifyListeners(); final response = await _apiService.get('/passages'); - + if (response.statusCode == 200) { final List passagesData = response.data; await processPassagesFromApi(passagesData); diff --git a/app/lib/core/repositories/region_repository.dart b/app/lib/core/repositories/region_repository.dart new file mode 100644 index 00000000..d4e3e4e1 --- /dev/null +++ b/app/lib/core/repositories/region_repository.dart @@ -0,0 +1,85 @@ +import 'package:flutter/foundation.dart'; +import 'package:geosector_app/core/constants/app_keys.dart'; +import 'package:geosector_app/core/data/models/region_model.dart'; +import 'package:hive_flutter/hive_flutter.dart'; + +class RegionRepository extends ChangeNotifier { + late Box _regionBox; + List _regions = []; + bool _isLoaded = false; + + // Getter pour les régions + List get regions => _regions; + bool get isLoaded => _isLoaded; + + // Initialisation du repository + Future init() async { + if (!Hive.isBoxOpen(AppKeys.regionsBoxName)) { + _regionBox = await Hive.openBox(AppKeys.regionsBoxName); + } else { + _regionBox = Hive.box(AppKeys.regionsBoxName); + } + _loadRegions(); + } + + // Chargement des régions depuis la boîte Hive + void _loadRegions() { + _regions = _regionBox.values.toList(); + _isLoaded = true; + notifyListeners(); + } + + // Mise à jour des régions depuis l'API + Future updateRegionsFromApi(List regionsData) async { + await _regionBox.clear(); + + for (var regionData in regionsData) { + final region = RegionModel.fromJson(regionData); + await _regionBox.put(region.id, region); + } + + _loadRegions(); + } + + // Récupérer une région par son ID + RegionModel? getRegionById(int id) { + return _regionBox.get(id); + } + + // Récupérer une région par son code postal (2 premiers chiffres) + RegionModel? getRegionByPostalCode(String postalCode) { + if (postalCode.length < 2) return null; + + final departement = postalCode.substring(0, 2); + + for (var region in _regions) { + if (region.departements != null && + region.departements!.split(',').contains(departement)) { + return region; + } + } + + return null; + } + + // Récupérer toutes les régions actives + List getActiveRegions() { + return _regions.where((region) => region.chkActive).toList(); + } + + // Convertir les régions en format pour le dropdown + List> getRegionsForDropdown() { + return _regions + .where((region) => region.chkActive) + .map((region) => { + 'id': region.id, + 'name': region.libelle, + }) + .toList(); + } + + // Fermeture de la boîte Hive + Future close() async { + await _regionBox.close(); + } +} diff --git a/flutt/lib/core/repositories/sector_repository.dart b/app/lib/core/repositories/sector_repository.dart similarity index 100% rename from flutt/lib/core/repositories/sector_repository.dart rename to app/lib/core/repositories/sector_repository.dart diff --git a/app/lib/core/repositories/user_repository.dart b/app/lib/core/repositories/user_repository.dart new file mode 100644 index 00000000..76770b5a --- /dev/null +++ b/app/lib/core/repositories/user_repository.dart @@ -0,0 +1,1949 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:js' as js; +import 'package:geosector_app/core/services/hive_web_fix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:geosector_app/core/constants/app_keys.dart'; +import 'package:geosector_app/core/services/api_service.dart'; +import 'package:geosector_app/core/services/sync_service.dart'; +import 'package:geosector_app/core/services/hive_reset_state_service.dart'; +import 'package:geosector_app/core/data/models/user_model.dart'; +import 'package:geosector_app/core/data/models/amicale_model.dart'; +import 'package:geosector_app/core/data/models/operation_model.dart'; +import 'package:geosector_app/core/data/models/sector_model.dart'; +import 'package:geosector_app/core/data/models/passage_model.dart'; +import 'package:geosector_app/core/data/models/membre_model.dart'; +import 'package:geosector_app/core/data/models/user_sector_model.dart'; +import 'package:geosector_app/core/repositories/operation_repository.dart'; +import 'package:geosector_app/core/repositories/sector_repository.dart'; +import 'package:geosector_app/core/repositories/passage_repository.dart'; +import 'package:geosector_app/core/repositories/client_repository.dart'; +import 'package:geosector_app/core/repositories/amicale_repository.dart'; +import 'package:geosector_app/chat/models/conversation_model.dart'; +import 'package:geosector_app/chat/models/message_model.dart'; +import 'package:geosector_app/presentation/widgets/loading_overlay.dart'; +import 'package:geosector_app/presentation/widgets/loading_progress_overlay.dart'; +import 'package:geosector_app/core/models/loading_state.dart'; + +class UserRepository extends ChangeNotifier { + // Contrôle de l'état du chargement des données + LoadingState _loadingState = LoadingState.initial; + LoadingState get loadingState => _loadingState; + + // Overlay pour afficher la progression du chargement + OverlayEntry? _progressOverlay; + + // Méthode pour mettre à jour l'état du chargement + void _updateLoadingState(LoadingState newState) { + _loadingState = newState; + notifyListeners(); + + // Mettre à jour l'overlay si présent + if (_progressOverlay != null) { + _progressOverlay!.markNeedsBuild(); + } + } + + // Utilisation de getters lazy pour n'accéder aux boîtes que lorsque nécessaire + Box get _userBox => Hive.box(AppKeys.usersBoxName); + Box get _amicaleBox => + Hive.box(AppKeys.amicaleBoxName); + + // Getters pour les autres boîtes qui vérifient si elles sont ouvertes avant accès + Box get _operationBox { + _ensureBoxIsOpen(AppKeys.operationsBoxName); + return Hive.box(AppKeys.operationsBoxName); + } + + Box get _sectorBox { + _ensureBoxIsOpen(AppKeys.sectorsBoxName); + return Hive.box(AppKeys.sectorsBoxName); + } + + Box get _passageBox { + _ensureBoxIsOpen(AppKeys.passagesBoxName); + return Hive.box(AppKeys.passagesBoxName); + } + + Box get _membreBox { + _ensureBoxIsOpen(AppKeys.membresBoxName); + return Hive.box(AppKeys.membresBoxName); + } + + Box get _userSectorBox { + _ensureBoxIsOpen(AppKeys.userSectorBoxName); + return Hive.box(AppKeys.userSectorBoxName); + } + + Box get _settingsBox { + _ensureBoxIsOpen(AppKeys.settingsBoxName); + return Hive.box(AppKeys.settingsBoxName); + } + + Box get _chatConversationBox { + _ensureBoxIsOpen(AppKeys.chatConversationsBoxName); + return Hive.box(AppKeys.chatConversationsBoxName); + } + + Box get _chatMessageBox { + _ensureBoxIsOpen(AppKeys.chatMessagesBoxName); + return Hive.box(AppKeys.chatMessagesBoxName); + } + + // Méthode pour initialiser les boîtes après connexion + Future _initializeBoxes() async { + debugPrint('Initialisation des boîtes Hive nécessaires...'); + await _ensureBoxIsOpen(AppKeys.amicaleBoxName); + await _ensureBoxIsOpen(AppKeys.operationsBoxName); + await _ensureBoxIsOpen(AppKeys.sectorsBoxName); + await _ensureBoxIsOpen(AppKeys.passagesBoxName); + await _ensureBoxIsOpen(AppKeys.membresBoxName); + await _ensureBoxIsOpen(AppKeys.userSectorBoxName); + // Les boîtes de chat sont déjà initialisées au démarrage + await _ensureBoxIsOpen(AppKeys.chatConversationsBoxName); + await _ensureBoxIsOpen(AppKeys.chatMessagesBoxName); + debugPrint('Toutes les boîtes Hive sont maintenant ouvertes'); + } + + final ApiService _apiService; + final SyncService? _syncService; + final OperationRepository? _operationRepository; + final SectorRepository? _sectorRepository; + final PassageRepository? _passageRepository; + + bool _isLoading = false; + + UserRepository(this._apiService, + {SyncService? syncService, + OperationRepository? operationRepository, + SectorRepository? sectorRepository, + PassageRepository? passageRepository}) + : _syncService = syncService, + _operationRepository = operationRepository, + _sectorRepository = sectorRepository, + _passageRepository = passageRepository { + // Initialiser la session si un utilisateur est déjà connecté + final currentUser = getCurrentUser(); + if (currentUser != null && currentUser.sessionId != null) { + setSessionId(currentUser.sessionId); + } + } + + // Cache pour l'utilisateur actuel + UserModel? _cachedCurrentUser; + + // Getters + bool get isLoading => _isLoading; + bool get isLoggedIn => currentUser != null; + int? get userId => currentUser?.id; + UserModel? get currentUser { + // Utiliser le cache s'il existe déjà + if (_cachedCurrentUser != null) { + return _cachedCurrentUser; + } + + // Sinon, récupérer l'utilisateur et le mettre en cache + _cachedCurrentUser = _getCurrentUserFromStorage(); + return _cachedCurrentUser; + } + + // Retourne le rôle de l'utilisateur sous forme d'entier + int getUserRole() { + final user = getCurrentUser(); + if (user == null) return 0; // Aucun utilisateur = aucun rôle + + // Convertir le rôle en int si c'est une String + if (user.role is String) { + return int.tryParse(user.role as String) ?? 1; + } else { + return user.role as int; + } + } + + // Méthode privée pour récupérer l'utilisateur depuis le stockage + UserModel? _getCurrentUserFromStorage() { + try { + // Vérifier d'abord si la boîte est ouverte + if (!Hive.isBoxOpen(AppKeys.usersBoxName)) { + try { + Hive.openBox(AppKeys.usersBoxName); + } catch (e) { + debugPrint( + 'Erreur lors de l\'ouverture de la boîte utilisateurs: $e'); + return null; + } + } + + // Chercher un utilisateur avec une session active - Il suffit qu'il ait un sessionId + final activeUsers = _userBox.values + .where((user) => + user.sessionId != null && // Vérifier que sessionId n'est pas null + user.sessionId! + .isNotEmpty) // Vérifier que sessionId n'est pas vide + .toList(); + + // S'il y a des utilisateurs actifs, retourner le premier + if (activeUsers.isNotEmpty) { + return activeUsers.first; + } + + return null; + } catch (e) { + debugPrint( + 'Erreur lors de la récupération de l\'utilisateur depuis le stockage: $e'); + return null; + } + } + + // Récupérer l'utilisateur actuellement connecté + UserModel? getCurrentUser() { + // Utiliser le getter currentUser qui gère le cache + return currentUser; + } + + // Mettre à jour le chemin de la page actuelle pour l'utilisateur connecté + Future updateLastPath(String path) async { + final currentUser = getCurrentUser(); + if (currentUser != null) { + final updatedUser = currentUser.copyWith(lastPath: path); + await saveUser(updatedUser); + } + } + + // Récupérer le dernier chemin visité par l'utilisateur + String? getLastPath() { + final currentUser = getCurrentUser(); + return currentUser?.lastPath; + } + + // Configurer la session dans l'API + void setSessionId(String? sessionId) { + _apiService.setSessionId(sessionId); + } + + // Login API PHP + Future> loginAPI(String username, String password, + {required String type}) async { + try { + return await _apiService.login(username, password, type: type); + } catch (e) { + debugPrint('Erreur login API: $e'); + rethrow; + } + } + + // Register API PHP - Uniquement pour les administrateurs + Future> registerAPI(String email, String name, + String amicaleName, String postalCode, String cityName) async { + try { + final Map data = { + 'email': email, + 'name': name, + 'amicale_name': amicaleName, + 'postal_code': postalCode, + 'city_name': cityName + }; + + final response = + await _apiService.post(AppKeys.registerEndpoint, data: data); + return response.data; + } catch (e) { + debugPrint('Erreur register API: $e'); + rethrow; + } + } + + // Logout API PHP + Future logoutAPI() async { + try { + await _apiService.logout(); + } catch (e) { + debugPrint('Erreur logout API: $e'); + rethrow; + } + } + + // Méthode d'inscription (uniquement pour les administrateurs) + Future register(String email, String password, String name, + String amicaleName, String postalCode, String cityName) async { + _isLoading = true; + notifyListeners(); + + try { + // Enregistrer l'administrateur via l'API + final apiResult = + await registerAPI(email, name, amicaleName, postalCode, cityName); + + // Créer l'administrateur local + final int userId = apiResult['user_id'] is String + ? int.parse(apiResult['user_id']) + : apiResult['user_id']; + final now = DateTime.now(); + final newAdmin = UserModel( + id: userId, + email: email, + name: name, + role: AppKeys.roleAdmin2, + createdAt: now, + lastSyncedAt: now, + isActive: true, + isSynced: true, + sessionId: apiResult['session_id'], + sessionExpiry: DateTime.parse(apiResult['session_expiry']), + ); + + // Sauvegarder dans le repository local + await saveUser(newAdmin); + + // Configurer la session dans l'API + setSessionId(newAdmin.sessionId); + + notifyListeners(); + return true; + } catch (e) { + debugPrint('Erreur d\'inscription: $e'); + return false; + } finally { + _isLoading = false; + notifyListeners(); + } + } + + // Login complet avec suivi de progression + Future login(String username, String password, + {required String type}) async { + _isLoading = true; + _updateLoadingState(LoadingState.initial.copyWith( + message: 'Connexion en cours...', + stepDescription: 'Préparation des données', + )); + notifyListeners(); + + try { + debugPrint('Début du processus de connexion pour: $username'); + + // Étape 1: Nettoyage des boîtes non référencées (5%) + _updateLoadingState(_loadingState.copyWith( + progress: 0.05, + stepDescription: 'Nettoyage des données obsolètes', + )); + + final nonDefinedBoxes = ['auth', 'locations', 'messages']; + for (final boxName in nonDefinedBoxes) { + try { + if (Hive.isBoxOpen(boxName)) { + debugPrint('Fermeture de la boîte non référencée: $boxName'); + await Hive.box(boxName).close(); + } + + // Supprimer la boîte du disque + await Hive.deleteBoxFromDisk(boxName); + debugPrint('Nettoyage: Box $boxName supprimée'); + } catch (e) { + debugPrint( + 'Erreur lors de la suppression de la boîte non référencée $boxName: $e'); + } + } + + // Étape 2: Nettoyage des données existantes (10%) + _updateLoadingState(_loadingState.copyWith( + progress: 0.10, + stepDescription: 'Préparation du stockage local', + )); + + debugPrint('Nettoyage des données existantes avant connexion...'); + + // Sur le web, utiliser notre méthode sécurisée pour nettoyer les boîtes Hive + if (kIsWeb) { + await HiveWebFix.safeCleanHiveBoxes( + excludeBoxes: [AppKeys.usersBoxName]); + } + // Sur iOS, nettoyer les fichiers Hive directement + else if (!kIsWeb && Platform.isIOS) { + await _cleanHiveFilesOnIOS(); + } + // Sur Android, nettoyer les fichiers Hive directement + else if (!kIsWeb && Platform.isAndroid) { + await _cleanHiveFilesOnAndroid(); + } + + // Étape 3: Recréation des boîtes (15%) + _updateLoadingState(_loadingState.copyWith( + progress: 0.15, + stepDescription: 'Initialisation des bases de données', + )); + + // Nettoyer les boîtes sans les fermer + await _clearAndRecreateBoxes(); + + // Initialiser les boîtes nécessaires avant d'appeler l'API + // Cela garantit que toutes les boîtes sont ouvertes avant le traitement des données + await _initializeBoxes(); + + // Étape 4: Connexion à l'API (25%) + _updateLoadingState(_loadingState.copyWith( + progress: 0.25, + stepDescription: 'Connexion au serveur', + )); + + // Appeler l'API + debugPrint('Appel de l\'API de connexion (type: $type)...'); + final apiResult = await loginAPI(username, password, type: type); + + // Vérifier le statut de la réponse + final status = apiResult['status'] as String?; + final message = apiResult['message'] as String?; + + // Si le statut n'est pas 'success', retourner false + if (status != 'success') { + debugPrint('Échec de connexion: $message'); + _updateLoadingState( + LoadingState.error(message ?? 'Échec de connexion')); + return false; + } + + // Étape 5: Traitement des données utilisateur (35%) + _updateLoadingState(_loadingState.copyWith( + progress: 0.35, + stepDescription: 'Traitement des données utilisateur', + )); + + // Logging détaillé de la réponse API pour diagnostiquer le problème d'interface/rôle + debugPrint('Connexion réussie, contenu de la réponse API:'); + apiResult.forEach((key, value) { + debugPrint(' $key: $value'); + }); + + // Si la clé 'user' existe, examiner son contenu + if (apiResult['user'] != null && + apiResult['user'] is Map) { + debugPrint('Détails utilisateur:'); + final userDetails = apiResult['user'] as Map; + userDetails.forEach((key, value) { + debugPrint(' $key: $value (${value.runtimeType})'); + }); + + // Construire un UserModel à partir des données utilisateur + final user = _processUserData( + userDetails, apiResult['session_id'], apiResult['session_expiry']); + + // Supprimer les anciennes références à interface et utiliser directement le rôle + await saveUser(user); + + // Configurer la session + setSessionId(user.sessionId); + + debugPrint('Utilisateur créé et sauvegardé: ${user.toJson()}'); + debugPrint('Vérification des rôles: role=${user.role}'); + debugPrint('Rôle utilisateur: ${getUserRole()}'); + } + + // Si la clé 'amicale' existe, examiner son contenu + if (apiResult['amicale'] != null) { + debugPrint('Détails amicale:'); + + // Passer directement les données d'amicale au repository + // Le repository a été modifié pour gérer tous les formats possibles + final amicaleRepository = AmicaleRepository(_apiService); + await amicaleRepository.processAmicalesData(apiResult['amicale']); + + debugPrint('Amicales traitées et stockées via AmicaleRepository'); + } + + // Si la clé 'clients' existe, examiner son contenu + if (apiResult['clients'] != null) { + debugPrint('Détails clients:'); + List clientsList; + + if (apiResult['clients'] is List) { + clientsList = apiResult['clients'] as List; + } else if (apiResult['clients'] is Map && + apiResult['clients'].containsKey('data')) { + clientsList = apiResult['clients']['data'] as List; + } else { + debugPrint('Format de données de clients non reconnu'); + clientsList = []; + } + + // Si l'utilisateur a un rôle > 2, traiter les clients + if (clientsList.isNotEmpty) { + debugPrint('Traitement de ${clientsList.length} clients de type 1'); + + // Utiliser le ClientRepository pour traiter les données des clients + final clientRepository = ClientRepository(_apiService); + await clientRepository.processClientsData(clientsList); + + debugPrint('Clients traités et stockés via ClientRepository'); + } else { + debugPrint('Aucun client à traiter (rôle utilisateur <= 2)'); + } + } + + // Étape 6: Traitement des opérations (50%) + _updateLoadingState(_loadingState.copyWith( + progress: 0.50, + stepDescription: 'Chargement des opérations', + )); + + debugPrint('Traitement des données...'); + + // Traitement des données des opérations, secteurs, passages et membres + // Ces données devraient être présentes dans la réponse API + if (apiResult.containsKey('operations')) { + await _processOperations(apiResult['operations']); + debugPrint('Nombre d\'opérations chargées: ${_operationBox.length}'); + } + + // Étape 7: Traitement des secteurs (65%) + _updateLoadingState(_loadingState.copyWith( + progress: 0.65, + stepDescription: 'Chargement des secteurs', + )); + + if (apiResult.containsKey('sectors')) { + await _processSectors(apiResult['sectors']); + debugPrint('Nombre de secteurs chargés: ${_sectorBox.length}'); + } + + // Étape 8: Traitement des passages (75%) + _updateLoadingState(_loadingState.copyWith( + progress: 0.75, + stepDescription: 'Chargement des passages', + )); + + if (apiResult.containsKey('passages')) { + await _processPassages(apiResult['passages']); + debugPrint('Nombre de passages chargés: ${_passageBox.length}'); + } + + // Étape 9: Traitement des membres (85%) + _updateLoadingState(_loadingState.copyWith( + progress: 0.85, + stepDescription: 'Chargement des membres', + )); + + // Traitement des membres + if (apiResult.containsKey('membres') || + apiResult.containsKey('members')) { + final membresData = apiResult['membres'] ?? apiResult['members']; + if (membresData != null) { + await _processMembres(membresData); + debugPrint('Nombre de membres chargés: ${_membreBox.length}'); + } + } + + // Étape 10: Traitement des associations utilisateurs-secteurs (95%) + _updateLoadingState(_loadingState.copyWith( + progress: 0.95, + stepDescription: 'Finalisation du chargement', + )); + + // Traitement des associations utilisateurs-secteurs + if (apiResult.containsKey('users_sectors')) { + await _processUserSectors(apiResult['users_sectors']); + debugPrint( + 'Nombre d\'associations utilisateurs-secteurs chargées: ${_userSectorBox.length}'); + } + + // Vérification finale du remplissage des boîtes + debugPrint('=== VÉRIFICATION DU REMPLISSAGE DES BOÎTES ==='); + debugPrint('Utilisateurs: ${_userBox.length}'); + debugPrint('Opérations: ${_operationBox.length}'); + debugPrint('Secteurs: ${_sectorBox.length}'); + debugPrint('Passages: ${_passageBox.length}'); + debugPrint('Membres: ${_membreBox.length}'); + + // Afficher le nombre d'associations utilisateurs-secteurs avec plus de détails + final userSectorCount = _userSectorBox.length; + debugPrint('Associations utilisateurs-secteurs: $userSectorCount'); + + // Si des associations existent, afficher quelques détails + if (userSectorCount > 0) { + debugPrint('--- Détails des associations utilisateurs-secteurs ---'); + int displayCount = 0; + for (final userSector in _userSectorBox.values) { + if (displayCount < 5) { + // Limiter à 5 pour éviter de surcharger la console + debugPrint( + ' User ${userSector.id} (${userSector.firstName}) -> Secteur ${userSector.fkSector} (${userSector.name})'); + displayCount++; + } else { + debugPrint(' ... et ${userSectorCount - 5} autres associations'); + break; + } + } + } + + // Étape 11: Vérification finale des données (95%) + _updateLoadingState(_loadingState.copyWith( + progress: 0.95, + stepDescription: 'Vérification finale des données', + )); + + // Vérifier que le nombre de passages dans la réponse API correspond au nombre dans la Hive Box + int passagesCountInResponse = 0; + if (apiResult.containsKey('passages')) { + if (apiResult['passages'] is List) { + passagesCountInResponse = (apiResult['passages'] as List).length; + } else if (apiResult['passages'] is Map && + apiResult['passages'].containsKey('data')) { + passagesCountInResponse = + (apiResult['passages']['data'] as List).length; + } + } + + int passagesCountInBox = _passageBox.length; + debugPrint( + 'Nombre de passages dans la réponse API: $passagesCountInResponse'); + debugPrint('Nombre de passages dans la Hive Box: $passagesCountInBox'); + + // Si les nombres ne correspondent pas, attendre un peu et revérifier + if (passagesCountInResponse > 0 && + passagesCountInBox < passagesCountInResponse) { + debugPrint( + 'Attente supplémentaire pour finaliser le chargement des passages...'); + await Future.delayed(const Duration(seconds: 1)); + passagesCountInBox = _passageBox.length; + debugPrint( + 'Après attente: Nombre de passages dans la Hive Box: $passagesCountInBox'); + } + + // Étape 12: Chargement terminé (100%) + _updateLoadingState(LoadingState.completed.copyWith( + progress: 1.0, + message: 'Chargement terminé avec succès', + stepDescription: 'Connexion réussie', + )); + + debugPrint('=========================================='); + debugPrint('VÉRIFICATION FINALE DES DONNÉES:'); + debugPrint('Utilisateurs: ${_userBox.length}'); + debugPrint('Opérations: ${_operationBox.length}'); + debugPrint('Secteurs: ${_sectorBox.length}'); + debugPrint('Passages: ${_passageBox.length}'); + debugPrint('Membres: ${_membreBox.length}'); + debugPrint('=========================================='); + + return true; + } catch (e) { + debugPrint('Erreur de connexion: $e'); + _updateLoadingState(LoadingState.error(e.toString())); + return false; + } finally { + _isLoading = false; + notifyListeners(); + } + } + + // Méthode pour vérifier si une boîte est ouverte et l'ouvrir si nécessaire + Future _ensureBoxIsOpen(String boxName) async { + try { + if (!Hive.isBoxOpen(boxName)) { + debugPrint('Ouverture de la boîte $boxName...'); + if (boxName == AppKeys.passagesBoxName) { + await Hive.openBox(boxName); + } else if (boxName == AppKeys.operationsBoxName) { + await Hive.openBox(boxName); + } else if (boxName == AppKeys.sectorsBoxName) { + await Hive.openBox(boxName); + } else if (boxName == AppKeys.usersBoxName) { + await Hive.openBox(boxName); + } else if (boxName == AppKeys.membresBoxName) { + await Hive.openBox(boxName); + } else if (boxName == AppKeys.settingsBoxName) { + await Hive.openBox(boxName); + } else if (boxName == AppKeys.chatConversationsBoxName) { + await Hive.openBox(boxName); + } else if (boxName == AppKeys.chatMessagesBoxName) { + await Hive.openBox(boxName); + } else if (boxName == AppKeys.userSectorBoxName) { + await Hive.openBox(boxName); + } else { + await Hive.openBox(boxName); + } + // Boîte ouverte avec succès + } else { + // La boîte est déjà ouverte + } + } catch (e) { + debugPrint('Erreur lors de l\'ouverture de la boîte $boxName: $e'); + throw Exception('Impossible d\'ouvrir la boîte $boxName: $e'); + } + } + + // Méthode pour vider et recréer toutes les boîtes Hive sauf la boîte des utilisateurs + Future _clearAndRecreateBoxes() async { + try { + debugPrint('Début de la suppression complète des données Hive...'); + + // Supprimer les références aux boîtes non définies dans AppKeys + // pour éviter les erreurs de suppression de boîtes non référencées + final nonDefinedBoxes = ['auth', 'locations', 'messages']; + for (final boxName in nonDefinedBoxes) { + try { + if (Hive.isBoxOpen(boxName)) { + debugPrint('Fermeture de la boîte non référencée: $boxName'); + await Hive.box(boxName).close(); + } + + // Supprimer la boîte du disque + await Hive.deleteBoxFromDisk(boxName); + debugPrint('Nettoyage: Box $boxName supprimée'); + } catch (e) { + debugPrint( + 'Erreur lors de la suppression de la boîte non référencée $boxName: $e'); + } + } + + // Sur le web, utiliser notre méthode sécurisée pour nettoyer les boîtes Hive + if (kIsWeb) { + await HiveWebFix.safeCleanHiveBoxes( + excludeBoxes: [AppKeys.usersBoxName]); + } + // Sur iOS, nettoyer les fichiers Hive directement + else if (Platform.isIOS) { + await _cleanHiveFilesOnIOS(); + } + // Sur Android, nettoyer les fichiers Hive directement + else if (Platform.isAndroid) { + await _cleanHiveFilesOnAndroid(); + } + + // Liste des noms de boîtes à supprimer + final boxesToDelete = [ + AppKeys.passagesBoxName, + AppKeys.operationsBoxName, + AppKeys.sectorsBoxName, + AppKeys.userSectorBoxName, + AppKeys.chatConversationsBoxName, + AppKeys.chatMessagesBoxName, + ]; + + // Vider chaque boîte sans la fermer + for (final boxName in boxesToDelete) { + try { + debugPrint('Nettoyage de la boîte: $boxName'); + + // Vérifier si la boîte est déjà ouverte + if (Hive.isBoxOpen(boxName)) { + // Vider la boîte sans la fermer + debugPrint('Boîte $boxName déjà ouverte, vidage sans fermeture'); + if (boxName == AppKeys.passagesBoxName) { + await _passageBox.clear(); + } else if (boxName == AppKeys.operationsBoxName) { + await _operationBox.clear(); + } else if (boxName == AppKeys.sectorsBoxName) { + await _sectorBox.clear(); + } else if (boxName == AppKeys.userSectorBoxName) { + await _userSectorBox.clear(); + } else if (boxName == AppKeys.chatConversationsBoxName) { + await _chatConversationBox.clear(); + } else if (boxName == AppKeys.chatMessagesBoxName) { + await _chatMessageBox.clear(); + } + } else { + // Supprimer la boîte du disque si elle n'est pas ouverte + debugPrint('Boîte $boxName non ouverte, suppression du disque'); + await Hive.deleteBoxFromDisk(boxName); + } + } catch (e) { + debugPrint('Erreur lors du nettoyage de la boîte $boxName: $e'); + // Tenter de supprimer la boîte du disque en cas d'erreur + try { + await Hive.deleteBoxFromDisk(boxName); + } catch (deleteError) { + debugPrint( + 'Impossible de supprimer la boîte $boxName: $deleteError'); + } + } + } + + // Attendre un court instant pour s'assurer que les opérations de suppression sont terminées + await Future.delayed(const Duration(milliseconds: 500)); + + // Recréer les boîtes avec la méthode sécurisée + debugPrint('Recréation des boîtes Hive...'); + + // Utiliser notre méthode pour s'assurer que les boîtes sont ouvertes + try { + // Passages + await _ensureBoxIsOpen(AppKeys.passagesBoxName); + + // Opérations + await _ensureBoxIsOpen(AppKeys.operationsBoxName); + + // Secteurs + await _ensureBoxIsOpen(AppKeys.sectorsBoxName); + + // Chat + await _ensureBoxIsOpen(AppKeys.chatConversationsBoxName); + await _ensureBoxIsOpen(AppKeys.chatMessagesBoxName); + + // Vérifier l'intégrité des boîtes après recréation + await _verifyHiveBoxesIntegrity(); + } catch (e) { + debugPrint('Erreur lors de la recréation des boîtes Hive: $e'); + // Tentative de récupération sur erreur + if (kIsWeb) { + debugPrint('Tentative de récupération sur le web...'); + await HiveWebFix.resetHiveCompletely(); + + // Réessayer d'ouvrir les boîtes + await _ensureBoxIsOpen(AppKeys.passagesBoxName); + await _ensureBoxIsOpen(AppKeys.operationsBoxName); + await _ensureBoxIsOpen(AppKeys.sectorsBoxName); + await _ensureBoxIsOpen(AppKeys.chatConversationsBoxName); + await _ensureBoxIsOpen(AppKeys.chatMessagesBoxName); + } + } + } catch (e) { + debugPrint('Erreur lors de la réinitialisation des boîtes Hive: $e'); + } + } + + // Méthode pour vérifier l'intégrité des boîtes Hive après recréation + Future _verifyHiveBoxesIntegrity() async { + try { + debugPrint('Vérification de l\'intégrité des boîtes Hive...'); + + // Liste des boîtes à vérifier avec leur type + final boxesToCheck = [ + {'name': AppKeys.passagesBoxName, 'type': 'passage'}, + {'name': AppKeys.operationsBoxName, 'type': 'operation'}, + {'name': AppKeys.sectorsBoxName, 'type': 'sector'}, + {'name': AppKeys.userSectorBoxName, 'type': 'user_sector'}, + {'name': AppKeys.amicaleBoxName, 'type': 'amicale'}, + {'name': AppKeys.chatConversationsBoxName, 'type': 'conversation'}, + {'name': AppKeys.chatMessagesBoxName, 'type': 'message'}, + ]; + + // Vérifier chaque boîte + for (final boxInfo in boxesToCheck) { + final boxName = boxInfo['name'] as String; + final boxType = boxInfo['type'] as String; + + try { + if (Hive.isBoxOpen(boxName)) { + // Utiliser une approche spécifique au type pour éviter les erreurs de typage + Box box; + try { + if (boxType == 'passage') { + box = _passageBox; + } else if (boxType == 'operation') { + box = _operationBox; + } else if (boxType == 'sector') { + box = _sectorBox; + } else if (boxType == 'user_sector') { + box = _userSectorBox; + } else if (boxType == 'conversation') { + box = _chatConversationBox; + } else if (boxType == 'message') { + box = _chatMessageBox; + } else { + box = Hive.box(boxName); + } + + final count = box.length; + debugPrint('Boîte $boxName: $count éléments'); + + // Si la boîte contient des éléments, c'est anormal après recréation + if (count > 0) { + debugPrint( + 'ATTENTION: La boîte $boxName contient encore des données après recréation'); + // Essayer de vider la boîte une dernière fois + await box.clear(); + debugPrint('Vidage forcé de la boîte $boxName effectué'); + } + } catch (typeError) { + debugPrint( + 'Erreur de typage lors de la vérification de $boxName: $typeError'); + + // Tentative alternative sans typage spécifique + try { + box = Hive.box(boxName); + final count = box.length; + debugPrint('Boîte $boxName (sans typage): $count éléments'); + + if (count > 0) { + await box.clear(); + debugPrint( + 'Vidage forcé de la boîte $boxName (sans typage) effectué'); + } + } catch (e2) { + debugPrint( + 'Impossible de vérifier la boîte $boxName même sans typage: $e2'); + } + } + } else { + debugPrint( + 'Boîte $boxName non ouverte, impossible de vérifier l\'intégrité'); + } + } catch (e) { + debugPrint('Erreur lors de la vérification de la boîte $boxName: $e'); + } + } + + debugPrint('Vérification d\'intégrité terminée'); + } catch (e) { + debugPrint( + 'Erreur lors de la vérification d\'intégrité des boîtes Hive: $e'); + } + } + + // Méthode spéciale pour nettoyer IndexedDB sur le web + Future _clearIndexedDB() async { + if (kIsWeb) { + try { + debugPrint('Nettoyage complet d\'IndexedDB sur le web...'); + // Utiliser JavaScript pour nettoyer IndexedDB + js.context.callMethod('eval', [ + ''' + var request = indexedDB.deleteDatabase("geosector_app"); + request.onsuccess = function() { console.log("IndexedDB nettoyé avec succès"); }; + request.onerror = function() { console.log("Erreur lors du nettoyage d\'IndexedDB"); }; + ''' + ]); + await Future.delayed(const Duration(milliseconds: 500)); + debugPrint('Nettoyage d\'IndexedDB terminé'); + } catch (e) { + debugPrint('Erreur lors du nettoyage d\'IndexedDB: $e'); + } + } + } + + // Méthode spéciale pour nettoyer les fichiers Hive sur iOS + Future _cleanHiveFilesOnIOS() async { + if (!kIsWeb && Platform.isIOS) { + try { + debugPrint('Nettoyage des fichiers Hive sur iOS...'); + final appDir = await getApplicationDocumentsDirectory(); + final hiveDir = Directory('${appDir.path}/hive'); + + if (await hiveDir.exists()) { + debugPrint('Suppression du répertoire Hive: ${hiveDir.path}'); + // Exclure le dossier des utilisateurs pour conserver les informations de session + final entries = await hiveDir.list().toList(); + for (var entry in entries) { + final name = entry.path.split('/').last; + // Ne pas supprimer la boîte des utilisateurs + if (!name.contains(AppKeys.usersBoxName)) { + debugPrint('Suppression de: ${entry.path}'); + if (entry is Directory) { + await entry.delete(recursive: true); + } else if (entry is File) { + await entry.delete(); + } + } + } + debugPrint('Nettoyage des fichiers Hive sur iOS terminé'); + } else { + debugPrint('Répertoire Hive non trouvé'); + } + } catch (e) { + debugPrint('Erreur lors du nettoyage des fichiers Hive sur iOS: $e'); + } + } + } + + // Méthode spéciale pour nettoyer les fichiers Hive sur Android + Future _cleanHiveFilesOnAndroid() async { + if (!kIsWeb && Platform.isAndroid) { + try { + debugPrint('Nettoyage des fichiers Hive sur Android...'); + final appDir = await getApplicationDocumentsDirectory(); + final hiveDir = Directory('${appDir.path}'); + + if (await hiveDir.exists()) { + debugPrint('Recherche des fichiers Hive dans: ${hiveDir.path}'); + // Sur Android, les fichiers Hive sont directement dans le répertoire de l'application + final entries = await hiveDir.list().toList(); + int filesDeleted = 0; + + for (var entry in entries) { + final name = entry.path.split('/').last; + // Ne supprimer que les fichiers Hive, mais pas la boîte des utilisateurs + if (name.endsWith('.hive') && + !name.contains(AppKeys.usersBoxName)) { + debugPrint('Suppression du fichier Hive: ${entry.path}'); + if (entry is File) { + await entry.delete(); + filesDeleted++; + + // Supprimer également les fichiers lock associés + final lockFile = File('${entry.path}.lock'); + if (await lockFile.exists()) { + await lockFile.delete(); + debugPrint('Suppression du fichier lock: ${lockFile.path}'); + } + } + } + } + + debugPrint( + 'Nettoyage des fichiers Hive sur Android terminé. $filesDeleted fichiers supprimés.'); + } else { + debugPrint('Répertoire d\'application non trouvé'); + } + } catch (e) { + debugPrint( + 'Erreur lors du nettoyage des fichiers Hive sur Android: $e'); + } + } + } + + /// Méthode de connexion avec affichage d'un overlay de chargement avec progression + /// Cette méthode remplace AuthService.login et utilise le nouvel overlay avec barre de progression + Future loginWithUI( + BuildContext context, String username, String password, + {required String type}) async { + try { + // Réinitialiser l'état de chargement + _updateLoadingState(LoadingState.initial.copyWith( + message: 'Connexion en cours...', + stepDescription: 'Préparation', + )); + + // Créer et afficher l'overlay de progression + _progressOverlay = LoadingProgressOverlayUtils.show( + context: context, + message: 'Connexion en cours...', + progress: 0.0, + stepDescription: 'Préparation', + blurAmount: 5.0, + ); + + // Écouter les changements d'état pour mettre à jour l'overlay + final listener = () { + if (_progressOverlay != null) { + // Mettre à jour l'overlay avec les nouvelles valeurs + LoadingProgressOverlayUtils.update( + overlayEntry: _progressOverlay!, + message: _loadingState.message, + progress: _loadingState.progress, + stepDescription: _loadingState.stepDescription, + ); + } + }; + + // Ajouter l'écouteur + addListener(listener); + + // Exécuter la connexion + final result = await login(username, password, type: type); + + // Attendre un court instant pour que l'utilisateur voie que le chargement est terminé + if (result) { + await Future.delayed(const Duration(milliseconds: 500)); + } else { + await Future.delayed(const Duration(seconds: 2)); + } + + // Supprimer l'overlay + if (_progressOverlay != null) { + _progressOverlay!.remove(); + _progressOverlay = null; + } + + // Supprimer l'écouteur + removeListener(listener); + + return result; + } catch (e) { + // En cas d'erreur, supprimer l'overlay et relancer l'erreur + if (_progressOverlay != null) { + _progressOverlay!.remove(); + _progressOverlay = null; + } + + _updateLoadingState(LoadingState.error(e.toString())); + return false; + } + } + + /// Méthode de déconnexion avec affichage d'un overlay de chargement + /// et redirection vers la page de démarrage + /// Cette méthode remplace AuthService.logout + Future logoutWithUI(BuildContext context) async { + final bool result = await LoadingOverlay.show( + context: context, + spinnerSize: 80.0, // Spinner plus grand + strokeWidth: 6.0, // Trait plus épais + future: logout(), + ); + + // Si la déconnexion a réussi, rediriger vers la page de démarrage + if (result && context.mounted) { + // Utiliser GoRouter pour naviguer vers la page de démarrage + GoRouter.of(context).go('/'); + } + + return result; + } + + // Logout complet (sans UI) + Future logout() async { + _isLoading = true; + notifyListeners(); + + try { + debugPrint('Début du processus de déconnexion...'); + debugPrint('État isLoggedIn avant déconnexion: $isLoggedIn'); + + // Récupérer l'utilisateur actuel avant de nettoyer les données + final currentUser = getCurrentUser(); + if (currentUser == null) { + debugPrint('Aucun utilisateur connecté, déconnexion terminée'); + // Nettoyage en profondeur même si aucun utilisateur n'est connecté + await _deepCleanHiveBoxes(); + debugPrint('État isLoggedIn après nettoyage: $isLoggedIn'); + return true; + } + + debugPrint('Déconnexion de l\'utilisateur: ${currentUser.email}'); + + // Appeler l'API pour déconnecter la session + if (currentUser.sessionId != null) { + debugPrint('Déconnexion de la session API...'); + try { + await logoutAPI(); + } catch (e) { + debugPrint('Erreur lors de la déconnexion API, mais on continue: $e'); + // Continuer le processus de déconnexion même si l'API échoue + } + } + + // Effacer la session de l'utilisateur + debugPrint('Mise à jour de l\'utilisateur pour effacer la session...'); + + // Supprimer la session API + setSessionId(null); + + // Réinitialiser le cache de l'utilisateur actuel + _cachedCurrentUser = null; + debugPrint('Cache utilisateur réinitialisé (_cachedCurrentUser = null)'); + + // MODIFICATION IMPORTANTE: Nettoyage complet de toutes les boîtes Hive + debugPrint('Nettoyage profond des données Hive après déconnexion...'); + await _deepCleanHiveBoxes(); + + // Vérifier l'état après nettoyage + debugPrint('État isLoggedIn après déconnexion: $isLoggedIn'); + debugPrint( + 'Valeur de currentUser après déconnexion: ${currentUser != null ? "non null" : "null"}'); + + // Vérifier si des utilisateurs restent dans la boîte + if (Hive.isBoxOpen(AppKeys.usersBoxName)) { + final remainingUsers = _userBox.values.toList(); + debugPrint( + 'Nombre d\'utilisateurs restants dans la boîte: ${remainingUsers.length}'); + } + + // Réinitialiser l'état de HiveResetStateService + hiveResetStateService.reset(); + debugPrint('État de HiveResetStateService réinitialisé'); + + debugPrint('Déconnexion terminée avec succès'); + + notifyListeners(); + return true; + } catch (e) { + debugPrint('Erreur de déconnexion: $e'); + return false; + } finally { + _isLoading = false; + notifyListeners(); + // Vérification finale + debugPrint('État final isLoggedIn: $isLoggedIn'); + } + } + + // Méthode pour nettoyer en profondeur toutes les boîtes Hive et réinitialiser l'application + Future _deepCleanHiveBoxes() async { + try { + debugPrint('Début du nettoyage profond des boîtes Hive...'); + + // Sauvegarder le username et le rôle du dernier utilisateur connecté pour le pré-remplissage + String? lastUsername; + int? lastRole; + UserModel? lastUser; + if (Hive.isBoxOpen(AppKeys.usersBoxName) && _userBox.isNotEmpty) { + try { + // Récupérer l'utilisateur actuel ou le dernier utilisateur connecté + lastUser = getCurrentUser() ?? _userBox.values.first; + if (lastUser != null) { + lastUsername = lastUser.username; + + // Convertir le rôle en int si nécessaire + if (lastUser.role is String) { + lastRole = int.tryParse(lastUser.role as String) ?? 0; + } else { + lastRole = lastUser.role as int; + } + + debugPrint( + 'Username sauvegardé pour pré-remplissage: $lastUsername'); + debugPrint('Rôle sauvegardé pour pré-remplissage: $lastRole'); + } + } catch (e) { + debugPrint('Erreur lors de la sauvegarde du username et du rôle: $e'); + } + } + + // 1. Vider toutes les boîtes sans les fermer + debugPrint('Vidage des boîtes Hive...'); + if (Hive.isBoxOpen(AppKeys.usersBoxName)) { + try { + await _userBox.clear(); + debugPrint('Boîte users vidée'); + + // Si nous avons un username sauvegardé, créer un utilisateur minimal pour le pré-remplissage + if (lastUsername != null && lastUsername.isNotEmpty) { + final minimalUser = UserModel( + id: lastUser?.id ?? DateTime.now().millisecondsSinceEpoch, + email: lastUser?.email ?? '', + username: lastUsername, + role: lastRole ?? + 0, // Conserver le rôle pour la vérification dans la page de login + createdAt: DateTime.now(), + lastSyncedAt: DateTime.now(), + isActive: false, + isSynced: false, + ); + await _userBox.put(minimalUser.id, minimalUser); + debugPrint( + 'Utilisateur minimal créé pour pré-remplissage du username avec rôle: $lastRole'); + } + } catch (e) { + debugPrint('Erreur lors du vidage de la boîte users: $e'); + } + } + + if (Hive.isBoxOpen(AppKeys.operationsBoxName)) { + try { + await _operationBox.clear(); + debugPrint('Boîte operations vidée'); + } catch (e) { + debugPrint('Erreur lors du vidage de la boîte operations: $e'); + } + } + + if (Hive.isBoxOpen(AppKeys.sectorsBoxName)) { + try { + await _sectorBox.clear(); + debugPrint('Boîte sectors vidée'); + } catch (e) { + debugPrint('Erreur lors du vidage de la boîte sectors: $e'); + } + } + + if (Hive.isBoxOpen(AppKeys.passagesBoxName)) { + try { + await _passageBox.clear(); + debugPrint('Boîte passages vidée'); + } catch (e) { + debugPrint('Erreur lors du vidage de la boîte passages: $e'); + } + } + + if (Hive.isBoxOpen(AppKeys.settingsBoxName)) { + try { + await _settingsBox.clear(); + debugPrint('Boîte settings vidée'); + } catch (e) { + debugPrint('Erreur lors du vidage de la boîte settings: $e'); + } + } + + if (Hive.isBoxOpen(AppKeys.membresBoxName)) { + try { + await _membreBox.clear(); + debugPrint('Boîte membres vidée'); + } catch (e) { + debugPrint('Erreur lors du vidage de la boîte membres: $e'); + } + } + + if (Hive.isBoxOpen(AppKeys.chatConversationsBoxName)) { + try { + await _chatConversationBox.clear(); + debugPrint('Boîte chat_conversations vidée'); + } catch (e) { + debugPrint( + 'Erreur lors du vidage de la boîte chat_conversations: $e'); + } + } + + if (Hive.isBoxOpen(AppKeys.chatMessagesBoxName)) { + try { + await _chatMessageBox.clear(); + debugPrint('Boîte chat_messages vidée'); + } catch (e) { + debugPrint('Erreur lors du vidage de la boîte chat_messages: $e'); + } + } + + if (Hive.isBoxOpen(AppKeys.userSectorBoxName)) { + try { + await _userSectorBox.clear(); + debugPrint('Boîte user_sector vidée'); + } catch (e) { + debugPrint('Erreur lors du vidage de la boîte user_sector: $e'); + } + } + + if (Hive.isBoxOpen(AppKeys.amicaleBoxName)) { + try { + await _amicaleBox.clear(); + debugPrint('Boîte amicale vidée'); + } catch (e) { + debugPrint('Erreur lors du vidage de la boîte amicales: $e'); + } + } + + // 2. Nettoyage spécifique à la plateforme + if (kIsWeb) { + await _clearIndexedDB(); + } else if (!kIsWeb && Platform.isIOS) { + await _cleanHiveFilesOnIOS(); + } else if (!kIsWeb && Platform.isAndroid) { + await _cleanHiveFilesOnAndroid(); + } + + // 3. Attendre que toutes les opérations de nettoyage soient terminées + debugPrint('Attente après nettoyage...'); + await Future.delayed(const Duration(milliseconds: 800)); + + // 4. Réinitialiser l'API Service + _apiService.setSessionId(null); + + // 5. Approche plus sûre : fermer, supprimer et rouvrir la boîte des utilisateurs + debugPrint('Approche sécurisée pour la boîte users...'); + + try { + // Vérifier si la boîte est ouverte avant de tenter de la fermer + if (Hive.isBoxOpen(AppKeys.usersBoxName)) { + debugPrint('Fermeture de la boîte users...'); + try { + await Hive.box(AppKeys.usersBoxName).close(); + debugPrint('Boîte users fermée avec succès'); + } catch (e) { + debugPrint('Erreur lors de la fermeture de la boîte users: $e'); + // Ne pas continuer avec la suppression si la fermeture a échoué + throw e; + } + + // Attendre un peu pour s'assurer que la fermeture est terminée + await Future.delayed(const Duration(milliseconds: 300)); + + // Supprimer la boîte du disque seulement si la fermeture a réussi + debugPrint('Suppression de la boîte users du disque...'); + try { + await Hive.deleteBoxFromDisk(AppKeys.usersBoxName); + debugPrint('Boîte users supprimée du disque avec succès'); + } catch (e) { + debugPrint('Erreur lors de la suppression de la boîte users: $e'); + // Ne pas continuer avec la réouverture si la suppression a échoué + throw e; + } + } else { + debugPrint( + 'La boîte users est déjà fermée, tentative de suppression directe...'); + try { + await Hive.deleteBoxFromDisk(AppKeys.usersBoxName); + debugPrint('Boîte users supprimée du disque avec succès'); + } catch (e) { + debugPrint( + 'Erreur lors de la suppression directe de la boîte users: $e'); + } + } + } catch (e) { + debugPrint( + 'Erreur lors du processus de nettoyage de la boîte users: $e'); + // Continuer malgré l'erreur, mais ne pas tenter de réouvrir la boîte + return; + } + + // Attendre un peu pour s'assurer que la suppression est terminée + await Future.delayed(const Duration(milliseconds: 500)); + + // Rouvrir la boîte (elle sera vide) + debugPrint('Réouverture de la boîte users (vide)...'); + await Hive.openBox(AppKeys.usersBoxName); + + // Vérifier que la boîte est bien vide + final checkUsers = _userBox.values.toList(); + debugPrint( + 'Après approche radicale: ${checkUsers.length} utilisateurs restants'); + + // Forcer la réinitialisation du cache + _cachedCurrentUser = null; + debugPrint('Cache utilisateur forcé à null'); + + debugPrint('Nettoyage profond des boîtes Hive terminé'); + } catch (e) { + debugPrint('Erreur lors du nettoyage profond des boîtes Hive: $e'); + } + } + + // Obtenir tous les utilisateurs locaux + List getAllUsers() { + return _userBox.values.toList(); + } + + // Obtenir un utilisateur par son ID + UserModel? getUserById(int id) { + return _userBox.get(id); + } + + // Obtenir un utilisateur par son email + UserModel? getUserByEmail(String email) { + try { + return _userBox.values.firstWhere( + (user) => user.email == email, + ); + } catch (e) { + return null; // Utilisateur non trouvé + } + } + + // Créer ou mettre à jour un utilisateur localement + Future saveUser(UserModel user) async { + await _userBox.put(user.id, user); + notifyListeners(); // Notifier les changements pour mettre à jour l'UI + return user; + } + + // Supprimer un utilisateur localement + Future deleteUser(String id) async { + await _userBox.delete(id); + } + + // Créer un nouvel utilisateur localement et tenter de le synchroniser + Future createUser({ + required String email, + required String name, + required int role, + }) async { + // Générer un ID numérique temporaire (timestamp) + final int tempId = DateTime.now().millisecondsSinceEpoch; + final now = DateTime.now(); + + final user = UserModel( + id: tempId, + email: email, + name: name, + role: role, + createdAt: now, + lastSyncedAt: now, + isSynced: false, + ); + + await _userBox.put(user.id, user); + + // Tenter de synchroniser si possible + await syncUser(user); + + return user; + } + + // Synchroniser un utilisateur spécifique avec le serveur + Future syncUser(UserModel user) async { + try { + final hasConnection = await _apiService.hasInternetConnection(); + + if (!hasConnection) { + return user; + } + + UserModel syncedUser; + + if (!user.isSynced) { + // Si l'utilisateur n'est pas encore synchronisé, le créer sur le serveur + syncedUser = await _apiService.createUser(user); + } else { + // Sinon, mettre à jour les informations + syncedUser = await _apiService.updateUser(user); + } + + // Mettre à jour l'utilisateur local avec les informations du serveur + final updatedUser = syncedUser.copyWith( + isSynced: true, + lastSyncedAt: DateTime.now(), + ); + + await _userBox.put(updatedUser.id, updatedUser); + return updatedUser; + } catch (e) { + // En cas d'erreur, garder l'utilisateur local tel quel + return user; + } + } + + // Synchroniser tous les utilisateurs non synchronisés + Future syncAllUsers() async { + try { + final hasConnection = await _apiService.hasInternetConnection(); + + if (!hasConnection) { + return; + } + + final unsyncedUsers = + _userBox.values.where((user) => !user.isSynced).toList(); + + if (unsyncedUsers.isEmpty) { + return; + } + + // Synchroniser en batch + final result = await _apiService.syncData(users: unsyncedUsers); + + // Mettre à jour les utilisateurs locaux + if (result['users'] != null) { + for (final userData in result['users']) { + final syncedUser = UserModel.fromJson(userData); + await _userBox.put( + syncedUser.id, + syncedUser.copyWith( + isSynced: true, + lastSyncedAt: DateTime.now(), + ), + ); + } + } + } catch (e) { + // Gérer les erreurs de synchronisation + print('Erreur de synchronisation des utilisateurs: $e'); + } + } + + // Rafraîchir les données depuis le serveur + Future refreshFromServer() async { + try { + final hasConnection = await _apiService.hasInternetConnection(); + + if (!hasConnection) { + return; + } + + // Récupérer tous les utilisateurs du serveur + final serverUsers = await _apiService.getUsers(); + + // Mettre à jour la base locale + for (final serverUser in serverUsers) { + final updatedUser = serverUser.copyWith( + isSynced: true, + lastSyncedAt: DateTime.now(), + ); + await _userBox.put(updatedUser.id, updatedUser); + } + } catch (e) { + // Gérer les erreurs + print('Erreur lors du rafraîchissement des données: $e'); + } + } + + // Synchroniser les données utilisateur + Future syncUserData() async { + if (_syncService != null && currentUser != null) { + await _syncService!.syncUserData(currentUser!.id); + } + } + + // Récupérer la dernière opération active (avec isActive == true) + OperationModel? getCurrentOperation() { + try { + // Récupérer toutes les opérations + final operations = _operationBox.values.toList(); + + // Filtrer pour ne garder que les opérations actives + final activeOperations = operations.where((op) => op.isActive).toList(); + + // Si aucune opération active n'est trouvée, retourner null + if (activeOperations.isEmpty) { + return operations.isNotEmpty ? operations.last : null; + } + + // Retourner la dernière opération active + return activeOperations.last; + } catch (e) { + debugPrint('Erreur lors de la récupération de l\'opération actuelle: $e'); + return null; + } + } + + // Récupérer tous les secteurs de l'utilisateur + List getUserSectors() { + try { + return _sectorBox.values.toList(); + } catch (e) { + debugPrint('Erreur lors de la récupération des secteurs: $e'); + return []; + } + } + + // Récupérer un secteur par son ID + SectorModel? getSectorById(int id) { + try { + return _sectorBox.get(id); + } catch (e) { + debugPrint('Erreur lors de la récupération du secteur: $e'); + return null; + } + } + + // Récupérer toutes les amicales + List getAllAmicales() { + try { + _ensureBoxIsOpen(AppKeys.amicaleBoxName); + return _amicaleBox.values.toList(); + } catch (e) { + debugPrint('Erreur lors de la récupération des amicales: $e'); + return []; + } + } + + // Récupérer tous les clients (entités de type 1) + List getAllClients() { + try { + _ensureBoxIsOpen(AppKeys.amicaleBoxName); + return _amicaleBox.values + .where((amicale) => amicale.fkType == 1) + .toList(); + } catch (e) { + debugPrint('Erreur lors de la récupération des clients: $e'); + return []; + } + } + + // Récupérer une amicale par son ID + AmicaleModel? getAmicaleById(int id) { + try { + _ensureBoxIsOpen(AppKeys.amicaleBoxName); + return _amicaleBox.get(id); + } catch (e) { + debugPrint('Erreur lors de la récupération de l\'amicale: $e'); + return null; + } + } + + // Récupérer l'amicale de l'utilisateur connecté + AmicaleModel? getCurrentUserAmicale() { + final user = getCurrentUser(); + if (user == null || user.fkEntite == null) { + return null; + } + + return getAmicaleById(user.fkEntite!); + } + + // Créer ou mettre à jour une amicale localement + Future saveAmicale(AmicaleModel amicale) async { + _ensureBoxIsOpen(AppKeys.amicaleBoxName); + await _amicaleBox.put(amicale.id, amicale); + notifyListeners(); // Notifier les changements pour mettre à jour l'UI + return amicale; + } + + // Méthode pour traiter les données des opérations reçues de l'API + Future _processOperations(dynamic operationsData) async { + try { + debugPrint('Traitement des données des opérations...'); + + // Vérifier que les données sont au bon format + if (operationsData == null) { + debugPrint('Aucune donnée d\'opération à traiter'); + return; + } + + List operationsList; + if (operationsData is List) { + operationsList = operationsData; + } else if (operationsData is Map && operationsData.containsKey('data')) { + operationsList = operationsData['data'] as List; + } else { + debugPrint('Format de données d\'opérations non reconnu'); + return; + } + + // Vider la boîte avant d'ajouter les nouvelles données + await _operationBox.clear(); + + // Traiter chaque opération + int count = 0; + for (final operationData in operationsList) { + try { + final operation = OperationModel.fromJson(operationData); + await _operationBox.put(operation.id, operation); + count++; + } catch (e) { + debugPrint('Erreur lors du traitement d\'une opération: $e'); + } + } + + debugPrint('$count opérations traitées et stockées'); + } catch (e) { + debugPrint('Erreur lors du traitement des opérations: $e'); + } + } + + // Méthode pour traiter les données des secteurs reçues de l'API + Future _processSectors(dynamic sectorsData) async { + try { + debugPrint('Traitement des données des secteurs...'); + + // Vérifier que les données sont au bon format + if (sectorsData == null) { + debugPrint('Aucune donnée de secteur à traiter'); + return; + } + + List sectorsList; + if (sectorsData is List) { + sectorsList = sectorsData; + } else if (sectorsData is Map && sectorsData.containsKey('data')) { + sectorsList = sectorsData['data'] as List; + } else { + debugPrint('Format de données de secteurs non reconnu'); + return; + } + + // Vider la boîte avant d'ajouter les nouvelles données + await _sectorBox.clear(); + + // Traiter chaque secteur + int count = 0; + for (final sectorData in sectorsList) { + try { + final sector = SectorModel.fromJson(sectorData); + await _sectorBox.put(sector.id, sector); + count++; + } catch (e) { + debugPrint('Erreur lors du traitement d\'un secteur: $e'); + } + } + + debugPrint('$count secteurs traités et stockés'); + } catch (e) { + debugPrint('Erreur lors du traitement des secteurs: $e'); + } + } + + // Méthode pour traiter les données des passages reçues de l'API + Future _processPassages(dynamic passagesData) async { + try { + debugPrint('Traitement des données des passages...'); + + // Vérifier que les données sont au bon format + if (passagesData == null) { + debugPrint('Aucune donnée de passage à traiter'); + return; + } + + List passagesList; + if (passagesData is List) { + passagesList = passagesData; + } else if (passagesData is Map && passagesData.containsKey('data')) { + passagesList = passagesData['data'] as List; + } else { + debugPrint('Format de données de passages non reconnu'); + return; + } + + // Vider la boîte avant d'ajouter les nouvelles données + await _passageBox.clear(); + + // Traiter chaque passage + int count = 0; + for (final passageData in passagesList) { + try { + final passage = PassageModel.fromJson(passageData); + await _passageBox.put(passage.id, passage); + count++; + } catch (e) { + debugPrint('Erreur lors du traitement d\'un passage: $e'); + } + } + + debugPrint('$count passages traités et stockés'); + } catch (e) { + debugPrint('Erreur lors du traitement des passages: $e'); + } + } + + // Méthode pour traiter les données des membres reçues de l'API + Future _processMembres(dynamic membresData) async { + try { + debugPrint('Traitement des données des membres...'); + + // Vérifier que les données sont au bon format + if (membresData == null) { + debugPrint('Aucune donnée de membre à traiter'); + return; + } + + List membresList; + if (membresData is List) { + membresList = membresData; + } else if (membresData is Map && membresData.containsKey('data')) { + membresList = membresData['data'] as List; + } else { + debugPrint('Format de données de membres non reconnu'); + return; + } + + // Vider la boîte avant d'ajouter les nouvelles données + await _membreBox.clear(); + + // Traiter chaque membre + int count = 0; + for (final membreData in membresList) { + try { + final membre = MembreModel.fromJson(membreData); + await _membreBox.put(membre.id, membre); + count++; + } catch (e) { + debugPrint('Erreur lors du traitement d\'un membre: $e'); + } + } + + debugPrint('$count membres traités et stockés'); + } catch (e) { + debugPrint('Erreur lors du traitement des membres: $e'); + } + } + + // Méthode pour traiter les données des associations utilisateurs-secteurs reçues de l'API + Future _processUserSectors(dynamic userSectorsData) async { + try { + debugPrint( + 'Traitement des données des associations utilisateurs-secteurs...'); + + // Vérifier que les données sont au bon format + if (userSectorsData == null) { + debugPrint( + 'Aucune donnée d\'association utilisateur-secteur à traiter'); + return; + } + + List userSectorsList; + if (userSectorsData is List) { + userSectorsList = userSectorsData; + } else if (userSectorsData is Map && + userSectorsData.containsKey('data')) { + userSectorsList = userSectorsData['data'] as List; + } else { + debugPrint( + 'Format de données d\'associations utilisateurs-secteurs non reconnu'); + return; + } + + // Vider la boîte avant d'ajouter les nouvelles données + await _userSectorBox.clear(); + + // Traiter chaque association utilisateur-secteur + int count = 0; + for (final userSectorData in userSectorsList) { + try { + final userSector = UserSectorModel.fromJson(userSectorData); + await _userSectorBox.put( + '${userSector.id}_${userSector.fkSector}', userSector); + count++; + } catch (e) { + debugPrint( + 'Erreur lors du traitement d\'une association utilisateur-secteur: $e'); + } + } + + debugPrint( + '$count associations utilisateurs-secteurs traitées et stockées'); + } catch (e) { + debugPrint( + 'Erreur lors du traitement des associations utilisateurs-secteurs: $e'); + } + } + + // Méthode pour traiter les données utilisateur reçues de l'API + UserModel _processUserData( + Map userData, String? sessionId, String? sessionExpiry) { + debugPrint('Traitement des données utilisateur: ${userData.toString()}'); + + // Convertir l'ID en int, qu'il soit déjà int ou string + final dynamic rawId = userData['id']; + final int id = rawId is String ? int.parse(rawId) : rawId as int; + + // Convertir le rôle en int, qu'il soit déjà int ou string + final dynamic rawRole = userData['fk_role']; + int role; + if (rawRole is String) { + role = int.tryParse(rawRole) ?? 1; + } else if (rawRole is int) { + role = rawRole; + } else { + // Valeur par défaut si le rôle n'est pas valide + role = 1; + } + + // Convertir fk_entite en int si présent + final dynamic rawFkEntite = userData['fk_entite']; + final int? fkEntite = rawFkEntite != null + ? (rawFkEntite is String ? int.parse(rawFkEntite) : rawFkEntite as int) + : null; + + // Convertir fk_titre en int si présent + final dynamic rawFkTitre = userData['fk_titre']; + final int? fkTitre = rawFkTitre != null + ? (rawFkTitre is String ? int.parse(rawFkTitre) : rawFkTitre as int) + : null; + + // Traiter les dates si présentes + DateTime? dateNaissance; + if (userData['date_naissance'] != null && + userData['date_naissance'] != '') { + try { + dateNaissance = DateTime.parse(userData['date_naissance']); + } catch (e) { + dateNaissance = null; + } + } + + DateTime? dateEmbauche; + if (userData['date_embauche'] != null && userData['date_embauche'] != '') { + try { + dateEmbauche = DateTime.parse(userData['date_embauche']); + } catch (e) { + dateEmbauche = null; + } + } + + debugPrint('Données traitées - id: $id, role: $role, fkEntite: $fkEntite'); + + // Créer un utilisateur avec toutes les données disponibles + return UserModel( + id: id, + email: userData['email'] ?? '', + name: userData['name'], + username: userData['username'], + firstName: userData['first_name'], + role: role, + createdAt: DateTime.now(), // Date actuelle comme fallback + lastSyncedAt: DateTime.now(), + isActive: true, + isSynced: true, + sessionId: sessionId, + sessionExpiry: + sessionExpiry != null ? DateTime.parse(sessionExpiry) : null, + sectName: userData['sect_name'], + fkEntite: fkEntite, + fkTitre: fkTitre, + phone: userData['phone'], + mobile: userData['mobile'], + dateNaissance: dateNaissance, + dateEmbauche: dateEmbauche, + ); + } +} diff --git a/flutt/lib/core/services/api_service.dart b/app/lib/core/services/api_service.dart similarity index 72% rename from flutt/lib/core/services/api_service.dart rename to app/lib/core/services/api_service.dart index 03215f44..779f6886 100644 --- a/flutt/lib/core/services/api_service.dart +++ b/app/lib/core/services/api_service.dart @@ -6,17 +6,67 @@ import 'package:flutter/foundation.dart'; import 'package:geosector_app/core/data/models/user_model.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:retry/retry.dart'; +import 'package:universal_html/html.dart' as html; class ApiService { final Dio _dio = Dio(); - final String _baseUrl = AppKeys.baseApiUrl; + late final String _baseUrl; + late final String _appIdentifier; String? _sessionId; + + // Détermine l'environnement actuel (DEV, REC, PROD) en fonction de l'URL + String _determineEnvironment() { + if (!kIsWeb) { + // En mode non-web, utiliser l'environnement de développement par défaut + return 'DEV'; + } + + final currentUrl = html.window.location.href.toLowerCase(); + + if (currentUrl.contains('dapp.geosector.fr')) { + return 'DEV'; + } else if (currentUrl.contains('rapp.geosector.fr')) { + return 'REC'; + } else { + return 'PROD'; + } + } + + // Configure l'URL de base API et l'identifiant d'application selon l'environnement + void _configureEnvironment() { + final env = _determineEnvironment(); + + switch (env) { + case 'DEV': + _baseUrl = AppKeys.baseApiUrlDev; + _appIdentifier = AppKeys.appIdentifierDev; + break; + case 'REC': + _baseUrl = AppKeys.baseApiUrlRec; + _appIdentifier = AppKeys.appIdentifierRec; + break; + default: // PROD + _baseUrl = AppKeys.baseApiUrlProd; + _appIdentifier = AppKeys.appIdentifierProd; + } + + debugPrint('GEOSECTOR 🔗 Environnement: $env, API: $_baseUrl'); + } ApiService() { + // Configurer l'environnement + _configureEnvironment(); + + // Configurer Dio _dio.options.baseUrl = _baseUrl; _dio.options.connectTimeout = AppKeys.connectionTimeout; _dio.options.receiveTimeout = AppKeys.receiveTimeout; - _dio.options.headers.addAll(AppKeys.defaultHeaders); + + // Ajouter les en-têtes par défaut avec l'identifiant d'application adapté à l'environnement + final headers = Map.from(AppKeys.defaultHeaders); + headers['X-App-Identifier'] = _appIdentifier; + + _dio.options.headers.addAll(headers); // Ajouter des intercepteurs pour l'authentification par session _dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) { @@ -39,6 +89,21 @@ class ApiService { void setSessionId(String? sessionId) { _sessionId = sessionId; } + + // Obtenir l'environnement actuel (utile pour le débogage) + String getCurrentEnvironment() { + return _determineEnvironment(); + } + + // Obtenir l'URL API actuelle (utile pour le débogage) + String getCurrentApiUrl() { + return _baseUrl; + } + + // Obtenir l'identifiant d'application actuel (utile pour le débogage) + String getCurrentAppIdentifier() { + return _appIdentifier; + } // Vérifier la connectivité réseau Future hasInternetConnection() async { @@ -83,7 +148,7 @@ class ApiService { } // Authentification avec PHP session - Future> login(String username, String password, {String type = 'admin'}) async { + Future> login(String username, String password, {required String type}) async { try { final response = await _dio.post(AppKeys.loginEndpoint, data: { 'username': username, diff --git a/flutt/lib/core/services/auth_service.dart b/app/lib/core/services/auth_service.dart similarity index 68% rename from flutt/lib/core/services/auth_service.dart rename to app/lib/core/services/auth_service.dart index a536ae2f..0f96465a 100644 --- a/flutt/lib/core/services/auth_service.dart +++ b/app/lib/core/services/auth_service.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:geosector_app/core/repositories/user_repository.dart'; import 'package:geosector_app/presentation/widgets/loading_overlay.dart'; @@ -10,7 +11,7 @@ class AuthService { /// Méthode de connexion avec affichage d'un overlay de chargement Future login(BuildContext context, String username, String password, - {String type = 'admin'}) async { + {required String type}) async { return await LoadingOverlay.show( context: context, spinnerSize: 80.0, // Spinner plus grand @@ -20,13 +21,22 @@ class AuthService { } /// Méthode de déconnexion avec affichage d'un overlay de chargement + /// et redirection vers la page de démarrage Future logout(BuildContext context) async { - return await LoadingOverlay.show( + final bool result = await LoadingOverlay.show( context: context, spinnerSize: 80.0, // Spinner plus grand strokeWidth: 6.0, // Trait plus épais future: _userRepository.logout(), ); + + // Si la déconnexion a réussi, rediriger vers la page de démarrage + if (result && context.mounted) { + // Utiliser GoRouter pour naviguer vers la page de démarrage + GoRouter.of(context).go('/'); + } + + return result; } /// Vérifie si un utilisateur est connecté @@ -34,8 +44,8 @@ class AuthService { return _userRepository.isLoggedIn; } - /// Vérifie si l'utilisateur connecté est un administrateur - bool isAdmin() { - return _userRepository.isAdmin(); + /// Récupère le rôle de l'utilisateur connecté + int getUserRole() { + return _userRepository.getUserRole(); } } diff --git a/flutt/lib/core/services/connectivity_service.dart b/app/lib/core/services/connectivity_service.dart similarity index 100% rename from flutt/lib/core/services/connectivity_service.dart rename to app/lib/core/services/connectivity_service.dart diff --git a/app/lib/core/services/hive_reset_service.dart b/app/lib/core/services/hive_reset_service.dart new file mode 100644 index 00000000..1d4559dd --- /dev/null +++ b/app/lib/core/services/hive_reset_service.dart @@ -0,0 +1,149 @@ +import 'package:flutter/foundation.dart'; +import 'package:hive_flutter/hive_flutter.dart'; + +// Importations conditionnelles pour le web vs non-web +import 'js_interface.dart'; +import 'package:geosector_app/core/constants/app_keys.dart'; +import 'package:geosector_app/core/services/hive_web_fix.dart'; +import 'package:geosector_app/core/data/models/user_model.dart'; +import 'package:geosector_app/core/data/models/amicale_model.dart'; +import 'package:geosector_app/core/data/models/client_model.dart'; +import 'package:geosector_app/core/data/models/operation_model.dart'; +import 'package:geosector_app/core/data/models/sector_model.dart'; +import 'package:geosector_app/core/data/models/passage_model.dart'; +import 'package:geosector_app/core/data/models/membre_model.dart'; +import 'package:geosector_app/core/data/models/user_sector_model.dart'; +import 'package:geosector_app/core/data/models/region_model.dart'; +import 'package:geosector_app/chat/models/chat_adapters.dart'; + +/// Service pour réinitialiser et recréer les Hive Boxes +/// Utilisé pour résoudre les problèmes d'incompatibilité après mise à jour des modèles +class HiveResetService { + /// Réinitialise complètement Hive et recrée les boîtes nécessaires + static Future resetAndRecreateHiveBoxes() async { + try { + debugPrint( + 'HiveResetService: Début de la réinitialisation complète de Hive'); + + // Approche plus radicale pour le web : supprimer directement IndexedDB + if (kIsWeb) { + // Utiliser JavaScript pour supprimer complètement la base de données IndexedDB + evalJs(''' + (function() { + return new Promise(function(resolve, reject) { + // Fermer toutes les connexions IndexedDB + if (window.indexedDB) { + console.log("Suppression complète d'IndexedDB..."); + var request = indexedDB.deleteDatabase("geosector_app"); + request.onsuccess = function() { + console.log("IndexedDB supprimé avec succès"); + resolve(true); + }; + request.onerror = function(event) { + console.log("Erreur lors de la suppression d'IndexedDB", event); + reject(event); + }; + } else { + console.log("IndexedDB n'est pas disponible"); + resolve(false); + } + }); + })(); + '''); + + // Attendre un peu pour s'assurer que la suppression est terminée + await Future.delayed(const Duration(milliseconds: 1000)); + + // Réinitialiser Hive + await Hive.initFlutter(); + } else { + // Pour les plateformes mobiles, on utilise une approche différente + await Hive.deleteFromDisk(); + await Hive.initFlutter(); + } + + // Réenregistrer tous les adaptateurs + _registerAdapters(); + + // Rouvrir les boîtes essentielles + await _reopenEssentialBoxes(); + + debugPrint( + 'HiveResetService: Réinitialisation complète terminée avec succès'); + return true; + } catch (e) { + debugPrint('HiveResetService: Erreur lors de la réinitialisation: $e'); + return false; + } + } + + /// Ferme toutes les boîtes Hive ouvertes + static Future _closeAllBoxes() async { + final boxNames = [ + AppKeys.usersBoxName, + AppKeys.amicaleBoxName, + AppKeys.clientsBoxName, + AppKeys.operationsBoxName, + AppKeys.sectorsBoxName, + AppKeys.passagesBoxName, + AppKeys.settingsBoxName, + AppKeys.membresBoxName, + AppKeys.userSectorBoxName, + AppKeys.chatConversationsBoxName, + AppKeys.chatMessagesBoxName, + AppKeys.regionsBoxName, + ]; + + for (final boxName in boxNames) { + if (Hive.isBoxOpen(boxName)) { + debugPrint('HiveResetService: Fermeture de la boîte $boxName'); + await Hive.box(boxName).close(); + } + } + } + + /// Enregistre tous les adaptateurs Hive + static void _registerAdapters() { + debugPrint('HiveResetService: Enregistrement des adaptateurs Hive'); + + // Enregistrer les adaptateurs pour les modèles principaux + Hive.registerAdapter(UserModelAdapter()); + Hive.registerAdapter(AmicaleModelAdapter()); + Hive.registerAdapter(ClientModelAdapter()); + Hive.registerAdapter(OperationModelAdapter()); + Hive.registerAdapter(SectorModelAdapter()); + Hive.registerAdapter(PassageModelAdapter()); + Hive.registerAdapter(MembreModelAdapter()); + Hive.registerAdapter(UserSectorModelAdapter()); + + // Enregistrer les adaptateurs pour le chat + Hive.registerAdapter(ConversationModelAdapter()); + Hive.registerAdapter(MessageModelAdapter()); + Hive.registerAdapter(ParticipantModelAdapter()); + Hive.registerAdapter(AnonymousUserModelAdapter()); + Hive.registerAdapter(AudienceTargetModelAdapter()); + Hive.registerAdapter(NotificationSettingsAdapter()); + + // Vérifier si RegionModelAdapter est disponible + try { + Hive.registerAdapter(RegionModelAdapter()); + } catch (e) { + debugPrint('HiveResetService: RegionModelAdapter non disponible: $e'); + } + } + + /// Rouvre les boîtes essentielles + static Future _reopenEssentialBoxes() async { + debugPrint('HiveResetService: Réouverture des boîtes essentielles'); + + // Ouvrir les boîtes essentielles au démarrage + await Hive.openBox(AppKeys.usersBoxName); + await Hive.openBox(AppKeys.amicaleBoxName); + await Hive.openBox(AppKeys.clientsBoxName); + await Hive.openBox(AppKeys.settingsBoxName); + + // Ouvrir les boîtes de chat + await Hive.openBox(AppKeys.chatConversationsBoxName); + await Hive.openBox(AppKeys.chatMessagesBoxName); + } +} diff --git a/app/lib/core/services/hive_reset_state_service.dart b/app/lib/core/services/hive_reset_state_service.dart new file mode 100644 index 00000000..372b61e7 --- /dev/null +++ b/app/lib/core/services/hive_reset_state_service.dart @@ -0,0 +1,40 @@ +import 'package:flutter/foundation.dart'; + +/// Service pour gérer l'état de réinitialisation de Hive +/// Permet de stocker l'information indiquant si Hive a été réinitialisé +/// et de notifier les widgets intéressés +class HiveResetStateService extends ChangeNotifier { + /// Indique si Hive a été réinitialisé + bool _wasReset = false; + + /// Indique si le dialogue de réinitialisation a déjà été affiché + bool _dialogShown = false; + + /// Getter pour savoir si Hive a été réinitialisé + bool get wasReset => _wasReset; + + /// Getter pour savoir si le dialogue a déjà été affiché + bool get dialogShown => _dialogShown; + + /// Marque Hive comme ayant été réinitialisé + void markAsReset() { + _wasReset = true; + notifyListeners(); + } + + /// Marque le dialogue comme ayant été affiché + void markDialogAsShown() { + _dialogShown = true; + notifyListeners(); + } + + /// Réinitialise l'état (à utiliser après une déconnexion par exemple) + void reset() { + _wasReset = false; + _dialogShown = false; + notifyListeners(); + } +} + +/// Instance globale du service +final hiveResetStateService = HiveResetStateService(); diff --git a/flutt/lib/core/services/hive_web_fix.dart b/app/lib/core/services/hive_web_fix.dart similarity index 100% rename from flutt/lib/core/services/hive_web_fix.dart rename to app/lib/core/services/hive_web_fix.dart diff --git a/app/lib/core/services/js_interface.dart b/app/lib/core/services/js_interface.dart new file mode 100644 index 00000000..d7a3cf3c --- /dev/null +++ b/app/lib/core/services/js_interface.dart @@ -0,0 +1,20 @@ +/// Interface pour les fonctionnalités JavaScript +/// Importe conditionnellement dart:js pour le web ou un stub pour les autres plateformes +library js_interface; + +import 'package:flutter/foundation.dart'; + +// Importation conditionnelle basée sur la plateforme +import 'js_stub.dart' if (dart.library.js) 'dart:js' as js; + +/// Exporte le contexte JavaScript pour être utilisé dans d'autres fichiers +final context = js.context; + +/// Fonction utilitaire pour évaluer du code JavaScript sur le web +/// Ne fait rien sur les plateformes non-web +dynamic evalJs(String code) { + if (kIsWeb) { + return js.context.callMethod('eval', [code]); + } + return null; +} diff --git a/app/lib/core/services/js_stub.dart b/app/lib/core/services/js_stub.dart new file mode 100644 index 00000000..33cb3f79 --- /dev/null +++ b/app/lib/core/services/js_stub.dart @@ -0,0 +1,11 @@ +/// Stub pour dart:js pour les plateformes non-web +/// Fournit une implémentation vide des fonctionnalités de dart:js +class JsContext { + dynamic callMethod(String method, [List? args]) { + // Ne fait rien sur les plateformes non-web + return null; + } +} + +/// Contexte JavaScript stub +final JsContext context = JsContext(); diff --git a/flutt/lib/core/services/location_service.dart b/app/lib/core/services/location_service.dart similarity index 100% rename from flutt/lib/core/services/location_service.dart rename to app/lib/core/services/location_service.dart diff --git a/flutt/lib/core/services/passage_data_service.dart b/app/lib/core/services/passage_data_service.dart similarity index 100% rename from flutt/lib/core/services/passage_data_service.dart rename to app/lib/core/services/passage_data_service.dart diff --git a/flutt/lib/core/services/sync_service.dart b/app/lib/core/services/sync_service.dart similarity index 100% rename from flutt/lib/core/services/sync_service.dart rename to app/lib/core/services/sync_service.dart diff --git a/app/lib/core/theme/app_theme.dart b/app/lib/core/theme/app_theme.dart new file mode 100644 index 00000000..fbda0a30 --- /dev/null +++ b/app/lib/core/theme/app_theme.dart @@ -0,0 +1,190 @@ +import 'package:flutter/material.dart'; + +class AppTheme { + // Couleurs du thème basées sur la maquette Figma + static const Color primaryColor = Color(0xFF20335E); // Bleu foncé + static const Color secondaryColor = Color(0xFF9DC7C8); // Bleu clair + static const Color accentColor = Color(0xFF00E09D); // Vert + static const Color errorColor = Color(0xFFE41B13); // Rouge + static const Color warningColor = Color(0xFFF7A278); // Orange + static const Color backgroundLightColor = + Color(0xFFF4F5F6); // Gris très clair + static const Color backgroundDarkColor = Color(0xFF111827); + static const Color textLightColor = Color(0xFF000000); // Noir + static const Color textDarkColor = Color(0xFFF9FAFB); + + // Thème clair + static ThemeData get lightTheme { + return ThemeData( + useMaterial3: true, + brightness: Brightness.light, + fontFamily: 'Figtree', // Utilisation directe de la police locale + colorScheme: ColorScheme.light( + primary: primaryColor, + secondary: secondaryColor, + tertiary: accentColor, + background: backgroundLightColor, + surface: Colors.white, + onPrimary: Colors.white, + onSecondary: Colors.white, + onBackground: textLightColor, + onSurface: textLightColor, + ), + textTheme: const TextTheme().copyWith( + displayLarge: const TextStyle(fontFamily: 'Figtree'), + displayMedium: const TextStyle(fontFamily: 'Figtree'), + displaySmall: const TextStyle(fontFamily: 'Figtree'), + headlineLarge: const TextStyle(fontFamily: 'Figtree'), + headlineMedium: const TextStyle(fontFamily: 'Figtree'), + headlineSmall: const TextStyle(fontFamily: 'Figtree'), + titleLarge: const TextStyle(fontFamily: 'Figtree'), + titleMedium: const TextStyle(fontFamily: 'Figtree'), + titleSmall: const TextStyle(fontFamily: 'Figtree'), + bodyLarge: const TextStyle(fontFamily: 'Figtree'), + bodyMedium: const TextStyle(fontFamily: 'Figtree'), + bodySmall: const TextStyle(fontFamily: 'Figtree'), + labelLarge: const TextStyle(fontFamily: 'Figtree'), + labelMedium: const TextStyle(fontFamily: 'Figtree'), + labelSmall: const TextStyle(fontFamily: 'Figtree'), + ), + appBarTheme: const AppBarTheme( + backgroundColor: primaryColor, + foregroundColor: Colors.white, + elevation: 0, + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + textStyle: const TextStyle( + fontFamily: 'Figtree', + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: backgroundLightColor, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: textLightColor.withOpacity(0.1), + width: 1, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: textLightColor.withOpacity(0.1), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: primaryColor, width: 2), + ), + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + ), + cardTheme: CardTheme( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + ); + } + + // Thème sombre + static ThemeData get darkTheme { + return ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + fontFamily: 'Figtree', // Utilisation directe de la police locale + colorScheme: ColorScheme.dark( + primary: primaryColor, + secondary: secondaryColor, + tertiary: accentColor, + background: backgroundDarkColor, + surface: const Color(0xFF1F2937), + onPrimary: Colors.white, + onSecondary: Colors.white, + onBackground: textDarkColor, + onSurface: textDarkColor, + ), + textTheme: const TextTheme().copyWith( + displayLarge: const TextStyle(fontFamily: 'Figtree'), + displayMedium: const TextStyle(fontFamily: 'Figtree'), + displaySmall: const TextStyle(fontFamily: 'Figtree'), + headlineLarge: const TextStyle(fontFamily: 'Figtree'), + headlineMedium: const TextStyle(fontFamily: 'Figtree'), + headlineSmall: const TextStyle(fontFamily: 'Figtree'), + titleLarge: const TextStyle(fontFamily: 'Figtree'), + titleMedium: const TextStyle(fontFamily: 'Figtree'), + titleSmall: const TextStyle(fontFamily: 'Figtree'), + bodyLarge: const TextStyle(fontFamily: 'Figtree'), + bodyMedium: const TextStyle(fontFamily: 'Figtree'), + bodySmall: const TextStyle(fontFamily: 'Figtree'), + labelLarge: const TextStyle(fontFamily: 'Figtree'), + labelMedium: const TextStyle(fontFamily: 'Figtree'), + labelSmall: const TextStyle(fontFamily: 'Figtree'), + ), + appBarTheme: const AppBarTheme( + backgroundColor: Color(0xFF1F2937), + foregroundColor: Colors.white, + elevation: 0, + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + textStyle: const TextStyle( + fontFamily: 'Figtree', + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: const Color(0xFF374151), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: textDarkColor.withOpacity(0.1), + width: 1, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: textDarkColor.withOpacity(0.1), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: primaryColor, width: 2), + ), + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + ), + cardTheme: CardTheme( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + color: const Color(0xFF1F2937), + ), + ); + } +} diff --git a/app/lib/main.dart b/app/lib/main.dart new file mode 100644 index 00000000..d17ea278 --- /dev/null +++ b/app/lib/main.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:geosector_app/app.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:geosector_app/core/data/models/user_model.dart'; +import 'package:geosector_app/core/data/models/amicale_model.dart'; +import 'package:geosector_app/core/data/models/client_model.dart'; +import 'package:geosector_app/core/data/models/operation_model.dart'; +import 'package:geosector_app/core/data/models/sector_model.dart'; +import 'package:geosector_app/core/data/models/passage_model.dart'; +import 'package:geosector_app/core/data/models/membre_model.dart'; +import 'package:geosector_app/core/data/models/user_sector_model.dart'; +import 'package:geosector_app/core/data/models/region_model.dart'; +import 'package:geosector_app/core/constants/app_keys.dart'; +import 'package:geosector_app/core/services/hive_reset_service.dart'; +import 'package:geosector_app/core/services/hive_reset_state_service.dart'; +// Import centralisé pour les modèles chat +import 'package:geosector_app/chat/models/chat_adapters.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Configurer le routage par chemin (URLs sans #) + setUrlStrategy(PathUrlStrategy()); + + // Initialiser Hive avec gestion des erreurs + bool hiveInitialized = false; + + try { + // Initialiser Hive + await Hive.initFlutter(); + + // Enregistrer les adaptateurs Hive pour les modèles principaux + Hive.registerAdapter(UserModelAdapter()); + Hive.registerAdapter(AmicaleModelAdapter()); + Hive.registerAdapter(ClientModelAdapter()); + Hive.registerAdapter(OperationModelAdapter()); + Hive.registerAdapter(SectorModelAdapter()); + Hive.registerAdapter(PassageModelAdapter()); + Hive.registerAdapter(MembreModelAdapter()); + Hive.registerAdapter(UserSectorModelAdapter()); + // TODO: Décommenter après avoir généré le fichier region_model.g.dart + // Hive.registerAdapter(RegionModelAdapter()); + + // Enregistrer les adaptateurs Hive pour le chat + Hive.registerAdapter(ConversationModelAdapter()); + Hive.registerAdapter(MessageModelAdapter()); + Hive.registerAdapter(ParticipantModelAdapter()); + Hive.registerAdapter(AnonymousUserModelAdapter()); + Hive.registerAdapter(AudienceTargetModelAdapter()); + Hive.registerAdapter(NotificationSettingsAdapter()); + + // Ouvrir uniquement les boîtes essentielles au démarrage + try { + // La boîte des utilisateurs est nécessaire pour vérifier si un utilisateur est déjà connecté + await Hive.openBox(AppKeys.usersBoxName); + // Boîte pour les amicales + await Hive.openBox(AppKeys.amicaleBoxName); + // Boîte pour les clients + await Hive.openBox(AppKeys.clientsBoxName); + // Boîte pour les préférences utilisateur générales + await Hive.openBox(AppKeys.settingsBoxName); + + // Ouvrir les boîtes de chat également au démarrage pour le cache local + await Hive.openBox(AppKeys.chatConversationsBoxName); + await Hive.openBox(AppKeys.chatMessagesBoxName); + + hiveInitialized = true; + } catch (e) { + debugPrint('Erreur lors de l\'ouverture des boîtes Hive: $e'); + // Une erreur s'est produite lors de l'ouverture des boîtes, probablement due à une incompatibilité + // Nous allons réinitialiser Hive + hiveInitialized = false; + } + } catch (e) { + debugPrint('Erreur lors de l\'initialisation de Hive: $e'); + hiveInitialized = false; + } + + // Si Hive n'a pas été initialisé correctement, marquer l'état pour afficher le dialogue + if (!hiveInitialized) { + debugPrint( + 'Incompatibilité détectée dans les données Hive. Marquage pour affichage du dialogue...'); + // Marquer Hive comme ayant été réinitialisé pour afficher le dialogue plus tard + hiveResetStateService.markAsReset(); + } + + // Les autres boîtes (operations, sectors, passages, user_sector) seront ouvertes après connexion + // dans UserRepository.login() via la méthode _ensureBoxIsOpen() + + // Définir l'orientation de l'application + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); + + // Lancer l'application directement sans AppProviders + runApp(const GeoSectorApp()); +} diff --git a/flutt/lib/presentation/MIGRATION.md b/app/lib/presentation/MIGRATION.md similarity index 100% rename from flutt/lib/presentation/MIGRATION.md rename to app/lib/presentation/MIGRATION.md diff --git a/flutt/lib/presentation/README.md b/app/lib/presentation/README.md similarity index 100% rename from flutt/lib/presentation/README.md rename to app/lib/presentation/README.md diff --git a/flutt/lib/presentation/admin/admin_communication_page.dart b/app/lib/presentation/admin/admin_communication_page.dart similarity index 100% rename from flutt/lib/presentation/admin/admin_communication_page.dart rename to app/lib/presentation/admin/admin_communication_page.dart diff --git a/flutt/lib/presentation/admin/admin_dashboard_home_page.dart b/app/lib/presentation/admin/admin_dashboard_home_page.dart similarity index 52% rename from flutt/lib/presentation/admin/admin_dashboard_home_page.dart rename to app/lib/presentation/admin/admin_dashboard_home_page.dart index 43d0d928..e679ac2d 100644 --- a/flutt/lib/presentation/admin/admin_dashboard_home_page.dart +++ b/app/lib/presentation/admin/admin_dashboard_home_page.dart @@ -1,6 +1,7 @@ import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales import 'package:flutter/material.dart'; -import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales +import 'package:flutter/foundation.dart' show kIsWeb; +import 'dart:math' as math; import 'package:hive_flutter/hive_flutter.dart'; import 'package:geosector_app/presentation/widgets/charts/activity_chart.dart'; import 'package:geosector_app/presentation/widgets/charts/passage_pie_chart.dart'; @@ -15,6 +16,29 @@ import 'package:geosector_app/core/data/models/sector_model.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:geosector_app/shared/app_theme.dart'; +/// Class pour dessiner les petits points blancs sur le fond +class DotsPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = Colors.white.withOpacity(0.5) + ..style = PaintingStyle.fill; + + final random = math.Random(42); // Seed fixe pour consistance + final numberOfDots = (size.width * size.height) ~/ 1500; + + for (int i = 0; i < numberOfDots; i++) { + final x = random.nextDouble() * size.width; + final y = random.nextDouble() * size.height; + final radius = 1.0 + random.nextDouble() * 2.0; + canvas.drawCircle(Offset(x, y), radius, paint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + class AdminDashboardHomePage extends StatefulWidget { const AdminDashboardHomePage({Key? key}) : super(key: key); @@ -29,6 +53,7 @@ class _AdminDashboardHomePageState extends State { List> memberStats = []; bool isDataLoaded = false; bool isLoading = true; + bool isFirstLoad = true; // Pour suivre le premier chargement // Données pour les graphiques List paymentData = []; @@ -44,62 +69,114 @@ class _AdminDashboardHomePageState extends State { _initFuture = _initHiveBoxes().then((_) { // Charger les données une fois les boîtes initialisées _loadDashboardData(); + + // Après l'affichage des logs "VERIFICATION FINALE DES DONNEES", + // attendre un court délai puis rafraîchir automatiquement les données + Future.delayed(const Duration(milliseconds: 500), () { + if (mounted) { + setState(() { + isLoading = true; // Afficher le spinner pendant le rafraîchissement + }); + _loadDashboardData(); // Rafraîchir les données + } + }); }); } // Méthode pour initialiser les boîtes Hive nécessaires Future _initHiveBoxes() async { try { - debugPrint('Initialisation des boîtes Hive...'); + debugPrint('AdminDashboardHomePage: Initialisation des boîtes Hive...'); - // Ouvrir la boîte des opérations si elle n'est pas déjà ouverte - if (!Hive.isBoxOpen(AppKeys.operationsBoxName)) { - debugPrint('Ouverture de la boîte operations...'); - try { - await Hive.openBox(AppKeys.operationsBoxName); - debugPrint('Boîte operations ouverte avec succès'); - } catch (boxError) { + // Liste des boîtes à ouvrir + final boxesToOpen = [ + { + 'name': AppKeys.operationsBoxName, + 'type': 'OperationModel', + 'opened': false + }, + { + 'name': AppKeys.passagesBoxName, + 'type': 'PassageModel', + 'opened': false + }, + { + 'name': AppKeys.sectorsBoxName, + 'type': 'SectorModel', + 'opened': false + }, + ]; + + // Ouvrir chaque boîte + for (final boxInfo in boxesToOpen) { + final boxName = boxInfo['name'] as String; + + if (!Hive.isBoxOpen(boxName)) { debugPrint( - 'Erreur lors de l\'ouverture de la boîte operations: $boxError'); - // Continuer malgré l'erreur + 'AdminDashboardHomePage: Ouverture de la boîte $boxName...'); + try { + switch (boxInfo['type']) { + case 'OperationModel': + await Hive.openBox(boxName); + break; + case 'PassageModel': + await Hive.openBox(boxName); + break; + case 'SectorModel': + await Hive.openBox(boxName); + break; + } + boxInfo['opened'] = true; + debugPrint( + 'AdminDashboardHomePage: Boîte $boxName ouverte avec succès'); + } catch (boxError) { + debugPrint( + 'AdminDashboardHomePage: Erreur lors de l\'ouverture de la boîte $boxName: $boxError'); + // Continuer malgré l'erreur + } + } else { + boxInfo['opened'] = true; + debugPrint('AdminDashboardHomePage: Boîte $boxName déjà ouverte'); } - } else { - debugPrint('Boîte operations déjà ouverte'); } - // Ouvrir la boîte des passages si elle n'est pas déjà ouverte - if (!Hive.isBoxOpen(AppKeys.passagesBoxName)) { - debugPrint('Ouverture de la boîte passages...'); - try { - await Hive.openBox(AppKeys.passagesBoxName); - debugPrint('Boîte passages ouverte avec succès'); - } catch (boxError) { - debugPrint( - 'Erreur lors de l\'ouverture de la boîte passages: $boxError'); - // Continuer malgré l'erreur - } + // Vérifier si toutes les boîtes ont été ouvertes + final allBoxesOpened = boxesToOpen.every((box) => box['opened'] == true); + + if (allBoxesOpened) { + debugPrint( + 'AdminDashboardHomePage: Toutes les boîtes Hive ont été ouvertes avec succès'); } else { - debugPrint('Boîte passages déjà ouverte'); + // Identifier les boîtes qui n'ont pas pu être ouvertes + final failedBoxes = boxesToOpen + .where((box) => box['opened'] == false) + .map((box) => box['name']) + .join(', '); + debugPrint( + 'AdminDashboardHomePage: Certaines boîtes n\'ont pas pu être ouvertes: $failedBoxes'); } - // Ouvrir la boîte des secteurs si elle n'est pas déjà ouverte - if (!Hive.isBoxOpen(AppKeys.sectorsBoxName)) { - debugPrint('Ouverture de la boîte sectors...'); - try { - await Hive.openBox(AppKeys.sectorsBoxName); - debugPrint('Boîte sectors ouverte avec succès'); - } catch (boxError) { - debugPrint( - 'Erreur lors de l\'ouverture de la boîte sectors: $boxError'); - // Continuer malgré l'erreur - } - } else { - debugPrint('Boîte sectors déjà ouverte'); + // Afficher le nombre d'éléments dans chaque boîte + debugPrint('VERIFICATION FINALE DES DONNEES'); + if (Hive.isBoxOpen(AppKeys.operationsBoxName)) { + final operationsBox = + Hive.box(AppKeys.operationsBoxName); + debugPrint('Nombre d\'opérations: ${operationsBox.length}'); + } + if (Hive.isBoxOpen(AppKeys.passagesBoxName)) { + final passagesBox = Hive.box(AppKeys.passagesBoxName); + debugPrint('Nombre de passages: ${passagesBox.length}'); + } + if (Hive.isBoxOpen(AppKeys.sectorsBoxName)) { + final sectorsBox = Hive.box(AppKeys.sectorsBoxName); + debugPrint('Nombre de secteurs: ${sectorsBox.length}'); } - debugPrint('Initialisation des boîtes Hive terminée'); + debugPrint( + 'AdminDashboardHomePage: Initialisation des boîtes Hive terminée'); } catch (e) { - debugPrint('Erreur lors de l\'initialisation des boîtes Hive: $e'); + debugPrint( + 'AdminDashboardHomePage: Erreur lors de l\'initialisation des boîtes Hive: $e'); // Ne pas propager l'erreur, mais retourner normalement // pour éviter que le FutureBuilder ne reste bloqué en état d'erreur } @@ -145,12 +222,15 @@ class _AdminDashboardHomePageState extends State { } Future _loadDashboardData() async { - setState(() { - isLoading = true; - }); + if (mounted) { + setState(() { + isLoading = true; + }); + } try { - debugPrint('Chargement des données du tableau de bord...'); + debugPrint( + 'AdminDashboardHomePage: Chargement des données du tableau de bord...'); // Utiliser les instances globales définies dans app.dart // Pas besoin de Provider.of car les instances sont déjà disponibles @@ -160,31 +240,38 @@ class _AdminDashboardHomePageState extends State { // Vérifier si la boîte Hive est ouverte if (!Hive.isBoxOpen(AppKeys.operationsBoxName)) { debugPrint( - 'Ouverture de la boîte operations dans _loadDashboardData...'); + 'AdminDashboardHomePage: Ouverture de la boîte operations dans _loadDashboardData...'); try { await Hive.openBox(AppKeys.operationsBoxName); debugPrint( - 'Boîte operations ouverte avec succès dans _loadDashboardData'); + 'AdminDashboardHomePage: Boîte operations ouverte avec succès dans _loadDashboardData'); } catch (boxError) { debugPrint( - 'Erreur lors de l\'ouverture de la boîte operations dans _loadDashboardData: $boxError'); + 'AdminDashboardHomePage: Erreur lors de l\'ouverture de la boîte operations dans _loadDashboardData: $boxError'); // Continuer malgré l'erreur } } // Récupérer l'opération en cours - debugPrint('Récupération de l\'opération en cours...'); + debugPrint( + 'AdminDashboardHomePage: Récupération de l\'opération en cours...'); currentOperation = userRepository.getCurrentOperation(); - debugPrint('Opération récupérée: ${currentOperation?.id ?? "null"}'); + debugPrint( + 'AdminDashboardHomePage: Opération récupérée: ${currentOperation?.id ?? "null"}'); } catch (boxError) { - debugPrint('Erreur lors de la récupération de l\'opération: $boxError'); + debugPrint( + 'AdminDashboardHomePage: Erreur lors de la récupération de l\'opération: $boxError'); // Afficher un message d'erreur ou gérer l'erreur de manière appropriée } if (currentOperation != null) { // Charger les passages pour l'opération en cours + debugPrint( + 'AdminDashboardHomePage: Chargement des passages pour l\'opération ${currentOperation.id}...'); final passages = passageRepository.getPassagesByOperation(currentOperation.id); + debugPrint( + 'AdminDashboardHomePage: ${passages.length} passages récupérés'); // Calculer le nombre total de passages totalPassages = passages.length; @@ -208,6 +295,15 @@ class _AdminDashboardHomePageState extends State { passagesByType[typeId] = (passagesByType[typeId] ?? 0) + 1; } + // Afficher les comptages par type pour le débogage + debugPrint('AdminDashboardHomePage: Comptage des passages par type:'); + passagesByType.forEach((typeId, count) { + final typeInfo = AppKeys.typesPassages[typeId]; + final typeName = typeInfo != null ? typeInfo['titre'] : 'Inconnu'; + debugPrint( + 'AdminDashboardHomePage: Type $typeId ($typeName): $count passages'); + }); + // Charger les statistiques par membre memberStats = []; final Map memberCounts = {}; @@ -234,17 +330,30 @@ class _AdminDashboardHomePageState extends State { // Trier les membres par nombre de passages (décroissant) memberStats .sort((a, b) => (b['count'] as int).compareTo(a['count'] as int)); + } else { + debugPrint( + 'AdminDashboardHomePage: Aucune opération en cours, impossible de charger les passages'); } - setState(() { - isDataLoaded = true; - isLoading = false; - }); + if (mounted) { + setState(() { + isDataLoaded = true; + isLoading = false; + isFirstLoad = false; // Marquer que le premier chargement est terminé + }); + } + + // Vérifier si les données sont correctement chargées + debugPrint( + 'AdminDashboardHomePage: Données chargées: isDataLoaded=$isDataLoaded, totalPassages=$totalPassages, passagesByType=${passagesByType.length} types'); } catch (e) { - debugPrint('Erreur lors du chargement des données: $e'); - setState(() { - isLoading = false; - }); + debugPrint( + 'AdminDashboardHomePage: Erreur lors du chargement des données: $e'); + if (mounted) { + setState(() { + isLoading = false; + }); + } } } @@ -302,208 +411,240 @@ class _AdminDashboardHomePageState extends State { ? 'Synthèse de l\'opération #${currentOperation.id} ${currentOperation.name}' : 'Synthèse de l\'opération'; - return SingleChildScrollView( - padding: const EdgeInsets.all(AppTheme.spacingL), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - title, - style: - Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - // Réduire la taille de police en version web - fontSize: isDesktop ? 18 : null, - ), - overflow: TextOverflow - .ellipsis, // Tronquer avec ... si trop long - maxLines: 1, // Forcer une seule ligne - ), - ), - const Spacer(), - // Bouton de rafraîchissement - if (!isLoading) - IconButton( - icon: const Icon(Icons.refresh), - tooltip: 'Rafraîchir les données', - onPressed: _loadDashboardData, - ) - else - const SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(strokeWidth: 2), - ), - ], + return Stack(children: [ + // Fond dégradé avec petits points blancs + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.white, Colors.red.shade300], ), - const SizedBox(height: AppTheme.spacingM), - // Afficher un indicateur de chargement si les données ne sont pas encore chargées - if (isLoading && !isDataLoaded) - const Center( - child: Padding( - padding: EdgeInsets.all(32.0), - child: CircularProgressIndicator(), - ), + ), + child: CustomPaint( + painter: DotsPainter(), + child: Container(width: double.infinity, height: double.infinity), + ), + ), + // Contenu de la page + SingleChildScrollView( + padding: const EdgeInsets.all(AppTheme.spacingL), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Titre avec bouton de rafraîchissement sur la même ligne + Row( + children: [ + Expanded( + child: Text( + title, + style: + Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + // Bouton de rafraîchissement + if (!isLoading) + IconButton( + icon: const Icon(Icons.refresh), + tooltip: 'Rafraîchir les données', + onPressed: _loadDashboardData, + ) + else + const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ], ), + const SizedBox(height: AppTheme.spacingM), + // Afficher un indicateur de chargement si les données ne sont pas encore chargées + if (isLoading && !isDataLoaded) + const Center( + child: Padding( + padding: EdgeInsets.all(32.0), + child: CircularProgressIndicator(), + ), + ), - // Afficher le contenu seulement si les données sont chargées ou en cours de mise à jour - if (isDataLoaded || isLoading) ...[ - // Cartes de synthèse - isDesktop - ? Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 2, - child: _buildSummaryCard( + // Afficher le contenu seulement si les données sont chargées ou en cours de mise à jour + if (isDataLoaded || isLoading) ...[ + // Cartes de synthèse + isDesktop + ? Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 2, + child: _buildSummaryCard( + context, + 'Passages totaux', + totalPassages.toString(), + Icons.map_outlined, + AppTheme.primaryColor, + ), + ), + const SizedBox(width: AppTheme.spacingM), + Expanded( + flex: 2, + child: _buildSummaryCard( + context, + 'Montant collecté', + '${totalAmounts.toStringAsFixed(2)} €', + Icons.euro_outlined, + AppTheme.buttonSuccessColor, + ), + ), + const SizedBox(width: AppTheme.spacingM), + Expanded( + flex: 3, + child: SectorDistributionCard( + key: ValueKey( + 'sector_distribution_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'), + height: 200, + forceRefresh: !isFirstLoad, + ), + ), + ], + ) + : Column( + children: [ + _buildSummaryCard( context, 'Passages totaux', totalPassages.toString(), Icons.map_outlined, AppTheme.primaryColor, ), - ), - const SizedBox(width: AppTheme.spacingM), - Expanded( - flex: 2, - child: _buildSummaryCard( + const SizedBox(height: AppTheme.spacingM), + _buildSummaryCard( context, 'Montant collecté', '${totalAmounts.toStringAsFixed(2)} €', Icons.euro_outlined, AppTheme.buttonSuccessColor, ), - ), - const SizedBox(width: AppTheme.spacingM), - Expanded( - flex: 3, - child: SectorDistributionCard( + const SizedBox(height: AppTheme.spacingM), + SectorDistributionCard( + key: ValueKey( + 'sector_distribution_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'), height: 200, + forceRefresh: !isFirstLoad, ), - ), - ], - ) - : Column( - children: [ - _buildSummaryCard( - context, - 'Passages totaux', - totalPassages.toString(), - Icons.map_outlined, - AppTheme.primaryColor, - ), - const SizedBox(height: AppTheme.spacingM), - _buildSummaryCard( - context, - 'Montant collecté', - '${totalAmounts.toStringAsFixed(2)} €', - Icons.euro_outlined, - AppTheme.buttonSuccessColor, - ), - const SizedBox(height: AppTheme.spacingM), - SectorDistributionCard( - height: 200, - ), - ], + ], + ), + + const SizedBox(height: AppTheme.spacingL), + + // Graphique d'activité + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + BorderRadius.circular(AppTheme.borderRadiusMedium), + boxShadow: AppTheme.cardShadow, + ), + child: ActivityChart( + key: ValueKey( + 'activity_chart_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'), + height: 350, + loadFromHive: true, + showAllPassages: + true, // Tous les passages, pas seulement ceux de l'utilisateur courant + title: 'Passages réalisés par jour (15 derniers jours)', + daysToShow: 15, + forceRefresh: !isFirstLoad, + ), + // Si vous avez besoin de passer l'ID de l'opération en cours, décommentez les lignes suivantes + // child: ActivityChart( + // height: 350, + // loadFromHive: true, + // showAllPassages: true, + // title: 'Passages réalisés par jour (15 derniers jours)', + // daysToShow: 15, + // operationId: userRepository.getCurrentOperation()?.id, + // ), + ), + + const SizedBox(height: AppTheme.spacingL), + + // Graphiques de répartition + isDesktop + ? Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _buildPassageTypeCard(context), + ), + const SizedBox(width: AppTheme.spacingM), + Expanded( + child: _buildPaymentTypeCard(context), + ), + ], + ) + : Column( + children: [ + _buildPassageTypeCard(context), + const SizedBox(height: AppTheme.spacingM), + _buildPaymentTypeCard(context), + ], + ), + + const SizedBox(height: AppTheme.spacingL), + + // Actions rapides - uniquement visible sur le web + if (kIsWeb) ...[ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + BorderRadius.circular(AppTheme.borderRadiusMedium), + boxShadow: AppTheme.cardShadow, ), - - const SizedBox(height: AppTheme.spacingL), - - // Graphique d'activité - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: - BorderRadius.circular(AppTheme.borderRadiusMedium), - boxShadow: AppTheme.cardShadow, - ), - child: const ActivityChart( - height: 350, - loadFromHive: true, - showAllPassages: - true, // Tous les passages, pas seulement ceux de l'utilisateur courant - title: 'Passages réalisés par jour (15 derniers jours)', - daysToShow: 15, - ), - // Si vous avez besoin de passer l'ID de l'opération en cours, décommentez les lignes suivantes - // child: ActivityChart( - // height: 350, - // loadFromHive: true, - // showAllPassages: true, - // title: 'Passages réalisés par jour (15 derniers jours)', - // daysToShow: 15, - // operationId: userRepository.getCurrentOperation()?.id, - // ), - ), - - const SizedBox(height: AppTheme.spacingL), - - // Graphiques de répartition - isDesktop - ? Row( + padding: const EdgeInsets.all(AppTheme.spacingM), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: _buildPassageTypeCard(context), + Text( + 'Actions sur cette opération', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: AppTheme.primaryColor, + ), ), - const SizedBox(width: AppTheme.spacingM), - Expanded( - child: _buildPaymentTypeCard(context), - ), - ], - ) - : Column( - children: [ - _buildPassageTypeCard(context), const SizedBox(height: AppTheme.spacingM), - _buildPaymentTypeCard(context), + Wrap( + spacing: AppTheme.spacingM, + runSpacing: AppTheme.spacingM, + children: [ + _buildActionButton( + context, + 'Exporter les données', + Icons.file_download_outlined, + AppTheme.buttonPrimaryColor, + () {}, + ), + _buildActionButton( + context, + 'Gérer les secteurs', + Icons.map_outlined, + AppTheme.accentColor, + () {}, + ), + ], + ), ], ), - - const SizedBox(height: AppTheme.spacingL), - - // Actions rapides - Text( - 'Actions rapides', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: AppTheme.spacingM), - Wrap( - spacing: AppTheme.spacingM, - runSpacing: AppTheme.spacingM, - children: [ - _buildActionButton( - context, - 'Exporter les données', - Icons.file_download_outlined, - AppTheme.buttonPrimaryColor, - () {}, - ), - _buildActionButton( - context, - 'Envoyer un message', - Icons.message_outlined, - AppTheme.buttonSuccessColor, - () {}, - ), - _buildActionButton( - context, - 'Gérer les secteurs', - Icons.map_outlined, - AppTheme.accentColor, - () {}, ), ], - ), + ], ], - ], - ), - ); + ), + ) + ]); }, ); } @@ -641,14 +782,97 @@ class _AdminDashboardHomePageState extends State { flex: 1, child: SizedBox( height: 180, // Taille réduite - child: const PassagePieChart( - size: 180, - loadFromHive: true, - showAllPassages: true, - isDonut: true, - innerRadius: '50%', - showIcons: false, - showLegend: false, + child: Builder( + builder: (context) { + // Vérifier si nous avons des données de passages + if (passagesByType.isEmpty) { + debugPrint( + 'AdminDashboardHomePage: Aucune donnée de passage disponible pour le graphique'); + return const Center( + child: Text('Aucune donnée disponible'), + ); + } + + // Si nous avons des données, afficher le graphique + // Mais d'abord, vérifier si tous les passages sont de type 2 (à finaliser) + // qui est exclu par défaut dans PassagePieChart + bool hasNonType2Passages = passagesByType.entries.any( + (entry) => entry.key != 2 && entry.value > 0); + + debugPrint( + 'AdminDashboardHomePage: Données pour le graphique: $passagesByType'); + + // Créer un widget personnalisé pour afficher le graphique ou un message + // selon le contenu des données + if (passagesByType.isEmpty) { + debugPrint( + 'AdminDashboardHomePage: Aucune donnée de passage disponible'); + return const Center( + child: Text('Aucune donnée disponible'), + ); + } + + // Vérifier si nous avons des données pour au moins un type + int totalPassages = 0; + passagesByType + .forEach((_, count) => totalPassages += count); + + if (totalPassages == 0) { + debugPrint( + 'AdminDashboardHomePage: Aucun passage trouvé'); + return const Center( + child: Text('Aucun passage trouvé'), + ); + } + + // Vérifier si tous les passages sont de type 2 (à finaliser) + if (!hasNonType2Passages) { + debugPrint( + 'AdminDashboardHomePage: Tous les passages sont de type 2 (à finaliser)'); + + // Créer un widget personnalisé pour afficher un message + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.info_outline, + color: Colors.orange, + size: 40, + ), + const SizedBox(height: 8), + const Text( + 'Uniquement des passages à finaliser', + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + '${passagesByType[2] ?? 0} passages', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.orange, + fontWeight: FontWeight.bold, + ), + ), + ], + ); + } + + // Sinon, afficher le graphique avec les données + debugPrint( + 'AdminDashboardHomePage: Affichage du graphique avec ${passagesByType.length} types'); + return PassagePieChart( + size: 180, + passagesByType: passagesByType, + loadFromHive: false, + isDonut: true, + innerRadius: '50%', + showIcons: false, + showLegend: false, + ); + }, ), ), ), @@ -769,10 +993,12 @@ class _AdminDashboardHomePageState extends State { innerRadius: '50%', showIcons: false, showLegend: false, - enable3DEffect: true, - effect3DIntensity: 1.5, + enable3DEffect: + false, // Désactiver l'effet 3D pour conserver les couleurs originales + effect3DIntensity: 0.0, // Pas d'intensité 3D enableEnhancedExplode: false, // Désactiver l'explosion - useGradient: true, + useGradient: + false, // Ne pas utiliser de dégradé pour conserver les couleurs originales ), ), ), diff --git a/flutt/lib/presentation/admin/admin_dashboard_page.dart b/app/lib/presentation/admin/admin_dashboard_page.dart similarity index 58% rename from flutt/lib/presentation/admin/admin_dashboard_page.dart rename to app/lib/presentation/admin/admin_dashboard_page.dart index 40d18f0c..1648f847 100644 --- a/flutt/lib/presentation/admin/admin_dashboard_page.dart +++ b/app/lib/presentation/admin/admin_dashboard_page.dart @@ -1,11 +1,13 @@ import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales import 'package:flutter/material.dart'; -import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales import 'package:hive_flutter/hive_flutter.dart'; import 'package:geosector_app/presentation/widgets/dashboard_layout.dart'; import 'package:geosector_app/core/repositories/user_repository.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:geosector_app/shared/app_theme.dart'; +import 'package:geosector_app/presentation/widgets/loading_progress_overlay.dart'; +import 'package:geosector_app/core/models/loading_state.dart'; +import 'dart:math' as math; // Import des pages admin import 'admin_dashboard_home_page.dart'; @@ -15,6 +17,29 @@ import 'admin_communication_page.dart'; import 'admin_map_page.dart'; import 'admin_entite.dart'; +/// Class pour dessiner les petits points blancs sur le fond +class DotsPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = Colors.white.withOpacity(0.5) + ..style = PaintingStyle.fill; + + final random = math.Random(42); // Seed fixe pour consistance + final numberOfDots = (size.width * size.height) ~/ 1500; + + for (int i = 0; i < numberOfDots; i++) { + final x = random.nextDouble() * size.width; + final y = random.nextDouble() * size.height; + final radius = 1.0 + random.nextDouble() * 2.0; + canvas.drawCircle(Offset(x, y), radius, paint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + class AdminDashboardPage extends StatefulWidget { const AdminDashboardPage({Key? key}) : super(key: key); @@ -22,21 +47,26 @@ class AdminDashboardPage extends StatefulWidget { State createState() => _AdminDashboardPageState(); } -class _AdminDashboardPageState extends State { +class _AdminDashboardPageState extends State + with WidgetsBindingObserver { int _selectedIndex = 0; // Liste des pages à afficher late final List _pages; - // Index de la page Amicale et membres (utilisé pour la navigation conditionnelle) + // Index de la page Amicale et membres static const int entitePageIndex = 5; // Référence à la boîte Hive pour les paramètres late Box _settingsBox; + // Overlay pour afficher la progression du chargement + OverlayEntry? _progressOverlay; + @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); try { debugPrint('Initialisation de AdminDashboardPage'); @@ -58,6 +88,9 @@ class _AdminDashboardPageState extends State { 'Utilisateur connecté: ${currentUser.username} (${currentUser.id})', ); } + + // Écouter les changements d'état du UserRepository + userRepository.addListener(_handleUserRepositoryChanges); } _pages = [ @@ -66,16 +99,54 @@ class _AdminDashboardPageState extends State { const AdminHistoryPage(), const AdminCommunicationPage(), const AdminMapPage(), - // La page AdminEntitePage est maintenant accessible uniquement via le menu Paramètres + const AdminEntitePage(), ]; // Initialiser et charger les paramètres _initSettings(); + + // Vérifier si des données sont en cours de chargement + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkLoadingState(); + }); } catch (e) { debugPrint('ERREUR CRITIQUE dans AdminDashboardPage.initState: $e'); } } + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + if (userRepository != null) { + userRepository.removeListener(_handleUserRepositoryChanges); + } + _removeProgressOverlay(); + super.dispose(); + } + + // Méthode pour gérer les changements d'état du UserRepository + void _handleUserRepositoryChanges() { + _checkLoadingState(); + } + + // Méthode pour vérifier l'état de chargement (barre de progression désactivée) + void _checkLoadingState() { + // La barre de progression est désactivée, ne rien faire + } + + // Méthodes pour gérer l'overlay de progression (désactivées) + void _showProgressOverlay(LoadingState state) { + // La barre de progression est désactivée, ne rien faire + } + + void _updateProgressOverlay(LoadingState state) { + // La barre de progression est désactivée, ne rien faire + } + + void _removeProgressOverlay() { + // La barre de progression est désactivée, ne rien faire + } + // Initialiser la boîte de paramètres et charger les préférences Future _initSettings() async { try { @@ -128,19 +199,38 @@ class _AdminDashboardPageState extends State { @override Widget build(BuildContext context) { - return DashboardLayout( - title: 'Tableau de bord Administration', - selectedIndex: _selectedIndex, - onDestinationSelected: (index) { - setState(() { - _selectedIndex = index; - _saveSettings(); // Sauvegarder l'index de page sélectionné - }); - }, - destinations: _buildNavigationDestinations(), - showNewPassageButton: false, - isAdmin: true, - body: _pages[_selectedIndex], + return Stack( + children: [ + // Fond dégradé avec petits points blancs + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.white, Colors.red.shade300], + ), + ), + child: CustomPaint( + painter: DotsPainter(), + child: Container(width: double.infinity, height: double.infinity), + ), + ), + // Contenu de la page + DashboardLayout( + title: 'Tableau de bord Administration', + selectedIndex: _selectedIndex, + onDestinationSelected: (index) { + setState(() { + _selectedIndex = index; + _saveSettings(); // Sauvegarder l'index de page sélectionné + }); + }, + destinations: _buildNavigationDestinations(), + showNewPassageButton: false, + isAdmin: true, + body: _pages[_selectedIndex], + ), + ], ); } @@ -175,8 +265,14 @@ class _AdminDashboardPageState extends State { ), ]; - // Nous ne voulons plus ajouter la destination "Amicale et membres" ici - // car elle est accessible uniquement via le menu Paramètres + // Ajouter la destination "Amicale et membres" + destinations.add( + const NavigationDestination( + icon: Icon(Icons.business_outlined), + selectedIcon: Icon(Icons.business), + label: 'Amicale', + ), + ); return destinations; } diff --git a/app/lib/presentation/admin/admin_debug_info_widget.dart b/app/lib/presentation/admin/admin_debug_info_widget.dart new file mode 100644 index 00000000..76a1800b --- /dev/null +++ b/app/lib/presentation/admin/admin_debug_info_widget.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:geosector_app/presentation/widgets/environment_info_widget.dart'; + +/// Widget d'information de débogage pour l'administrateur +/// À intégrer où nécessaire dans l'interface administrateur +class AdminDebugInfoWidget extends StatelessWidget { + const AdminDebugInfoWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.all(16.0), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.bug_report, color: Colors.grey), + const SizedBox(width: 8), + Text( + 'Informations de débogage', + style: Theme.of(context).textTheme.titleMedium, + ), + ], + ), + const SizedBox(height: 16), + ListTile( + leading: const Icon(Icons.info_outline), + title: const Text('Environnement'), + subtitle: const Text('Afficher les informations sur l\'environnement actuel'), + onTap: () => EnvironmentInfoWidget.show(context), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + tileColor: Colors.grey.withOpacity(0.1), + ), + // Autres options de débogage peuvent être ajoutées ici + ], + ), + ), + ); + } +} diff --git a/app/lib/presentation/admin/admin_entite.dart b/app/lib/presentation/admin/admin_entite.dart new file mode 100644 index 00000000..84c2655d --- /dev/null +++ b/app/lib/presentation/admin/admin_entite.dart @@ -0,0 +1,361 @@ +import 'package:flutter/material.dart'; +import 'dart:math' as math; +import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales +import 'package:geosector_app/core/data/models/amicale_model.dart'; +import 'package:geosector_app/core/data/models/membre_model.dart'; +import 'package:geosector_app/presentation/widgets/amicale_table_widget.dart'; +import 'package:geosector_app/presentation/widgets/membre_table_widget.dart'; + +/// Class pour dessiner les petits points blancs sur le fond +class DotsPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = Colors.white.withOpacity(0.5) + ..style = PaintingStyle.fill; + + final random = math.Random(42); // Seed fixe pour consistance + final numberOfDots = (size.width * size.height) ~/ 1500; + + for (int i = 0; i < numberOfDots; i++) { + final x = random.nextDouble() * size.width; + final y = random.nextDouble() * size.height; + final radius = 1.0 + random.nextDouble() * 2.0; + canvas.drawCircle(Offset(x, y), radius, paint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + +/// Page d'administration de l'amicale et des membres +/// Cette page est intégrée dans le tableau de bord administrateur +class AdminEntitePage extends StatefulWidget { + const AdminEntitePage({Key? key}) : super(key: key); + + @override + State createState() => _AdminEntitePageState(); +} + +class _AdminEntitePageState extends State { + bool _isLoading = true; + AmicaleModel? _amicale; + List _membres = []; + String? _errorMessage; + + @override + void initState() { + super.initState(); + _loadData(); + } + + Future _loadData() async { + setState(() { + _isLoading = true; + _errorMessage = null; + }); + + try { + // Récupérer l'utilisateur connecté en utilisant l'instance globale + final currentUser = userRepository.getCurrentUser(); + + if (currentUser == null) { + setState(() { + _errorMessage = 'Utilisateur non connecté'; + _isLoading = false; + }); + return; + } + + // Vérifier si fkEntite est null + if (currentUser.fkEntite == null) { + setState(() { + _errorMessage = 'Utilisateur non associé à une amicale'; + _isLoading = false; + }); + return; + } + + // Récupérer l'amicale de l'utilisateur en utilisant l'instance globale + final amicale = amicaleRepository.getAmicaleById(currentUser.fkEntite!); + + if (amicale == null) { + setState(() { + _errorMessage = 'Amicale non trouvée'; + _isLoading = false; + }); + return; + } + + // Récupérer tous les membres + // Note: Dans un cas réel, nous devrions filtrer les membres par amicale, + // mais le modèle MembreModel n'a pas de champ fkEntite pour le moment + final membres = membreRepository.getAllMembres(); + + setState(() { + _amicale = amicale; + _membres = membres; + _isLoading = false; + }); + } catch (e) { + setState(() { + _errorMessage = 'Erreur lors du chargement des données: $e'; + _isLoading = false; + }); + } + } + + void _handleEditAmicale(AmicaleModel amicale) { + // Afficher une boîte de dialogue de confirmation + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Modifier l\'amicale'), + content: Text('Voulez-vous modifier l\'amicale ${amicale.name} ?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + // Naviguer vers la page de modification + // Navigator.of(context).push( + // MaterialPageRoute( + // builder: (context) => EditAmicalePage(amicale: amicale), + // ), + // ); + }, + child: const Text('Modifier'), + ), + ], + ), + ); + } + + void _handleEditMembre(MembreModel membre) { + // Afficher une boîte de dialogue de confirmation + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Modifier le membre'), + content: Text( + 'Voulez-vous modifier le membre ${membre.firstName} ${membre.name} ?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + // Naviguer vers la page de modification + // Navigator.of(context).push( + // MaterialPageRoute( + // builder: (context) => EditMembrePage(membre: membre), + // ), + // ); + }, + child: const Text('Modifier'), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Stack( + children: [ + // Fond dégradé avec petits points blancs + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.white, Colors.red.shade300], + ), + ), + child: CustomPaint( + painter: DotsPainter(), + child: Container(width: double.infinity, height: double.infinity), + ), + ), + // Contenu principal + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Titre de la page + Text( + 'Mon amicale et ses membres', + style: theme.textTheme.headlineMedium?.copyWith( + color: theme.colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 24), + + // Message d'erreur si présent + if (_errorMessage != null) + Container( + padding: const EdgeInsets.all(12), + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.red.withOpacity(0.3)), + ), + child: Row( + children: [ + const Icon(Icons.error_outline, color: Colors.red), + const SizedBox(width: 12), + Expanded( + child: Text( + _errorMessage!, + style: const TextStyle(color: Colors.red), + ), + ), + ], + ), + ), + + // Contenu principal + if (_isLoading) + const Expanded( + child: Center( + child: CircularProgressIndicator(), + ), + ) + else if (_amicale == null) + Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.business_outlined, + size: 64, + color: theme.colorScheme.primary.withOpacity(0.7), + ), + const SizedBox(height: 16), + Text( + 'Aucune amicale associée', + style: theme.textTheme.titleLarge, + ), + const SizedBox(height: 8), + Text( + 'Vous n\'êtes pas associé à une amicale.', + textAlign: TextAlign.center, + style: theme.textTheme.bodyLarge, + ), + ], + ), + ), + ) + else + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Section Amicale + Text( + 'Informations de l\'amicale', + style: theme.textTheme.titleLarge?.copyWith( + color: theme.colorScheme.primary, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 16), + + // Tableau Amicale + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: AmicaleTableWidget( + amicales: [_amicale!], + // Pas de bouton de suppression pour sa propre amicale + onDelete: null, + ), + ), + + const SizedBox(height: 32), + + // Section Membres + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Membres de l\'amicale', + style: theme.textTheme.titleLarge?.copyWith( + color: theme.colorScheme.primary, + fontWeight: FontWeight.w600, + ), + ), + ElevatedButton.icon( + onPressed: () { + // Naviguer vers la page d'ajout de membre + // Navigator.of(context).push( + // MaterialPageRoute( + // builder: (context) => AddMembrePage(amicaleId: _amicale!.id), + // ), + // ); + }, + icon: const Icon(Icons.add), + label: const Text('Ajouter un membre'), + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.primary, + foregroundColor: Colors.white, + ), + ), + ], + ), + const SizedBox(height: 16), + + // Tableau Membres + Expanded( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: MembreTableWidget( + membres: _membres, + onEdit: _handleEditMembre, + // Pas de bouton de suppression pour les membres de sa propre amicale + // sauf si l'utilisateur a un rôle élevé + onDelete: null, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/flutt/lib/presentation/admin/admin_history_page.dart b/app/lib/presentation/admin/admin_history_page.dart similarity index 82% rename from flutt/lib/presentation/admin/admin_history_page.dart rename to app/lib/presentation/admin/admin_history_page.dart index 9ceb3740..7902df0a 100644 --- a/flutt/lib/presentation/admin/admin_history_page.dart +++ b/app/lib/presentation/admin/admin_history_page.dart @@ -10,6 +10,30 @@ import 'package:geosector_app/core/repositories/sector_repository.dart'; import 'package:geosector_app/core/repositories/user_repository.dart'; import 'package:geosector_app/core/theme/app_theme.dart'; import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart'; +import 'dart:math' as math; + +/// Class pour dessiner les petits points blancs sur le fond +class DotsPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = Colors.white.withOpacity(0.5) + ..style = PaintingStyle.fill; + + final random = math.Random(42); // Seed fixe pour consistance + final numberOfDots = (size.width * size.height) ~/ 1500; + + for (int i = 0; i < numberOfDots; i++) { + final x = random.nextDouble() * size.width; + final y = random.nextDouble() * size.height; + final radius = 1.0 + random.nextDouble() * 2.0; + canvas.drawCircle(Offset(x, y), radius, paint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} class AdminHistoryPage extends StatefulWidget { const AdminHistoryPage({Key? key}) : super(key: key); @@ -193,12 +217,26 @@ class _AdminHistoryPageState extends State { Widget build(BuildContext context) { // Afficher un widget de chargement ou d'erreur si nécessaire if (_isLoading) { - return const Scaffold( - backgroundColor: - Color(0xFFFFEBEE), // Fond rouge clair pour l'interface admin - body: Center( - child: CircularProgressIndicator(), - ), + return Stack( + children: [ + // Fond dégradé avec petits points blancs + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.white, Colors.red.shade300], + ), + ), + child: CustomPaint( + painter: DotsPainter(), + child: Container(width: double.infinity, height: double.infinity), + ), + ), + const Center( + child: CircularProgressIndicator(), + ), + ], ); } @@ -207,113 +245,142 @@ class _AdminHistoryPageState extends State { } // Retourner le widget principal avec les données chargées - return Scaffold( - backgroundColor: - const Color(0xFFFFEBEE), // Fond rouge clair pour l'interface admin - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Titre de la page - Text( - 'Historique des passages', - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - ), + return Stack( + children: [ + // Fond dégradé avec petits points blancs + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.white, Colors.red.shade300], ), - - const SizedBox(height: 16), - - // Filtres supplémentaires (secteur, utilisateur, période) - _buildAdditionalFilters(context), - - const SizedBox(height: 16), - - // Widget de liste des passages - Expanded( - child: PassagesListWidget( - passages: _formattedPassages, - showFilters: true, - showSearch: true, - showActions: true, - initialSearchQuery: searchQuery, - initialTypeFilter: selectedType, - initialPaymentFilter: selectedPaymentMethod, - // Exclure les passages de type 2 (À finaliser) - excludePassageTypes: [2], - // Filtres par utilisateur et secteur - filterByUserId: selectedUserId, - filterBySectorId: selectedSectorId, - // Période par défaut (dernier mois) - periodFilter: 'lastMonth', - // Plage de dates personnalisée si définie - dateRange: selectedDateRange, - onPassageSelected: (passage) { - _showDetailsDialog(context, passage); - }, - onReceiptView: (passage) { - _showReceiptDialog(context, passage); - }, - onDetailsView: (passage) { - _showDetailsDialog(context, passage); - }, - onPassageEdit: (passage) { - // Action pour modifier le passage - // Cette fonctionnalité pourrait être implémentée ultérieurement - }, - ), - ), - ], + ), + child: CustomPaint( + painter: DotsPainter(), + child: Container(width: double.infinity, height: double.infinity), + ), ), - ), + // Contenu de la page + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Titre de la page + Text( + 'Historique des passages', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + + const SizedBox(height: 16), + + // Filtres supplémentaires (secteur, utilisateur, période) + _buildAdditionalFilters(context), + + const SizedBox(height: 16), + + // Widget de liste des passages + Expanded( + child: PassagesListWidget( + passages: _formattedPassages, + showFilters: true, + showSearch: true, + showActions: true, + initialSearchQuery: searchQuery, + initialTypeFilter: selectedType, + initialPaymentFilter: selectedPaymentMethod, + // Exclure les passages de type 2 (À finaliser) + excludePassageTypes: [2], + // Filtres par utilisateur et secteur + filterByUserId: selectedUserId, + filterBySectorId: selectedSectorId, + // Période par défaut (dernier mois) + periodFilter: 'lastMonth', + // Plage de dates personnalisée si définie + dateRange: selectedDateRange, + onPassageSelected: (passage) { + _showDetailsDialog(context, passage); + }, + onReceiptView: (passage) { + _showReceiptDialog(context, passage); + }, + onDetailsView: (passage) { + _showDetailsDialog(context, passage); + }, + onPassageEdit: (passage) { + // Action pour modifier le passage + // Cette fonctionnalité pourrait être implémentée ultérieurement + }, + ), + ), + ], + ), + ), + ], ); } // Widget d'erreur pour afficher un message d'erreur Widget _buildErrorWidget(String message) { - return Scaffold( - backgroundColor: - const Color(0xFFFFEBEE), // Fond rouge clair pour l'interface admin - body: Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.error_outline, - color: Colors.red, - size: 64, - ), - const SizedBox(height: 16), - Text( - 'Erreur', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.red[700], - ), - ), - const SizedBox(height: 8), - Text( - message, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 16), - ), - const SizedBox(height: 24), - ElevatedButton( - onPressed: () { - // Recharger la page - setState(() {}); - }, - child: const Text('Réessayer'), - ), - ], + return Stack( + children: [ + // Fond dégradé avec petits points blancs + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.white, Colors.red.shade300], + ), + ), + child: CustomPaint( + painter: DotsPainter(), + child: Container(width: double.infinity, height: double.infinity), ), ), - ), + Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + color: Colors.red, + size: 64, + ), + const SizedBox(height: 16), + Text( + 'Erreur', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.red[700], + ), + ), + const SizedBox(height: 8), + Text( + message, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () { + // Recharger la page + setState(() {}); + }, + child: const Text('Réessayer'), + ), + ], + ), + ), + ), + ], ); } @@ -534,6 +601,7 @@ class _AdminHistoryPageState extends State { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), + color: Colors.white, // Fond opaque child: Padding( padding: const EdgeInsets.all(16.0), child: Column( diff --git a/flutt/lib/presentation/admin/admin_map_page.dart b/app/lib/presentation/admin/admin_map_page.dart similarity index 100% rename from flutt/lib/presentation/admin/admin_map_page.dart rename to app/lib/presentation/admin/admin_map_page.dart diff --git a/app/lib/presentation/admin/admin_statistics_page.dart b/app/lib/presentation/admin/admin_statistics_page.dart new file mode 100644 index 00000000..d4cb9c75 --- /dev/null +++ b/app/lib/presentation/admin/admin_statistics_page.dart @@ -0,0 +1,582 @@ +import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales +import 'package:flutter/material.dart'; +import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales +import 'package:geosector_app/presentation/widgets/charts/activity_chart.dart'; +import 'package:geosector_app/presentation/widgets/charts/passage_pie_chart.dart'; +import 'package:geosector_app/presentation/widgets/charts/payment_pie_chart.dart'; +import 'package:geosector_app/presentation/widgets/charts/payment_data.dart'; +import 'package:geosector_app/presentation/widgets/charts/combined_chart.dart'; +import 'package:geosector_app/core/repositories/passage_repository.dart'; +import 'package:geosector_app/core/repositories/user_repository.dart'; +import '../../shared/app_theme.dart'; +import 'dart:math' as math; + +/// Class pour dessiner les petits points blancs sur le fond +class DotsPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = Colors.white.withOpacity(0.5) + ..style = PaintingStyle.fill; + + final random = math.Random(42); // Seed fixe pour consistance + final numberOfDots = (size.width * size.height) ~/ 1500; + + for (int i = 0; i < numberOfDots; i++) { + final x = random.nextDouble() * size.width; + final y = random.nextDouble() * size.height; + final radius = 1.0 + random.nextDouble() * 2.0; + canvas.drawCircle(Offset(x, y), radius, paint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + +class AdminStatisticsPage extends StatefulWidget { + const AdminStatisticsPage({Key? key}) : super(key: key); + + @override + State createState() => _AdminStatisticsPageState(); +} + +class _AdminStatisticsPageState extends State { + // Filtres + String _selectedPeriod = 'Jour'; + String _selectedFilterType = 'Secteur'; + String _selectedSector = 'Tous'; + String _selectedUser = 'Tous'; + int _daysToShow = 15; + + // Liste des périodes et types de filtre + final List _periods = ['Jour', 'Semaine', 'Mois', 'Année']; + final List _filterTypes = ['Secteur', 'Membre']; + + // Données simulées pour les secteurs et membres (à remplacer par des données réelles) + final List _sectors = [ + 'Tous', + 'Secteur Nord', + 'Secteur Sud', + 'Secteur Est', + 'Secteur Ouest' + ]; + final List _members = [ + 'Tous', + 'Jean Dupont', + 'Marie Martin', + 'Pierre Legrand', + 'Sophie Petit', + 'Lucas Moreau' + ]; + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + final isDesktop = screenWidth > 800; + + return Stack( + children: [ + // Fond dégradé avec petits points blancs + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.white, Colors.red.shade300], + ), + ), + child: CustomPaint( + painter: DotsPainter(), + child: Container(width: double.infinity, height: double.infinity), + ), + ), + // Contenu de la page + SingleChildScrollView( + padding: const EdgeInsets.all(AppTheme.spacingL), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Titre et description + Text( + 'Analyse des statistiques', + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: AppTheme.spacingS), + Text( + 'Visualisez les statistiques de passages et de collecte pour votre amicale.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.grey[600], + ), + ), + const SizedBox(height: AppTheme.spacingL), + + // Filtres + Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(AppTheme.borderRadiusMedium), + ), + color: Colors.white, // Fond opaque + child: Padding( + padding: const EdgeInsets.all(AppTheme.spacingM), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Filtres', + style: + Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: AppTheme.spacingM), + isDesktop + ? Row( + children: [ + Expanded(child: _buildPeriodDropdown()), + const SizedBox(width: AppTheme.spacingM), + Expanded(child: _buildDaysDropdown()), + const SizedBox(width: AppTheme.spacingM), + Expanded(child: _buildFilterTypeDropdown()), + const SizedBox(width: AppTheme.spacingM), + Expanded(child: _buildFilterDropdown()), + ], + ) + : Column( + children: [ + _buildPeriodDropdown(), + const SizedBox(height: AppTheme.spacingM), + _buildDaysDropdown(), + const SizedBox(height: AppTheme.spacingM), + _buildFilterTypeDropdown(), + const SizedBox(height: AppTheme.spacingM), + _buildFilterDropdown(), + ], + ), + ], + ), + ), + ), + const SizedBox(height: AppTheme.spacingL), + + // Graphique d'activité principal + Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(AppTheme.borderRadiusMedium), + ), + color: Colors.white, // Fond opaque + child: Padding( + padding: const EdgeInsets.all(AppTheme.spacingM), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Évolution des passages', + style: + Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: AppTheme.spacingM), + ActivityChart( + height: 350, + loadFromHive: true, + showAllPassages: true, + title: '', + daysToShow: _daysToShow, + periodType: _selectedPeriod, + userId: _selectedUser != 'Tous' + ? _getUserIdFromName(_selectedUser) + : null, + // Si on filtre par secteur, on devrait passer l'ID du secteur + ), + ], + ), + ), + ), + const SizedBox(height: AppTheme.spacingL), + + // Graphiques de répartition + isDesktop + ? Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _buildChartCard( + 'Répartition par type de passage', + PassagePieChart( + size: 300, + loadFromHive: true, + showAllPassages: true, + userId: _selectedUser != 'Tous' + ? _getUserIdFromName(_selectedUser) + : null, + ), + ), + ), + const SizedBox(width: AppTheme.spacingM), + Expanded( + child: _buildChartCard( + 'Répartition par mode de paiement', + PaymentPieChart( + payments: [ + PaymentData( + typeId: 1, + amount: 1500.0, + color: const Color(0xFFFFC107), + icon: Icons.toll, + title: 'Espèce', + ), + PaymentData( + typeId: 2, + amount: 2500.0, + color: const Color(0xFF8BC34A), + icon: Icons.wallet, + title: 'Chèque', + ), + PaymentData( + typeId: 3, + amount: 1000.0, + color: const Color(0xFF00B0FF), + icon: Icons.credit_card, + title: 'CB', + ), + ], + size: 300, + ), + ), + ), + ], + ) + : Column( + children: [ + _buildChartCard( + 'Répartition par type de passage', + PassagePieChart( + size: 300, + loadFromHive: true, + showAllPassages: true, + userId: _selectedUser != 'Tous' + ? _getUserIdFromName(_selectedUser) + : null, + ), + ), + const SizedBox(height: AppTheme.spacingM), + _buildChartCard( + 'Répartition par mode de paiement', + PaymentPieChart( + payments: [ + PaymentData( + typeId: 1, + amount: 1500.0, + color: const Color(0xFFFFC107), + icon: Icons.toll, + title: 'Espèce', + ), + PaymentData( + typeId: 2, + amount: 2500.0, + color: const Color(0xFF8BC34A), + icon: Icons.wallet, + title: 'Chèque', + ), + PaymentData( + typeId: 3, + amount: 1000.0, + color: const Color(0xFF00B0FF), + icon: Icons.credit_card, + title: 'CB', + ), + ], + size: 300, + ), + ), + ], + ), + const SizedBox(height: AppTheme.spacingL), + + // Graphique combiné (si disponible) + _buildChartCard( + 'Comparaison passages/montants', + const SizedBox( + height: 350, + child: Center( + child: Text('Graphique combiné à implémenter'), + ), + ), + ), + + const SizedBox(height: AppTheme.spacingL), + + // Actions + Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(AppTheme.borderRadiusMedium), + ), + color: Colors.white, // Fond opaque + child: Padding( + padding: const EdgeInsets.all(AppTheme.spacingM), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Actions', + style: + Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: AppTheme.spacingM), + Wrap( + spacing: AppTheme.spacingM, + runSpacing: AppTheme.spacingM, + children: [ + ElevatedButton.icon( + onPressed: () { + // Exporter les statistiques + }, + icon: const Icon(Icons.file_download), + label: const Text('Exporter les statistiques'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.primaryColor, + foregroundColor: Colors.white, + ), + ), + ElevatedButton.icon( + onPressed: () { + // Imprimer les statistiques + }, + icon: const Icon(Icons.print), + label: const Text('Imprimer'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.buttonSecondaryColor, + foregroundColor: Colors.white, + ), + ), + ElevatedButton.icon( + onPressed: () { + // Partager les statistiques + }, + icon: const Icon(Icons.share), + label: const Text('Partager'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.accentColor, + foregroundColor: Colors.white, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ], + ); + } + + // Dropdown pour la période + Widget _buildPeriodDropdown() { + return InputDecorator( + decoration: InputDecoration( + labelText: 'Période', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: AppTheme.spacingM, + vertical: AppTheme.spacingS, + ), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: _selectedPeriod, + isDense: true, + isExpanded: true, + items: _periods.map((String period) { + return DropdownMenuItem( + value: period, + child: Text(period), + ); + }).toList(), + onChanged: (String? newValue) { + if (newValue != null) { + setState(() { + _selectedPeriod = newValue; + }); + } + }, + ), + ), + ); + } + + // Dropdown pour le nombre de jours + Widget _buildDaysDropdown() { + return InputDecorator( + decoration: InputDecoration( + labelText: 'Nombre de jours', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: AppTheme.spacingM, + vertical: AppTheme.spacingS, + ), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: _daysToShow, + isDense: true, + isExpanded: true, + items: [7, 15, 30, 60, 90, 180, 365].map((int days) { + return DropdownMenuItem( + value: days, + child: Text('$days jours'), + ); + }).toList(), + onChanged: (int? newValue) { + if (newValue != null) { + setState(() { + _daysToShow = newValue; + }); + } + }, + ), + ), + ); + } + + // Dropdown pour le type de filtre + Widget _buildFilterTypeDropdown() { + return InputDecorator( + decoration: InputDecoration( + labelText: 'Filtrer par', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: AppTheme.spacingM, + vertical: AppTheme.spacingS, + ), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: _selectedFilterType, + isDense: true, + isExpanded: true, + items: _filterTypes.map((String type) { + return DropdownMenuItem( + value: type, + child: Text(type), + ); + }).toList(), + onChanged: (String? newValue) { + if (newValue != null) { + setState(() { + _selectedFilterType = newValue; + // Réinitialiser les filtres spécifiques + _selectedSector = 'Tous'; + _selectedUser = 'Tous'; + }); + } + }, + ), + ), + ); + } + + // Dropdown pour le filtre spécifique (secteur ou membre) + Widget _buildFilterDropdown() { + final List items = + _selectedFilterType == 'Secteur' ? _sectors : _members; + final String value = + _selectedFilterType == 'Secteur' ? _selectedSector : _selectedUser; + + return InputDecorator( + decoration: InputDecoration( + labelText: _selectedFilterType, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: AppTheme.spacingM, + vertical: AppTheme.spacingS, + ), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: value, + isDense: true, + isExpanded: true, + items: items.map((String item) { + return DropdownMenuItem( + value: item, + child: Text(item), + ); + }).toList(), + onChanged: (String? newValue) { + if (newValue != null) { + setState(() { + if (_selectedFilterType == 'Secteur') { + _selectedSector = newValue; + } else { + _selectedUser = newValue; + } + }); + } + }, + ), + ), + ); + } + + // Widget pour envelopper un graphique dans une carte + Widget _buildChartCard(String title, Widget chart) { + return Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), + ), + color: Colors.white, // Fond opaque + child: Padding( + padding: const EdgeInsets.all(AppTheme.spacingM), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: AppTheme.spacingM), + chart, + ], + ), + ), + ); + } + + // Méthode utilitaire pour obtenir l'ID utilisateur à partir de son nom + int? _getUserIdFromName(String name) { + // Dans un cas réel, cela nécessiterait une requête au repository + // Pour l'exemple, on utilise une correspondance simple + if (name == 'Jean Dupont') return 1; + if (name == 'Marie Martin') return 2; + if (name == 'Pierre Legrand') return 3; + if (name == 'Sophie Petit') return 4; + if (name == 'Lucas Moreau') return 5; + return null; + } +} diff --git a/app/lib/presentation/auth/login_page.dart b/app/lib/presentation/auth/login_page.dart new file mode 100644 index 00000000..f0acd726 --- /dev/null +++ b/app/lib/presentation/auth/login_page.dart @@ -0,0 +1,904 @@ +import 'package:flutter/material.dart'; +import 'dart:math' as math; +import 'package:flutter/foundation.dart' show kIsWeb, kDebugMode; +import 'dart:js' as js; +import 'package:go_router/go_router.dart'; +import 'package:go_router/src/state.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:geosector_app/core/repositories/user_repository.dart'; +import 'package:geosector_app/presentation/widgets/custom_button.dart'; +import 'package:geosector_app/presentation/widgets/custom_text_field.dart'; +import 'package:geosector_app/core/services/location_service.dart'; +import 'package:geosector_app/core/services/connectivity_service.dart'; +import 'package:geosector_app/presentation/widgets/connectivity_indicator.dart'; +import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales + +class LoginPage extends StatefulWidget { + final String? loginType; + + const LoginPage({super.key, this.loginType}); + + @override + State createState() => _LoginPageState(); +} + +// Class pour dessiner les petits points blancs sur le fond +class DotsPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = Colors.white.withOpacity(0.5) + ..style = PaintingStyle.fill; + + final random = math.Random(42); // Seed fixe pour consistance + final numberOfDots = (size.width * size.height) ~/ 1500; + + for (int i = 0; i < numberOfDots; i++) { + final x = random.nextDouble() * size.width; + final y = random.nextDouble() * size.height; + final radius = 1.0 + random.nextDouble() * 2.0; + canvas.drawCircle(Offset(x, y), radius, paint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + +class _LoginPageState extends State { + final _formKey = GlobalKey(); + final _usernameController = TextEditingController(); + final _passwordController = TextEditingController(); + final _usernameFocusNode = FocusNode(); + bool _obscurePassword = true; + + // Type de connexion (utilisateur ou administrateur) + late String _loginType; + + // État des permissions de géolocalisation + bool _checkingPermission = true; + bool _hasLocationPermission = false; + String? _locationErrorMessage; + + // État de la connexion Internet + bool _isConnected = false; + + @override + void initState() { + super.initState(); + + // Vérification du type de connexion + if (widget.loginType == null) { + // Si aucun type n'est spécifié, naviguer vers la splash page + print( + 'LoginPage: Aucun type de connexion spécifié, navigation vers splash page'); + WidgetsBinding.instance.addPostFrameCallback((_) { + GoRouter.of(context).go('/'); + }); + _loginType = ''; + } else { + _loginType = widget.loginType!; + print('LoginPage: Type de connexion utilisé: $_loginType'); + } + + // En mode web, essayer de détecter le paramètre dans l'URL directement + // UNIQUEMENT si le loginType n'est pas déjà 'user' + if (kIsWeb && _loginType != 'user') { + try { + final uri = Uri.parse(Uri.base.toString()); + + // 1. Vérifier d'abord si nous avons déjà le paramètre 'type=user' + final typeParam = uri.queryParameters['type']; + if (typeParam != null && typeParam.trim().toLowerCase() == 'user') { + setState(() { + _loginType = 'user'; + }); + } + // 2. Sinon, vérifier le fragment d'URL (hash) + else if (uri.fragment.trim().toLowerCase() == 'user') { + setState(() { + _loginType = 'user'; + }); + } + + // 3. Enfin, si toujours pas de type 'user', vérifier le sessionStorage + if (_loginType != 'user') { + WidgetsBinding.instance.addPostFrameCallback((_) { + try { + // Utiliser une approche plus robuste pour accéder au sessionStorage + // Éviter d'utiliser hasProperty qui peut causer des erreurs + final result = js.context.callMethod('eval', [ + ''' + (function() { + try { + if (window.sessionStorage) { + var value = sessionStorage.getItem('loginType'); + return value; + } + return null; + } catch (e) { + console.error('Error accessing sessionStorage:', e); + return null; + } + })() + ''' + ]); + + if (result != null && + result is String && + result.toLowerCase() == 'user') { + setState(() { + _loginType = 'user'; + print( + 'LoginPage: Type détecté depuis sessionStorage: $_loginType'); + }); + } + } catch (e) { + print('LoginPage: Erreur lors de l\'accès au sessionStorage: $e'); + } + }); + } + } catch (e) { + print('Erreur lors de la récupération des paramètres d\'URL: $e'); + } + } + + // Vérifier les permissions de géolocalisation au démarrage seulement sur mobile + if (!kIsWeb) { + _checkLocationPermission(); + } else { + // En version web, on considère que les permissions sont accordées + setState(() { + _checkingPermission = false; + _hasLocationPermission = true; + }); + } + + // Initialiser l'état de la connexion + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _isConnected = connectivityService.isConnected; + }); + } + }); + + // Pré-remplir le champ username avec l'identifiant du dernier utilisateur connecté + // seulement si le rôle correspond au type de login + WidgetsBinding.instance.addPostFrameCallback((_) { + final users = userRepository.getAllUsers(); + + if (users.isNotEmpty) { + // Trouver l'utilisateur le plus récent (celui avec la date de dernière connexion la plus récente) + users.sort((a, b) => (b.lastSyncedAt).compareTo(a.lastSyncedAt)); + final lastUser = users.first; + + // Convertir le rôle en int si nécessaire + int roleValue; + if (lastUser.role is String) { + roleValue = int.tryParse(lastUser.role as String) ?? 0; + } else { + roleValue = lastUser.role as int; + } + + // Vérifier si le rôle correspond au type de login + bool roleMatches = false; + if (_loginType == 'user' && roleValue == 1) { + roleMatches = true; + debugPrint('Rôle utilisateur (1) correspond au type de login (user)'); + } else if (_loginType == 'admin' && roleValue > 1) { + roleMatches = true; + debugPrint( + 'Rôle administrateur (${roleValue}) correspond au type de login (admin)'); + } + + // Pré-remplir le champ username seulement si le rôle correspond + if (roleMatches) { + // Utiliser le username s'il existe, sinon utiliser l'email comme fallback + if (lastUser.username != null && lastUser.username!.isNotEmpty) { + _usernameController.text = lastUser.username!; + // Déplacer le focus sur le champ mot de passe puisque le username est déjà rempli + _usernameFocusNode.unfocus(); + debugPrint('Champ username pré-rempli avec: ${lastUser.username}'); + } else if (lastUser.email.isNotEmpty) { + _usernameController.text = lastUser.email; + _usernameFocusNode.unfocus(); + debugPrint( + 'Champ username pré-rempli avec email: ${lastUser.email}'); + } + } else { + debugPrint( + 'Le rôle (${roleValue}) ne correspond pas au type de login ($_loginType), champ username non pré-rempli'); + } + } + }); + } + + /// Vérifie les permissions de géolocalisation + Future _checkLocationPermission() async { + // Ne pas vérifier les permissions en version web + if (kIsWeb) { + setState(() { + _hasLocationPermission = true; + _checkingPermission = false; + }); + return; + } + + setState(() { + _checkingPermission = true; + }); + + // Vérifier si les services de localisation sont activés et si l'application a la permission + final hasPermission = await LocationService.checkAndRequestPermission(); + final errorMessage = await LocationService.getLocationErrorMessage(); + + setState(() { + _hasLocationPermission = hasPermission; + _locationErrorMessage = errorMessage; + _checkingPermission = false; + }); + } + + @override + void dispose() { + _usernameController.dispose(); + _passwordController.dispose(); + _usernameFocusNode.dispose(); + super.dispose(); + } + + /// Construit l'écran de chargement pendant la vérification des permissions + Widget _buildLoadingScreen(ThemeData theme) { + return Scaffold( + body: SafeArea( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Logo simlifié + Image.asset( + 'assets/images/logo-geosector-1024.png', + height: 160, + ), + const SizedBox(height: 32), + const CircularProgressIndicator(), + const SizedBox(height: 24), + Text( + 'Vérification des permissions...', + style: theme.textTheme.titleMedium, + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ); + } + + /// Construit l'écran de demande de permission de géolocalisation + Widget _buildLocationPermissionScreen(ThemeData theme) { + return Scaffold( + body: Stack( + children: [ + // Fond dégradé avec petits points blancs + AnimatedContainer( + duration: const Duration(milliseconds: 500), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: _loginType == 'user' + ? [Colors.white, Colors.green.shade300] + : [Colors.white, Colors.red.shade300], + ), + ), + child: CustomPaint( + painter: DotsPainter(), + child: Container(width: double.infinity, height: double.infinity), + ), + ), + SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500), + child: Card( + elevation: 8, + shadowColor: _loginType == 'user' + ? Colors.green.withOpacity(0.5) + : Colors.red.withOpacity(0.5), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0)), + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Logo simplifié + Image.asset( + 'assets/images/logo-geosector-1024.png', + height: 160, + ), + const SizedBox(height: 24), + Text( + 'Accès à la localisation requis', + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.primary, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + + // Message d'erreur + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: theme.colorScheme.error.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: + theme.colorScheme.error.withOpacity(0.3)), + ), + child: Column( + children: [ + Icon( + Icons.location_disabled, + color: theme.colorScheme.error, + size: 48, + ), + const SizedBox(height: 16), + Text( + _locationErrorMessage ?? + 'L\'accès à la localisation est nécessaire pour utiliser cette application.', + style: theme.textTheme.bodyLarge, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Text( + 'Cette application utilise votre position pour enregistrer les passages et assurer le suivi des secteurs géographiques. Sans cette permission, l\'application ne peut pas fonctionner correctement.', + style: theme.textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + ], + ), + ), + const SizedBox(height: 32), + + // Instructions pour activer la localisation + Text( + 'Comment activer la localisation :', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + _buildInstructionStep(theme, 1, + 'Ouvrez les paramètres de votre appareil'), + _buildInstructionStep(theme, 2, + 'Accédez aux paramètres de confidentialité ou de localisation'), + _buildInstructionStep(theme, 3, + 'Recherchez GEOSECTOR dans la liste des applications'), + _buildInstructionStep(theme, 4, + 'Activez l\'accès à la localisation pour cette application'), + const SizedBox(height: 32), + + // Boutons d'action + CustomButton( + onPressed: () async { + // Ouvrir les paramètres de l'application + await LocationService.openAppSettings(); + }, + text: 'Ouvrir les paramètres de l\'application', + icon: Icons.settings, + ), + const SizedBox(height: 16), + CustomButton( + onPressed: () async { + // Ouvrir les paramètres de localisation + await LocationService.openLocationSettings(); + }, + text: 'Ouvrir les paramètres de localisation', + icon: Icons.location_on, + backgroundColor: theme.colorScheme.secondary, + ), + const SizedBox(height: 16), + CustomButton( + onPressed: () { + // Vérifier à nouveau les permissions + _checkLocationPermission(); + }, + text: 'Vérifier à nouveau', + icon: Icons.refresh, + backgroundColor: theme.colorScheme.tertiary, + ), + ], + ), + ), + ), + ), + ), + ), + ), + ], + ), + ); + } + + /// Construit une étape d'instruction pour activer la localisation + Widget _buildInstructionStep( + ThemeData theme, int stepNumber, String instruction) { + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: theme.colorScheme.primary, + shape: BoxShape.circle, + ), + child: Center( + child: Text( + '$stepNumber', + style: TextStyle( + color: theme.colorScheme.onPrimary, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + instruction, + style: theme.textTheme.bodyMedium, + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + print('DEBUG BUILD: Reconstruction de LoginPage avec type: $_loginType'); + + // Utiliser l'instance globale de userRepository + final theme = Theme.of(context); + final size = MediaQuery.of(context).size; + + // Afficher l'écran de permission de géolocalisation si l'utilisateur n'a pas accordé la permission (sauf en version web) + if (!kIsWeb && _checkingPermission) { + return _buildLoadingScreen(theme); + } else if (!kIsWeb && !_hasLocationPermission) { + return _buildLocationPermissionScreen(theme); + } + + return Scaffold( + body: Stack( + children: [ + // Fond dégradé avec petits points blancs + AnimatedContainer( + duration: const Duration(milliseconds: 500), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: _loginType == 'user' + ? [Colors.white, Colors.green.shade300] + : [Colors.white, Colors.red.shade300], + ), + ), + child: CustomPaint( + painter: DotsPainter(), + child: Container(width: double.infinity, height: double.infinity), + ), + ), + SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500), + child: Card( + elevation: 8, + shadowColor: _loginType == 'user' + ? Colors.green.withOpacity(0.5) + : Colors.red.withOpacity(0.5), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0)), + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Logo simplifié avec chemin direct + Image.asset( + 'assets/images/logo-geosector-1024.png', + height: 140, + ), + const SizedBox(height: 24), + Text( + _loginType == 'user' + ? 'Connexion Utilisateur' + : 'Connexion Administrateur', + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: _loginType == 'user' + ? Colors.green + : Colors.red, + ), + textAlign: TextAlign.center, + ), + // Ajouter un texte de débogage uniquement en mode développement + if (kDebugMode) + Text( + 'Type de connexion: $_loginType', + style: + TextStyle(fontSize: 10, color: Colors.grey), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'Bienvenue sur GEOSECTOR', + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.onBackground + .withOpacity(0.7), + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + + // Indicateur de connectivité + ConnectivityIndicator(), + const SizedBox(height: 16), + + // Formulaire de connexion + Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + CustomTextField( + controller: _usernameController, + label: 'Identifiant', + hintText: 'Entrez votre identifiant', + prefixIcon: Icons.person_outline, + keyboardType: TextInputType.text, + autofocus: true, + focusNode: _usernameFocusNode, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer votre identifiant'; + } + return null; + }, + ), + const SizedBox(height: 16), + CustomTextField( + controller: _passwordController, + label: 'Mot de passe', + hintText: 'Entrez votre mot de passe', + prefixIcon: Icons.lock_outline, + obscureText: _obscurePassword, + suffixIcon: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer votre mot de passe'; + } + return null; + }, + onFieldSubmitted: (_) async { + if (!userRepository.isLoading && + _formKey.currentState!.validate()) { + // Vérifier que le type de connexion est spécifié + if (_loginType.isEmpty) { + print( + 'Login: Type non spécifié, redirection vers la page de démarrage'); + context.go('/'); + return; + } + + print( + 'Login: Tentative avec type: $_loginType'); + + final success = + await userRepository.login( + _usernameController.text.trim(), + _passwordController.text, + type: _loginType, + ); + + if (success && mounted) { + // Récupérer directement le rôle de l'utilisateur + final user = + userRepository.getCurrentUser(); + if (user == null) { + debugPrint( + 'ERREUR: Utilisateur non trouvé après connexion réussie'); + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text( + 'Erreur de connexion. Veuillez réessayer.'), + backgroundColor: Colors.red, + ), + ); + return; + } + + // Convertir le rôle en int si nécessaire + int roleValue; + if (user.role is String) { + roleValue = int.tryParse( + user.role as String) ?? + 1; + } else { + roleValue = user.role as int; + } + + debugPrint( + 'Role de l\'utilisateur: $roleValue'); + + // Redirection simple basée sur le rôle + if (roleValue > 1) { + debugPrint( + 'Redirection vers /admin (rôle > 1)'); + context.go('/admin'); + } else { + debugPrint( + 'Redirection vers /user (rôle = 1)'); + context.go('/user'); + } + } else if (mounted) { + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text( + 'Échec de la connexion. Vérifiez vos identifiants.'), + backgroundColor: Colors.red, + ), + ); + } + } + }, + ), + const SizedBox(height: 8), + + // Mot de passe oublié + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () { + // Naviguer vers la page de récupération de mot de passe + }, + child: Text( + 'Mot de passe oublié ?', + style: TextStyle( + color: theme.colorScheme.primary, + ), + ), + ), + ), + const SizedBox(height: 24), + + // Bouton de connexion + CustomButton( + onPressed: (userRepository.isLoading || + !_isConnected) + ? null + : () async { + if (_formKey.currentState! + .validate()) { + // Vérifier à nouveau les permissions de géolocalisation avant de se connecter (sauf en version web) + if (!kIsWeb) { + await _checkLocationPermission(); + + // Si l'utilisateur n'a toujours pas accordé les permissions, ne pas continuer + if (!_hasLocationPermission) { + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text( + 'L\'accès à la localisation est nécessaire pour utiliser cette application.'), + backgroundColor: Colors.red, + ), + ); + return; + } + } + + // Vérifier la connexion Internet + await connectivityService + .checkConnectivity(); + + if (!connectivityService + .isConnected) { + ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: const Text( + 'Aucune connexion Internet. La connexion n\'est pas possible hors ligne.'), + backgroundColor: + theme.colorScheme.error, + duration: const Duration( + seconds: 3), + action: SnackBarAction( + label: 'Réessayer', + onPressed: () async { + await connectivityService + .checkConnectivity(); + if (connectivityService + .isConnected && + mounted) { + ScaffoldMessenger.of( + context) + .showSnackBar( + SnackBar( + content: Text( + 'Connexion Internet ${connectivityService.connectionType} détectée.'), + backgroundColor: + Colors.green, + ), + ); + } + }, + ), + ), + ); + return; + } + + // Vérifier que le type de connexion est spécifié + if (_loginType.isEmpty) { + print( + 'Login: Type non spécifié, redirection vers la page de démarrage'); + context.go('/'); + return; + } + + print( + 'Login: Tentative avec type: $_loginType'); + + // Utiliser directement userRepository avec l'overlay de chargement + final success = await userRepository + .loginWithUI( + context, + _usernameController.text.trim(), + _passwordController.text, + type: _loginType, + ); + + if (success && mounted) { + debugPrint( + 'Connexion réussie, tentative de redirection...'); + + // Récupérer directement le rôle de l'utilisateur + final user = userRepository + .getCurrentUser(); + if (user == null) { + debugPrint( + 'ERREUR: Utilisateur non trouvé après connexion réussie'); + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text( + 'Erreur de connexion. Veuillez réessayer.'), + backgroundColor: Colors.red, + ), + ); + return; + } + + // Convertir le rôle en int si nécessaire + int roleValue; + if (user.role is String) { + roleValue = int.tryParse( + user.role as String) ?? + 1; + } else { + roleValue = user.role as int; + } + + debugPrint( + 'Role de l\'utilisateur: $roleValue'); + + // Redirection simple basée sur le rôle + if (roleValue > 1) { + debugPrint( + 'Redirection vers /admin (rôle > 1)'); + context.go('/admin'); + } else { + debugPrint( + 'Redirection vers /user (rôle = 1)'); + context.go('/user'); + } + } else if (mounted) { + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text( + 'Échec de la connexion. Vérifiez vos identifiants.'), + backgroundColor: Colors.red, + ), + ); + } + } + }, + text: _isConnected + ? 'Se connecter' + : 'Connexion Internet requise', + isLoading: userRepository.isLoading, + ), + const SizedBox(height: 24), + + // Inscription administrateur uniquement + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Pas encore de compte ?', + style: theme.textTheme.bodyMedium, + ), + TextButton( + onPressed: () { + context.go('/register'); + }, + child: Text( + 'Inscription Administrateur', + style: TextStyle( + color: theme.colorScheme.tertiary, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + + // Lien vers la page d'accueil + TextButton( + onPressed: () { + context.go('/'); + }, + child: Text( + 'Retour à l\'accueil', + style: TextStyle( + color: theme.colorScheme.secondary, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/flutt/lib/presentation/auth/register_page.dart b/app/lib/presentation/auth/register_page.dart similarity index 97% rename from flutt/lib/presentation/auth/register_page.dart rename to app/lib/presentation/auth/register_page.dart index 20090fcb..2354983a 100644 --- a/flutt/lib/presentation/auth/register_page.dart +++ b/app/lib/presentation/auth/register_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:geosector_app/core/repositories/user_repository.dart'; import 'package:geosector_app/presentation/widgets/custom_button.dart'; import 'package:geosector_app/presentation/widgets/custom_text_field.dart'; @@ -68,8 +69,8 @@ class _RegisterPageState extends State { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Logo et titre - Image.asset( - 'assets/images/geosector-logo-200.png', + SvgPicture.asset( + 'assets/images/icon-geosector.svg', height: 140, fit: BoxFit.contain, ), @@ -289,13 +290,13 @@ class _RegisterPageState extends State { ], ), - // Lien vers la page publique + // Lien vers la page d'accueil TextButton( onPressed: () { - context.go('/public'); + context.go('/'); }, child: Text( - 'Revenir sur le site GEOSECTOR', + 'Revenir à l\'accueil', style: TextStyle( color: theme.colorScheme.secondary, ), diff --git a/app/lib/presentation/auth/splash_page.dart b/app/lib/presentation/auth/splash_page.dart new file mode 100644 index 00000000..070af06a --- /dev/null +++ b/app/lib/presentation/auth/splash_page.dart @@ -0,0 +1,513 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:geosector_app/core/repositories/user_repository.dart'; +import 'package:geosector_app/core/theme/app_theme.dart'; +import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales +import 'package:geosector_app/core/constants/app_keys.dart'; +import 'package:geosector_app/core/data/models/user_model.dart'; +import 'package:geosector_app/core/data/models/amicale_model.dart'; +import 'package:geosector_app/core/data/models/client_model.dart'; +import 'package:geosector_app/core/data/models/operation_model.dart'; +import 'package:geosector_app/core/data/models/sector_model.dart'; +import 'package:geosector_app/core/data/models/passage_model.dart'; +import 'package:geosector_app/core/data/models/membre_model.dart'; +import 'package:geosector_app/core/data/models/user_sector_model.dart'; +import 'package:geosector_app/chat/models/conversation_model.dart'; +import 'package:geosector_app/chat/models/message_model.dart'; +import 'package:geosector_app/core/services/hive_reset_state_service.dart'; +import 'package:geosector_app/presentation/widgets/clear_cache_dialog.dart'; +import 'dart:async'; +import 'dart:math' as math; + +class SplashPage extends StatefulWidget { + const SplashPage({super.key}); + + @override + State createState() => _SplashPageState(); +} + +// Class pour dessiner les petits points blancs sur le fond +class DotsPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = Colors.white.withOpacity(0.5) + ..style = PaintingStyle.fill; + + final random = math.Random(42); // Seed fixe pour consistance + final numberOfDots = (size.width * size.height) ~/ 1500; + + for (int i = 0; i < numberOfDots; i++) { + final x = random.nextDouble() * size.width; + final y = random.nextDouble() * size.height; + final radius = 1.0 + random.nextDouble() * 2.0; + canvas.drawCircle(Offset(x, y), radius, paint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + +class _SplashPageState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _scaleAnimation; + bool _isInitializing = true; + String _statusMessage = "Initialisation..."; + double _progress = 0.0; + bool _showButtons = false; + + final List _initializationSteps = [ + "Initialisation des services...", + "Vérification de l'authentification...", + "Chargement des données..." + ]; + + @override + void initState() { + super.initState(); + + // Animation controller sur 5 secondes (augmenté de 3 à 5 secondes) + _animationController = AnimationController( + vsync: this, + duration: const Duration(seconds: 5), + ); + + // Animation de 4x la taille à 1x la taille (augmenté de 3x à 4x) + _scaleAnimation = Tween( + begin: 4.0, // Commencer à 4x la taille + end: 1.0, // Terminer à la taille normale + ).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeOutBack, // Curve pour un effet de rebond + ), + ); + + // Démarrer l'animation immédiatement + _animationController.forward(); + + // Vérifier si Hive a été réinitialisé + _checkHiveReset(); + + // Simuler le processus d'initialisation + _startInitialization(); + } + + // Méthode pour vérifier si Hive a été réinitialisé et afficher le dialogue si nécessaire + void _checkHiveReset() { + // Vérifier si Hive a été réinitialisé et si le dialogue n'a pas encore été affiché + if (hiveResetStateService.wasReset && !hiveResetStateService.dialogShown) { + // Attendre que le widget soit complètement construit + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + // Afficher le dialogue de nettoyage du cache + ClearCacheDialog.show( + context, + onClose: () { + // Marquer le dialogue comme ayant été affiché + hiveResetStateService.markDialogAsShown(); + }, + ); + } + }); + } + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + void _startInitialization() async { + // Étape 1: Initialisation des services + if (mounted) { + setState(() { + _statusMessage = _initializationSteps[0]; + _progress = 0.2; + }); + } + + // Initialiser toutes les boîtes Hive + await _initializeAllHiveBoxes(); + await Future.delayed(const Duration(milliseconds: 500)); + + // Étape 2: Vérification de l'authentification + if (mounted) { + setState(() { + _statusMessage = _initializationSteps[1]; + _progress = 0.4; + }); + } + await Future.delayed(const Duration(milliseconds: 500)); + + // Étape 3: Chargement des données + if (mounted) { + setState(() { + _statusMessage = _initializationSteps[2]; + _progress = 1.0; // Directement à 100% après la 3ème étape + }); + } + await Future.delayed(const Duration(milliseconds: 500)); + + if (mounted) { + setState(() { + _isInitializing = false; + _showButtons = true; + }); + + // Attendre quelques secondes avant de rediriger automatiquement + // si l'utilisateur est déjà connecté + if (userRepository.isLoggedIn) { + Timer(const Duration(seconds: 2), () { + _redirectToAppropriateScreen(); + }); + } + } + } + + // Méthode pour initialiser toutes les boîtes Hive + Future _initializeAllHiveBoxes() async { + try { + debugPrint('Initialisation de toutes les boîtes Hive...'); + + // Structure pour les boîtes à ouvrir avec leurs noms d'affichage + final boxesToOpen = [ + {'name': AppKeys.usersBoxName, 'display': 'Préparation utilisateurs'}, + {'name': AppKeys.amicaleBoxName, 'display': 'Préparation amicale'}, + {'name': AppKeys.clientsBoxName, 'display': 'Préparation clients'}, + {'name': AppKeys.regionsBoxName, 'display': 'Préparation régions'}, + { + 'name': AppKeys.operationsBoxName, + 'display': 'Préparation opérations' + }, + {'name': AppKeys.sectorsBoxName, 'display': 'Préparation secteurs'}, + {'name': AppKeys.passagesBoxName, 'display': 'Préparation passages'}, + {'name': AppKeys.membresBoxName, 'display': 'Préparation membres'}, + { + 'name': AppKeys.userSectorBoxName, + 'display': 'Préparation secteurs utilisateurs' + }, + {'name': AppKeys.settingsBoxName, 'display': 'Préparation paramètres'}, + { + 'name': AppKeys.chatConversationsBoxName, + 'display': 'Préparation conversations' + }, + { + 'name': AppKeys.chatMessagesBoxName, + 'display': 'Préparation messages' + }, + ]; + + // Calculer l'incrément de progression pour chaque boîte + final progressIncrement = 0.2 / boxesToOpen.length; + double currentProgress = 0.0; + + // Ouvrir chaque boîte si elle n'est pas déjà ouverte + for (int i = 0; i < boxesToOpen.length; i++) { + final boxName = boxesToOpen[i]['name'] as String; + final displayName = boxesToOpen[i]['display'] as String; + + // Mettre à jour la barre de progression et le message + currentProgress += progressIncrement; + if (mounted) { + setState(() { + _statusMessage = displayName; + _progress = + 0.2 * (currentProgress / 0.2); // Normaliser entre 0 et 0.2 + }); + } + + if (!Hive.isBoxOpen(boxName)) { + debugPrint('Ouverture de la boîte $boxName ($displayName)...'); + + // Ouvrir la boîte avec le type approprié + if (boxName == AppKeys.usersBoxName) { + await Hive.openBox(boxName); + } else if (boxName == AppKeys.amicaleBoxName) { + await Hive.openBox(boxName); + } else if (boxName == AppKeys.clientsBoxName) { + await Hive.openBox(boxName); + } else if (boxName == AppKeys.regionsBoxName) { + // Ouvrir la boîte des régions sans type spécifique pour l'instant + // car RegionModelAdapter n'est pas encore enregistré + await Hive.openBox(boxName); + } else if (boxName == AppKeys.operationsBoxName) { + await Hive.openBox(boxName); + } else if (boxName == AppKeys.sectorsBoxName) { + await Hive.openBox(boxName); + } else if (boxName == AppKeys.passagesBoxName) { + await Hive.openBox(boxName); + } else if (boxName == AppKeys.membresBoxName) { + await Hive.openBox(boxName); + } else if (boxName == AppKeys.userSectorBoxName) { + await Hive.openBox(boxName); + } else if (boxName == AppKeys.chatConversationsBoxName) { + await Hive.openBox(boxName); + } else if (boxName == AppKeys.chatMessagesBoxName) { + await Hive.openBox(boxName); + } else { + await Hive.openBox(boxName); + } + + debugPrint('Boîte $boxName ouverte avec succès'); + } else { + debugPrint('Boîte $boxName déjà ouverte'); + } + + // Ajouter une temporisation entre chaque ouverture + await Future.delayed(const Duration(milliseconds: 500)); + } + + // Mettre à jour la barre de progression à 0.2 (20%) à la fin + if (mounted) { + setState(() { + _statusMessage = 'Toutes les boîtes sont prêtes'; + _progress = 0.2; + }); + } + + debugPrint('Toutes les boîtes Hive sont maintenant ouvertes'); + } catch (e) { + debugPrint('Erreur lors de l\'initialisation des boîtes Hive: $e'); + + // En cas d'erreur, mettre à jour le message + if (mounted) { + setState(() { + _statusMessage = 'Erreur lors de l\'initialisation des données'; + }); + } + } + } + + void _redirectToAppropriateScreen() { + if (!mounted) return; + + // Utiliser l'instance globale de userRepository définie dans app.dart + if (userRepository.isLoggedIn) { + debugPrint('SplashPage: Redirection d\'utilisateur connecté'); + + // Récupérer directement le rôle utilisateur + final roleValue = userRepository.getUserRole(); + debugPrint('SplashPage: Rôle utilisateur = $roleValue'); + + // Redirection simple basée sur le rôle + if (roleValue > 1) { + debugPrint('SplashPage: Redirection vers /admin (rôle $roleValue > 1)'); + context.go('/admin'); + } else { + debugPrint('SplashPage: Redirection vers /user (rôle $roleValue = 1)'); + context.go('/user'); + } + } + // Ne redirige plus vers la landing page + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final size = MediaQuery.of(context).size; + + return Scaffold( + body: Stack( + children: [ + // Fond dégradé avec petits points blancs + AnimatedContainer( + duration: const Duration(milliseconds: 500), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.white, Colors.blue.shade300], + ), + ), + child: CustomPaint( + painter: DotsPainter(), + child: Container(width: double.infinity, height: double.infinity), + ), + ), + + // Contenu principal + SafeArea( + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Spacer(flex: 2), + + // Logo avec animation de réduction + AnimatedBuilder( + animation: _scaleAnimation, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: child, + ); + }, + child: Image.asset( + 'assets/images/logo-geosector-1024.png', + height: 180, // Augmenté de 140 à 180 + ), + ), + + const SizedBox(height: 24), + + // Titre avec animation fade-in + AnimatedOpacity( + opacity: _isInitializing ? 0.9 : 1.0, + duration: const Duration(milliseconds: 500), + child: Text( + 'Geosector', + style: theme.textTheme.headlineLarge?.copyWith( + color: theme.colorScheme.primary, + fontWeight: FontWeight.bold, + letterSpacing: 1.2, + ), + ), + ), + + const SizedBox(height: 16), + + // Sous-titre avec nouveau slogan + AnimatedOpacity( + opacity: _isInitializing ? 0.8 : 1.0, + duration: const Duration(milliseconds: 500), + child: Text( + 'Une application puissante et intuitive de gestion de vos distributions de calendriers', + textAlign: TextAlign.center, + style: theme.textTheme.bodyLarge?.copyWith( + color: + theme.colorScheme.onBackground.withOpacity(0.7), + fontWeight: FontWeight.w500, + ), + ), + ), + + const Spacer(flex: 1), + + // Indicateur de chargement + if (_isInitializing) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: LinearProgressIndicator( + value: _progress, + backgroundColor: Colors.grey.withOpacity(0.2), + valueColor: AlwaysStoppedAnimation( + theme.colorScheme.primary, + ), + minHeight: 10, // Augmenté de 6 à 10 + ), + ), + ), + const SizedBox(height: 16), + Text( + _statusMessage, + style: theme.textTheme.bodyMedium?.copyWith( + color: + theme.colorScheme.onBackground.withOpacity(0.7), + ), + ), + ], + + // Boutons après l'initialisation + if (_showButtons) ...[ + // Bouton Connexion Utilisateur + AnimatedOpacity( + opacity: _showButtons ? 1.0 : 0.0, + duration: const Duration(milliseconds: 500), + child: ElevatedButton( + onPressed: () { + context.go('/login', extra: {'type': 'user'}); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 40, + vertical: 16, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + elevation: 2, + ), + child: const Text( + 'Connexion Utilisateur', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + const SizedBox(height: 16), + + // Bouton Connexion Administrateur + AnimatedOpacity( + opacity: _showButtons ? 1.0 : 0.0, + duration: const Duration(milliseconds: 500), + child: ElevatedButton( + onPressed: () { + context.go('/login', extra: {'type': 'admin'}); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 40, + vertical: 16, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + elevation: 2, + ), + child: const Text( + 'Connexion Administrateur', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + + const SizedBox(height: 16), + + // Lien d'inscription + AnimatedOpacity( + opacity: _showButtons ? 1.0 : 0.0, + duration: const Duration(milliseconds: 500), + child: TextButton( + onPressed: () { + context.go('/register'); + }, + child: Text( + 'Pas encore inscrit ?', + style: TextStyle( + color: theme.colorScheme.primary, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + + const Spacer(flex: 1), + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/app/lib/presentation/public/landing_page.dart b/app/lib/presentation/public/landing_page.dart new file mode 100644 index 00000000..cf526508 --- /dev/null +++ b/app/lib/presentation/public/landing_page.dart @@ -0,0 +1 @@ +// Ce fichier sera supprimé, remplacé par la fonctionnalité directe dans splash_page.dart \ No newline at end of file diff --git a/flutt/lib/presentation/user/user_communication_page.dart b/app/lib/presentation/user/user_communication_page.dart similarity index 100% rename from flutt/lib/presentation/user/user_communication_page.dart rename to app/lib/presentation/user/user_communication_page.dart diff --git a/app/lib/presentation/user/user_dashboard_home_page.dart b/app/lib/presentation/user/user_dashboard_home_page.dart new file mode 100644 index 00000000..4ae3857e --- /dev/null +++ b/app/lib/presentation/user/user_dashboard_home_page.dart @@ -0,0 +1,965 @@ +import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales +import 'package:flutter/material.dart'; +import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales +import 'package:geosector_app/core/repositories/user_repository.dart'; +import 'package:geosector_app/core/repositories/passage_repository.dart'; +import 'package:geosector_app/core/theme/app_theme.dart'; +import 'package:geosector_app/core/constants/app_keys.dart'; +import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart'; +import 'package:geosector_app/presentation/widgets/charts/charts.dart'; + +class UserDashboardHomePage extends StatefulWidget { + const UserDashboardHomePage({super.key}); + + @override + State createState() => _UserDashboardHomePageState(); +} + +class _UserDashboardHomePageState extends State { + // Formater une date au format JJ/MM/YYYY + String _formatDate(DateTime date) { + return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}'; + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + // Utiliser l'instance globale définie dans app.dart + final size = MediaQuery.of(context).size; + final isDesktop = size.width > 900; + + return Scaffold( + backgroundColor: Colors.transparent, + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Builder(builder: (context) { + // Récupérer l'opération actuelle + final operation = userRepository.getCurrentOperation(); + if (operation != null) { + return Text( + '${operation.name} (${_formatDate(operation.dateDebut)}-${_formatDate(operation.dateFin)})', + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.primary, + ), + ); + } else { + return Text( + 'Tableau de bord', + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.primary, + ), + ); + } + }), + const SizedBox(height: 24), + + // Synthèse des passages + _buildSummaryCards(isDesktop), + + const SizedBox(height: 24), + + // Graphique des passages + _buildPassagesChart(context, theme), + + const SizedBox(height: 24), + + // Derniers passages + _buildRecentPassages(context, theme), + ], + ), + ), + ), + ); + } + + // Construction des cartes de synthèse + Widget _buildSummaryCards(bool isDesktop) { + return Column( + children: [ + _buildCombinedPassagesCard(context, isDesktop), + const SizedBox(height: 16), + _buildCombinedPaymentsCard(isDesktop), + ], + ); + } + + // Méthode pour charger les données de règlements de manière asynchrone + Future> _loadPaymentData() async { + // Utiliser un délai plus long pour s'assurer que les données sont chargées + await Future.delayed(const Duration(milliseconds: 1500)); + + // Utiliser les instances globales définies dans app.dart + final currentUser = userRepository.getCurrentUser(); + final int? currentUserId = currentUser?.id; + + // Récupérer tous les passages + final passages = passageRepository.getAllPassages(); + + // Vérifier si les données sont complètement chargées + final int totalPassages = passages.length; + debugPrint( + 'Nombre total de passages chargés pour règlements: $totalPassages'); + + // Si le nombre de passages est trop faible, on considère que les données ne sont pas complètement chargées + if (totalPassages < 100 && !_dataFullyLoaded) { + // Attendre un peu plus et réessayer + await Future.delayed(const Duration(milliseconds: 1000)); + // Récupérer à nouveau les passages + final newPassages = passageRepository.getAllPassages(); + final newTotalPassages = newPassages.length; + debugPrint( + 'Nouveau nombre total de passages chargés pour règlements: $newTotalPassages'); + + // Si le nombre a augmenté, utiliser les nouvelles données + if (newTotalPassages > totalPassages) { + passages.clear(); + passages.addAll(newPassages); + debugPrint( + 'Utilisation des nouvelles données de passages pour règlements'); + } + } + + // Initialiser les montants par type de règlement + final Map paymentAmounts = { + 0: 0.0, // Pas de règlement + 1: 0.0, // Espèces + 2: 0.0, // Chèques + 3: 0.0, // CB + }; + + // Compteur pour les passages avec montant > 0 + int passagesWithPaymentCount = 0; + + // Parcourir les passages et calculer les montants par type de règlement + for (final passage in passages) { + // Vérifier si le passage appartient à l'utilisateur actuel + if (currentUserId != null && passage.fkUser == currentUserId) { + final int typeReglement = passage.fkTypeReglement; + + // Convertir la chaîne de montant en double + double montant = 0.0; + try { + // Gérer les formats possibles (virgule ou point) + String montantStr = passage.montant.replaceAll(',', '.'); + montant = double.tryParse(montantStr) ?? 0.0; + } catch (e) { + debugPrint('Erreur de conversion du montant: ${passage.montant}'); + } + + // Ne compter que les passages avec un montant > 0 + if (montant > 0) { + passagesWithPaymentCount++; + + // Ajouter au montant total par type de règlement + if (paymentAmounts.containsKey(typeReglement)) { + paymentAmounts[typeReglement] = + (paymentAmounts[typeReglement] ?? 0.0) + montant; + } else { + // Si le type n'est pas dans notre map, l'ajouter à la catégorie par défaut (0: Pas de règlement) + paymentAmounts[0] = (paymentAmounts[0] ?? 0.0) + montant; + // Type de règlement inconnu, ajouté à la catégorie 'Pas de règlement' + } + } + } + } + + // Afficher les montants par type de règlement pour le débogage + debugPrint('=== MONTANTS PAR TYPE DE RÈGLEMENT ==='); + paymentAmounts.forEach((typeId, amount) { + final typeTitle = AppKeys.typesReglements[typeId]?['titre'] ?? 'Inconnu'; + debugPrint('Type $typeId ($typeTitle): ${amount.toStringAsFixed(2)} €'); + }); + debugPrint('====================================='); + + // Calculer le total des règlements + final double totalPayments = + paymentAmounts.values.fold(0.0, (sum, amount) => sum + amount); + + // Convertir les montants en objets PaymentData pour le graphique + final List paymentDataList = + PaymentUtils.getPaymentDataFromAmounts(paymentAmounts); + + // Vérifier si des types de règlement ont un montant de 0 + // Si c'est le cas, ajouter un petit montant pour qu'ils apparaissent dans le graphique + for (var payment in paymentDataList) { + if (payment.amount == 0 && payment.typeId != 0) { + // Ignorer le type 0 (Pas de règlement) + debugPrint( + 'Type ${payment.typeId} (${payment.title}) a un montant de 0, ajout d\'un petit montant pour l\'affichage'); + // Trouver l'index dans la liste + final index = paymentDataList.indexOf(payment); + // Remplacer par un nouvel objet avec un petit montant + paymentDataList[index] = PaymentData( + typeId: payment.typeId, + amount: 0.01, // Petit montant pour qu'il apparaisse dans le graphique + color: payment.color, + icon: payment.icon, + title: payment.title, + ); + } + } + + // Retourner les données calculées + return { + 'paymentAmounts': paymentAmounts, + 'totalPayments': totalPayments, + 'passagesWithPaymentCount': passagesWithPaymentCount, + 'paymentDataList': paymentDataList, + }; + } + + // Construction d'une carte combinée pour les règlements (liste + graphique) + Widget _buildCombinedPaymentsCard(bool isDesktop) { + return FutureBuilder>( + // Utiliser un Future pour s'assurer que les données sont chargées + future: _loadPaymentData(), + builder: (context, snapshot) { + // Afficher un spinner pendant le chargement + if (snapshot.connectionState == ConnectionState.waiting) { + return Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: const SizedBox( + height: 300, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Chargement des données de règlements...'), + ], + ), + ), + ), + ); + } + + // En cas d'erreur + if (snapshot.hasError) { + return Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: SizedBox( + height: 300, + child: Center( + child: Text( + 'Erreur lors du chargement des données: ${snapshot.error}'), + ), + ), + ); + } + + // Si les données sont disponibles + if (snapshot.hasData) { + final data = snapshot.data!; + final paymentAmounts = + Map.from(data['paymentAmounts'] as Map); + final totalPayments = data['totalPayments'] as double; + final passagesWithPaymentCount = + data['passagesWithPaymentCount'] as int; + final paymentDataList = data['paymentDataList'] as List; + + return Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Stack( + children: [ + // Symbole euro en arrière-plan + Positioned.fill( + child: Center( + child: Icon( + Icons.euro_symbol, + size: 180, + color: Colors.blue.withOpacity(0.07), // Bleuté et estompé + ), + ), + ), + // Contenu principal + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.payments, + color: AppTheme.accentColor, + size: 24, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Mes règlements sur $passagesWithPaymentCount passages', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + Text( + '${totalPayments.toStringAsFixed(2)} €', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: AppTheme.accentColor, + ), + ), + ], + ), + const Divider(height: 24), + SizedBox( + height: 250, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Liste des règlements (côté gauche) + Expanded( + flex: isDesktop ? 1 : 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...AppKeys.typesReglements.entries + .map((entry) { + final int typeId = entry.key; + final Map typeData = + entry.value; + final double amount = + paymentAmounts[typeId] ?? 0.0; + final Color color = + Color(typeData['couleur'] as int); + + return Padding( + padding: + const EdgeInsets.only(bottom: 8.0), + child: Row( + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + child: Icon( + typeData['icon_data'] as IconData, + color: Colors.white, + size: 16, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + typeData['titre'] as String, + style: const TextStyle( + fontSize: 14, + ), + ), + ), + Text( + '${amount.toStringAsFixed(2)} €', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: color, + ), + ), + ], + ), + ); + }).toList(), + ], + ), + ), + + // Séparateur vertical + if (isDesktop) const VerticalDivider(width: 24), + + // Graphique en camembert (côté droit) + Expanded( + flex: isDesktop ? 1 : 2, + child: Container( + padding: const EdgeInsets.all(4.0), + // Réduire légèrement la taille pour éviter la troncature + child: FittedBox( + fit: BoxFit.contain, + child: SizedBox( + width: + 200, // Taille réduite pour éviter la troncature + height: 200, + child: PaymentPieChart( + payments: paymentDataList, + size: + 200, // Taille fixe au lieu de double.infinity + labelSize: + 10, // Réduire davantage la taille des étiquettes + showPercentage: true, + showIcons: false, // Désactiver les icônes + showLegend: false, + isDonut: true, + innerRadius: + '55%', // Augmenter légèrement le rayon interne + enable3DEffect: + false, // Désactiver l'effet 3D pour préserver les couleurs originales + effect3DIntensity: + 0.0, // Pas d'intensité 3D + enableEnhancedExplode: + false, // Désactiver l'effet d'explosion amélioré + useGradient: + false, // Ne pas utiliser de dégradés + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } + + // Par défaut, retourner un widget vide + return const SizedBox.shrink(); + }, + ); + } + + // Variable pour suivre si les données sont complètement chargées + bool _dataFullyLoaded = false; + + // Méthode pour charger les données de passages de manière asynchrone + Future> _loadPassageData() async { + // Utiliser un délai plus long pour s'assurer que les données sont chargées + await Future.delayed(const Duration(milliseconds: 1500)); + + // Utiliser les instances globales définies dans app.dart + final currentUser = userRepository.getCurrentUser(); + final int? currentUserId = currentUser?.id; + + // Récupérer tous les passages + final passages = passageRepository.getAllPassages(); + + // Vérifier si les données sont complètement chargées + final int totalPassages = passages.length; + debugPrint('Nombre total de passages chargés: $totalPassages'); + + // Si le nombre de passages est trop faible, on considère que les données ne sont pas complètement chargées + if (totalPassages < 100 && !_dataFullyLoaded) { + // Attendre un peu plus et réessayer + await Future.delayed(const Duration(milliseconds: 1000)); + // Récupérer à nouveau les passages + final newPassages = passageRepository.getAllPassages(); + final newTotalPassages = newPassages.length; + debugPrint('Nouveau nombre total de passages chargés: $newTotalPassages'); + + // Si le nombre a augmenté, utiliser les nouvelles données + if (newTotalPassages > totalPassages) { + passages.clear(); + passages.addAll(newPassages); + debugPrint('Utilisation des nouvelles données de passages'); + } + } + + // Marquer les données comme complètement chargées pour éviter de refaire cette vérification + _dataFullyLoaded = true; + + // Compter les passages par type + final Map passagesCounts = { + 1: 0, // Effectués + 2: 0, // À finaliser + 3: 0, // Refusés + 4: 0, // Dons + 5: 0, // Lots + 6: 0, // Maisons vides + }; + + // Créer un map pour compter les types de passages + final Map typesCount = {}; + final Map userTypesCount = {}; + + // Parcourir les passages et les compter par type + for (final passage in passages) { + final typeId = passage.fkType; + final int passageUserId = passage.fkUser; + + // Compter les occurrences de chaque type pour le débogage + typesCount[typeId] = (typesCount[typeId] ?? 0) + 1; + + // Vérifier si le passage appartient à l'utilisateur actuel ou est de type 2 + bool shouldCount = typeId == 2 || + (currentUserId != null && passageUserId == currentUserId); + + if (shouldCount) { + // Compter pour les statistiques de l'utilisateur + userTypesCount[typeId] = (userTypesCount[typeId] ?? 0) + 1; + + // Ajouter au compteur des passages par type + if (passagesCounts.containsKey(typeId)) { + passagesCounts[typeId] = passagesCounts[typeId]! + 1; + } else { + // Si le type n'est pas dans notre map, l'ajouter à la catégorie par défaut (2: À finaliser) + passagesCounts[2] = passagesCounts[2]! + 1; + // Type de passage inconnu ajouté à 'A finaliser' + } + } + } + + // Calculer le total des passages pour l'utilisateur (somme des valeurs dans userTypesCount) + final int totalUserPassages = + userTypesCount.values.fold(0, (sum, count) => sum + count); + + // Retourner les données calculées + return { + 'passagesCounts': passagesCounts, + 'totalUserPassages': totalUserPassages, + }; + } + + // Construction d'une carte combinée pour les passages (liste + graphique) + Widget _buildCombinedPassagesCard(BuildContext context, bool isDesktop) { + return FutureBuilder>( + // Utiliser un Future pour s'assurer que les données sont chargées + future: _loadPassageData(), + builder: (context, snapshot) { + // Afficher un spinner pendant le chargement + if (snapshot.connectionState == ConnectionState.waiting) { + return Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: const SizedBox( + height: 300, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Chargement des données de passages...'), + ], + ), + ), + ), + ); + } + + // En cas d'erreur + if (snapshot.hasError) { + return Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: SizedBox( + height: 300, + child: Center( + child: Text( + 'Erreur lors du chargement des données: ${snapshot.error}'), + ), + ), + ); + } + + // Si les données sont disponibles + if (snapshot.hasData) { + final data = snapshot.data!; + final passagesCounts = + Map.from(data['passagesCounts'] as Map); + final totalUserPassages = data['totalUserPassages'] as int; + + return Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.route, + color: AppTheme.primaryColor, + size: 24, + ), + const SizedBox(width: 8), + Expanded( + child: Builder(builder: (context) { + return Text( + 'Mes passages', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ); + }), + ), + Text( + totalUserPassages.toString(), + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + ), + ], + ), + const Divider(height: 24), + SizedBox( + height: 250, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Liste des passages (côté gauche) + Expanded( + flex: isDesktop ? 1 : 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...AppKeys.typesPassages.entries.map((entry) { + final int typeId = entry.key; + final Map typeData = + entry.value; + final int count = passagesCounts[typeId] ?? 0; + final Color color = + Color(typeData['couleur2'] as int); + final IconData iconData = + typeData['icon_data'] as IconData; + + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + child: Icon( + iconData, + color: Colors.white, + size: 16, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + typeData['titres'] as String, + style: const TextStyle( + fontSize: 14, + ), + ), + ), + Text( + count.toString(), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: color, + ), + ), + ], + ), + ); + }).toList(), + ], + ), + ), + + // Séparateur vertical + if (isDesktop) const VerticalDivider(width: 24), + + // Graphique en camembert (côté droit) + Expanded( + flex: isDesktop ? 1 : 2, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: PassagePieChart( + passagesByType: passagesCounts, + size: double.infinity, + labelSize: 12, + showPercentage: true, + showIcons: false, + showLegend: false, + isDonut: true, + innerRadius: '50%', + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + // Par défaut, retourner un widget vide + return const SizedBox.shrink(); + }, + ); + } + + // Construction du graphique des passages + Widget _buildPassagesChart(BuildContext context, ThemeData theme) { + // Définir les types de passages à exclure + // Selon la mémoire, le type 2 correspond aux passages "A finaliser" + // et nous voulons les exclure du comptage pour l'utilisateur actuel + final List excludePassageTypes = [2]; + + // Utiliser le même mécanisme de chargement asynchrone que pour les autres graphiques + return FutureBuilder>( + // Utiliser le même Future que pour le graphique des passages + future: _loadPassageData(), + builder: (context, snapshot) { + // Afficher un spinner pendant le chargement + if (snapshot.connectionState == ConnectionState.waiting) { + return Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: const SizedBox( + height: 350, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Chargement des données d\'activité...'), + ], + ), + ), + ), + ); + } + + // En cas d'erreur + if (snapshot.hasError) { + return Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: SizedBox( + height: 350, + child: Center( + child: Text( + 'Erreur lors du chargement des données: ${snapshot.error}'), + ), + ), + ); + } + + // Si les données sont disponibles, afficher le graphique + return Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Titre supprimé car déjà présent dans le widget ActivityChart + SizedBox( + height: + 350, // Augmentation de la hauteur à 350px pour résoudre le problème de l'axe Y + child: ActivityChart( + // Utiliser le chargement depuis Hive directement dans le widget + loadFromHive: true, + // Ne pas filtrer par utilisateur (afficher tous les passages) + showAllPassages: true, + // Exclure les passages de type 2 (A finaliser) + excludePassageTypes: excludePassageTypes, + // Afficher les 15 derniers jours + daysToShow: 15, + periodType: 'Jour', + height: + 350, // Augmentation de la hauteur à 350px aussi dans le widget + ), + ), + ], + ), + ), + ); + }, + ); + } + + // Construction de la liste des derniers passages + Widget _buildRecentPassages(BuildContext context, ThemeData theme) { + // Utiliser le même mécanisme de chargement asynchrone que pour les autres widgets + return FutureBuilder>( + // Utiliser le même Future que pour les autres widgets + future: _loadPassageData(), + builder: (context, snapshot) { + // Afficher un spinner pendant le chargement + if (snapshot.connectionState == ConnectionState.waiting) { + return Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: const SizedBox( + height: 300, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Chargement des derniers passages...'), + ], + ), + ), + ), + ); + } + + // En cas d'erreur + if (snapshot.hasError) { + return Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: SizedBox( + height: 300, + child: Center( + child: Text( + 'Erreur lors du chargement des données: ${snapshot.error}'), + ), + ), + ); + } + + // Si les données sont disponibles, afficher la liste des passages récents + // Utiliser les instances globales définies dans app.dart + final allPassages = passageRepository.getAllPassages(); + allPassages.sort((a, b) => b.passedAt.compareTo(a.passedAt)); + + // Limiter aux 10 passages les plus récents + final recentPassagesModels = allPassages.take(10).toList(); + + // Convertir les modèles de passage au format attendu par le widget PassagesListWidget + final List> recentPassages = + recentPassagesModels.map((passage) { + // Construire l'adresse complète à partir des champs disponibles + final String address = + '${passage.numero} ${passage.rue}${passage.rueBis.isNotEmpty ? ' ${passage.rueBis}' : ''}, ${passage.ville}'; + + // Convertir le montant en double + final double amount = double.tryParse(passage.montant) ?? 0.0; + + return { + 'id': passage.id.toString(), + 'address': address, + 'amount': amount, + 'date': passage.passedAt, + 'type': passage.fkType, + 'payment': passage.fkTypeReglement, + 'name': passage.name, + 'notes': passage.remarque, + 'hasReceipt': passage.nomRecu.isNotEmpty, + 'hasError': passage.emailErreur.isNotEmpty, + 'fkUser': passage.fkUser, // Ajouter l'ID de l'utilisateur + }; + }).toList(); + + return Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Derniers passages', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + TextButton( + onPressed: () { + // Naviguer vers la page d'historique + }, + child: const Text('Voir tout'), + ), + ], + ), + ), + // Utilisation du widget commun PassagesListWidget + PassagesListWidget( + passages: recentPassages, + showFilters: false, + showSearch: false, + showActions: true, // Activer l'affichage des boutons d'action + maxPassages: 10, + // Exclure les passages de type 2 (À finaliser) + excludePassageTypes: [2], + // Filtrer par utilisateur courant + filterByUserId: userRepository.getCurrentUser()?.id, + // Période par défaut (derniers 15 jours) + periodFilter: 'last15', + onPassageSelected: (passage) { + // Action lors de la sélection d'un passage + debugPrint('Passage sélectionné: ${passage['id']}'); + }, + onDetailsView: (passage) { + // Action lors de l'affichage des détails + debugPrint('Affichage des détails: ${passage['id']}'); + }, + // Callback pour le bouton de modification + onPassageEdit: (passage) { + // Action lors de la modification d'un passage + debugPrint('Modification du passage: ${passage['id']}'); + // Ici, vous pourriez ouvrir un formulaire d'édition + }, + // Callback pour le bouton de reçu (uniquement pour les passages de type 1) + onReceiptView: (passage) { + // Action lors de la demande d'affichage du reçu + debugPrint( + 'Affichage du reçu pour le passage: ${passage['id']}'); + // Ici, vous pourriez générer et afficher un PDF + }, + ), + ], + ), + ); + }, + ); + } +} diff --git a/flutt/lib/presentation/user/user_dashboard_page.dart b/app/lib/presentation/user/user_dashboard_page.dart similarity index 97% rename from flutt/lib/presentation/user/user_dashboard_page.dart rename to app/lib/presentation/user/user_dashboard_page.dart index 8bb4573b..08d6ffde 100644 --- a/flutt/lib/presentation/user/user_dashboard_page.dart +++ b/app/lib/presentation/user/user_dashboard_page.dart @@ -6,7 +6,6 @@ import 'package:hive_flutter/hive_flutter.dart'; import 'package:geosector_app/core/repositories/user_repository.dart'; import 'package:geosector_app/core/theme/app_theme.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; -import 'package:geosector_app/core/services/auth_service.dart'; import 'package:geosector_app/presentation/widgets/dashboard_layout.dart'; // Import des pages utilisateur @@ -105,11 +104,9 @@ class _UserDashboardPageState extends State { label: const Text('Se déconnecter', style: TextStyle(color: Colors.white)), onPressed: () async { - final authService = AuthService(userRepository); - await authService.logout(context); - if (mounted) { - context.go('/login'); - } + // Utiliser directement userRepository pour la déconnexion + await userRepository.logoutWithUI(context); + // La redirection est gérée dans logoutWithUI }, style: TextButton.styleFrom( backgroundColor: AppTheme.accentColor, @@ -137,7 +134,7 @@ class _UserDashboardPageState extends State { NavigationDestination( icon: Icon(Icons.dashboard_outlined), selectedIcon: Icon(Icons.dashboard), - label: 'Accueil', + label: 'Tableau de bord', ), NavigationDestination( icon: Icon(Icons.bar_chart_outlined), diff --git a/flutt/lib/presentation/user/user_history_page.dart b/app/lib/presentation/user/user_history_page.dart similarity index 100% rename from flutt/lib/presentation/user/user_history_page.dart rename to app/lib/presentation/user/user_history_page.dart diff --git a/flutt/lib/presentation/user/user_map_page.dart b/app/lib/presentation/user/user_map_page.dart similarity index 100% rename from flutt/lib/presentation/user/user_map_page.dart rename to app/lib/presentation/user/user_map_page.dart diff --git a/flutt/lib/presentation/user/user_statistics_page.dart b/app/lib/presentation/user/user_statistics_page.dart similarity index 100% rename from flutt/lib/presentation/user/user_statistics_page.dart rename to app/lib/presentation/user/user_statistics_page.dart diff --git a/app/lib/presentation/widgets/amicale_row_widget.dart b/app/lib/presentation/widgets/amicale_row_widget.dart new file mode 100644 index 00000000..6386d420 --- /dev/null +++ b/app/lib/presentation/widgets/amicale_row_widget.dart @@ -0,0 +1,167 @@ +import 'package:flutter/material.dart'; +import 'package:geosector_app/app.dart'; +import 'package:geosector_app/core/data/models/amicale_model.dart'; + +/// Widget pour afficher une ligne du tableau d'amicales +/// Affiche les colonnes id, name, codePostal, libRegion et une colonne Actions +/// La colonne Actions contient un bouton Delete pour les utilisateurs avec rôle > 2 +/// La ligne entière est cliquable pour afficher les détails de l'amicale +class AmicaleRowWidget extends StatelessWidget { + final AmicaleModel amicale; + final Function(AmicaleModel)? onTap; + final Function(AmicaleModel)? onDelete; + final bool isHeader; + final bool isAlternate; + + const AmicaleRowWidget({ + Key? key, + required this.amicale, + this.onTap, + this.onDelete, + this.isHeader = false, + this.isAlternate = false, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final userRole = userRepository.getUserRole(); + + // Définir les styles en fonction du type de ligne (en-tête ou données) + final textStyle = isHeader + ? theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.primary, + ) + : theme.textTheme.bodyMedium; + + // Couleur de fond en fonction du type de ligne + final backgroundColor = isHeader + ? theme.colorScheme.primary.withOpacity(0.1) + : (isAlternate + ? theme.colorScheme.surface + : theme.colorScheme.background); + + return InkWell( + onTap: isHeader || onTap == null ? null : () => onTap!(amicale), + child: Container( + decoration: BoxDecoration( + color: backgroundColor, + border: Border( + bottom: BorderSide( + color: theme.dividerColor.withOpacity(0.3), + width: 1, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + // Colonne ID + Expanded( + flex: 1, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + isHeader ? 'ID' : amicale.id.toString(), + style: textStyle, + overflow: TextOverflow.ellipsis, + ), + ), + ), + + // Colonne Nom + Expanded( + flex: 4, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + isHeader ? 'Nom' : amicale.name, + style: textStyle, + overflow: TextOverflow.ellipsis, + ), + ), + ), + + // Colonne Code Postal + Expanded( + flex: 2, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + isHeader ? 'Code Postal' : amicale.codePostal, + style: textStyle, + overflow: TextOverflow.ellipsis, + ), + ), + ), + + // Colonne Ville + Expanded( + flex: 2, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + isHeader ? 'Ville' : (amicale.ville ?? ''), + style: textStyle, + overflow: TextOverflow.ellipsis, + ), + ), + ), + + // Colonne Région + Expanded( + flex: 3, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + isHeader ? 'Région' : (amicale.libRegion ?? ''), + style: textStyle, + overflow: TextOverflow.ellipsis, + ), + ), + ), + + // Colonne Actions - seulement si l'utilisateur a le rôle > 2 et onDelete n'est pas null + if (isHeader || (userRole > 2 && onDelete != null)) + Expanded( + flex: 2, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: isHeader + ? Text( + 'Actions', + style: textStyle, + overflow: TextOverflow.ellipsis, + ) + : Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + // Bouton Delete + IconButton( + icon: Icon( + Icons.delete, + color: theme.colorScheme.error, + size: 20, + ), + tooltip: 'Supprimer', + onPressed: () => onDelete!(amicale), + constraints: const BoxConstraints( + minWidth: 36, + minHeight: 36, + ), + padding: EdgeInsets.zero, + visualDensity: VisualDensity.compact, + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/app/lib/presentation/widgets/amicale_table_widget.dart b/app/lib/presentation/widgets/amicale_table_widget.dart new file mode 100644 index 00000000..b35ee4ad --- /dev/null +++ b/app/lib/presentation/widgets/amicale_table_widget.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; +import 'package:geosector_app/app.dart'; +import 'package:geosector_app/core/data/models/amicale_model.dart'; +import 'package:geosector_app/core/repositories/region_repository.dart'; +import 'package:geosector_app/core/repositories/user_repository.dart'; +import 'package:geosector_app/presentation/widgets/amicale_row_widget.dart'; +import 'package:geosector_app/presentation/widgets/entite_form.dart'; +import 'package:provider/provider.dart'; + +/// Widget de tableau pour afficher une liste d'amicales +/// +/// Ce widget affiche un tableau avec les colonnes : +/// - ID +/// - Nom +/// - Code Postal +/// - Région +/// - Actions (boutons selon les droits de l'utilisateur) +/// +/// Lorsqu'on clique sur une ligne, une modale s'affiche avec le formulaire EntiteForm +class AmicaleTableWidget extends StatelessWidget { + final List amicales; + final Function(AmicaleModel)? onDelete; + final bool isLoading; + final String? emptyMessage; + final bool readOnly; + + const AmicaleTableWidget({ + Key? key, + required this.amicales, + this.onDelete, + this.isLoading = false, + this.emptyMessage, + this.readOnly = false, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // En-tête du tableau - utiliser AmicaleRowWidget pour l'en-tête + AmicaleRowWidget( + amicale: AmicaleModel( + id: 0, + name: '', + codePostal: '', + ville: '', + libRegion: '', + ), + isHeader: true, + onTap: null, + onDelete: null, + ), + + // Corps du tableau + Container( + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(8), + bottomRight: Radius.circular(8), + ), + border: Border.all( + color: theme.colorScheme.primary.withOpacity(0.1), + width: 1, + ), + ), + child: _buildTableContent(context), + ), + ], + ); + } + + Widget _buildTableContent(BuildContext context) { + // Afficher un indicateur de chargement si isLoading est true + if (isLoading) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 32.0), + child: Center(child: CircularProgressIndicator()), + ); + } + + // Afficher un message si la liste est vide + if (amicales.isEmpty) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 32.0), + child: Center( + child: Text( + emptyMessage ?? 'Aucune amicale trouvée', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: + Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + ), + ); + } + + // Afficher la liste des amicales + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: amicales.length, + itemBuilder: (context, index) { + final amicale = amicales[index]; + return AmicaleRowWidget( + amicale: amicale, + isAlternate: index % 2 == 1, // Alterner les couleurs + onTap: (selectedAmicale) => + _showAmicaleDetails(context, selectedAmicale), + onDelete: onDelete, + ); + }, + ); + } + + // Afficher une modale avec le formulaire EntiteForm + void _showAmicaleDetails(BuildContext context, AmicaleModel amicale) { + // Utiliser l'instance globale de userRepository définie dans app.dart + final userRepo = userRepository; + // Créer une instance de RegionRepository + final regionRepo = RegionRepository(); + + showDialog( + context: context, + builder: (dialogContext) => MultiProvider( + providers: [ + // Fournir les repositories nécessaires au formulaire + Provider.value(value: userRepo), + Provider.value(value: regionRepo), + ], + child: Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Container( + width: MediaQuery.of(dialogContext).size.width * 0.6, + padding: const EdgeInsets.all(24), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Détails de l\'amicale', + style: Theme.of(dialogContext) + .textTheme + .headlineSmall + ?.copyWith( + fontWeight: FontWeight.bold, + color: + Theme.of(dialogContext).colorScheme.primary, + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(dialogContext).pop(), + ), + ], + ), + const SizedBox(height: 16), + // Formulaire EntiteForm en mode lecture seule + EntiteForm( + amicale: amicale, + readOnly: readOnly, + onSubmit: (updatedAmicale) { + Navigator.of(dialogContext).pop(); + // Ici, vous pourriez ajouter une logique pour mettre à jour l'amicale + }, + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/flutt/lib/presentation/widgets/charts/activity_chart.dart b/app/lib/presentation/widgets/charts/activity_chart.dart similarity index 96% rename from flutt/lib/presentation/widgets/charts/activity_chart.dart rename to app/lib/presentation/widgets/charts/activity_chart.dart index 534fb046..e8c9e443 100644 --- a/flutt/lib/presentation/widgets/charts/activity_chart.dart +++ b/app/lib/presentation/widgets/charts/activity_chart.dart @@ -51,6 +51,9 @@ class ActivityChart extends StatefulWidget { /// Si vrai, n'applique aucun filtrage par utilisateur (affiche tous les passages) final bool showAllPassages; + /// Si vrai, force le rechargement des données + final bool forceRefresh; + const ActivityChart({ super.key, this.passageData, @@ -66,6 +69,7 @@ class ActivityChart extends StatefulWidget { this.columnWidth = 0.8, this.columnSpacing = 0.2, this.showAllPassages = false, + this.forceRefresh = false, }) : assert(loadFromHive || passageData != null, 'Soit loadFromHive doit être true, soit passageData doit être fourni'); @@ -140,10 +144,10 @@ class _ActivityChartState extends State } void _loadData() { - // Si les données ont déjà été chargées, ne pas les recharger - if (_dataLoaded) return; + // Marquer comme chargé immédiatement pour éviter les appels multiples pendant le chargement + // Mais permettre un rechargement ultérieur si nécessaire + if (_dataLoaded && _hasData) return; - // Marquer comme chargé immédiatement pour éviter les appels multiples _dataLoaded = true; setState(() { @@ -224,9 +228,13 @@ class _ActivityChartState extends State !listEquals( oldWidget.excludePassageTypes, widget.excludePassageTypes) || oldWidget.showAllPassages != widget.showAllPassages; + final bool refreshForced = widget.forceRefresh && !oldWidget.forceRefresh; - // Si des paramètres importants ont changé, recharger les données - if (periodChanged || dataSourceChanged || filteringChanged) { + // Si des paramètres importants ont changé ou si forceRefresh est passé à true, recharger les données + if (periodChanged || + dataSourceChanged || + filteringChanged || + refreshForced) { _selectedDays = widget.daysToShow; _dataLoaded = false; // Réinitialiser l'état pour forcer le rechargement _loadData(); diff --git a/flutt/lib/presentation/widgets/charts/charts.dart b/app/lib/presentation/widgets/charts/charts.dart similarity index 100% rename from flutt/lib/presentation/widgets/charts/charts.dart rename to app/lib/presentation/widgets/charts/charts.dart diff --git a/flutt/lib/presentation/widgets/charts/combined_chart.dart b/app/lib/presentation/widgets/charts/combined_chart.dart similarity index 100% rename from flutt/lib/presentation/widgets/charts/combined_chart.dart rename to app/lib/presentation/widgets/charts/combined_chart.dart diff --git a/flutt/lib/presentation/widgets/charts/passage_data.dart b/app/lib/presentation/widgets/charts/passage_data.dart similarity index 100% rename from flutt/lib/presentation/widgets/charts/passage_data.dart rename to app/lib/presentation/widgets/charts/passage_data.dart diff --git a/flutt/lib/presentation/widgets/charts/passage_pie_chart.dart b/app/lib/presentation/widgets/charts/passage_pie_chart.dart similarity index 81% rename from flutt/lib/presentation/widgets/charts/passage_pie_chart.dart rename to app/lib/presentation/widgets/charts/passage_pie_chart.dart index 18d65ae3..ad35bcc6 100644 --- a/flutt/lib/presentation/widgets/charts/passage_pie_chart.dart +++ b/app/lib/presentation/widgets/charts/passage_pie_chart.dart @@ -181,23 +181,65 @@ class _PassagePieChartState extends State /// Charge les données de passage depuis Hive en utilisant le service PassageDataService void _loadPassageDataFromHive(BuildContext context) { - // Éviter les appels multiples - if (_isLoading && _dataLoaded) return; + // Éviter les appels multiples pendant le chargement + if (_isLoading) { + debugPrint('PassagePieChart: Déjà en cours de chargement, ignoré'); + return; + } + + // Si les données sont déjà chargées et non vides, ne pas recharger + if (_dataLoaded && _passagesByType.isNotEmpty) { + debugPrint('PassagePieChart: Données déjà chargées, ignoré'); + return; + } + + debugPrint('PassagePieChart: Début du chargement des données'); + setState(() { + _isLoading = true; + }); // Charger les données dans un addPostFrameCallback pour éviter les problèmes de cycle de vie WidgetsBinding.instance.addPostFrameCallback((_) { // Vérifier si le widget est toujours monté - if (!mounted) return; + if (!mounted) { + debugPrint('PassagePieChart: Widget démonté, chargement annulé'); + return; + } try { + debugPrint('PassagePieChart: Création du service de données'); // Utiliser les instances globales définies dans app.dart + // Vérifier que les repositories sont disponibles + if (passageRepository == null) { + debugPrint('PassagePieChart: ERREUR - passageRepository est null'); + if (mounted) { + setState(() { + _isLoading = false; + }); + } + return; + } + + if (userRepository == null) { + debugPrint('PassagePieChart: ERREUR - userRepository est null'); + if (mounted) { + setState(() { + _isLoading = false; + }); + } + return; + } + // Créer une instance du service de données final passageDataService = PassageDataService( passageRepository: passageRepository, userRepository: userRepository, ); + debugPrint( + 'PassagePieChart: Chargement des données avec excludePassageTypes=${widget.excludePassageTypes}, userId=${widget.userId}, showAllPassages=${widget.showAllPassages}'); + // Utiliser le service pour charger les données final data = passageDataService.loadPassageDataForPieChart( excludePassageTypes: widget.excludePassageTypes, @@ -205,6 +247,8 @@ class _PassagePieChartState extends State showAllPassages: widget.showAllPassages, ); + debugPrint('PassagePieChart: Données chargées: $data'); + // Mettre à jour les données et les états if (mounted) { setState(() { @@ -218,9 +262,12 @@ class _PassagePieChartState extends State // Préparer les données du graphique _prepareChartData(); + debugPrint('PassagePieChart: Données préparées pour le graphique'); } } catch (e) { // Gérer les erreurs et réinitialiser l'état pour permettre une future tentative + debugPrint( + 'PassagePieChart: ERREUR lors du chargement des données: $e'); if (mounted) { setState(() { _isLoading = false; @@ -234,26 +281,69 @@ class _PassagePieChartState extends State List _prepareChartData() { // Utiliser les données en cache si disponibles if (_cachedChartData != null) { + debugPrint('PassagePieChart: Utilisation des données en cache'); return _cachedChartData!; } + debugPrint('PassagePieChart: Préparation des données pour le graphique'); + debugPrint('PassagePieChart: Données brutes: $_passagesByType'); + + // Vérifier si les données sont vides + if (_passagesByType.isEmpty) { + debugPrint('PassagePieChart: Aucune donnée disponible'); + return []; + } + + // Vérifier si les données contiennent uniquement des passages de type 2 + bool onlyType2 = true; + _passagesByType.forEach((typeId, count) { + if (typeId != 2 && count > 0) { + onlyType2 = false; + } + }); + + if (onlyType2) { + debugPrint( + 'PassagePieChart: Les données contiennent uniquement des passages de type 2'); + } + final List chartData = []; // Créer les données du graphique _passagesByType.forEach((typeId, count) { // Vérifier que le type existe et que le compteur est positif if (count > 0 && AppKeys.typesPassages.containsKey(typeId)) { - final typeInfo = AppKeys.typesPassages[typeId]!; - chartData.add(PassageChartData( - typeId: typeId, - count: count, - title: typeInfo['titre'] as String, - color: Color(typeInfo['couleur2'] as int), - icon: typeInfo['icon_data'] as IconData, - )); + // Vérifier si le type est exclu + bool isExcluded = widget.excludePassageTypes.contains(typeId); + if (isExcluded) { + debugPrint('PassagePieChart: Type $typeId exclu'); + } else { + final typeInfo = AppKeys.typesPassages[typeId]!; + final typeName = typeInfo['titre'] as String; + debugPrint( + 'PassagePieChart: Ajout du type $typeId ($typeName) avec $count passages'); + + chartData.add(PassageChartData( + typeId: typeId, + count: count, + title: typeName, + color: Color(typeInfo['couleur2'] as int), + icon: typeInfo['icon_data'] as IconData, + )); + } + } else { + if (count <= 0) { + debugPrint('PassagePieChart: Type $typeId ignoré car count=$count'); + } else if (!AppKeys.typesPassages.containsKey(typeId)) { + debugPrint( + 'PassagePieChart: Type $typeId ignoré car non défini dans AppKeys.typesPassages'); + } } }); + debugPrint( + 'PassagePieChart: ${chartData.length} types de passages ajoutés au graphique'); + // Mettre en cache les données générées _cachedChartData = chartData; diff --git a/flutt/lib/presentation/widgets/charts/passage_utils.dart b/app/lib/presentation/widgets/charts/passage_utils.dart similarity index 100% rename from flutt/lib/presentation/widgets/charts/passage_utils.dart rename to app/lib/presentation/widgets/charts/passage_utils.dart diff --git a/flutt/lib/presentation/widgets/charts/payment_data.dart b/app/lib/presentation/widgets/charts/payment_data.dart similarity index 100% rename from flutt/lib/presentation/widgets/charts/payment_data.dart rename to app/lib/presentation/widgets/charts/payment_data.dart diff --git a/flutt/lib/presentation/widgets/charts/payment_pie_chart.dart b/app/lib/presentation/widgets/charts/payment_pie_chart.dart similarity index 96% rename from flutt/lib/presentation/widgets/charts/payment_pie_chart.dart rename to app/lib/presentation/widgets/charts/payment_pie_chart.dart index 951abe33..e374b6b0 100644 --- a/flutt/lib/presentation/widgets/charts/payment_pie_chart.dart +++ b/app/lib/presentation/widgets/charts/payment_pie_chart.dart @@ -196,11 +196,14 @@ class _PaymentPieChartState extends State }, dataLabelSettings: DataLabelSettings( isVisible: true, - labelPosition: ChartDataLabelPosition.outside, - textStyle: TextStyle(fontSize: widget.labelSize), - connectorLineSettings: const ConnectorLineSettings( - type: ConnectorType.curve, - length: '15%', + labelPosition: ChartDataLabelPosition + .inside, // Afficher les étiquettes à l'intérieur du donut + textStyle: TextStyle( + fontSize: widget.labelSize, + color: Colors + .white, // Texte blanc pour meilleure lisibilité + fontWeight: FontWeight + .bold, // Texte en gras pour meilleure lisibilité ), ), innerRadius: widget.innerRadius, diff --git a/flutt/lib/presentation/widgets/charts/payment_utils.dart b/app/lib/presentation/widgets/charts/payment_utils.dart similarity index 100% rename from flutt/lib/presentation/widgets/charts/payment_utils.dart rename to app/lib/presentation/widgets/charts/payment_utils.dart diff --git a/flutt/lib/presentation/widgets/chat/chat_input.dart b/app/lib/presentation/widgets/chat/chat_input.dart similarity index 100% rename from flutt/lib/presentation/widgets/chat/chat_input.dart rename to app/lib/presentation/widgets/chat/chat_input.dart diff --git a/flutt/lib/presentation/widgets/chat/chat_messages.dart b/app/lib/presentation/widgets/chat/chat_messages.dart similarity index 100% rename from flutt/lib/presentation/widgets/chat/chat_messages.dart rename to app/lib/presentation/widgets/chat/chat_messages.dart diff --git a/flutt/lib/presentation/widgets/chat/chat_sidebar.dart b/app/lib/presentation/widgets/chat/chat_sidebar.dart similarity index 100% rename from flutt/lib/presentation/widgets/chat/chat_sidebar.dart rename to app/lib/presentation/widgets/chat/chat_sidebar.dart diff --git a/app/lib/presentation/widgets/clear_cache_dialog.dart b/app/lib/presentation/widgets/clear_cache_dialog.dart new file mode 100644 index 00000000..6e11e26f --- /dev/null +++ b/app/lib/presentation/widgets/clear_cache_dialog.dart @@ -0,0 +1,153 @@ +import 'package:flutter/material.dart'; + +/// Widget de dialogue pour informer l'utilisateur qu'il doit vider le cache de son navigateur +/// et recharger l'application en raison d'une incompatibilité après une mise à jour. +class ClearCacheDialog extends StatelessWidget { + /// Callback appelé lorsque l'utilisateur ferme le dialogue + final VoidCallback? onClose; + + const ClearCacheDialog({ + Key? key, + this.onClose, + }) : super(key: key); + + /// Affiche le dialogue de nettoyage du cache + static Future show(BuildContext context, + {VoidCallback? onClose}) async { + return showDialog( + context: context, + barrierDismissible: + false, // L'utilisateur doit appuyer sur un bouton pour fermer le dialogue + builder: (BuildContext dialogContext) { + return ClearCacheDialog(onClose: onClose); + }, + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + title: Row( + children: [ + Icon( + Icons.warning_amber_rounded, + color: Colors.orange, + size: 28, + ), + const SizedBox(width: 12), + Text( + 'Mise à jour requise', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.onSurface, + ), + ), + ], + ), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Une incompatibilité a été détectée avec les données stockées localement. Veuillez suivre ces étapes pour résoudre le problème :', + style: theme.textTheme.bodyLarge, + ), + const SizedBox(height: 16), + _buildInstructionStep( + context, + 1, + 'Videz le cache de votre navigateur', + 'Dans Chrome : Menu > Plus d\'outils > Effacer les données de navigation > Sélectionnez "Cookies et données de site" > Effacer les données'), + const SizedBox(height: 12), + _buildInstructionStep( + context, + 2, + 'Fermez complètement le navigateur', + 'Assurez-vous de fermer toutes les fenêtres du navigateur'), + const SizedBox(height: 12), + _buildInstructionStep(context, 3, 'Rouvrez l\'application', + 'Reconnectez-vous à l\'application pour récupérer vos données depuis le serveur'), + const SizedBox(height: 16), + Text( + 'Note : Cette opération est nécessaire en raison d\'une mise à jour de la structure des données. Toutes vos données seront récupérées depuis le serveur après reconnexion.', + style: theme.textTheme.bodySmall?.copyWith( + fontStyle: FontStyle.italic, + color: theme.colorScheme.onSurface.withOpacity(0.7), + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + if (onClose != null) { + onClose!(); + } + }, + style: TextButton.styleFrom( + foregroundColor: theme.colorScheme.primary, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + child: const Text('J\'ai compris'), + ), + ], + ); + } + + /// Construit une étape d'instruction avec un numéro, un titre et une description + Widget _buildInstructionStep( + BuildContext context, int stepNumber, String title, String description) { + final theme = Theme.of(context); + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: theme.colorScheme.primary, + shape: BoxShape.circle, + ), + child: Center( + child: Text( + '$stepNumber', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + description, + style: theme.textTheme.bodyMedium, + ), + ], + ), + ), + ], + ); + } +} diff --git a/flutt/lib/presentation/widgets/connectivity_indicator.dart b/app/lib/presentation/widgets/connectivity_indicator.dart similarity index 100% rename from flutt/lib/presentation/widgets/connectivity_indicator.dart rename to app/lib/presentation/widgets/connectivity_indicator.dart diff --git a/flutt/lib/presentation/widgets/custom_button.dart b/app/lib/presentation/widgets/custom_button.dart similarity index 100% rename from flutt/lib/presentation/widgets/custom_button.dart rename to app/lib/presentation/widgets/custom_button.dart diff --git a/app/lib/presentation/widgets/custom_text_field.dart b/app/lib/presentation/widgets/custom_text_field.dart new file mode 100644 index 00000000..23ffc3a5 --- /dev/null +++ b/app/lib/presentation/widgets/custom_text_field.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class CustomTextField extends StatelessWidget { + final TextEditingController controller; + final String label; + final String? hintText; + final IconData? prefixIcon; + final Widget? suffixIcon; + final bool obscureText; + final TextInputType keyboardType; + final String? Function(String?)? validator; + final List? inputFormatters; + final int? maxLines; + final int? minLines; + final bool readOnly; + final VoidCallback? onTap; + final Function(String)? onChanged; + final bool autofocus; + final FocusNode? focusNode; + final String? errorText; + final Color? fillColor; + final String? helperText; + final Function(String)? onFieldSubmitted; + + const CustomTextField({ + super.key, + required this.controller, + required this.label, + this.hintText, + this.prefixIcon, + this.suffixIcon, + this.obscureText = false, + this.keyboardType = TextInputType.text, + this.validator, + this.inputFormatters, + this.maxLines = 1, + this.minLines, + this.readOnly = false, + this.onTap, + this.onChanged, + this.autofocus = false, + this.focusNode, + this.errorText, + this.fillColor, + this.helperText, + this.onFieldSubmitted, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (label.isNotEmpty) ...[ + Text( + label, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w500, + color: theme.colorScheme.onBackground, + ), + ), + const SizedBox(height: 8), + ], + // Ajouter un Container avec une ombre pour créer un effet d'élévation + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: TextFormField( + controller: controller, + obscureText: obscureText, + keyboardType: keyboardType, + validator: validator, + inputFormatters: inputFormatters, + maxLines: maxLines, + minLines: minLines, + readOnly: readOnly, + onTap: onTap, + onChanged: onChanged, + onFieldSubmitted: onFieldSubmitted, + autofocus: autofocus, + focusNode: focusNode, + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.onBackground, + ), + decoration: InputDecoration( + hintText: hintText, + hintStyle: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.onBackground.withOpacity(0.5), + ), + errorText: errorText, + helperText: helperText, + helperStyle: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onBackground.withOpacity(0.6), + ), + prefixIcon: prefixIcon != null + ? Icon(prefixIcon, color: theme.colorScheme.primary) + : null, + suffixIcon: suffixIcon, + // Couleur de fond différente selon l'état (lecture seule ou éditable) + fillColor: fillColor ?? + (readOnly + ? const Color(0xFFF8F9FA) // Gris plus clair pour readOnly + : const Color( + 0xFFECEFF1)), // Gris plus foncé pour éditable + filled: true, + // Ajouter une élévation avec une petite ombre + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + // Ajouter une ombre pour créer un effet d'élévation + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + gapPadding: 0, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: theme.colorScheme.primary, + width: 2, + ), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: theme.colorScheme.error, + width: 2, + ), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: theme.colorScheme.error, + width: 2, + ), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + ), + ), + ), + ], + ); + } +} diff --git a/flutt/lib/presentation/widgets/dashboard_app_bar.dart b/app/lib/presentation/widgets/dashboard_app_bar.dart similarity index 81% rename from flutt/lib/presentation/widgets/dashboard_app_bar.dart rename to app/lib/presentation/widgets/dashboard_app_bar.dart index cabe265a..49b3263a 100644 --- a/flutt/lib/presentation/widgets/dashboard_app_bar.dart +++ b/app/lib/presentation/widgets/dashboard_app_bar.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:geosector_app/app.dart'; import 'package:geosector_app/presentation/widgets/connectivity_indicator.dart'; +import 'package:geosector_app/presentation/widgets/profile_dialog.dart'; import 'package:go_router/go_router.dart'; /// AppBar personnalisée pour les tableaux de bord @@ -56,7 +57,7 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget { return Padding( padding: const EdgeInsets.all(8.0), child: Image.asset( - 'assets/images/geosector-logo-80.png', + 'assets/images/logo-geosector-1024.png', width: 40, height: 40, ), @@ -98,6 +99,28 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget { ); } + // Ajouter le bouton "Mon compte" + actions.add( + IconButton( + icon: const Icon(Icons.person), + tooltip: 'Mon compte', + onPressed: () { + // Afficher la boîte de dialogue de profil avec l'utilisateur actuel + final user = userRepository.currentUser; + if (user != null) { + ProfileDialog.show(context, user); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Erreur: Utilisateur non trouvé'), + backgroundColor: theme.colorScheme.error, + ), + ); + } + }, + ), + ); + // Ajouter le bouton de déconnexion actions.add( IconButton( @@ -120,16 +143,10 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget { TextButton( onPressed: () async { Navigator.of(context).pop(); - // Appeler la méthode logout du userRepository - // qui nettoie les hive boxes, lance la requête API logout - // et supprime user.sessionId - await userRepository.logout(); - - // Rediriger vers la landing page - if (context.mounted) { - // Utiliser go_router pour la navigation - context.go('/public'); - } + // Utiliser directement userRepository pour la déconnexion + // qui gère à la fois le nettoyage des données et la redirection + await userRepository.logoutWithUI(context); + // La redirection est gérée dans logoutWithUI }, child: const Text('Déconnexion'), ), diff --git a/flutt/lib/presentation/widgets/dashboard_layout.dart b/app/lib/presentation/widgets/dashboard_layout.dart similarity index 97% rename from flutt/lib/presentation/widgets/dashboard_layout.dart rename to app/lib/presentation/widgets/dashboard_layout.dart index d04f286f..23059a59 100644 --- a/flutt/lib/presentation/widgets/dashboard_layout.dart +++ b/app/lib/presentation/widgets/dashboard_layout.dart @@ -80,6 +80,8 @@ class DashboardLayout extends StatelessWidget { } return Scaffold( + backgroundColor: Colors + .transparent, // Fond transparent pour laisser voir le AdminBackground appBar: DashboardAppBar( title: title, pageTitle: destinations[selectedIndex].label, diff --git a/app/lib/presentation/widgets/docs/amicale_widgets_documentation.md b/app/lib/presentation/widgets/docs/amicale_widgets_documentation.md new file mode 100644 index 00000000..57d6dadb --- /dev/null +++ b/app/lib/presentation/widgets/docs/amicale_widgets_documentation.md @@ -0,0 +1,141 @@ +# Documentation des Widgets Amicale + +Cette documentation explique comment utiliser les widgets `AmicaleRowWidget` et `AmicaleTableWidget` pour afficher et gérer les données des amicales dans l'application. + +## AmicaleRowWidget + +Le widget `AmicaleRowWidget` représente une ligne dans un tableau d'amicales. Il affiche les informations d'une amicale avec les colonnes suivantes : + +- ID +- Nom +- Code Postal +- Région +- Actions (boutons selon les droits de l'utilisateur) + +### Propriétés + +| Propriété | Type | Description | +| ------------- | --------------- | ---------------------------------------------------------------------------------- | +| `amicale` | `AmicaleModel` | **Obligatoire**. L'objet amicale à afficher. | +| `onEdit` | `VoidCallback?` | Fonction appelée lorsque l'utilisateur clique sur le bouton d'édition. | +| `onDelete` | `VoidCallback?` | Fonction appelée lorsque l'utilisateur clique sur le bouton de suppression. | +| `isAlternate` | `bool` | Indique si la ligne doit avoir une couleur de fond alternée. Par défaut à `false`. | + +### Gestion des droits d'accès + +Le widget gère automatiquement l'affichage des boutons d'action en fonction du rôle de l'utilisateur : + +- Le bouton d'édition (crayon) est visible pour tous les utilisateurs avec un rôle > 1 +- Le bouton de suppression (corbeille) est visible uniquement pour les utilisateurs avec un rôle > 2 + +### Exemple d'utilisation + +```dart +AmicaleRowWidget( + amicale: amicale, + isAlternate: index % 2 == 1, // Alterner les couleurs + onEdit: () { + // Code pour gérer l'édition + }, + onDelete: () { + // Code pour gérer la suppression + }, +) +``` + +## AmicaleTableWidget + +Le widget `AmicaleTableWidget` affiche un tableau complet d'amicales avec un en-tête et des lignes. Il utilise le widget `AmicaleRowWidget` pour afficher chaque ligne. + +### Propriétés + +| Propriété | Type | Description | +| -------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| `amicales` | `List` | **Obligatoire**. La liste des amicales à afficher. | +| `onEdit` | `Function(AmicaleModel)?` | Fonction appelée lorsque l'utilisateur clique sur le bouton d'édition d'une amicale. | +| `onDelete` | `Function(AmicaleModel)?` | Fonction appelée lorsque l'utilisateur clique sur le bouton de suppression d'une amicale. | +| `isLoading` | `bool` | Indique si les données sont en cours de chargement. Affiche un indicateur de chargement si `true`. Par défaut à `false`. | +| `emptyMessage` | `String?` | Message à afficher lorsque la liste des amicales est vide. | + +### États du tableau + +Le widget gère automatiquement différents états : + +1. **Chargement** : Affiche un indicateur de chargement circulaire lorsque `isLoading` est `true`. +2. **Liste vide** : Affiche un message lorsque la liste des amicales est vide. +3. **Affichage normal** : Affiche la liste des amicales avec des lignes alternées. + +### Exemple d'utilisation + +```dart +AmicaleTableWidget( + amicales: _amicales, + isLoading: _isLoading, + onEdit: (amicale) { + // Code pour gérer l'édition de l'amicale + }, + onDelete: (amicale) { + // Code pour gérer la suppression de l'amicale + }, + emptyMessage: 'Aucune amicale trouvée. Veuillez en créer une nouvelle.', +) +``` + +## Intégration avec AmicaleRepository + +Pour utiliser ces widgets avec le repository des amicales, vous devez : + +1. Récupérer les amicales depuis le repository : + +```dart +final amicaleRepository = Provider.of(context, listen: false); +final amicales = amicaleRepository.getAllAmicales(); +``` + +2. Gérer les actions d'édition et de suppression : + +```dart +void _handleEdit(AmicaleModel amicale) { + // Naviguer vers la page d'édition ou afficher une boîte de dialogue +} + +Future _handleDelete(AmicaleModel amicale) async { + // Afficher une confirmation puis supprimer + final amicaleRepository = Provider.of(context, listen: false); + await amicaleRepository.deleteAmicale(amicale.id); + + // Recharger la liste + setState(() { + _amicales = amicaleRepository.getAllAmicales(); + }); +} +``` + +## Exemple complet + +Un exemple complet d'utilisation est disponible dans le fichier `app/lib/presentation/widgets/examples/amicale_table_example.dart`. + +## Personnalisation + +### Styles + +Les widgets utilisent les styles du thème de l'application pour la cohérence visuelle. Vous pouvez personnaliser l'apparence en modifiant le thème ou en surchargeant les styles dans votre implémentation. + +### Colonnes et flexibilité + +Les colonnes du tableau ont des valeurs de flex prédéfinies pour une mise en page optimale : + +- ID : flex 1 +- Nom : flex 4 +- Code Postal : flex 2 +- Région : flex 3 +- Actions : flex 2 + +Vous pouvez ajuster ces valeurs en modifiant le code source si nécessaire. + +## Bonnes pratiques + +1. **Gestion des erreurs** : Ajoutez toujours une gestion des erreurs lors de l'interaction avec le repository. +2. **Confirmation des actions** : Demandez toujours une confirmation avant de supprimer une amicale. +3. **Actualisation des données** : Prévoyez un moyen de rafraîchir les données (bouton ou pull-to-refresh). +4. **Pagination** : Pour les grandes listes, envisagez d'implémenter une pagination. diff --git a/app/lib/presentation/widgets/docs/entite_form_documentation.md b/app/lib/presentation/widgets/docs/entite_form_documentation.md new file mode 100644 index 00000000..ec44f823 --- /dev/null +++ b/app/lib/presentation/widgets/docs/entite_form_documentation.md @@ -0,0 +1,204 @@ +# Documentation du Widget EntiteForm + +Cette documentation décrit le widget `EntiteForm` créé pour la création et la modification des entités (amicales) dans l'application GeoSector. + +## Description + +Le widget `EntiteForm` est un formulaire complet permettant de créer ou modifier une entité (amicale). Il gère l'affichage de tous les champs nécessaires, la validation des données et les restrictions d'accès basées sur le rôle de l'utilisateur. + +## Propriétés + +- `amicale` (AmicaleModel?, optionnel) : Le modèle d'amicale à modifier. Si null, le formulaire sera en mode création. +- `onSubmit` (Function(AmicaleModel)?, optionnel) : Callback appelé lorsque le formulaire est soumis avec succès. +- `readOnly` (bool, défaut: false) : Si true, tous les champs du formulaire seront en lecture seule. + +## Champs du formulaire + +Le formulaire inclut les champs suivants : + +### Informations générales + +- **Nom** : Nom de l'amicale (obligatoire) + +### Adresse + +- **Adresse ligne 1** : Première ligne d'adresse +- **Adresse ligne 2** : Seconde ligne d'adresse (optionnelle) +- **Code Postal** : Code postal (validation pour 5 chiffres) +- **Ville** : Nom de la ville +- **Région** : Sélection de la région via un dropdown + +### Contact + +- **Téléphone fixe** : Numéro de téléphone fixe (validation pour 10 chiffres) +- **Téléphone mobile** : Numéro de téléphone mobile (validation pour 10 chiffres) +- **Email** : Adresse email (obligatoire, avec validation de format) + +### Informations avancées (visibles uniquement pour les administrateurs ou si déjà remplies) + +- **GPS Latitude** : Coordonnée GPS latitude +- **GPS Longitude** : Coordonnée GPS longitude +- **Stripe ID** : Identifiant Stripe pour les paiements + +### Options + +- **Mode démo** : Indique si l'amicale est en mode démo +- **Copie des mails reçus** : Indique si l'amicale reçoit une copie des emails +- **Accepte les SMS** : Indique si l'amicale accepte les SMS +- **Actif** : Indique si l'amicale est active + +## Restrictions d'accès + +Certains champs sont soumis à des restrictions d'accès basées sur le rôle de l'utilisateur : + +- Les champs suivants sont en lecture seule pour les utilisateurs avec un rôle ≤ 2 : + - fkRegion/libRegion + - gpsLat + - gpsLng + - stripeId + - chkDemo + - chkActive + +## Exemple d'utilisation + +Un exemple complet d'utilisation est disponible dans le fichier `app/lib/presentation/widgets/examples/entite_form_example.dart`. + +### Utilisation simple + +```dart +// Création d'une nouvelle amicale +EntiteForm( + onSubmit: (amicale) { + // Gérer la soumission + print('Nouvelle amicale: ${amicale.name}'); + }, +) + +// Modification d'une amicale existante +EntiteForm( + amicale: amicaleExistante, + onSubmit: (amicale) { + // Gérer la soumission + print('Amicale modifiée: ${amicale.name}'); + }, +) + +// Affichage en lecture seule +EntiteForm( + amicale: amicaleExistante, + readOnly: true, +) +``` + +### Utilisation avec gestion d'état + +```dart +class _MyWidgetState extends State { + AmicaleModel? _amicale; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadAmicale(); + } + + Future _loadAmicale() async { + setState(() { + _isLoading = true; + }); + + try { + if (widget.amicaleId != null) { + // Récupérer l'amicale depuis le repository + final amicaleRepository = Provider.of(context, listen: false); + final amicale = amicaleRepository.getAmicaleById(widget.amicaleId!); + + setState(() { + _amicale = amicale; + _isLoading = false; + }); + } else { + // Création d'une nouvelle amicale + setState(() { + _amicale = null; + _isLoading = false; + }); + } + } catch (e) { + debugPrint('Erreur lors du chargement de l\'amicale: $e'); + setState(() { + _isLoading = false; + }); + } + } + + void _handleSubmit(AmicaleModel amicale) async { + try { + final amicaleRepository = Provider.of(context, listen: false); + + // Sauvegarder l'amicale + final savedAmicale = await amicaleRepository.saveAmicale(amicale); + + // Afficher un message de confirmation + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Amicale ${savedAmicale.name} sauvegardée avec succès'), + backgroundColor: Colors.green, + ), + ); + } catch (e) { + debugPrint('Erreur lors de la sauvegarde de l\'amicale: $e'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors de la sauvegarde: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return _isLoading + ? const Center(child: CircularProgressIndicator()) + : EntiteForm( + amicale: _amicale, + onSubmit: _handleSubmit, + ); + } +} +``` + +## Intégration avec le système de rôles + +Le widget utilise le `UserRepository` pour déterminer le rôle de l'utilisateur actuel et appliquer les restrictions d'accès en conséquence. Assurez-vous que le `UserRepository` est disponible dans l'arbre des widgets via un `Provider`. + +```dart +// Dans le widget parent +return MultiProvider( + providers: [ + Provider( + create: (context) => userRepository, + ), + Provider( + create: (context) => amicaleRepository, + ), + ], + child: MyWidget(), +); +``` + +## Personnalisation + +Le widget utilise le thème de l'application pour le style. Vous pouvez personnaliser l'apparence en modifiant le thème ou en étendant le widget pour créer votre propre version personnalisée. + +## Validation des données + +Le formulaire inclut une validation pour les champs suivants : + +- **Nom** : Ne peut pas être vide +- **Code Postal** : Doit contenir 5 chiffres s'il est rempli +- **Téléphone fixe** : Doit contenir 10 chiffres s'il est rempli +- **Téléphone mobile** : Doit contenir 10 chiffres s'il est rempli +- **Email** : Ne peut pas être vide et doit contenir un '@' et un '.' diff --git a/app/lib/presentation/widgets/docs/entite_form_with_regions_documentation.md b/app/lib/presentation/widgets/docs/entite_form_with_regions_documentation.md new file mode 100644 index 00000000..c4b4e407 --- /dev/null +++ b/app/lib/presentation/widgets/docs/entite_form_with_regions_documentation.md @@ -0,0 +1,160 @@ +# Documentation du Widget EntiteForm avec RegionRepository + +Cette documentation explique comment utiliser le widget `EntiteForm` avec le `RegionRepository` pour afficher et gérer les régions dans le formulaire d'entité. + +## Intégration du RegionRepository + +Le widget `EntiteForm` est conçu pour fonctionner avec le `RegionRepository` afin de récupérer la liste des régions disponibles pour le champ de sélection de région. Voici comment l'intégrer : + +### 1. Initialisation du RegionRepository + +Le `RegionRepository` doit être initialisé et fourni au widget `EntiteForm` via un `Provider`. Voici un exemple d'initialisation : + +```dart +final regionRepository = RegionRepository(); +await regionRepository.init(); +``` + +### 2. Fournir le RegionRepository via Provider + +Pour que le widget `EntiteForm` puisse accéder au `RegionRepository`, vous devez le fournir via un `Provider` : + +```dart +MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: regionRepository), + // Autres providers si nécessaire + ], + child: EntiteForm( + amicale: amicale, + onSubmit: handleSubmit, + readOnly: false, + ), +) +``` + +### 3. Mise à jour des régions depuis l'API + +Lorsque l'API renvoie les données des régions dans la réponse de login, vous devez les mettre à jour dans le `RegionRepository` : + +```dart +// Dans le service qui gère la connexion +void handleLoginResponse(Map response) { + // Autres traitements... + + // Mise à jour des régions si présentes dans la réponse + if (response.containsKey('regions') && response['regions'] is List) { + final regionRepository = Provider.of(context, listen: false); + regionRepository.updateRegionsFromApi(response['regions']); + } +} +``` + +## Fonctionnement avec les restrictions d'accès + +Le widget `EntiteForm` gère automatiquement les restrictions d'accès basées sur le rôle de l'utilisateur : + +- Pour les utilisateurs avec un rôle ≤ 2, le champ de sélection de région est en lecture seule +- Pour les utilisateurs avec un rôle > 2, le champ de sélection de région est modifiable + +## Filtrage des régions selon le code postal + +Le `RegionRepository` offre une méthode `getRegionByPostalCode` qui permet de filtrer les régions en fonction du code postal : + +```dart +// Récupérer la région correspondant au code postal +final codePostal = '75001'; +final region = regionRepository.getRegionByPostalCode(codePostal); +if (region != null) { + // Utiliser la région trouvée + print('Région trouvée : ${region.libelle}'); +} +``` + +Cette fonctionnalité est particulièrement utile pour pré-remplir le champ de région lorsque l'utilisateur entre un code postal. + +## Exemple complet d'utilisation + +Un exemple complet d'utilisation est disponible dans le fichier `app/lib/presentation/widgets/examples/entite_form_with_regions_example.dart`. + +### Exemple simplifié + +```dart +class MyWidget extends StatefulWidget { + @override + State createState() => _MyWidgetState(); +} + +class _MyWidgetState extends State { + late RegionRepository _regionRepository; + AmicaleModel? _amicale; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _regionRepository = RegionRepository(); + _initData(); + } + + Future _initData() async { + setState(() { + _isLoading = true; + }); + + try { + // Initialiser le repository des régions + await _regionRepository.init(); + + // Charger l'amicale si nécessaire + // ... + + setState(() { + _isLoading = false; + }); + } catch (e) { + debugPrint('Erreur lors de l\'initialisation: $e'); + setState(() { + _isLoading = false; + }); + } + } + + void _handleSubmit(AmicaleModel amicale) { + // Traiter la soumission du formulaire + } + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: _regionRepository), + ], + child: Scaffold( + appBar: AppBar(title: Text('Formulaire d\'entité')), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: EntiteForm( + amicale: _amicale, + onSubmit: _handleSubmit, + readOnly: false, + ), + ), + ), + ); + } +} +``` + +## Mise à jour du code postal et de la région + +Pour mettre à jour automatiquement la région lorsque l'utilisateur change le code postal, vous pouvez étendre le widget `EntiteForm` ou créer un wrapper qui écoute les changements du champ de code postal et met à jour la région en conséquence. + +## Remarques importantes + +1. Assurez-vous que le `RegionRepository` est initialisé avant d'afficher le formulaire. +2. Le widget `EntiteForm` s'adapte automatiquement au rôle de l'utilisateur pour les restrictions d'accès. +3. Les régions sont filtrées en fonction du code postal de l'amicale pour les utilisateurs avec un rôle ≤ 2. +4. Pour les utilisateurs avec un rôle > 2, toutes les régions sont disponibles dans le dropdown. diff --git a/app/lib/presentation/widgets/docs/membre_widgets_documentation.md b/app/lib/presentation/widgets/docs/membre_widgets_documentation.md new file mode 100644 index 00000000..1ecc0e9b --- /dev/null +++ b/app/lib/presentation/widgets/docs/membre_widgets_documentation.md @@ -0,0 +1,207 @@ +# Documentation des Widgets Membre + +Cette documentation décrit les widgets créés pour afficher et gérer les données des membres dans l'application GeoSector. + +## Widgets disponibles + +### 1. MembreRowWidget + +Widget qui représente une ligne individuelle dans un tableau de membres. Il affiche les informations d'un membre et des boutons d'action pour l'édition et la suppression. + +#### Propriétés + +- `membre` (MembreModel, requis) : Le modèle de membre à afficher +- `onEdit` (Function()?, optionnel) : Callback appelé lorsque le bouton d'édition est pressé +- `onDelete` (Function()?, optionnel) : Callback appelé lorsque le bouton de suppression est pressé + +#### Colonnes affichées + +- ID : Identifiant unique du membre +- Prénom (firstName) : Prénom du membre +- Nom (name) : Nom de famille du membre +- Secteur (sectName) : Nom du secteur auquel le membre est associé +- Rôle (fkRole) : Rôle du membre (affiché sous forme de texte : User, Admin, Super) +- Actions : Boutons d'édition et de suppression + +### 2. MembreTableWidget + +Widget qui affiche un tableau complet de membres avec en-tête et lignes. Il utilise le widget `MembreRowWidget` pour afficher chaque ligne. + +#### Propriétés + +- `membres` (List, requis) : La liste des membres à afficher +- `onEdit` (Function(MembreModel)?, optionnel) : Callback appelé lorsque le bouton d'édition est pressé pour un membre +- `onDelete` (Function(MembreModel)?, optionnel) : Callback appelé lorsque le bouton de suppression est pressé pour un membre +- `showHeader` (bool, défaut: true) : Indique si l'en-tête du tableau doit être affiché +- `height` (double?, optionnel) : Hauteur du tableau (null pour prendre toute la hauteur disponible) +- `padding` (EdgeInsetsGeometry?, optionnel) : Padding du tableau + +## Exemple d'utilisation + +Un exemple complet d'utilisation est disponible dans le fichier `app/lib/presentation/widgets/examples/membre_table_example.dart`. + +### Utilisation simple + +```dart +// S'assurer que la boîte Hive est ouverte +if (!Hive.isBoxOpen(AppKeys.membresBoxName)) { + await Hive.openBox(AppKeys.membresBoxName); +} + +// Récupérer les membres depuis la boîte Hive +final membresBox = Hive.box(AppKeys.membresBoxName); +final membres = membresBox.values.toList(); + +// Afficher le tableau +return MembreTableWidget( + membres: membres, + onEdit: (membre) { + // Gérer l'édition + }, + onDelete: (membre) { + // Gérer la suppression + }, +); +``` + +### Utilisation avec gestion d'état + +```dart +class _MyWidgetState extends State { + List _membres = []; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadMembres(); + } + + Future _loadMembres() async { + setState(() { + _isLoading = true; + }); + + try { + // S'assurer que la boîte Hive est ouverte + if (!Hive.isBoxOpen(AppKeys.membresBoxName)) { + await Hive.openBox(AppKeys.membresBoxName); + } + + // Récupérer les membres depuis la boîte Hive + final membresBox = Hive.box(AppKeys.membresBoxName); + final membres = membresBox.values.toList(); + + setState(() { + _membres = membres; + _isLoading = false; + }); + } catch (e) { + debugPrint('Erreur lors du chargement des membres: $e'); + setState(() { + _isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return _isLoading + ? const Center(child: CircularProgressIndicator()) + : MembreTableWidget( + membres: _membres, + onEdit: _handleEdit, + onDelete: _handleDelete, + ); + } +} +``` + +## Gestion des événements + +### Édition d'un membre + +```dart +void _handleEdit(MembreModel membre) { + // Exemple de gestion de l'événement d'édition + debugPrint('Édition du membre: ${membre.firstName} ${membre.name} (ID: ${membre.id})'); + + // Ici, vous pourriez ouvrir une boîte de dialogue ou naviguer vers une page d'édition + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Édition de membre'), + content: Text('Vous avez demandé à éditer le membre ${membre.firstName} ${membre.name}'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Fermer'), + ), + ], + ), + ); +} +``` + +### Suppression d'un membre + +```dart +void _handleDelete(MembreModel membre) { + // Exemple de gestion de l'événement de suppression + debugPrint('Suppression du membre: ${membre.firstName} ${membre.name} (ID: ${membre.id})'); + + // Demander confirmation avant de supprimer + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Confirmation de suppression'), + content: Text('Êtes-vous sûr de vouloir supprimer le membre ${membre.firstName} ${membre.name} ?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Annuler'), + ), + TextButton( + onPressed: () async { + // Fermer la boîte de dialogue + Navigator.of(context).pop(); + + try { + // Supprimer le membre de la boîte Hive + final membresBox = Hive.box(AppKeys.membresBoxName); + await membresBox.delete(membre.id); + + // Mettre à jour l'état + setState(() { + _membres = _membres.where((m) => m.id != membre.id).toList(); + }); + + // Afficher un message de confirmation + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Membre ${membre.firstName} ${membre.name} supprimé'), + backgroundColor: Colors.green, + ), + ); + } catch (e) { + debugPrint('Erreur lors de la suppression du membre: $e'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors de la suppression: $e'), + backgroundColor: Colors.red, + ), + ); + } + }, + child: const Text('Supprimer'), + style: TextButton.styleFrom(foregroundColor: Colors.red), + ), + ], + ), + ); +} +``` + +## Personnalisation + +Les widgets utilisent le thème de l'application pour le style. Vous pouvez personnaliser l'apparence en modifiant le thème ou en étendant les widgets pour créer vos propres versions personnalisées. diff --git a/app/lib/presentation/widgets/entite_form.dart b/app/lib/presentation/widgets/entite_form.dart new file mode 100644 index 00000000..c0c903bb --- /dev/null +++ b/app/lib/presentation/widgets/entite_form.dart @@ -0,0 +1,644 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:geosector_app/core/data/models/amicale_model.dart'; +import 'package:geosector_app/core/repositories/user_repository.dart'; +import 'package:geosector_app/core/repositories/region_repository.dart'; +import 'package:provider/provider.dart'; +import 'custom_text_field.dart'; + +class EntiteForm extends StatefulWidget { + final AmicaleModel? amicale; + final Function(AmicaleModel)? onSubmit; + final bool readOnly; + + const EntiteForm({ + Key? key, + this.amicale, + this.onSubmit, + this.readOnly = false, + }) : super(key: key); + + @override + State createState() => _EntiteFormState(); +} + +class _EntiteFormState extends State { + final _formKey = GlobalKey(); + + // Controllers + late final TextEditingController _nameController; + late final TextEditingController _adresse1Controller; + late final TextEditingController _adresse2Controller; + late final TextEditingController _codePostalController; + late final TextEditingController _villeController; + late final TextEditingController _phoneController; + late final TextEditingController _mobileController; + late final TextEditingController _emailController; + late final TextEditingController _gpsLatController; + late final TextEditingController _gpsLngController; + late final TextEditingController _stripeIdController; + + // Form values + int? _fkRegion; + String? _libRegion; + bool _chkDemo = false; + bool _chkCopieMailRecu = false; + bool _chkAcceptSms = false; + bool _chkActive = true; + bool _chkStripe = false; + + // Liste des régions (sera chargée depuis le store) + List> _regions = []; + + @override + void initState() { + super.initState(); + + // Initialize controllers with amicale data if available + final amicale = widget.amicale; + _nameController = TextEditingController(text: amicale?.name ?? ''); + _adresse1Controller = TextEditingController(text: amicale?.adresse1 ?? ''); + _adresse2Controller = TextEditingController(text: amicale?.adresse2 ?? ''); + _codePostalController = + TextEditingController(text: amicale?.codePostal ?? ''); + _villeController = TextEditingController(text: amicale?.ville ?? ''); + _phoneController = TextEditingController(text: amicale?.phone ?? ''); + _mobileController = TextEditingController(text: amicale?.mobile ?? ''); + _emailController = TextEditingController(text: amicale?.email ?? ''); + _gpsLatController = TextEditingController(text: amicale?.gpsLat ?? ''); + _gpsLngController = TextEditingController(text: amicale?.gpsLng ?? ''); + _stripeIdController = TextEditingController(text: amicale?.stripeId ?? ''); + + _fkRegion = amicale?.fkRegion; + _libRegion = amicale?.libRegion; + _chkDemo = amicale?.chkDemo ?? false; + _chkCopieMailRecu = amicale?.chkCopieMailRecu ?? false; + _chkAcceptSms = amicale?.chkAcceptSms ?? false; + _chkActive = amicale?.chkActive ?? true; + _chkStripe = amicale?.chkStripe ?? false; + + // Charger les régions depuis le repository + WidgetsBinding.instance.addPostFrameCallback((_) { + _loadRegions(); + }); + } + + void _loadRegions() { + try { + final regionRepository = + Provider.of(context, listen: false); + if (!regionRepository.isLoaded) { + // Initialiser le repository si ce n'est pas déjà fait + regionRepository.init().then((_) { + setState(() { + _regions = regionRepository.getRegionsForDropdown(); + }); + }); + } else { + setState(() { + _regions = regionRepository.getRegionsForDropdown(); + }); + } + } catch (e) { + debugPrint('Erreur lors du chargement des régions: $e'); + // Utiliser une liste vide en cas d'erreur + setState(() { + _regions = []; + }); + } + } + + @override + void dispose() { + _nameController.dispose(); + _adresse1Controller.dispose(); + _adresse2Controller.dispose(); + _codePostalController.dispose(); + _villeController.dispose(); + _phoneController.dispose(); + _mobileController.dispose(); + _emailController.dispose(); + _gpsLatController.dispose(); + _gpsLngController.dispose(); + _stripeIdController.dispose(); + super.dispose(); + } + + void _submitForm() { + if (_formKey.currentState!.validate()) { + final amicale = widget.amicale?.copyWith( + name: _nameController.text, + adresse1: _adresse1Controller.text, + adresse2: _adresse2Controller.text, + codePostal: _codePostalController.text, + ville: _villeController.text, + fkRegion: _fkRegion, + libRegion: _libRegion, + phone: _phoneController.text, + mobile: _mobileController.text, + email: _emailController.text, + gpsLat: _gpsLatController.text, + gpsLng: _gpsLngController.text, + stripeId: _stripeIdController.text, + chkDemo: _chkDemo, + chkCopieMailRecu: _chkCopieMailRecu, + chkAcceptSms: _chkAcceptSms, + chkActive: _chkActive, + chkStripe: _chkStripe, + ) ?? + AmicaleModel( + id: 0, // Sera remplacé par l'API + name: _nameController.text, + adresse1: _adresse1Controller.text, + adresse2: _adresse2Controller.text, + codePostal: _codePostalController.text, + ville: _villeController.text, + fkRegion: _fkRegion, + libRegion: _libRegion, + phone: _phoneController.text, + mobile: _mobileController.text, + email: _emailController.text, + gpsLat: _gpsLatController.text, + gpsLng: _gpsLngController.text, + stripeId: _stripeIdController.text, + chkDemo: _chkDemo, + chkCopieMailRecu: _chkCopieMailRecu, + chkAcceptSms: _chkAcceptSms, + chkActive: _chkActive, + ); + + if (widget.onSubmit != null) { + widget.onSubmit!(amicale); + } + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final userRepository = Provider.of(context, listen: false); + final userRole = userRepository.getUserRole(); + + // Déterminer si l'utilisateur peut modifier les champs restreints + final bool canEditRestrictedFields = userRole > 2; + + // Lecture seule pour les champs restreints si l'utilisateur n'a pas les droits + final bool restrictedFieldsReadOnly = + widget.readOnly || !canEditRestrictedFields; + + // Calculer la largeur maximale du formulaire pour les écrans larges + final screenWidth = MediaQuery.of(context).size.width; + final formMaxWidth = screenWidth > 800 ? 600.0 : screenWidth; + + return Form( + key: _formKey, + child: Container( + constraints: BoxConstraints(maxWidth: formMaxWidth), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Nom + CustomTextField( + controller: _nameController, + label: "Nom", + readOnly: widget.readOnly, + validator: (value) { + if (value == null || value.isEmpty) { + return "Veuillez entrer un nom"; + } + return null; + }, + ), + const SizedBox(height: 16), + + // Bloc Adresse + Text( + "Adresse", + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.onBackground, + ), + ), + const SizedBox(height: 8), + + // Adresse 1 + CustomTextField( + controller: _adresse1Controller, + label: "Adresse ligne 1", + readOnly: widget.readOnly, + ), + const SizedBox(height: 16), + + // Adresse 2 + CustomTextField( + controller: _adresse2Controller, + label: "Adresse ligne 2", + readOnly: widget.readOnly, + ), + const SizedBox(height: 16), + + // Code Postal et Ville + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Code Postal + Expanded( + flex: 1, + child: CustomTextField( + controller: _codePostalController, + label: "Code Postal", + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(5), + ], + readOnly: widget.readOnly, + validator: (value) { + if (value != null && + value.isNotEmpty && + value.length < 5) { + return "Le code postal doit contenir 5 chiffres"; + } + return null; + }, + ), + ), + const SizedBox(width: 16), + // Ville + Expanded( + flex: 2, + child: CustomTextField( + controller: _villeController, + label: "Ville", + readOnly: widget.readOnly, + ), + ), + ], + ), + const SizedBox(height: 16), + + // Région + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Région", + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w500, + color: theme.colorScheme.onBackground, + ), + ), + const SizedBox(height: 8), + _buildRegionDropdown(restrictedFieldsReadOnly), + ], + ), + const SizedBox(height: 16), + + // Contact + Text( + "Contact", + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.onBackground, + ), + ), + const SizedBox(height: 8), + + // Téléphone fixe et mobile sur la même ligne + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Téléphone fixe + Expanded( + child: CustomTextField( + controller: _phoneController, + label: "Téléphone fixe", + keyboardType: TextInputType.phone, + readOnly: widget.readOnly, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(10), + ], + validator: (value) { + if (value != null && + value.isNotEmpty && + value.length < 10) { + return "Le numéro de téléphone doit contenir 10 chiffres"; + } + return null; + }, + ), + ), + const SizedBox(width: 16), + // Téléphone mobile + Expanded( + child: CustomTextField( + controller: _mobileController, + label: "Téléphone mobile", + keyboardType: TextInputType.phone, + readOnly: widget.readOnly, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(10), + ], + validator: (value) { + if (value != null && + value.isNotEmpty && + value.length < 10) { + return "Le numéro de mobile doit contenir 10 chiffres"; + } + return null; + }, + ), + ), + ], + ), + const SizedBox(height: 16), + + // Email + CustomTextField( + controller: _emailController, + label: "Email", + keyboardType: TextInputType.emailAddress, + readOnly: widget.readOnly, + validator: (value) { + if (value == null || value.isEmpty) { + return "Veuillez entrer l'adresse email"; + } + if (!value.contains('@') || !value.contains('.')) { + return "Veuillez entrer une adresse email valide"; + } + return null; + }, + ), + const SizedBox(height: 16), + + // Informations avancées (visibles uniquement pour les administrateurs) + if (canEditRestrictedFields || + (_gpsLatController.text.isNotEmpty || + _gpsLngController.text.isNotEmpty || + _stripeIdController.text.isNotEmpty)) ...[ + Text( + "Informations avancées", + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.onBackground, + ), + ), + const SizedBox(height: 8), + + // GPS Latitude et Longitude + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // GPS Latitude + Expanded( + child: CustomTextField( + controller: _gpsLatController, + label: "GPS Latitude", + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + readOnly: restrictedFieldsReadOnly, + ), + ), + const SizedBox(width: 16), + // GPS Longitude + Expanded( + child: CustomTextField( + controller: _gpsLngController, + label: "GPS Longitude", + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + readOnly: restrictedFieldsReadOnly, + ), + ), + ], + ), + const SizedBox(height: 16), + + // Stripe Checkbox et Stripe ID sur la même ligne + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Checkbox Stripe + Checkbox( + value: _chkStripe, + onChanged: restrictedFieldsReadOnly + ? null + : (value) { + setState(() { + _chkStripe = value!; + }); + }, + activeColor: const Color(0xFF20335E), + ), + Text( + "Stripe activé", + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onBackground, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 16), + // Stripe ID + Expanded( + child: CustomTextField( + controller: _stripeIdController, + label: "Stripe ID", + readOnly: restrictedFieldsReadOnly, + ), + ), + ], + ), + const SizedBox(height: 16), + ], + + // Options + Text( + "Options", + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.onBackground, + ), + ), + const SizedBox(height: 8), + + // Checkbox Demo + _buildCheckboxOption( + label: "Mode démo", + value: _chkDemo, + onChanged: restrictedFieldsReadOnly + ? null + : (value) { + setState(() { + _chkDemo = value!; + }); + }, + ), + const SizedBox(height: 8), + + // Checkbox Copie Mail Reçu + _buildCheckboxOption( + label: "Copie des mails reçus", + value: _chkCopieMailRecu, + onChanged: widget.readOnly + ? null + : (value) { + setState(() { + _chkCopieMailRecu = value!; + }); + }, + ), + const SizedBox(height: 8), + + // Checkbox Accept SMS + _buildCheckboxOption( + label: "Accepte les SMS", + value: _chkAcceptSms, + onChanged: widget.readOnly + ? null + : (value) { + setState(() { + _chkAcceptSms = value!; + }); + }, + ), + const SizedBox(height: 8), + + // Checkbox Active + _buildCheckboxOption( + label: "Actif", + value: _chkActive, + onChanged: restrictedFieldsReadOnly + ? null + : (value) { + setState(() { + _chkActive = value!; + }); + }, + ), + const SizedBox(height: 25), + + // Bouton Enregistrer + if (!widget.readOnly) + Center( + child: ElevatedButton( + onPressed: _submitForm, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF20335E), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 24, vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + minimumSize: const Size(200, 50), + ), + child: const Text( + 'Enregistrer', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildCheckboxOption({ + required String label, + required bool value, + required Function(bool?)? onChanged, + }) { + final theme = Theme.of(context); + + return Row( + children: [ + Checkbox( + value: value, + onChanged: onChanged, + activeColor: const Color(0xFF20335E), + ), + Text( + label, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onBackground, + fontWeight: FontWeight.w500, + ), + ), + ], + ); + } + + Widget _buildRegionDropdown(bool readOnly) { + final theme = Theme.of(context); + + // Si en lecture seule, afficher simplement le texte + if (readOnly && _libRegion != null) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + _libRegion!, + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.onBackground, + ), + ), + ); + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 5), + decoration: BoxDecoration( + color: const Color(0xFFF4F5F6).withOpacity(0.85), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: const Color(0xFF20335E).withOpacity(0.1), + width: 1, + ), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: _fkRegion, + isExpanded: true, + hint: const Text("Sélectionnez une région"), + icon: const Icon( + Icons.keyboard_arrow_down, + color: Color(0xFF20335E), + ), + style: theme.textTheme.bodyMedium?.copyWith( + color: const Color(0xFF20335E), + ), + dropdownColor: Colors.white, + items: _regions + .map>((Map region) { + return DropdownMenuItem( + value: region['id'] as int, + child: Text(region['name'] as String), + ); + }).toList(), + onChanged: readOnly + ? null + : (int? newValue) { + setState(() { + _fkRegion = newValue; + // Trouver le libellé correspondant + if (newValue != null) { + final selectedRegion = _regions.firstWhere( + (region) => region['id'] == newValue, + orElse: () => {'id': newValue, 'name': ''}, + ); + _libRegion = selectedRegion['name'] as String; + } else { + _libRegion = null; + } + }); + }, + ), + ), + ); + } +} diff --git a/app/lib/presentation/widgets/environment_info_widget.dart b/app/lib/presentation/widgets/environment_info_widget.dart new file mode 100644 index 00000000..30b0fd71 --- /dev/null +++ b/app/lib/presentation/widgets/environment_info_widget.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:geosector_app/core/services/api_service.dart'; + +/// Widget qui affiche les informations sur l'environnement actuel +/// Utile pour le débogage +class EnvironmentInfoWidget extends StatelessWidget { + final bool showInDialog; + + const EnvironmentInfoWidget({ + Key? key, + this.showInDialog = false, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final apiService = Provider.of(context, listen: false); + final environment = apiService.getCurrentEnvironment(); + final apiUrl = apiService.getCurrentApiUrl(); + final appIdentifier = apiService.getCurrentAppIdentifier(); + + final content = Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '🌍 Environnement GeoSector', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: _getEnvironmentColor(environment)), + ), + const SizedBox(height: 16), + _buildInfoRow(context, 'Environnement', environment), + const Divider(), + _buildInfoRow(context, 'URL API', apiUrl), + const Divider(), + _buildInfoRow(context, 'App Identifier', appIdentifier), + const SizedBox(height: 20), + if (!showInDialog) + Align( + alignment: Alignment.center, + child: TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Fermer'), + ), + ), + ], + ), + ); + + if (showInDialog) { + return content; + } + + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: content, + ); + } + + Widget _buildInfoRow(BuildContext context, String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 120, + child: Text( + label, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(fontWeight: FontWeight.bold), + ), + ), + Expanded( + child: Text( + value, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ], + ), + ); + } + + Color _getEnvironmentColor(String environment) { + switch (environment) { + case 'DEV': + return Colors.green; + case 'REC': + return Colors.orange; + case 'PROD': + return Colors.red; + default: + return Colors.grey; + } + } + + /// Méthode statique pour afficher les informations sur l'environnement + /// dans une boîte de dialogue + static void show(BuildContext context) { + showDialog( + context: context, + builder: (context) => const EnvironmentInfoWidget(), + ); + } +} diff --git a/app/lib/presentation/widgets/examples/amicale_table_example.dart b/app/lib/presentation/widgets/examples/amicale_table_example.dart new file mode 100644 index 00000000..7a4d0a5d --- /dev/null +++ b/app/lib/presentation/widgets/examples/amicale_table_example.dart @@ -0,0 +1,234 @@ +import 'package:flutter/material.dart'; +import 'package:geosector_app/core/data/models/amicale_model.dart'; +import 'package:geosector_app/core/repositories/amicale_repository.dart'; +import 'package:geosector_app/presentation/widgets/amicale_table_widget.dart'; +import 'package:provider/provider.dart'; + +/// Exemple d'utilisation du widget AmicaleTableWidget +/// +/// Ce widget montre comment intégrer le tableau d'amicales dans une page +/// et comment gérer les actions d'édition et de suppression. +class AmicaleTableExample extends StatefulWidget { + const AmicaleTableExample({Key? key}) : super(key: key); + + @override + State createState() => _AmicaleTableExampleState(); +} + +class _AmicaleTableExampleState extends State { + bool _isLoading = true; + List _amicales = []; + String? _errorMessage; + + @override + void initState() { + super.initState(); + _loadAmicales(); + } + + Future _loadAmicales() async { + setState(() { + _isLoading = true; + _errorMessage = null; + }); + + try { + // Récupérer les amicales depuis le repository + final amicaleRepository = + Provider.of(context, listen: false); + final amicales = amicaleRepository.getAllAmicales(); + + setState(() { + _amicales = amicales; + _isLoading = false; + }); + } catch (e) { + setState(() { + _errorMessage = 'Erreur lors du chargement des amicales: $e'; + _isLoading = false; + }); + } + } + + void _handleEdit(AmicaleModel amicale) { + // Afficher une boîte de dialogue de confirmation + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Modifier l\'amicale'), + content: Text('Voulez-vous modifier l\'amicale ${amicale.name} ?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + // Naviguer vers la page de modification + // Navigator.of(context).push( + // MaterialPageRoute( + // builder: (context) => EditAmicalePage(amicale: amicale), + // ), + // ); + }, + child: const Text('Modifier'), + ), + ], + ), + ); + } + + void _handleDelete(AmicaleModel amicale) { + // Afficher une boîte de dialogue de confirmation + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Supprimer l\'amicale'), + content: Text( + 'Êtes-vous sûr de vouloir supprimer l\'amicale ${amicale.name} ?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Annuler'), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + onPressed: () { + Navigator.of(context).pop(); + _deleteAmicale(amicale); + }, + child: const Text('Supprimer'), + ), + ], + ), + ); + } + + Future _deleteAmicale(AmicaleModel amicale) async { + try { + setState(() { + _isLoading = true; + }); + + // Supprimer l'amicale via le repository + final amicaleRepository = + Provider.of(context, listen: false); + await amicaleRepository.deleteAmicale(amicale.id); + + // Recharger la liste + await _loadAmicales(); + + // Afficher un message de succès + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Amicale ${amicale.name} supprimée avec succès'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + setState(() { + _isLoading = false; + _errorMessage = 'Erreur lors de la suppression: $e'; + }); + + // Afficher un message d'erreur + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur: $_errorMessage'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Liste des amicales'), + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: _loadAmicales, + tooltip: 'Actualiser', + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Titre et description + Text( + 'Gestion des amicales', + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 8), + Text( + 'Consultez, modifiez ou supprimez les amicales selon vos droits d\'accès.', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 24), + + // Message d'erreur si présent + if (_errorMessage != null) + Container( + padding: const EdgeInsets.all(12), + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.red.withOpacity(0.3)), + ), + child: Row( + children: [ + const Icon(Icons.error_outline, color: Colors.red), + const SizedBox(width: 12), + Expanded( + child: Text( + _errorMessage!, + style: const TextStyle(color: Colors.red), + ), + ), + ], + ), + ), + + // Tableau des amicales + Expanded( + child: AmicaleTableWidget( + amicales: _amicales, + isLoading: _isLoading, + onDelete: _handleDelete, + emptyMessage: + 'Aucune amicale trouvée. Veuillez en créer une nouvelle.', + readOnly: false, // Permettre la modification dans la modale + ), + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + // Naviguer vers la page de création + // Navigator.of(context).push( + // MaterialPageRoute( + // builder: (context) => CreateAmicalePage(), + // ), + // ); + }, + tooltip: 'Ajouter une amicale', + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/app/lib/presentation/widgets/examples/entite_form_example.dart b/app/lib/presentation/widgets/examples/entite_form_example.dart new file mode 100644 index 00000000..d43c3a8a --- /dev/null +++ b/app/lib/presentation/widgets/examples/entite_form_example.dart @@ -0,0 +1,195 @@ +import 'package:flutter/material.dart'; +import 'package:geosector_app/core/data/models/amicale_model.dart'; +import 'package:geosector_app/core/repositories/amicale_repository.dart'; +import 'package:geosector_app/core/repositories/user_repository.dart'; +import 'package:geosector_app/presentation/widgets/entite_form.dart'; +import 'package:provider/provider.dart'; + +/// Exemple d'utilisation du widget EntiteForm +/// +/// Ce widget montre comment intégrer le formulaire d'entité dans une page +/// et comment gérer les événements de soumission. +class EntiteFormExample extends StatefulWidget { + final int? amicaleId; + final bool readOnly; + + const EntiteFormExample({ + Key? key, + this.amicaleId, + this.readOnly = false, + }) : super(key: key); + + @override + State createState() => _EntiteFormExampleState(); +} + +class _EntiteFormExampleState extends State { + AmicaleModel? _amicale; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadAmicale(); + } + + Future _loadAmicale() async { + setState(() { + _isLoading = true; + }); + + try { + if (widget.amicaleId != null) { + // Récupérer l'amicale depuis le repository + final amicaleRepository = + Provider.of(context, listen: false); + final amicale = amicaleRepository.getAmicaleById(widget.amicaleId!); + + setState(() { + _amicale = amicale; + _isLoading = false; + }); + } else { + // Création d'une nouvelle amicale + setState(() { + _amicale = null; + _isLoading = false; + }); + } + } catch (e) { + debugPrint('Erreur lors du chargement de l\'amicale: $e'); + setState(() { + _isLoading = false; + }); + } + } + + void _handleSubmit(AmicaleModel amicale) async { + try { + final amicaleRepository = + Provider.of(context, listen: false); + + // Sauvegarder l'amicale + final savedAmicale = await amicaleRepository.saveAmicale(amicale); + + // Afficher un message de confirmation + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text('Amicale ${savedAmicale.name} sauvegardée avec succès'), + backgroundColor: Colors.green, + ), + ); + + // Retourner à la page précédente + Navigator.of(context).pop(); + } + } catch (e) { + debugPrint('Erreur lors de la sauvegarde de l\'amicale: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors de la sauvegarde: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + @override + Widget build(BuildContext context) { + final userRepository = Provider.of(context, listen: false); + final userRole = userRepository.getUserRole(); + final bool canCreate = userRole > + 1; // Seuls les utilisateurs avec rôle > 1 peuvent créer/modifier + + return Scaffold( + appBar: AppBar( + title: Text(widget.amicaleId != null + ? (widget.readOnly + ? 'Détails de l\'amicale' + : 'Modifier l\'amicale') + : 'Nouvelle amicale'), + actions: [ + if (!widget.readOnly && _amicale != null) + IconButton( + icon: const Icon(Icons.delete), + onPressed: () => _showDeleteConfirmation(context), + tooltip: 'Supprimer', + ), + ], + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : !canCreate && _amicale == null + ? const Center( + child: Text( + 'Vous n\'avez pas les droits pour créer une amicale'), + ) + : SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: EntiteForm( + amicale: _amicale, + onSubmit: widget.readOnly ? null : _handleSubmit, + readOnly: widget.readOnly, + ), + ), + ); + } + + void _showDeleteConfirmation(BuildContext context) { + if (_amicale == null) return; + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Confirmation de suppression'), + content: Text( + 'Êtes-vous sûr de vouloir supprimer l\'amicale ${_amicale!.name} ?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Annuler'), + ), + TextButton( + onPressed: () async { + Navigator.of(context).pop(); + + try { + final amicaleRepository = + Provider.of(context, listen: false); + await amicaleRepository.deleteAmicale(_amicale!.id); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Amicale ${_amicale!.name} supprimée'), + backgroundColor: Colors.green, + ), + ); + + // Retourner à la page précédente + Navigator.of(context).pop(); + } + } catch (e) { + debugPrint('Erreur lors de la suppression de l\'amicale: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors de la suppression: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + }, + child: const Text('Supprimer'), + style: TextButton.styleFrom(foregroundColor: Colors.red), + ), + ], + ), + ); + } +} diff --git a/app/lib/presentation/widgets/examples/entite_form_with_regions_example.dart b/app/lib/presentation/widgets/examples/entite_form_with_regions_example.dart new file mode 100644 index 00000000..447ba251 --- /dev/null +++ b/app/lib/presentation/widgets/examples/entite_form_with_regions_example.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:geosector_app/core/data/models/amicale_model.dart'; +import 'package:geosector_app/core/repositories/amicale_repository.dart'; +import 'package:geosector_app/core/repositories/region_repository.dart'; +import 'package:geosector_app/core/repositories/user_repository.dart'; +import 'package:geosector_app/presentation/widgets/entite_form.dart'; +import 'package:provider/provider.dart'; + +/// Exemple d'utilisation du widget EntiteForm avec le RegionRepository +/// +/// Ce widget montre comment intégrer le formulaire d'entité dans une page +/// et comment utiliser le RegionRepository pour charger les régions. +class EntiteFormWithRegionsExample extends StatefulWidget { + final int? amicaleId; + final bool readOnly; + + const EntiteFormWithRegionsExample({ + Key? key, + this.amicaleId, + this.readOnly = false, + }) : super(key: key); + + @override + State createState() => + _EntiteFormWithRegionsExampleState(); +} + +class _EntiteFormWithRegionsExampleState + extends State { + AmicaleModel? _amicale; + bool _isLoading = true; + late RegionRepository _regionRepository; + + @override + void initState() { + super.initState(); + _regionRepository = RegionRepository(); + _initRepositories(); + } + + Future _initRepositories() async { + setState(() { + _isLoading = true; + }); + + try { + // Initialiser le repository des régions + await _regionRepository.init(); + + // Charger l'amicale si un ID est fourni + if (widget.amicaleId != null) { + final amicaleRepository = + Provider.of(context, listen: false); + final amicale = amicaleRepository.getAmicaleById(widget.amicaleId!); + + setState(() { + _amicale = amicale; + _isLoading = false; + }); + } else { + setState(() { + _isLoading = false; + }); + } + } catch (e) { + debugPrint('Erreur lors de l\'initialisation: $e'); + setState(() { + _isLoading = false; + }); + } + } + + void _handleSubmit(AmicaleModel amicale) async { + try { + final amicaleRepository = + Provider.of(context, listen: false); + + // Sauvegarder l'amicale + final savedAmicale = await amicaleRepository.saveAmicale(amicale); + + // Afficher un message de confirmation + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text('Amicale ${savedAmicale.name} sauvegardée avec succès'), + backgroundColor: Colors.green, + ), + ); + + // Retourner à la page précédente + Navigator.of(context).pop(); + } + } catch (e) { + debugPrint('Erreur lors de la sauvegarde de l\'amicale: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors de la sauvegarde: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + // Fournir le RegionRepository pour qu'il soit accessible par le widget EntiteForm + ChangeNotifierProvider.value( + value: _regionRepository), + ], + child: Scaffold( + appBar: AppBar( + title: Text(widget.amicaleId != null + ? (widget.readOnly + ? 'Détails de l\'amicale' + : 'Modifier l\'amicale') + : 'Nouvelle amicale'), + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: EntiteForm( + amicale: _amicale, + onSubmit: widget.readOnly ? null : _handleSubmit, + readOnly: widget.readOnly, + ), + ), + ), + ); + } +} diff --git a/app/lib/presentation/widgets/examples/membre_table_example.dart b/app/lib/presentation/widgets/examples/membre_table_example.dart new file mode 100644 index 00000000..1f111ad4 --- /dev/null +++ b/app/lib/presentation/widgets/examples/membre_table_example.dart @@ -0,0 +1,167 @@ +import 'package:flutter/material.dart'; +import 'package:geosector_app/core/constants/app_keys.dart'; +import 'package:geosector_app/core/data/models/membre_model.dart'; +import 'package:geosector_app/presentation/widgets/membre_table_widget.dart'; +import 'package:hive_flutter/hive_flutter.dart'; + +/// Exemple d'utilisation du widget MembreTableWidget +/// +/// Ce widget montre comment intégrer le tableau de membres dans une page +/// et comment gérer les événements d'édition et de suppression. +class MembreTableExample extends StatefulWidget { + const MembreTableExample({Key? key}) : super(key: key); + + @override + State createState() => _MembreTableExampleState(); +} + +class _MembreTableExampleState extends State { + List _membres = []; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadMembres(); + } + + Future _loadMembres() async { + setState(() { + _isLoading = true; + }); + + try { + // S'assurer que la boîte Hive est ouverte + if (!Hive.isBoxOpen(AppKeys.membresBoxName)) { + await Hive.openBox(AppKeys.membresBoxName); + } + + // Récupérer les membres depuis la boîte Hive + final membresBox = Hive.box(AppKeys.membresBoxName); + final membres = membresBox.values.toList(); + + setState(() { + _membres = membres; + _isLoading = false; + }); + } catch (e) { + debugPrint('Erreur lors du chargement des membres: $e'); + setState(() { + _isLoading = false; + }); + } + } + + void _handleEdit(MembreModel membre) { + // Exemple de gestion de l'événement d'édition + debugPrint( + 'Édition du membre: ${membre.firstName} ${membre.name} (ID: ${membre.id})'); + + // Ici, vous pourriez ouvrir une boîte de dialogue ou naviguer vers une page d'édition + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Édition de membre'), + content: Text( + 'Vous avez demandé à éditer le membre ${membre.firstName} ${membre.name}'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Fermer'), + ), + ], + ), + ); + } + + void _handleDelete(MembreModel membre) { + // Exemple de gestion de l'événement de suppression + debugPrint( + 'Suppression du membre: ${membre.firstName} ${membre.name} (ID: ${membre.id})'); + + // Demander confirmation avant de supprimer + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Confirmation de suppression'), + content: Text( + 'Êtes-vous sûr de vouloir supprimer le membre ${membre.firstName} ${membre.name} ?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Annuler'), + ), + TextButton( + onPressed: () async { + // Fermer la boîte de dialogue + Navigator.of(context).pop(); + + try { + // Supprimer le membre de la boîte Hive + final membresBox = + Hive.box(AppKeys.membresBoxName); + await membresBox.delete(membre.id); + + // Mettre à jour l'état + setState(() { + _membres = _membres.where((m) => m.id != membre.id).toList(); + }); + + // Afficher un message de confirmation + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Membre ${membre.firstName} ${membre.name} supprimé'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + debugPrint('Erreur lors de la suppression du membre: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors de la suppression: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + }, + child: const Text('Supprimer'), + style: TextButton.styleFrom(foregroundColor: Colors.red), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Tableau des Membres'), + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: _loadMembres, + tooltip: 'Rafraîchir', + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : MembreTableWidget( + membres: _membres, + onEdit: _handleEdit, + onDelete: _handleDelete, + height: + null, // Utiliser null pour que le widget prenne toute la hauteur disponible + ), + ), + ); + } +} diff --git a/flutt/lib/presentation/widgets/help_dialog.dart b/app/lib/presentation/widgets/help_dialog.dart similarity index 100% rename from flutt/lib/presentation/widgets/help_dialog.dart rename to app/lib/presentation/widgets/help_dialog.dart diff --git a/app/lib/presentation/widgets/hive_reset_dialog.dart b/app/lib/presentation/widgets/hive_reset_dialog.dart new file mode 100644 index 00000000..da567a56 --- /dev/null +++ b/app/lib/presentation/widgets/hive_reset_dialog.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; + +/// Widget de dialogue pour informer l'utilisateur que les données locales ont été réinitialisées +/// en raison d'une incompatibilité après une mise à jour de l'application. +class HiveResetDialog extends StatelessWidget { + /// Callback appelé lorsque l'utilisateur ferme le dialogue + final VoidCallback? onClose; + + const HiveResetDialog({ + Key? key, + this.onClose, + }) : super(key: key); + + /// Affiche le dialogue de réinitialisation Hive + static Future show(BuildContext context, + {VoidCallback? onClose}) async { + return showDialog( + context: context, + barrierDismissible: + false, // L'utilisateur doit appuyer sur un bouton pour fermer le dialogue + builder: (BuildContext dialogContext) { + return HiveResetDialog(onClose: onClose); + }, + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + title: Row( + children: [ + Icon( + Icons.sync_problem, + color: theme.colorScheme.error, + size: 28, + ), + const SizedBox(width: 12), + Text( + 'Données réinitialisées', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.onSurface, + ), + ), + ], + ), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Suite à une mise à jour de l\'application, vos données locales ont dû être réinitialisées pour assurer la compatibilité.', + style: theme.textTheme.bodyLarge, + ), + const SizedBox(height: 16), + Text( + 'Que s\'est-il passé ?', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + 'Une incompatibilité a été détectée entre la structure des données de la version précédente et celle de la version actuelle. Pour éviter tout problème, les données locales ont été effacées.', + style: theme.textTheme.bodyMedium, + ), + const SizedBox(height: 16), + Text( + 'Que dois-je faire ?', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + 'Vous devez vous reconnecter à votre compte pour récupérer vos données depuis le serveur. Toutes vos données seront restaurées automatiquement après la connexion.', + style: theme.textTheme.bodyMedium, + ), + const SizedBox(height: 16), + Text( + 'Note : Si vous aviez des modifications non synchronisées, elles ont été perdues. Nous vous recommandons de synchroniser régulièrement vos données.', + style: theme.textTheme.bodySmall?.copyWith( + fontStyle: FontStyle.italic, + color: theme.colorScheme.onSurface.withOpacity(0.7), + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + if (onClose != null) { + onClose!(); + } + }, + style: TextButton.styleFrom( + foregroundColor: theme.colorScheme.primary, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + child: const Text('J\'ai compris'), + ), + ], + ); + } +} diff --git a/flutt/lib/presentation/widgets/loading_overlay.dart b/app/lib/presentation/widgets/loading_overlay.dart similarity index 100% rename from flutt/lib/presentation/widgets/loading_overlay.dart rename to app/lib/presentation/widgets/loading_overlay.dart diff --git a/app/lib/presentation/widgets/loading_progress_overlay.dart b/app/lib/presentation/widgets/loading_progress_overlay.dart new file mode 100644 index 00000000..9840cd88 --- /dev/null +++ b/app/lib/presentation/widgets/loading_progress_overlay.dart @@ -0,0 +1,199 @@ +import 'package:flutter/material.dart'; +import 'dart:ui'; + +/// Widget d'overlay de chargement amélioré qui affiche une barre de progression +/// avec un effet de flou sur l'arrière-plan et un message détaillé sur l'étape en cours +class LoadingProgressOverlay extends StatefulWidget { + final String? message; + final double progress; + final String? stepDescription; + final Color backgroundColor; + final Color progressColor; + final Color textColor; + final double blurAmount; + final bool showPercentage; + + const LoadingProgressOverlay({ + Key? key, + this.message, + required this.progress, + this.stepDescription, + this.backgroundColor = Colors.black54, + this.progressColor = Colors.white, + this.textColor = Colors.white, + this.blurAmount = 5.0, + this.showPercentage = true, + }) : super(key: key); + + @override + State createState() => _LoadingProgressOverlayState(); +} + +class _LoadingProgressOverlayState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _progressAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + ); + _progressAnimation = Tween(begin: 0, end: widget.progress).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + ); + _animationController.forward(); + } + + @override + void didUpdateWidget(LoadingProgressOverlay oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.progress != widget.progress) { + _progressAnimation = Tween( + begin: oldWidget.progress, + end: widget.progress, + ).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + ); + _animationController.reset(); + _animationController.forward(); + } + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BackdropFilter( + filter: ImageFilter.blur( + sigmaX: widget.blurAmount, sigmaY: widget.blurAmount), + child: Container( + color: widget.backgroundColor, + child: Center( + child: Container( + width: MediaQuery.of(context).size.width * 0.85, + padding: const EdgeInsets.all(30), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.9), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black45, + blurRadius: 15, + spreadRadius: 5, + offset: const Offset(0, 5), + ), + ], + border: Border.all( + color: Colors.white.withOpacity(0.1), + width: 1.5, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.message != null) ...[ + Text( + widget.message!, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: widget.textColor, + letterSpacing: 0.5, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + ], + AnimatedBuilder( + animation: _progressAnimation, + builder: (context, child) { + return Column( + children: [ + LinearProgressIndicator( + value: _progressAnimation.value, + backgroundColor: + widget.progressColor.withOpacity(0.3), + valueColor: AlwaysStoppedAnimation( + widget.progressColor), + minHeight: 15, + borderRadius: BorderRadius.circular(8), + ), + if (widget.showPercentage) ...[ + const SizedBox(height: 8), + Text( + '${(_progressAnimation.value * 100).toInt()}%', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: widget.textColor, + letterSpacing: 1.2, + ), + ), + ], + ], + ); + }, + ), + if (widget.stepDescription != null) ...[ + const SizedBox(height: 16), + Text( + widget.stepDescription!, + style: TextStyle( + fontSize: 16, + color: widget.textColor.withOpacity(0.9), + fontStyle: FontStyle.italic, + ), + textAlign: TextAlign.center, + ), + ], + ], + ), + ), + ), + ), + ); + } +} + +/// Classe utilitaire pour gérer l'overlay de chargement avec progression +class LoadingProgressOverlayUtils { + /// Méthode pour afficher l'overlay de chargement avec progression + static OverlayEntry show({ + required BuildContext context, + String? message, + double progress = 0.0, + String? stepDescription, + double blurAmount = 5.0, + bool showPercentage = true, + }) { + final overlayEntry = OverlayEntry( + builder: (context) => LoadingProgressOverlay( + message: message, + progress: progress, + stepDescription: stepDescription, + blurAmount: blurAmount, + showPercentage: showPercentage, + ), + ); + + Overlay.of(context).insert(overlayEntry); + return overlayEntry; + } + + /// Méthode pour mettre à jour l'overlay existant + static void update({ + required OverlayEntry overlayEntry, + String? message, + required double progress, + String? stepDescription, + }) { + overlayEntry.markNeedsBuild(); + } +} diff --git a/flutt/lib/presentation/widgets/mapbox_map.dart b/app/lib/presentation/widgets/mapbox_map.dart similarity index 92% rename from flutt/lib/presentation/widgets/mapbox_map.dart rename to app/lib/presentation/widgets/mapbox_map.dart index b7ea1b09..e80da595 100644 --- a/flutt/lib/presentation/widgets/mapbox_map.dart +++ b/app/lib/presentation/widgets/mapbox_map.dart @@ -2,33 +2,34 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; +import 'package:geosector_app/app.dart'; // Pour accéder à l'instance globale de ApiService /// Widget de carte réutilisable utilisant Mapbox -/// +/// /// Ce widget encapsule un FlutterMap avec des tuiles Mapbox et fournit /// des fonctionnalités pour afficher des marqueurs, des polygones et des contrôles. class MapboxMap extends StatefulWidget { /// Position initiale de la carte final LatLng initialPosition; - + /// Niveau de zoom initial final double initialZoom; - + /// Liste des marqueurs à afficher final List? markers; - + /// Liste des polygones à afficher final List? polygons; - + /// Contrôleur de carte externe (optionnel) final MapController? mapController; - + /// Callback appelé lorsque la carte est déplacée final void Function(MapEvent)? onMapEvent; - + /// Afficher les boutons de contrôle (zoom, localisation) final bool showControls; - + /// Style de la carte Mapbox (optionnel) /// Si non spécifié, utilise le style par défaut 'mapbox/streets-v12' final String? mapStyle; @@ -52,7 +53,7 @@ class MapboxMap extends StatefulWidget { class _MapboxMapState extends State { /// Contrôleur de carte interne late final MapController _mapController; - + /// Niveau de zoom actuel double _currentZoom = 13.0; @@ -103,9 +104,12 @@ class _MapboxMapState extends State { @override Widget build(BuildContext context) { // Déterminer l'URL du template de tuiles Mapbox - final String mapboxToken = AppKeys.mapboxApiKey; + // Utiliser l'environnement actuel pour obtenir la bonne clé API + final String environment = apiService.getCurrentEnvironment(); + final String mapboxToken = AppKeys.getMapboxApiKey(environment); final String mapStyle = widget.mapStyle ?? 'mapbox/streets-v11'; - final String urlTemplate = 'https://api.mapbox.com/styles/v1/$mapStyle/tiles/256/{z}/{x}/{y}@2x?access_token=$mapboxToken'; + final String urlTemplate = + 'https://api.mapbox.com/styles/v1/$mapStyle/tiles/256/{z}/{x}/{y}@2x?access_token=$mapboxToken'; return Stack( children: [ @@ -122,7 +126,7 @@ class _MapboxMapState extends State { _currentZoom = _mapController.camera.zoom; }); } - + // Appeler le callback externe si fourni if (widget.onMapEvent != null) { widget.onMapEvent!(event); @@ -139,17 +143,17 @@ class _MapboxMapState extends State { 'accessToken': mapboxToken, }, ), - + // Polygones if (widget.polygons != null && widget.polygons!.isNotEmpty) PolygonLayer(polygons: widget.polygons!), - + // Marqueurs if (widget.markers != null && widget.markers!.isNotEmpty) MarkerLayer(markers: widget.markers!), ], ), - + // Boutons de contrôle if (widget.showControls) Positioned( @@ -168,7 +172,7 @@ class _MapboxMapState extends State { }, ), const SizedBox(height: 8), - + // Bouton de zoom - _buildMapButton( icon: Icons.remove, @@ -180,7 +184,7 @@ class _MapboxMapState extends State { }, ), const SizedBox(height: 8), - + // Bouton de localisation _buildMapButton( icon: Icons.my_location, diff --git a/app/lib/presentation/widgets/membre_row_widget.dart b/app/lib/presentation/widgets/membre_row_widget.dart new file mode 100644 index 00000000..1ea4f9e1 --- /dev/null +++ b/app/lib/presentation/widgets/membre_row_widget.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; +import 'package:geosector_app/core/data/models/membre_model.dart'; + +class MembreRowWidget extends StatelessWidget { + final MembreModel membre; + final Function()? onEdit; + final Function()? onDelete; + + const MembreRowWidget({ + Key? key, + required this.membre, + this.onEdit, + this.onDelete, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Container( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8.0), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + // ID + Expanded( + flex: 1, + child: Text( + membre.id.toString(), + style: theme.textTheme.bodyMedium, + ), + ), + + // Prénom (firstName) + Expanded( + flex: 2, + child: Text( + membre.firstName, + style: theme.textTheme.bodyMedium, + overflow: TextOverflow.ellipsis, + ), + ), + + // Nom (name) + Expanded( + flex: 2, + child: Text( + membre.name, + style: theme.textTheme.bodyMedium, + overflow: TextOverflow.ellipsis, + ), + ), + + // Secteur (sectName) + Expanded( + flex: 2, + child: Text( + membre.sectName ?? '', + style: theme.textTheme.bodyMedium, + overflow: TextOverflow.ellipsis, + ), + ), + + // Rôle (fkRole) + Expanded( + flex: 1, + child: Text( + _getRoleName(membre.fkRole), + style: theme.textTheme.bodyMedium, + ), + ), + + // Actions + Expanded( + flex: 2, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // Bouton Edit + IconButton( + icon: const Icon(Icons.edit, size: 20), + color: theme.colorScheme.primary, + onPressed: onEdit, + tooltip: 'Modifier', + constraints: const BoxConstraints(), + padding: const EdgeInsets.all(8), + ), + + // Bouton Delete + IconButton( + icon: const Icon(Icons.delete, size: 20), + color: theme.colorScheme.error, + onPressed: onDelete, + tooltip: 'Supprimer', + constraints: const BoxConstraints(), + padding: const EdgeInsets.all(8), + ), + ], + ), + ), + ], + ), + ); + } + + // Méthode pour convertir l'ID de rôle en nom lisible + String _getRoleName(int roleId) { + switch (roleId) { + case 1: + return 'User'; + case 2: + return 'Admin'; + case 3: + return 'Super'; + default: + return roleId.toString(); + } + } +} diff --git a/app/lib/presentation/widgets/membre_table_widget.dart b/app/lib/presentation/widgets/membre_table_widget.dart new file mode 100644 index 00000000..9b08c258 --- /dev/null +++ b/app/lib/presentation/widgets/membre_table_widget.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:geosector_app/core/data/models/membre_model.dart'; +import 'package:geosector_app/presentation/widgets/membre_row_widget.dart'; + +class MembreTableWidget extends StatelessWidget { + final List membres; + final Function(MembreModel)? onEdit; + final Function(MembreModel)? onDelete; + final bool showHeader; + final double? height; + final EdgeInsetsGeometry? padding; + + const MembreTableWidget({ + Key? key, + required this.membres, + this.onEdit, + this.onDelete, + this.showHeader = true, + this.height, + this.padding, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Container( + height: height, + padding: padding ?? const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8.0), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // En-tête du tableau + if (showHeader) + Padding( + padding: + const EdgeInsets.only(bottom: 16.0, left: 16.0, right: 16.0), + child: Row( + children: [ + // ID + Expanded( + flex: 1, + child: Text( + 'ID', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + + // Prénom (firstName) + Expanded( + flex: 2, + child: Text( + 'Prénom', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + + // Nom (name) + Expanded( + flex: 2, + child: Text( + 'Nom', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + + // Secteur (sectName) + Expanded( + flex: 2, + child: Text( + 'Secteur', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + + // Rôle (fkRole) + Expanded( + flex: 1, + child: Text( + 'Rôle', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + + // Actions + Expanded( + flex: 2, + child: Text( + 'Actions', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.end, + ), + ), + ], + ), + ), + + // Liste des membres + Expanded( + child: membres.isEmpty + ? Center( + child: Text( + 'Aucun membre disponible', + style: theme.textTheme.bodyMedium, + ), + ) + : ListView.separated( + itemCount: membres.length, + separatorBuilder: (context, index) => + const SizedBox(height: 8.0), + itemBuilder: (context, index) { + final membre = membres[index]; + return MembreRowWidget( + membre: membre, + onEdit: onEdit != null ? () => onEdit!(membre) : null, + onDelete: + onDelete != null ? () => onDelete!(membre) : null, + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/app/lib/presentation/widgets/passage_form.dart b/app/lib/presentation/widgets/passage_form.dart new file mode 100644 index 00000000..cd63c7c6 --- /dev/null +++ b/app/lib/presentation/widgets/passage_form.dart @@ -0,0 +1,422 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'custom_text_field.dart'; + +class PassageForm extends StatefulWidget { + final Function(Map)? onSubmit; + final Map? initialData; + + const PassageForm({ + Key? key, + this.onSubmit, + this.initialData, + }) : super(key: key); + + @override + State createState() => _PassageFormState(); +} + +class _PassageFormState extends State { + final _formKey = GlobalKey(); + + // Controllers + late final TextEditingController _villeController; + late final TextEditingController _adresseController; + late final TextEditingController _nomHabitantController; + late final TextEditingController _emailController; + late final TextEditingController _montantController; + late final TextEditingController _commentairesController; + + // Form values + String _typeHabitat = 'Individuel'; + String _typeReglement = 'Espèces'; + + @override + void initState() { + super.initState(); + + // Initialize controllers with initial data if available + final data = widget.initialData ?? {}; + _villeController = TextEditingController(text: data['ville'] ?? ''); + _adresseController = TextEditingController(text: data['adresse'] ?? ''); + _nomHabitantController = + TextEditingController(text: data['nomHabitant'] ?? ''); + _emailController = TextEditingController(text: data['email'] ?? ''); + _montantController = TextEditingController(text: data['montant'] ?? ''); + _commentairesController = + TextEditingController(text: data['commentaires'] ?? ''); + + _typeHabitat = data['typeHabitat'] ?? 'Individuel'; + _typeReglement = data['typeReglement'] ?? 'Espèces'; + } + + @override + void dispose() { + _villeController.dispose(); + _adresseController.dispose(); + _nomHabitantController.dispose(); + _emailController.dispose(); + _montantController.dispose(); + _commentairesController.dispose(); + super.dispose(); + } + + void _submitForm() { + if (_formKey.currentState!.validate()) { + final formData = { + 'ville': _villeController.text, + 'adresse': _adresseController.text, + 'typeHabitat': _typeHabitat, + 'nomHabitant': _nomHabitantController.text, + 'email': _emailController.text, + 'montant': _montantController.text, + 'typeReglement': _typeReglement, + 'commentaires': _commentairesController.text, + }; + + if (widget.onSubmit != null) { + widget.onSubmit!(formData); + } + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Ville + CustomTextField( + controller: _villeController, + label: 'Ville', + validator: (value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer une ville'; + } + return null; + }, + ), + const SizedBox(height: 16), + + // Adresse + CustomTextField( + controller: _adresseController, + label: 'Adresse', + validator: (value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer une adresse'; + } + return null; + }, + ), + const SizedBox(height: 16), + + // Type d'habitat + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Type d'habitat", + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w500, + color: theme.colorScheme.onBackground, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + _buildRadioOption( + value: 'Individuel', + groupValue: _typeHabitat, + onChanged: (value) { + setState(() { + _typeHabitat = value!; + }); + }, + ), + const SizedBox(width: 40), + _buildRadioOption( + value: 'Collectif', + groupValue: _typeHabitat, + onChanged: (value) { + setState(() { + _typeHabitat = value!; + }); + }, + ), + ], + ), + ], + ), + const SizedBox(height: 16), + + // Nom de l'habitant + CustomTextField( + controller: _nomHabitantController, + label: "Nom de l'habitant", + validator: (value) { + if (value == null || value.isEmpty) { + return "Veuillez entrer le nom de l'habitant"; + } + return null; + }, + ), + const SizedBox(height: 16), + + // Email de l'habitant + CustomTextField( + controller: _emailController, + label: "Adresse email de l'habitant", + keyboardType: TextInputType.emailAddress, + validator: (value) { + if (value == null || value.isEmpty) { + return null; // Email optionnel + } + // Simple email validation + if (!value.contains('@') || !value.contains('.')) { + return "Veuillez entrer une adresse email valide"; + } + return null; + }, + ), + const SizedBox(height: 16), + + // Montant et Type de règlement + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Montant reçu + Expanded( + flex: 1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Montant reçu", + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w500, + color: theme.colorScheme.onBackground, + ), + ), + const SizedBox(height: 8), + TextFormField( + controller: _montantController, + keyboardType: + TextInputType.numberWithOptions(decimal: true), + inputFormatters: [ + FilteringTextInputFormatter.allow( + RegExp(r'^\d+\.?\d{0,2}')), + ], + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.onBackground, + ), + decoration: InputDecoration( + hintText: '0.00 €', + hintStyle: theme.textTheme.bodyLarge?.copyWith( + color: + theme.colorScheme.onBackground.withOpacity(0.5), + ), + fillColor: const Color(0xFFF4F5F6), + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: + theme.colorScheme.onBackground.withOpacity(0.1), + width: 1, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: + theme.colorScheme.onBackground.withOpacity(0.1), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: theme.colorScheme.primary, + width: 2, + ), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Requis'; + } + return null; + }, + ), + ], + ), + ), + const SizedBox(width: 20), + // Type de règlement + Expanded( + flex: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Type de règlement", + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w500, + color: theme.colorScheme.onBackground, + ), + ), + const SizedBox(height: 8), + _buildDropdown(), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + + // Commentaires + CustomTextField( + controller: _commentairesController, + label: "Commentaires", + hintText: "Placeholder", + maxLines: 3, + ), + const SizedBox(height: 25), + + // Titre de section + Text( + "Mise à jour du passage effectué", + style: theme.textTheme.bodyLarge?.copyWith( + color: const Color(0xFF20335E), + ), + ), + const SizedBox(height: 16), + + // Bouton Enregistrer + Center( + child: ElevatedButton( + onPressed: _submitForm, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF20335E), + foregroundColor: Colors.white, + padding: + const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + minimumSize: const Size(200, 50), + ), + child: const Text( + 'Enregistrer', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildRadioOption({ + required String value, + required String groupValue, + required Function(String?) onChanged, + }) { + final theme = Theme.of(context); + final isSelected = value == groupValue; + + return Row( + children: [ + Radio( + value: value, + groupValue: groupValue, + onChanged: onChanged, + activeColor: const Color(0xFF20335E), + ), + Text( + value, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onBackground, + fontWeight: FontWeight.w500, + ), + ), + ], + ); + } + + Widget _buildDropdown() { + final theme = Theme.of(context); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 5), + decoration: BoxDecoration( + color: const Color(0xFFF4F5F6).withOpacity(0.85), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: const Color(0xFF20335E).withOpacity(0.1), + width: 1, + ), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: _typeReglement, + isExpanded: true, + icon: const Icon( + Icons.keyboard_arrow_down, + color: Color(0xFF20335E), + ), + style: theme.textTheme.bodyMedium?.copyWith( + color: const Color(0xFF20335E), + ), + dropdownColor: Colors.white, + items: ['Espèces', 'CB', 'Chèque'] + .map>((String value) { + return DropdownMenuItem( + value: value, + child: Row( + children: [ + _getPaymentIcon(value), + const SizedBox(width: 8), + Text(value), + ], + ), + ); + }).toList(), + onChanged: (String? newValue) { + setState(() { + _typeReglement = newValue!; + }); + }, + ), + ), + ); + } + + Widget _getPaymentIcon(String type) { + switch (type) { + case 'Espèces': + return const Icon(Icons.payments_outlined, + color: Color(0xFF20335E), size: 20); + case 'CB': + return const Icon(Icons.credit_card, + color: Color(0xFF20335E), size: 20); + case 'Chèque': + return const Icon(Icons.account_balance_wallet_outlined, + color: Color(0xFF20335E), size: 20); + default: + return const SizedBox.shrink(); + } + } +} diff --git a/flutt/lib/presentation/widgets/passages/passages_list_widget.dart b/app/lib/presentation/widgets/passages/passages_list_widget.dart similarity index 100% rename from flutt/lib/presentation/widgets/passages/passages_list_widget.dart rename to app/lib/presentation/widgets/passages/passages_list_widget.dart diff --git a/app/lib/presentation/widgets/profile_dialog.dart b/app/lib/presentation/widgets/profile_dialog.dart new file mode 100644 index 00000000..d47e14f3 --- /dev/null +++ b/app/lib/presentation/widgets/profile_dialog.dart @@ -0,0 +1,268 @@ +import 'package:flutter/material.dart'; +import 'package:geosector_app/app.dart'; +import 'package:geosector_app/core/data/models/user_model.dart'; +import 'package:geosector_app/presentation/widgets/user_form.dart'; + +class ProfileDialog extends StatefulWidget { + final UserModel user; + + const ProfileDialog({ + Key? key, + required this.user, + }) : super(key: key); + + /// Méthode statique pour afficher la boîte de dialogue + static Future show(BuildContext context, UserModel user) { + return showDialog( + context: context, + builder: (context) => ProfileDialog(user: user), + ); + } + + @override + State createState() => _ProfileDialogState(); +} + +class _ProfileDialogState extends State { + final _formKey = GlobalKey(); + late UserModel _user; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _user = widget.user; + } + + // Fonction pour capitaliser la première lettre de chaque mot + String _capitalizeFirstLetter(String text) { + if (text.isEmpty) return text; + + return text.split(' ').map((word) { + if (word.isEmpty) return word; + return word[0].toUpperCase() + word.substring(1).toLowerCase(); + }).join(' '); + } + + // Fonction pour mettre en majuscule + String _toUpperCase(String text) { + return text.toUpperCase(); + } + + // Fonction pour valider et soumettre le formulaire + Future _saveProfile(UserModel updatedUser) async { + // Validation supplémentaire + if (!_validateUser(updatedUser)) { + return; + } + + setState(() { + _isLoading = true; + }); + + try { + // Formatage des données + final formattedUser = updatedUser.copyWith( + name: _toUpperCase(updatedUser.name ?? ''), + firstName: _capitalizeFirstLetter(updatedUser.firstName ?? ''), + ); + + // Sauvegarde de l'utilisateur + await userRepository.saveUser(formattedUser); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Profil mis à jour avec succès'), + backgroundColor: Colors.green, + ), + ); + Navigator.of(context).pop(true); // Fermer la modale avec succès + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors de la mise à jour du profil: $e'), + backgroundColor: Colors.red, + ), + ); + } + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + // Validation supplémentaire + bool _validateUser(UserModel user) { + // Vérifier que l'email est valide + if (user.email.isEmpty || + !user.email.contains('@') || + !user.email.contains('.')) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Veuillez entrer une adresse email valide'), + backgroundColor: Colors.red, + ), + ); + return false; + } + + // Vérifier que le nom ou le sectName est renseigné + if ((user.name == null || user.name!.isEmpty) && + (user.sectName == null || user.sectName!.isEmpty)) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Le nom ou le nom du secteur doit être renseigné'), + backgroundColor: Colors.red, + ), + ); + return false; + } + + // Vérifier que le téléphone fixe est valide s'il est renseigné + if (user.phone != null && + user.phone!.isNotEmpty && + user.phone!.length != 10) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: + Text('Le numéro de téléphone fixe doit contenir 10 chiffres'), + backgroundColor: Colors.red, + ), + ); + return false; + } + + // Vérifier que le téléphone mobile est valide s'il est renseigné + if (user.mobile != null && + user.mobile!.isNotEmpty && + user.mobile!.length != 10) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: + Text('Le numéro de téléphone mobile doit contenir 10 chiffres'), + backgroundColor: Colors.red, + ), + ); + return false; + } + + return true; + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + backgroundColor: Colors.transparent, + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + constraints: const BoxConstraints( + maxWidth: 600, + maxHeight: 700, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Titre + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Mon compte', + style: theme.textTheme.headlineSmall?.copyWith( + color: theme.colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), + tooltip: 'Fermer', + ), + ], + ), + const Divider(), + const SizedBox(height: 10), + + // Formulaire + Expanded( + child: SingleChildScrollView( + child: UserForm( + user: _user, + onSubmit: _saveProfile, + ), + ), + ), + + // Boutons + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: + _isLoading ? null : () => Navigator.of(context).pop(), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 12), + ), + child: const Text('Fermer'), + ), + const SizedBox(width: 10), + ElevatedButton( + onPressed: _isLoading + ? null + : () { + // Appeler directement la méthode onSubmit du UserForm + // qui va déclencher la validation et la soumission + _saveProfile(_user); + }, + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 12), + ), + child: _isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: + AlwaysStoppedAnimation(Colors.white), + ), + ) + : const Text('Enregistrer'), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/flutt/lib/presentation/widgets/responsive_navigation.dart b/app/lib/presentation/widgets/responsive_navigation.dart similarity index 90% rename from flutt/lib/presentation/widgets/responsive_navigation.dart rename to app/lib/presentation/widgets/responsive_navigation.dart index d08eaa1e..d27b90b0 100644 --- a/flutt/lib/presentation/widgets/responsive_navigation.dart +++ b/app/lib/presentation/widgets/responsive_navigation.dart @@ -135,18 +135,13 @@ class _ResponsiveNavigationState extends State { /// Construction du layout pour les écrans de bureau (web) Widget _buildDesktopLayout() { - // Utiliser une couleur de fond différente selon le type d'utilisateur - final backgroundColor = widget.isAdmin - ? const Color(0xFFFFEBEE) // Fond rouge clair pour l'interface admin - : const Color( - 0xFFE8F5E9); // Fond vert clair pour l'interface utilisateur - return Row( children: [ _buildSidebar(), Expanded( child: Container( - color: backgroundColor, + color: Colors + .transparent, // Fond transparent pour voir l'AdminBackground child: widget.body, ), ), @@ -156,14 +151,8 @@ class _ResponsiveNavigationState extends State { /// Construction du layout pour les écrans mobiles Widget _buildMobileLayout() { - // Utiliser une couleur de fond différente selon le type d'utilisateur - final backgroundColor = widget.isAdmin - ? const Color(0xFFFFEBEE) // Fond rouge clair pour l'interface admin - : const Color( - 0xFFE8F5E9); // Fond vert clair pour l'interface utilisateur - return Container( - color: backgroundColor, + color: Colors.transparent, // Fond transparent pour voir l'AdminBackground child: widget.body, ); } @@ -384,12 +373,12 @@ class _ResponsiveNavigationState extends State { subtitle: null, isSidebarMinimized: _isSidebarMinimized, onTap: () { - // Afficher la boîte de dialogue de profil avec l'ID de l'utilisateur actuel + // Afficher la boîte de dialogue de profil avec l'utilisateur actuel // Utiliser l'instance globale définie dans app.dart final user = userRepository.currentUser; - if (user != null && user.id != null) { - // Convertir l'ID en chaîne de caractères si nécessaire - ProfileDialog.show(context, user.id!.toString()); + if (user != null) { + // Passer l'objet utilisateur complet + ProfileDialog.show(context, user); } else { // Afficher un message d'erreur si l'utilisateur n'est pas trouvé ScaffoldMessenger.of(context).showSnackBar( @@ -401,7 +390,10 @@ class _ResponsiveNavigationState extends State { } }, ), - if (widget.isAdmin && userRepository.currentUser?.role == 2) + // Option "Amicale & membres" - uniquement pour les administrateurs avec le rôle 2 et en version web + if (widget.isAdmin && + userRepository.currentUser?.role == 2 && + MediaQuery.of(context).size.width > 900) _SettingsItem( icon: Icons.people, title: 'Amicale & membres', @@ -415,6 +407,43 @@ class _ResponsiveNavigationState extends State { // puisse le récupérer et sélectionner le bon onglet final settingsBox = Hive.box(AppKeys.settingsBoxName); settingsBox.put('adminSelectedPageIndex', 5); + + // Notifier l'utilisateur que la page est en cours de chargement + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: + Text('Chargement de la page Amicale & membres...'), + duration: Duration(seconds: 2), + ), + ); + + // Attendre un court instant pour permettre à la navigation de se terminer + Future.delayed(const Duration(milliseconds: 300), () { + // Forcer la sélection de l'onglet Amicale & membres + if (widget.isAdmin && widget.selectedIndex != 5) { + widget.onDestinationSelected(5); + } + }); + }, + ), + + // Option "Opérations" - uniquement pour les administrateurs et en version web + if (widget.isAdmin && MediaQuery.of(context).size.width > 900) + _SettingsItem( + icon: Icons.calendar_today, + title: 'Opérations', + isSidebarMinimized: _isSidebarMinimized, + onTap: () { + // Navigation vers le tableau de bord admin + context.go('/admin'); + + // Note: Pas de page spécifique pour le moment, juste un placeholder + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Fonctionnalité à venir'), + duration: Duration(seconds: 2), + ), + ); }, ), const SizedBox(height: 16), diff --git a/flutt/lib/presentation/widgets/sector_distribution_card.dart b/app/lib/presentation/widgets/sector_distribution_card.dart similarity index 95% rename from flutt/lib/presentation/widgets/sector_distribution_card.dart rename to app/lib/presentation/widgets/sector_distribution_card.dart index bf4ccc48..db9cd806 100644 --- a/flutt/lib/presentation/widgets/sector_distribution_card.dart +++ b/app/lib/presentation/widgets/sector_distribution_card.dart @@ -9,12 +9,14 @@ class SectorDistributionCard extends StatefulWidget { final String title; final double? height; final EdgeInsetsGeometry? padding; + final bool forceRefresh; const SectorDistributionCard({ Key? key, this.title = 'Répartition par secteur', this.height, this.padding, + this.forceRefresh = false, }) : super(key: key); @override @@ -31,6 +33,16 @@ class _SectorDistributionCardState extends State { _loadSectorData(); } + @override + void didUpdateWidget(SectorDistributionCard oldWidget) { + super.didUpdateWidget(oldWidget); + + // Recharger les données si forceRefresh est passé à true + if (widget.forceRefresh && !oldWidget.forceRefresh) { + _loadSectorData(); + } + } + Future _loadSectorData() async { setState(() { isLoading = true; diff --git a/app/lib/presentation/widgets/user_form.dart b/app/lib/presentation/widgets/user_form.dart new file mode 100644 index 00000000..5c37a340 --- /dev/null +++ b/app/lib/presentation/widgets/user_form.dart @@ -0,0 +1,370 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:intl/intl.dart'; +import 'package:geosector_app/core/data/models/user_model.dart'; +import 'custom_text_field.dart'; + +class UserForm extends StatefulWidget { + final UserModel? user; + final Function(UserModel)? onSubmit; + final bool readOnly; + + const UserForm({ + Key? key, + this.user, + this.onSubmit, + this.readOnly = false, + }) : super(key: key); + + @override + State createState() => _UserFormState(); +} + +class _UserFormState extends State { + final _formKey = GlobalKey(); + + // Controllers + late final TextEditingController _usernameController; + late final TextEditingController _firstNameController; + late final TextEditingController _nameController; + late final TextEditingController _phoneController; + late final TextEditingController _mobileController; + late final TextEditingController _emailController; + late final TextEditingController _dateNaissanceController; + late final TextEditingController _dateEmbaucheController; + + // Form values + int _fkTitre = 1; // 1 = M., 2 = Mme + DateTime? _dateNaissance; + DateTime? _dateEmbauche; + + @override + void initState() { + super.initState(); + + // Initialize controllers with user data if available + final user = widget.user; + _usernameController = TextEditingController(text: user?.username ?? ''); + _firstNameController = TextEditingController(text: user?.firstName ?? ''); + _nameController = TextEditingController(text: user?.name ?? ''); + _phoneController = TextEditingController(text: user?.phone ?? ''); + _mobileController = TextEditingController(text: user?.mobile ?? ''); + _emailController = TextEditingController(text: user?.email ?? ''); + + _dateNaissance = user?.dateNaissance; + _dateEmbauche = user?.dateEmbauche; + + _dateNaissanceController = TextEditingController( + text: _dateNaissance != null + ? DateFormat('dd/MM/yyyy').format(_dateNaissance!) + : ''); + + _dateEmbaucheController = TextEditingController( + text: _dateEmbauche != null + ? DateFormat('dd/MM/yyyy').format(_dateEmbauche!) + : ''); + + _fkTitre = user?.fkTitre ?? 1; + } + + @override + void dispose() { + _usernameController.dispose(); + _firstNameController.dispose(); + _nameController.dispose(); + _phoneController.dispose(); + _mobileController.dispose(); + _emailController.dispose(); + _dateNaissanceController.dispose(); + _dateEmbaucheController.dispose(); + super.dispose(); + } + + // Méthode simplifiée pour sélectionner une date + void _selectDate(BuildContext context, bool isDateNaissance) { + // Utiliser un bloc try-catch pour capturer toutes les erreurs possibles + try { + // Afficher le sélecteur de date sans spécifier de locale + showDatePicker( + context: context, + initialDate: DateTime.now(), // Toujours utiliser la date actuelle + firstDate: DateTime(1900), + lastDate: DateTime.now(), + // Ne pas spécifier de locale pour éviter les problèmes + ).then((DateTime? picked) { + // Vérifier si une date a été sélectionnée + if (picked != null) { + setState(() { + // Mettre à jour la date et le texte du contrôleur + if (isDateNaissance) { + _dateNaissance = picked; + _dateNaissanceController.text = + DateFormat('dd/MM/yyyy').format(picked); + } else { + _dateEmbauche = picked; + _dateEmbaucheController.text = + DateFormat('dd/MM/yyyy').format(picked); + } + }); + } + }).catchError((error) { + // Gérer les erreurs spécifiques au sélecteur de date + debugPrint('Erreur lors de la sélection de la date: $error'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors de la sélection de la date'), + backgroundColor: Colors.red, + ), + ); + }); + } catch (e) { + // Gérer toutes les autres erreurs + debugPrint('Exception lors de l\'affichage du sélecteur de date: $e'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Impossible d\'afficher le sélecteur de date'), + backgroundColor: Colors.red, + ), + ); + } + } + + void _submitForm() { + if (_formKey.currentState!.validate()) { + final user = widget.user?.copyWith( + firstName: _firstNameController.text, + name: _nameController.text, + phone: _phoneController.text, + mobile: _mobileController.text, + email: _emailController.text, + fkTitre: _fkTitre, + dateNaissance: _dateNaissance, + dateEmbauche: _dateEmbauche, + ) ?? + UserModel( + id: 0, // Sera remplacé par l'API + firstName: _firstNameController.text, + name: _nameController.text, + phone: _phoneController.text, + mobile: _mobileController.text, + email: _emailController.text, + fkTitre: _fkTitre, + dateNaissance: _dateNaissance, + dateEmbauche: _dateEmbauche, + role: 1, // Valeur par défaut + createdAt: DateTime.now(), + lastSyncedAt: DateTime.now(), + ); + + if (widget.onSubmit != null) { + widget.onSubmit!(user); + } + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Nom d'utilisateur (en lecture seule) + CustomTextField( + controller: _usernameController, + label: "Nom d'utilisateur", + readOnly: true, // Toujours en lecture seule + prefixIcon: Icons.account_circle, + ), + const SizedBox(height: 16), + + // Titre (M. ou Mme) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Titre", + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w500, + color: theme.colorScheme.onBackground, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + _buildRadioOption( + value: 1, + label: 'M.', + groupValue: _fkTitre, + onChanged: widget.readOnly + ? null + : (value) { + setState(() { + _fkTitre = value!; + }); + }, + ), + const SizedBox(width: 40), + _buildRadioOption( + value: 2, + label: 'Mme', + groupValue: _fkTitre, + onChanged: widget.readOnly + ? null + : (value) { + setState(() { + _fkTitre = value!; + }); + }, + ), + ], + ), + ], + ), + const SizedBox(height: 16), + + // Prénom + CustomTextField( + controller: _firstNameController, + label: "Prénom", + readOnly: widget.readOnly, + validator: (value) { + if (value == null || value.isEmpty) { + return "Veuillez entrer le prénom"; + } + return null; + }, + ), + const SizedBox(height: 16), + + // Nom + CustomTextField( + controller: _nameController, + label: "Nom", + readOnly: widget.readOnly, + validator: (value) { + if (value == null || value.isEmpty) { + return "Veuillez entrer le nom"; + } + return null; + }, + ), + const SizedBox(height: 16), + + // Téléphone fixe + CustomTextField( + controller: _phoneController, + label: "Téléphone fixe", + keyboardType: TextInputType.phone, + readOnly: widget.readOnly, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(10), + ], + validator: (value) { + if (value != null && value.isNotEmpty && value.length < 10) { + return "Le numéro de téléphone doit contenir 10 chiffres"; + } + return null; + }, + ), + const SizedBox(height: 16), + + // Téléphone mobile + CustomTextField( + controller: _mobileController, + label: "Téléphone mobile", + keyboardType: TextInputType.phone, + readOnly: widget.readOnly, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(10), + ], + validator: (value) { + if (value != null && value.isNotEmpty && value.length < 10) { + return "Le numéro de mobile doit contenir 10 chiffres"; + } + return null; + }, + ), + const SizedBox(height: 16), + + // Email + CustomTextField( + controller: _emailController, + label: "Email", + keyboardType: TextInputType.emailAddress, + readOnly: widget.readOnly, + validator: (value) { + if (value == null || value.isEmpty) { + return "Veuillez entrer l'adresse email"; + } + if (!value.contains('@') || !value.contains('.')) { + return "Veuillez entrer une adresse email valide"; + } + return null; + }, + ), + const SizedBox(height: 16), + + // Date de naissance + CustomTextField( + controller: _dateNaissanceController, + label: "Date de naissance", + readOnly: true, + onTap: widget.readOnly ? null : () => _selectDate(context, true), + suffixIcon: Icon( + Icons.calendar_today, + color: theme.colorScheme.primary, + ), + ), + const SizedBox(height: 16), + + // Date d'embauche + CustomTextField( + controller: _dateEmbaucheController, + label: "Date d'embauche", + readOnly: true, + onTap: widget.readOnly ? null : () => _selectDate(context, false), + suffixIcon: Icon( + Icons.calendar_today, + color: theme.colorScheme.primary, + ), + ), + // Espace en bas du formulaire + const SizedBox(height: 16), + ], + ), + ); + } + + Widget _buildRadioOption({ + required int value, + required String label, + required int groupValue, + required Function(int?)? onChanged, + }) { + final theme = Theme.of(context); + final isSelected = value == groupValue; + + return Row( + children: [ + Radio( + value: value, + groupValue: groupValue, + onChanged: onChanged, + activeColor: const Color(0xFF20335E), + ), + Text( + label, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onBackground, + fontWeight: FontWeight.w500, + ), + ), + ], + ); + } +} diff --git a/flutt/lib/shared/app_theme.dart b/app/lib/shared/app_theme.dart similarity index 100% rename from flutt/lib/shared/app_theme.dart rename to app/lib/shared/app_theme.dart diff --git a/app/lib/shared/widgets/admin_background.dart b/app/lib/shared/widgets/admin_background.dart new file mode 100644 index 00000000..54ffbdc8 --- /dev/null +++ b/app/lib/shared/widgets/admin_background.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'dart:math' as math; + +/// Widget pour créer un fond dégradé avec des petits points blancs +/// Utilisé pour les pages d'administration +class AdminBackground extends StatelessWidget { + final Widget child; + + const AdminBackground({ + Key? key, + required this.child, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + // Fond dégradé avec petits points blancs + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.white, Colors.red.shade300], + ), + ), + child: CustomPaint( + painter: DotsPainter(), + child: Container(width: double.infinity, height: double.infinity), + ), + ), + // Contenu de la page + child, + ], + ); + } +} + +/// Class pour dessiner les petits points blancs sur le fond +class DotsPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = Colors.white.withOpacity(0.5) + ..style = PaintingStyle.fill; + + final random = math.Random(42); // Seed fixe pour consistance + final numberOfDots = (size.width * size.height) ~/ 1500; + + for (int i = 0; i < numberOfDots; i++) { + final x = random.nextDouble() * size.width; + final y = random.nextDouble() * size.height; + final radius = 1.0 + random.nextDouble() * 2.0; + canvas.drawCircle(Offset(x, y), radius, paint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/app/livre-app.sh b/app/livre-app.sh new file mode 100755 index 00000000..18b38653 --- /dev/null +++ b/app/livre-app.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +# Vérification des arguments +if [ $# -ne 2 ]; then + echo "Usage: $0 " + echo "Example: $0 dva-geo rca-geo" + exit 1 +fi + +HOST_IP="195.154.80.116" +HOST_USER=root +HOST_KEY=/Users/pierre/.ssh/id_rsa_mbpi +HOST_PORT=22 + +SOURCE_CONTAINER=$1 +DEST_CONTAINER=$2 +APP_PATH="/var/www/geosector/app" +TIMESTAMP=$(date +"%Y%m%d_%H%M%S") +BACKUP_DIR="${APP_PATH}_backup_${TIMESTAMP}" +PROJECT="default" + +echo "🔄 Copie de l'application Flutter Web de $SOURCE_CONTAINER vers $DEST_CONTAINER (projet: $PROJECT)" + +# Vérifier si les containers existent +echo "🔍 Vérification des containers..." +ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus info $SOURCE_CONTAINER --project $PROJECT" > /dev/null 2>&1 +if [ $? -ne 0 ]; then + echo "❌ Erreur: Le container source $SOURCE_CONTAINER n'existe pas ou n'est pas accessible dans le projet $PROJECT" + exit 1 +fi + +ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus info $DEST_CONTAINER --project $PROJECT" > /dev/null 2>&1 +if [ $? -ne 0 ]; then + echo "❌ Erreur: Le container destination $DEST_CONTAINER n'existe pas ou n'est pas accessible dans le projet $PROJECT" + exit 1 +fi + +# Créer une sauvegarde du dossier de destination avant de le remplacer +echo "📦 Création d'une sauvegarde sur $DEST_CONTAINER..." +# Vérifier si le dossier APP existe +ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- test -d $APP_PATH" +if [ $? -eq 0 ]; then + # Le dossier existe, créer une sauvegarde + ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- cp -r $APP_PATH $BACKUP_DIR" + echo "✅ Sauvegarde créée dans $BACKUP_DIR" +else + echo "⚠️ Le dossier APP n'existe pas sur la destination" +fi + +# Copier le dossier APP entre les containers +echo "📋 Copie des fichiers en cours..." + +# Approche directe: utiliser incus copy pour copier directement entre containers +echo "📤 Transfert direct entre containers..." +# Nettoyer le dossier de destination +ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- rm -rf $APP_PATH" +ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- mkdir -p $APP_PATH" + +# Copier directement du container source vers le container destination +ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $SOURCE_CONTAINER --project $PROJECT -- tar -cf - -C $APP_PATH . | incus exec $DEST_CONTAINER --project $PROJECT -- tar -xf - -C $APP_PATH" +if [ $? -ne 0 ]; then + echo "❌ Erreur lors du transfert direct entre containers" + echo "⚠️ Tentative de restauration de la sauvegarde..." + # Vérifier si la sauvegarde existe + ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- test -d $BACKUP_DIR" + if [ $? -eq 0 ]; then + # La sauvegarde existe, la restaurer + ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- cp -r $BACKUP_DIR $APP_PATH" + echo "✅ Restauration réussie" + else + echo "❌ Échec de la restauration" + fi + exit 1 +fi + +# Changer le propriétaire et les permissions des fichiers +echo "👤 Application des droits et permissions pour tous les fichiers..." + +# Définir le propriétaire pour tous les fichiers +ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- chown -R nginx:nginx $APP_PATH" + +# Appliquer les permissions de base pour les dossiers (755) +ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- find $APP_PATH -type d -exec chmod 755 {} \;" + +# Appliquer les permissions pour les fichiers (644) +ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- find $APP_PATH -type f -exec chmod 644 {} \;" + +echo "✅ Propriétaire et permissions appliqués avec succès" + +# Vérifier la copie +echo "✅ Vérification de la copie..." +ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- test -d $APP_PATH" +if [ $? -eq 0 ]; then + echo "✅ Copie réussie" +else + echo "❌ Erreur: Le dossier APP n'a pas été copié correctement" +fi + +echo "✅ Opération terminée! L'application Flutter Web a été copiée de $SOURCE_CONTAINER vers $DEST_CONTAINER" +echo "📁 Une sauvegarde a été créée dans $BACKUP_DIR sur $DEST_CONTAINER" +echo "👤 Les fichiers appartiennent maintenant à l'utilisateur nginx" diff --git a/app/pubspec.lock b/app/pubspec.lock new file mode 100644 index 00000000..f2b7a589 --- /dev/null +++ b/app/pubspec.lock @@ -0,0 +1,1223 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + url: "https://pub.dev" + source: hosted + version: "76.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.3" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + url: "https://pub.dev" + source: hosted + version: "6.11.0" + archive: + dependency: transitive + description: + name: archive + sha256: "7dcbd0f87fe5f61cb28da39a1a8b70dbc106e2fe0516f7836eb7bb2948481a12" + url: "https://pub.dev" + source: hosted + version: "4.0.5" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + url: "https://pub.dev" + source: hosted + version: "2.12.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + url: "https://pub.dev" + source: hosted + version: "4.0.4" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 + url: "https://pub.dev" + source: hosted + version: "2.4.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" + url: "https://pub.dev" + source: hosted + version: "2.4.15" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" + url: "https://pub.dev" + source: hosted + version: "8.0.0" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 + url: "https://pub.dev" + source: hosted + version: "8.9.5" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + url: "https://pub.dev" + source: hosted + version: "4.10.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: "04bf81bb0b77de31557b58d052b24b3eee33f09a6e7a8c68a3e247c7df19ec27" + url: "https://pub.dev" + source: hosted + version: "6.1.3" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + dart_earcut: + dependency: transitive + description: + name: dart_earcut + sha256: e485001bfc05dcbc437d7bfb666316182e3522d4c3f9668048e004d0eb2ce43b + url: "https://pub.dev" + source: hosted + version: "1.2.0" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" + url: "https://pub.dev" + source: hosted + version: "2.3.8" + dbus: + dependency: transitive + description: + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + url: "https://pub.dev" + source: hosted + version: "0.7.11" + dio: + dependency: "direct main" + description: + name: dio + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" + url: "https://pub.dev" + source: hosted + version: "5.8.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" + event_bus: + dependency: transitive + description: + name: event_bus + sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + url: "https://pub.dev" + source: hosted + version: "1.3.2" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "5276944c6ffc975ae796569a826c38a62d2abcf264e26b88fa6f482e107f4237" + url: "https://pub.dev" + source: hosted + version: "0.70.2" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + url: "https://pub.dev" + source: hosted + version: "0.13.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "33b3e0269ae9d51669957a923f2376bee96299b09915d856395af8c4238aebfa" + url: "https://pub.dev" + source: hosted + version: "19.1.0" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: e3c277b2daab8e36ac5a6820536668d07e83851aeeb79c446e525a70710770a5 + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "2569b973fc9d1f63a37410a9f7c1c552081226c597190cb359ef5d5762d1631c" + url: "https://pub.dev" + source: hosted + version: "9.0.0" + flutter_local_notifications_windows: + dependency: transitive + description: + name: flutter_local_notifications_windows + sha256: f8fc0652a601f83419d623c85723a3e82ad81f92b33eaa9bcc21ea1b94773e6e + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_map: + dependency: "direct main" + description: + name: flutter_map + sha256: f7d0379477274f323c3f3bc12d369a2b42eb86d1e7bd2970ae1ea3cff782449a + url: "https://pub.dev" + source: hosted + version: "8.1.1" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b + url: "https://pub.dev" + source: hosted + version: "2.0.17" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: f62bcd90459e63210bbf9c35deb6a51c521f992a78de19a1fe5c11704f9530e2 + url: "https://pub.dev" + source: hosted + version: "13.0.4" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d + url: "https://pub.dev" + source: hosted + version: "4.6.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.dev" + source: hosted + version: "2.3.13" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" + url: "https://pub.dev" + source: hosted + version: "4.2.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 + url: "https://pub.dev" + source: hosted + version: "4.1.3" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://pub.dev" + source: hosted + version: "0.2.5" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 + url: "https://pub.dev" + source: hosted + version: "14.8.1" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + url: "https://pub.dev" + source: hosted + version: "6.2.1" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_flutter: + dependency: "direct main" + description: + name: hive_flutter + sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc + url: "https://pub.dev" + source: hosted + version: "1.1.0" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" + http: + dependency: transitive + description: + name: http + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + url: "https://pub.dev" + source: hosted + version: "1.3.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + url: "https://pub.dev" + source: hosted + version: "4.5.4" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + latlong2: + dependency: "direct main" + description: + name: latlong2 + sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe" + url: "https://pub.dev" + source: hosted + version: "0.9.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + url: "https://pub.dev" + source: hosted + version: "10.0.8" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.dev" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + lists: + dependency: transitive + description: + name: lists + sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + logger: + dependency: transitive + description: + name: logger + sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 + url: "https://pub.dev" + source: hosted + version: "2.5.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" + url: "https://pub.dev" + source: hosted + version: "0.1.3-main.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mgrs_dart: + dependency: transitive + description: + name: mgrs_dart + sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mqtt5_client: + dependency: "direct main" + description: + name: mqtt5_client + sha256: "9e15d1cd888035bcd5b204325c6678b32858ea3fc94bc1e41d5aa99d3a43f49f" + url: "https://pub.dev" + source: hosted + version: "4.11.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" + url: "https://pub.dev" + source: hosted + version: "2.2.16" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + polylabel: + dependency: transitive + description: + name: polylabel + sha256: "41b9099afb2aa6c1730bdd8a0fab1400d287694ec7615dd8516935fa3144214b" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + posix: + dependency: transitive + description: + name: posix + sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a + url: "https://pub.dev" + source: hosted + version: "6.0.1" + proj4dart: + dependency: transitive + description: + name: proj4dart + sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e + url: "https://pub.dev" + source: hosted + version: "2.1.0" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" + url: "https://pub.dev" + source: hosted + version: "6.1.5" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + retry: + dependency: "direct main" + description: + name: retry + sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + url: "https://pub.dev" + source: hosted + version: "1.3.5" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + syncfusion_flutter_charts: + dependency: "direct main" + description: + name: syncfusion_flutter_charts + sha256: "22d633491d63e0986dc84dd07f7e68e1d2e9796a35b52ba7d39440fa3f79fa11" + url: "https://pub.dev" + source: hosted + version: "29.1.35" + syncfusion_flutter_core: + dependency: transitive + description: + name: syncfusion_flutter_core + sha256: be5eff439bb6c16eb2f297aa3dde779cc5352485f2df1825557d14f09d5b26b6 + url: "https://pub.dev" + source: hosted + version: "29.1.35" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.dev" + source: hosted + version: "0.7.4" + timezone: + dependency: transitive + description: + name: timezone + sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1 + url: "https://pub.dev" + source: hosted + version: "0.10.1" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + unicode: + dependency: transitive + description: + name: unicode + sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + universal_html: + dependency: "direct main" + description: + name: universal_html + sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971" + url: "https://pub.dev" + source: hosted + version: "2.2.4" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4" + url: "https://pub.dev" + source: hosted + version: "6.3.15" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" + url: "https://pub.dev" + source: hosted + version: "1.1.18" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + url: "https://pub.dev" + source: hosted + version: "1.1.16" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + url: "https://pub.dev" + source: hosted + version: "14.3.1" + watcher: + dependency: transitive + description: + name: watcher + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + wkt_parser: + dependency: transitive + description: + name: wkt_parser + sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/flutt/pubspec.yaml b/app/pubspec.yaml similarity index 60% rename from flutt/pubspec.yaml rename to app/pubspec.yaml index cf7b4dc6..0bf4f263 100644 --- a/flutt/pubspec.yaml +++ b/app/pubspec.yaml @@ -1,7 +1,7 @@ name: geosector_app -description: 'GEOSECTOR - Une application de gestion de distribution par secteurs géographiques' +description: 'GEOSECTOR - Gestion de distribution des calendriers par secteurs géographiques pour les amicales de pompiers' publish_to: 'none' -version: 0.2.1 +version: 0.2.2 environment: sdk: '>=3.0.0 <4.0.0' @@ -18,6 +18,7 @@ dependencies: hive: ^2.2.3 hive_flutter: ^1.1.0 path_provider: ^2.1.1 + provider: ^6.1.2 # API & Réseau dio: ^5.3.3 @@ -39,6 +40,7 @@ dependencies: flutter_map: ^8.1.1 latlong2: ^0.9.1 geolocator: ^13.0.4 + universal_html: ^2.2.4 # Pour accéder à la localisation du navigateur (detection env) # Chat et notifications mqtt5_client: ^4.11.0 @@ -53,17 +55,21 @@ dev_dependencies: flutter_launcher_icons: ^0.13.1 flutter_launcher_icons: - android: 'launcher_icon' + android: true ios: true - image_path: 'assets/images/geosector-logo.png' + image_path: 'assets/images/icons/icon-1024.png' + min_sdk_android: 21 + adaptive_icon_background: '#FFFFFF' + adaptive_icon_foreground: 'assets/images/icons/icon-1024.png' + remove_alpha_ios: true web: generate: true - image_path: 'assets/images/geosector-logo.png' - background_color: '#ffffff' - theme_color: '#ffffff' + image_path: 'assets/images/icons/icon-1024.png' + background_color: '#FFFFFF' + theme_color: '#4B77BE' windows: generate: true - image_path: 'assets/images/geosector-logo.png' + image_path: 'assets/images/icons/icon-1024.png' icon_size: 48 flutter: @@ -73,3 +79,8 @@ flutter: - assets/images/ - assets/icons/ - assets/animations/ + + fonts: + - family: Figtree + fonts: + - asset: assets/fonts/Figtree-VariableFont_wght.ttf diff --git a/app/test/api_environment_test.dart b/app/test/api_environment_test.dart new file mode 100644 index 00000000..a5d7511f --- /dev/null +++ b/app/test/api_environment_test.dart @@ -0,0 +1,34 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:geosector_app/core/constants/app_keys.dart'; + +void main() { + group('Environment Configuration Tests', () { + test('API URLs are correctly configured', () { + // Vérifier que les URLs sont différentes pour chaque environnement + expect(AppKeys.baseApiUrlDev, 'https://dapp.geosector.fr/api/geo'); + expect(AppKeys.baseApiUrlRec, 'https://rapp.geosector.fr/api/geo'); + expect(AppKeys.baseApiUrlProd, 'https://app.geosector.fr/api/geo'); + + // Vérifier qu'elles sont différentes les unes des autres + expect(AppKeys.baseApiUrlDev != AppKeys.baseApiUrlProd, true); + expect(AppKeys.baseApiUrlRec != AppKeys.baseApiUrlProd, true); + expect(AppKeys.baseApiUrlDev != AppKeys.baseApiUrlRec, true); + }); + + test('App Identifiers are correctly configured', () { + // Vérifier que les identifiants sont configurés correctement + expect(AppKeys.appIdentifierDev, 'dapp.geosector.fr'); + expect(AppKeys.appIdentifierRec, 'rapp.geosector.fr'); + expect(AppKeys.appIdentifierProd, 'app.geosector.fr'); + + // Vérifier qu'ils sont différents les uns des autres + expect(AppKeys.appIdentifierDev != AppKeys.appIdentifierProd, true); + expect(AppKeys.appIdentifierRec != AppKeys.appIdentifierProd, true); + expect(AppKeys.appIdentifierDev != AppKeys.appIdentifierRec, true); + }); + + // Note: Les tests de détection d'environnement seraient plus difficiles + // à implémenter dans un environnement de test car cela nécessiterait de + // mocker l'objet window.location.href qui est dans universal_html + }); +} diff --git a/flutt/test/widget_test.dart b/app/test/widget_test.dart similarity index 100% rename from flutt/test/widget_test.dart rename to app/test/widget_test.dart diff --git a/flutt/update_imports.sh b/app/update_imports.sh similarity index 100% rename from flutt/update_imports.sh rename to app/update_imports.sh diff --git a/app/web/favicon-16.png b/app/web/favicon-16.png new file mode 100644 index 00000000..f9441458 Binary files /dev/null and b/app/web/favicon-16.png differ diff --git a/app/web/favicon-32.png b/app/web/favicon-32.png new file mode 100644 index 00000000..f5dffe2c Binary files /dev/null and b/app/web/favicon-32.png differ diff --git a/app/web/favicon-64.png b/app/web/favicon-64.png new file mode 100644 index 00000000..53b6ae36 Binary files /dev/null and b/app/web/favicon-64.png differ diff --git a/app/web/favicon.png b/app/web/favicon.png new file mode 100644 index 00000000..f5dffe2c Binary files /dev/null and b/app/web/favicon.png differ diff --git a/app/web/icons/Icon-152.png b/app/web/icons/Icon-152.png new file mode 100644 index 00000000..4043aa2d Binary files /dev/null and b/app/web/icons/Icon-152.png differ diff --git a/app/web/icons/Icon-167.png b/app/web/icons/Icon-167.png new file mode 100644 index 00000000..1fcc514a Binary files /dev/null and b/app/web/icons/Icon-167.png differ diff --git a/app/web/icons/Icon-180.png b/app/web/icons/Icon-180.png new file mode 100644 index 00000000..d2b40c1e Binary files /dev/null and b/app/web/icons/Icon-180.png differ diff --git a/app/web/icons/Icon-192.png b/app/web/icons/Icon-192.png new file mode 100644 index 00000000..34447be7 Binary files /dev/null and b/app/web/icons/Icon-192.png differ diff --git a/app/web/icons/Icon-512.png b/app/web/icons/Icon-512.png new file mode 100644 index 00000000..058f9806 Binary files /dev/null and b/app/web/icons/Icon-512.png differ diff --git a/app/web/icons/Icon-maskable-192.png b/app/web/icons/Icon-maskable-192.png new file mode 100644 index 00000000..34447be7 Binary files /dev/null and b/app/web/icons/Icon-maskable-192.png differ diff --git a/app/web/icons/Icon-maskable-512.png b/app/web/icons/Icon-maskable-512.png new file mode 100644 index 00000000..058f9806 Binary files /dev/null and b/app/web/icons/Icon-maskable-512.png differ diff --git a/flutt/web/index.html b/app/web/index.html similarity index 77% rename from flutt/web/index.html rename to app/web/index.html index f86c9430..ce2cd0f2 100644 --- a/flutt/web/index.html +++ b/app/web/index.html @@ -25,10 +25,18 @@ - + + + + + + + + + GEOSECTOR diff --git a/flutt/web/manifest.json b/app/web/manifest.json similarity index 78% rename from flutt/web/manifest.json rename to app/web/manifest.json index f6745617..ab3c7169 100644 --- a/flutt/web/manifest.json +++ b/app/web/manifest.json @@ -1,11 +1,11 @@ { - "name": "geosector_app", - "short_name": "geosector_app", + "name": "GEOSECTOR", + "short_name": "GEOSECTOR", "start_url": ".", "display": "standalone", - "background_color": "#ffffff", - "theme_color": "#ffffff", - "description": "A new Flutter project.", + "background_color": "#FFFFFF", + "theme_color": "#4B77BE", + "description": "Application de gestion de distribution par secteurs géographiques", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ diff --git a/docs/DB-diagram.md b/docs/DB-diagram.md deleted file mode 100644 index c7865eb6..00000000 --- a/docs/DB-diagram.md +++ /dev/null @@ -1,292 +0,0 @@ -erDiagram - users ||--o{ ope_pass : "fk_user" - users ||--o{ ope_users : "fk_user" - users ||--o{ ope_users_sectors : "fk_user" - users ||--o{ ope_users_suivis : "fk_user" - users ||--o{ ope_pass_histo : "fk_user" - users ||--o{ medias : "fk_user_creat/fk_user_modif" - users }o--|| users_entites : "fk_entite" - users }o--|| x_users_roles : "fk_role" - users }o--|| x_users_categories : "fk_categorie" - users }o--|| x_users_sous_categories : "fk_sous_categorie" - users }o--|| x_users_grades : "fk_grade" - - operations ||--o{ ope_pass : "fk_operation" - operations ||--o{ ope_users : "fk_operation" - operations ||--o{ ope_sectors : "fk_operation" - operations ||--o{ ope_users_sectors : "fk_operation" - operations ||--o{ ope_users_suivis : "fk_operation" - operations }o--|| users_entites : "fk_entite" - - sectors ||--o{ ope_users_sectors : "fk_sector" - sectors ||--o{ sectors_adresses : "fk_sector" - sectors ||--o{ sectors_streets : "fk_sector" - - ope_sectors ||--o{ ope_users_sectors : "fk_sector" - ope_sectors ||--o{ ope_pass : "fk_sector" - - ope_pass ||--o{ ope_pass_histo : "fk_pass" - ope_pass ||--o{ ope_pass_recus : "fk_pass" - ope_pass ||--o{ email_queue : "rowid" - ope_pass }o--|| x_types_reglements : "fk_type_reglement" - - x_users_categories ||--o{ x_users_sous_categories : "fk_user_categorie" - - x_pays ||--o{ x_regions : "fk_pays" - x_regions ||--o{ x_departements : "fk_region" - x_departements ||--o{ x_villes : "fk_departement" - x_pays }o--|| x_devises : "fk_devise" - - users_entites }o--|| x_regions : "fk_region" - users_entites }o--|| x_entites_types : "fk_type" - - email_counter { - int id PK - timestamp hour_start - int count - } - - email_queue { - int id PK - int rowid "ope_pass.rowid" - varchar to_email - varchar subject - text body - enum status - } - - medias { - int rowid PK - varchar support - int support_rowid - varchar fichier - varchar type_fichier - varchar description - datetime date_creat - int fk_user_creat FK - datetime date_modif - int fk_user_modif FK - } - - ope_pass { - int rowid PK - int fk_operation FK - int fk_sector FK - int fk_user FK - varchar fk_adresse - datetime date_eve - int fk_type - varchar numero - varchar rue - varchar ville - int fk_habitat - decimal montant - int fk_type_reglement FK - } - - ope_pass_histo { - int rowid PK - int fk_pass FK - int fk_user FK - datetime date_histo - varchar sujet - varchar remarque - } - - ope_pass_recus { - int rowid PK - int fk_pass FK - varchar chemin - varchar nom_recu - datetime date_recu - } - - ope_sectors { - int rowid PK - int fk_operation FK - varchar libelle - text sector - varchar color - } - - ope_users { - int rowid PK - int fk_operation FK - int fk_user FK - tinyint active - } - - ope_users_sectors { - int rowid PK - int fk_operation FK - int fk_user FK - int fk_sector FK - tinyint active - } - - ope_users_suivis { - int rowid PK - int fk_operation FK - int fk_user FK - datetime date_suivi - varchar latitude - varchar longitude - } - - operations { - int rowid PK - int fk_entite FK - varchar libelle - date date_deb - date date_fin - tinyint active - } - - sectors { - int rowid PK - varchar libelle - text sector - varchar color - tinyint active - } - - sectors_adresses { - int rowid PK - varchar fk_adresse - int fk_sector FK - varchar numero - varchar rue - varchar cp - varchar ville - varchar gps_lat - varchar gps_lng - } - - sectors_streets { - int rowid PK - int fk_sector FK - varchar fk_adresse - varchar osm_lat - varchar osm_lng - varchar osm_name - varchar osm_street - varchar osm_city - } - - users { - int rowid PK - int fk_entite FK - int fk_titre - varchar libelle - varchar prenom - varchar username - varchar userpass - varchar email - int fk_role FK - int fk_categorie FK - int fk_sous_categorie FK - int fk_grade FK - tinyint active - } - - users_entites { - int rowid PK - varchar libelle - varchar adresse1 - varchar cp - varchar ville - int fk_region FK - int fk_type FK - varchar email - tinyint active - } - - x_departements { - int rowid PK - varchar code - int fk_region FK - varchar libelle - } - - x_devises { - int rowid PK - varchar code - varchar symbole - varchar libelle - } - - x_entites_types { - int rowid PK - varchar libelle - tinyint active - } - - x_pays { - int rowid PK - varchar code - int fk_continent - int fk_devise FK - varchar libelle - } - - x_regions { - int rowid PK - int fk_pays FK - varchar libelle - varchar libelle_long - } - - x_types_passages { - int rowid PK - varchar libelle - varchar color_button - varchar color_mark - } - - x_types_reglements { - int rowid PK - varchar libelle - tinyint active - } - - x_users_categories { - int rowid PK - varchar libelle - tinyint active - } - - x_users_grades { - int rowid PK - varchar libelle - tinyint active - } - - x_users_roles { - int rowid PK - varchar libelle - tinyint active - } - - x_users_sous_categories { - int rowid PK - int fk_user_categorie FK - varchar libelle - tinyint active - } - - x_villes { - int rowid PK - int fk_departement FK - varchar libelle - varchar cp - varchar code_insee - } - - z_sessions { - text sid - int fk_user FK - varchar role - timestamp date_modified - varchar ip - varchar browser - } \ No newline at end of file diff --git a/docs/architecture.md b/docs/architecture.md index 3b7c90cc..7e86dd3c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -94,12 +94,13 @@ La couche de données est responsable de la gestion des données et comprend : La couche de services fournit des fonctionnalités d'infrastructure et d'intégration : - `api_service.dart` : Communication avec l'API backend -- `auth_service.dart` : Gestion de l'authentification - `connectivity_service.dart` : Surveillance de la connectivité réseau - `location_service.dart` : Services de géolocalisation - `passage_data_service.dart` : Traitement des données de passage - `sync_service.dart` : Synchronisation des données locales/serveur +> **Note importante** : La classe `auth_service.dart` a été supprimée et ses fonctionnalités ont été intégrées directement dans `UserRepository` pour simplifier l'architecture et éviter les problèmes de synchronisation entre les deux classes. + ## Gestion d'état et injection de dépendances L'application utilise des instances globales pour la gestion d'état et l'accès aux services. Ces instances sont définies dans le fichier `app.dart` : @@ -195,9 +196,12 @@ Le flux d'authentification suit ce processus : 1. Vérification initiale de session persistante (Hive) 2. Redirection vers Login/Register si nécessaire -3. Authentification via API Service -4. Stockage sécurisé des informations de session +3. Authentification via `UserRepository` qui communique avec l'API +4. Stockage sécurisé des informations de session dans Hive 5. Redirection vers l'interface appropriée (admin vs utilisateur) +6. Lors de la déconnexion, nettoyage complet des boîtes Hive et redirection vers la page de démarrage + +> **Amélioration récente** : Le processus d'authentification a été simplifié en centralisant toute la logique dans `UserRepository`, qui gère maintenant à la fois la connexion, la déconnexion, l'affichage des overlays de chargement et les redirections. ## Widgets communs diff --git a/docs/convert-csv-googleagenda-to-gitlab.php b/docs/convert-csv-googleagenda-to-gitlab.php new file mode 100755 index 00000000..08a8a051 --- /dev/null +++ b/docs/convert-csv-googleagenda-to-gitlab.php @@ -0,0 +1,147 @@ + array_search('Subject', $header), + 'start_date' => array_search('Start Date', $header), + 'start_time' => array_search('Start Time', $header), + 'end_date' => array_search('End Date', $header), + 'end_time' => array_search('End Time', $header), + 'all_day' => array_search('All Day Event', $header), + 'description' => array_search('Description', $header), + 'location' => array_search('Location', $header), + 'private' => array_search('Private', $header) +]; + +// Écrire l'en-tête GitLab +fputcsv($output, ['title', 'description', 'labels', 'due_date']); + +// Compteur d'événements traités +$event_count = 0; + +// Traiter chaque ligne du fichier source +while (($data = fgetcsv($input)) !== false) { + // S'assurer que la ligne contient au moins un sujet + if (!isset($data[$column_indices['subject']]) || empty(trim($data[$column_indices['subject']]))) { + continue; // Ignorer les lignes sans sujet + } + + // Extraire les données + $title = $data[$column_indices['subject']]; + + // Préparer la description + $description_parts = []; + + // Ajouter les dates + $start_date = $column_indices['start_date'] !== false ? $data[$column_indices['start_date']] : ''; + $start_time = $column_indices['start_time'] !== false ? $data[$column_indices['start_time']] : ''; + $end_date = $column_indices['end_date'] !== false ? $data[$column_indices['end_date']] : ''; + $end_time = $column_indices['end_time'] !== false ? $data[$column_indices['end_time']] : ''; + $all_day = $column_indices['all_day'] !== false ? ($data[$column_indices['all_day']] === 'TRUE') : false; + + // Formater la date et l'heure + $date_info = "Date: " . $start_date; + if (!$all_day && !empty($start_time)) { + $date_info .= " " . $start_time; + } + + if (!empty($end_date) && $end_date !== $start_date) { + $date_info .= " - " . $end_date; + if (!$all_day && !empty($end_time)) { + $date_info .= " " . $end_time; + } + } elseif (!$all_day && !empty($end_time) && $end_time !== $start_time) { + $date_info .= " - " . $end_time; + } + + $description_parts[] = $date_info; + + // Ajouter le lieu + if ($column_indices['location'] !== false && !empty($data[$column_indices['location']])) { + $description_parts[] = "Lieu: " . $data[$column_indices['location']]; + } + + // Ajouter la description + if ($column_indices['description'] !== false && !empty($data[$column_indices['description']])) { + $description_parts[] = $data[$column_indices['description']]; + } + + // Joindre toutes les parties de la description + $description = implode("\n\n", $description_parts); + + // Définir les étiquettes (labels) + $labels = "agenda,planning"; + + // Si c'est un événement privé, ajouter l'étiquette "privé" + if ($column_indices['private'] !== false && $data[$column_indices['private']] === 'TRUE') { + $labels .= ",privé"; + } + + // Déterminer la date d'échéance (due_date) + $due_date = !empty($end_date) ? $end_date : $start_date; + + // Écrire la ligne dans le fichier de destination + fputcsv($output, [$title, $description, $labels, $due_date]); + + $event_count++; +} + +// Fermer les fichiers +fclose($input); +fclose($output); + +echo "Conversion terminée avec succès!\n"; +echo "Nombre d'événements convertis: {$event_count}\n"; +echo "Le fichier '{$dest_file}' est prêt à être importé dans GitLab.\n"; +?> \ No newline at end of file diff --git a/docs/geo_app.dump b/docs/geo_app.dump new file mode 100644 index 00000000..330e1391 --- /dev/null +++ b/docs/geo_app.dump @@ -0,0 +1,918 @@ +-- MySQL dump 10.13 Distrib 8.0.27, for macos11 (arm64) +-- +-- Host: in3.d6soft.fr Database: geo_app +-- ------------------------------------------------------ +-- Server version 11.4.5-MariaDB + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `chat_anonymous_users` +-- + +DROP TABLE IF EXISTS `chat_anonymous_users`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `chat_anonymous_users` ( + `id` varchar(50) NOT NULL, + `device_id` varchar(100) NOT NULL, + `name` varchar(100) DEFAULT NULL, + `email` varchar(100) DEFAULT NULL, + `created_at` timestamp NOT NULL DEFAULT current_timestamp(), + `converted_to_user_id` int(10) unsigned DEFAULT NULL, + `metadata` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`metadata`)), + PRIMARY KEY (`id`), + KEY `idx_device_id` (`device_id`), + KEY `idx_converted_user` (`converted_to_user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `chat_attachments` +-- + +DROP TABLE IF EXISTS `chat_attachments`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `chat_attachments` ( + `id` varchar(50) NOT NULL, + `fk_message` varchar(50) NOT NULL, + `file_name` varchar(255) NOT NULL, + `file_path` varchar(500) NOT NULL, + `file_type` varchar(100) NOT NULL, + `file_size` int(10) unsigned NOT NULL, + `created_at` timestamp NOT NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + KEY `idx_message` (`fk_message`), + CONSTRAINT `fk_chat_attachments_message` FOREIGN KEY (`fk_message`) REFERENCES `chat_messages` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `chat_audience_targets` +-- + +DROP TABLE IF EXISTS `chat_audience_targets`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `chat_audience_targets` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `fk_room` varchar(50) NOT NULL, + `target_type` enum('role','entity','all','combined') NOT NULL DEFAULT 'all', + `target_id` varchar(50) DEFAULT NULL, + `role_filter` varchar(20) DEFAULT NULL, + `entity_filter` varchar(50) DEFAULT NULL, + `date_creation` timestamp NOT NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + KEY `idx_room` (`fk_room`), + KEY `idx_type` (`target_type`), + CONSTRAINT `fk_chat_audience_targets_room` FOREIGN KEY (`fk_room`) REFERENCES `chat_rooms` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `chat_broadcast_lists` +-- + +DROP TABLE IF EXISTS `chat_broadcast_lists`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `chat_broadcast_lists` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `fk_room` varchar(50) NOT NULL, + `name` varchar(100) NOT NULL, + `description` text DEFAULT NULL, + `fk_user_creator` int(10) unsigned NOT NULL, + `date_creation` timestamp NOT NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + KEY `idx_room` (`fk_room`), + KEY `idx_user_creator` (`fk_user_creator`), + CONSTRAINT `fk_chat_broadcast_lists_room` FOREIGN KEY (`fk_room`) REFERENCES `chat_rooms` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Temporary view structure for view `chat_conversations_unread` +-- + +DROP TABLE IF EXISTS `chat_conversations_unread`; +/*!50001 DROP VIEW IF EXISTS `chat_conversations_unread`*/; +SET @saved_cs_client = @@character_set_client; +/*!50503 SET character_set_client = utf8mb4 */; +/*!50001 CREATE VIEW `chat_conversations_unread` AS SELECT + 1 AS `id`, + 1 AS `type`, + 1 AS `title`, + 1 AS `date_creation`, + 1 AS `fk_user`, + 1 AS `fk_entite`, + 1 AS `statut`, + 1 AS `description`, + 1 AS `reply_permission`, + 1 AS `is_pinned`, + 1 AS `expiry_date`, + 1 AS `updated_at`, + 1 AS `total_messages`, + 1 AS `read_messages`, + 1 AS `unread_messages`, + 1 AS `last_message_date`*/; +SET character_set_client = @saved_cs_client; + +-- +-- Table structure for table `chat_messages` +-- + +DROP TABLE IF EXISTS `chat_messages`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `chat_messages` ( + `id` varchar(50) NOT NULL, + `fk_room` varchar(50) NOT NULL, + `fk_user` int(10) unsigned DEFAULT NULL, + `sender_type` enum('user','anonymous','system') NOT NULL DEFAULT 'user', + `content` text DEFAULT NULL, + `content_type` enum('text','image','file') NOT NULL DEFAULT 'text', + `date_sent` timestamp NOT NULL DEFAULT current_timestamp(), + `date_delivered` timestamp NULL DEFAULT NULL, + `date_read` timestamp NULL DEFAULT NULL, + `statut` enum('envoye','livre','lu','error') NOT NULL DEFAULT 'envoye', + `is_announcement` tinyint(1) unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `idx_room` (`fk_room`), + KEY `idx_user` (`fk_user`), + KEY `idx_date` (`date_sent`), + KEY `idx_status` (`statut`), + KEY `idx_messages_unread` (`fk_room`,`statut`), + CONSTRAINT `fk_chat_messages_room` FOREIGN KEY (`fk_room`) REFERENCES `chat_rooms` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `chat_notifications` +-- + +DROP TABLE IF EXISTS `chat_notifications`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `chat_notifications` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `fk_user` int(10) unsigned NOT NULL, + `fk_message` varchar(50) DEFAULT NULL, + `fk_room` varchar(50) DEFAULT NULL, + `type` varchar(50) NOT NULL, + `contenu` text DEFAULT NULL, + `date_creation` timestamp NOT NULL DEFAULT current_timestamp(), + `date_lecture` timestamp NULL DEFAULT NULL, + `statut` enum('non_lue','lue') NOT NULL DEFAULT 'non_lue', + PRIMARY KEY (`id`), + KEY `idx_user` (`fk_user`), + KEY `idx_message` (`fk_message`), + KEY `idx_room` (`fk_room`), + KEY `idx_statut` (`statut`), + KEY `idx_notifications_unread` (`fk_user`,`statut`), + CONSTRAINT `fk_chat_notifications_message` FOREIGN KEY (`fk_message`) REFERENCES `chat_messages` (`id`) ON DELETE SET NULL, + CONSTRAINT `fk_chat_notifications_room` FOREIGN KEY (`fk_room`) REFERENCES `chat_rooms` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `chat_offline_queue` +-- + +DROP TABLE IF EXISTS `chat_offline_queue`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `chat_offline_queue` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `user_id` int(10) unsigned NOT NULL, + `operation_type` varchar(50) NOT NULL, + `operation_data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL CHECK (json_valid(`operation_data`)), + `created_at` timestamp NOT NULL DEFAULT current_timestamp(), + `processed_at` timestamp NULL DEFAULT NULL, + `status` enum('pending','processing','completed','failed') NOT NULL DEFAULT 'pending', + `error_message` text DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_status` (`status`), + KEY `idx_created_at` (`created_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `chat_participants` +-- + +DROP TABLE IF EXISTS `chat_participants`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `chat_participants` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `id_room` varchar(50) NOT NULL, + `id_user` int(10) unsigned DEFAULT NULL, + `anonymous_id` varchar(50) DEFAULT NULL, + `role` enum('administrateur','participant','en_lecture_seule') NOT NULL DEFAULT 'participant', + `date_ajout` timestamp NOT NULL DEFAULT current_timestamp(), + `notification_activee` tinyint(1) unsigned NOT NULL DEFAULT 1, + `last_read_message_id` varchar(50) DEFAULT NULL, + `via_target` tinyint(1) unsigned NOT NULL DEFAULT 0, + `can_reply` tinyint(1) unsigned DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uc_room_user` (`id_room`,`id_user`), + KEY `idx_room` (`id_room`), + KEY `idx_user` (`id_user`), + KEY `idx_anonymous_id` (`anonymous_id`), + KEY `idx_participants_active` (`id_room`,`id_user`,`notification_activee`), + CONSTRAINT `fk_chat_participants_room` FOREIGN KEY (`id_room`) REFERENCES `chat_rooms` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `chat_read_messages` +-- + +DROP TABLE IF EXISTS `chat_read_messages`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `chat_read_messages` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `fk_message` varchar(50) NOT NULL, + `fk_user` int(10) unsigned NOT NULL, + `date_read` timestamp NOT NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + UNIQUE KEY `uc_message_user` (`fk_message`,`fk_user`), + KEY `idx_message` (`fk_message`), + KEY `idx_user` (`fk_user`), + CONSTRAINT `fk_chat_read_messages_message` FOREIGN KEY (`fk_message`) REFERENCES `chat_messages` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `chat_rooms` +-- + +DROP TABLE IF EXISTS `chat_rooms`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `chat_rooms` ( + `id` varchar(50) NOT NULL, + `type` enum('privee','groupe','liste_diffusion','broadcast','announcement') NOT NULL, + `title` varchar(100) DEFAULT NULL, + `date_creation` timestamp NOT NULL DEFAULT current_timestamp(), + `fk_user` int(10) unsigned NOT NULL, + `fk_entite` int(10) unsigned DEFAULT NULL, + `statut` enum('active','archive') NOT NULL DEFAULT 'active', + `description` text DEFAULT NULL, + `reply_permission` enum('all','admins_only','sender_only','none') NOT NULL DEFAULT 'all', + `is_pinned` tinyint(1) unsigned NOT NULL DEFAULT 0, + `expiry_date` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp(), + PRIMARY KEY (`id`), + KEY `idx_user` (`fk_user`), + KEY `idx_entite` (`fk_entite`), + KEY `idx_type` (`type`), + KEY `idx_statut` (`statut`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `email_counter` +-- + +DROP TABLE IF EXISTS `email_counter`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `email_counter` ( + `id` int(10) unsigned NOT NULL DEFAULT 1, + `hour_start` timestamp NULL DEFAULT NULL, + `count` int(10) unsigned DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `email_queue` +-- + +DROP TABLE IF EXISTS `email_queue`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `email_queue` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `fk_pass` int(10) unsigned NOT NULL DEFAULT 0, + `to_email` varchar(255) DEFAULT NULL, + `subject` varchar(255) DEFAULT NULL, + `body` text DEFAULT NULL, + `headers` text DEFAULT NULL, + `created_at` timestamp NULL DEFAULT current_timestamp(), + `status` enum('pending','sent','failed') DEFAULT 'pending', + `attempts` int(10) unsigned DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `entites` +-- + +DROP TABLE IF EXISTS `entites`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `entites` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `encrypted_name` varchar(255) DEFAULT NULL, + `adresse1` varchar(45) DEFAULT '', + `adresse2` varchar(45) DEFAULT '', + `code_postal` varchar(5) DEFAULT '', + `ville` varchar(45) DEFAULT '', + `fk_region` int(10) unsigned DEFAULT NULL, + `fk_type` int(10) unsigned DEFAULT 1, + `encrypted_phone` varchar(128) DEFAULT '', + `encrypted_mobile` varchar(128) DEFAULT '', + `encrypted_email` varchar(255) DEFAULT '', + `gps_lat` varchar(20) NOT NULL DEFAULT '', + `gps_lng` varchar(20) NOT NULL DEFAULT '', + `chk_stripe` tinyint(1) unsigned NOT NULL DEFAULT 0, + `encrypted_stripe_id` varchar(255) DEFAULT '', + `encrypted_iban` varchar(255) DEFAULT '', + `encrypted_bic` varchar(128) DEFAULT '', + `chk_demo` tinyint(1) unsigned DEFAULT 1, + `chk_mdp_manuel` tinyint(1) unsigned NOT NULL DEFAULT 1 COMMENT 'Gestion des mots de passe manuelle O/N', + `chk_copie_mail_recu` tinyint(1) unsigned NOT NULL DEFAULT 0, + `chk_accept_sms` tinyint(1) unsigned NOT NULL DEFAULT 0, + `created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création', + `fk_user_creat` int(10) unsigned DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification', + `fk_user_modif` int(10) unsigned DEFAULT NULL, + `chk_active` tinyint(1) unsigned DEFAULT 1, + PRIMARY KEY (`id`), + KEY `entites_ibfk_1` (`fk_region`), + KEY `entites_ibfk_2` (`fk_type`), + CONSTRAINT `entites_ibfk_1` FOREIGN KEY (`fk_region`) REFERENCES `x_regions` (`id`) ON UPDATE CASCADE, + CONSTRAINT `entites_ibfk_2` FOREIGN KEY (`fk_type`) REFERENCES `x_entites_types` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=1229 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `medias` +-- + +DROP TABLE IF EXISTS `medias`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `medias` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `support` varchar(45) NOT NULL DEFAULT '', + `support_id` int(10) unsigned NOT NULL DEFAULT 0, + `fichier` varchar(250) NOT NULL DEFAULT '', + `description` varchar(100) NOT NULL DEFAULT '', + `created_at` timestamp NOT NULL DEFAULT current_timestamp(), + `fk_user_creat` int(10) unsigned NOT NULL DEFAULT 0, + `updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp(), + `fk_user_modif` int(10) unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `id_UNIQUE` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=176 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `ope_pass` +-- + +DROP TABLE IF EXISTS `ope_pass`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `ope_pass` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `fk_operation` int(10) unsigned NOT NULL DEFAULT 0, + `fk_sector` int(10) unsigned DEFAULT 0, + `fk_user` int(10) unsigned NOT NULL DEFAULT 0, + `fk_adresse` varchar(25) DEFAULT '' COMMENT 'adresses.cp??.id', + `passed_at` timestamp NULL DEFAULT NULL COMMENT 'Date du passage', + `fk_type` int(10) unsigned DEFAULT 0, + `numero` varchar(10) NOT NULL DEFAULT '', + `rue` varchar(75) NOT NULL DEFAULT '', + `rue_bis` varchar(1) NOT NULL DEFAULT '', + `ville` varchar(75) NOT NULL DEFAULT '', + `fk_habitat` int(10) unsigned DEFAULT 1, + `appt` varchar(5) DEFAULT '', + `niveau` varchar(5) DEFAULT '', + `residence` varchar(75) DEFAULT '', + `gps_lat` varchar(20) NOT NULL DEFAULT '', + `gps_lng` varchar(20) NOT NULL DEFAULT '', + `encrypted_name` varchar(255) NOT NULL DEFAULT '', + `montant` decimal(7,2) NOT NULL DEFAULT 0.00, + `fk_type_reglement` int(10) unsigned DEFAULT 1, + `remarque` text DEFAULT '', + `encrypted_email` varchar(255) DEFAULT '', + `nom_recu` varchar(50) DEFAULT NULL, + `date_recu` timestamp NULL DEFAULT NULL COMMENT 'Date de réception', + `date_creat_recu` timestamp NULL DEFAULT NULL COMMENT 'Date de création du reçu', + `date_sent_recu` timestamp NULL DEFAULT NULL COMMENT 'Date envoi du reçu', + `email_erreur` varchar(30) DEFAULT '', + `chk_email_sent` tinyint(1) unsigned NOT NULL DEFAULT 0, + `encrypted_phone` varchar(128) NOT NULL DEFAULT '', + `is_striped` tinyint(1) unsigned NOT NULL DEFAULT 0, + `docremis` tinyint(1) unsigned DEFAULT 0, + `date_repasser` timestamp NULL DEFAULT NULL COMMENT 'Date prévue pour repasser', + `nb_passages` int(11) DEFAULT 1 COMMENT 'Nb passages pour les a repasser', + `chk_gps_maj` tinyint(1) unsigned DEFAULT 0, + `chk_map_create` tinyint(1) unsigned DEFAULT 0, + `chk_mobile` tinyint(1) unsigned DEFAULT 0, + `chk_synchro` tinyint(1) unsigned DEFAULT 1 COMMENT 'chk synchro entre web et appli', + `chk_api_adresse` tinyint(1) unsigned DEFAULT 0, + `chk_maj_adresse` tinyint(1) unsigned DEFAULT 0, + `anomalie` tinyint(1) unsigned DEFAULT 0, + `created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création', + `fk_user_creat` int(10) unsigned DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification', + `fk_user_modif` int(10) unsigned DEFAULT NULL, + `chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + KEY `fk_operation` (`fk_operation`), + KEY `fk_sector` (`fk_sector`), + KEY `fk_user` (`fk_user`), + KEY `fk_type` (`fk_type`), + KEY `fk_type_reglement` (`fk_type_reglement`), + KEY `email` (`encrypted_email`), + CONSTRAINT `ope_pass_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE, + CONSTRAINT `ope_pass_ibfk_2` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON UPDATE CASCADE, + CONSTRAINT `ope_pass_ibfk_3` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON UPDATE CASCADE, + CONSTRAINT `ope_pass_ibfk_4` FOREIGN KEY (`fk_type_reglement`) REFERENCES `x_types_reglements` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=19499566 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `ope_pass_histo` +-- + +DROP TABLE IF EXISTS `ope_pass_histo`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `ope_pass_histo` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `fk_pass` int(10) unsigned NOT NULL DEFAULT 0, + `date_histo` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date historique', + `sujet` varchar(50) DEFAULT NULL, + `remarque` varchar(250) NOT NULL DEFAULT '', + PRIMARY KEY (`id`), + KEY `ope_pass_histo_fk_pass_IDX` (`fk_pass`) USING BTREE, + KEY `ope_pass_histo_date_histo_IDX` (`date_histo`) USING BTREE, + CONSTRAINT `ope_pass_histo_ibfk_1` FOREIGN KEY (`fk_pass`) REFERENCES `ope_pass` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=6752 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `ope_sectors` +-- + +DROP TABLE IF EXISTS `ope_sectors`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `ope_sectors` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `fk_operation` int(10) unsigned NOT NULL DEFAULT 0, + `fk_old_sector` int(10) unsigned DEFAULT NULL, + `libelle` varchar(75) NOT NULL DEFAULT '', + `sector` text NOT NULL DEFAULT '', + `color` varchar(7) NOT NULL DEFAULT '#4B77BE', + `created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création', + `fk_user_creat` int(10) unsigned NOT NULL DEFAULT 0, + `updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification', + `fk_user_modif` int(10) unsigned NOT NULL DEFAULT 0, + `chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `id` (`id`), + KEY `fk_operation` (`fk_operation`), + CONSTRAINT `ope_sectors_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=27675 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `ope_users` +-- + +DROP TABLE IF EXISTS `ope_users`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `ope_users` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `fk_operation` int(10) unsigned NOT NULL DEFAULT 0, + `fk_user` int(10) unsigned NOT NULL DEFAULT 0, + `created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création', + `fk_user_creat` int(10) unsigned DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification', + `fk_user_modif` int(10) unsigned DEFAULT NULL, + `chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `id_UNIQUE` (`id`), + KEY `ope_users_ibfk_1` (`fk_operation`), + KEY `ope_users_ibfk_2` (`fk_user`), + CONSTRAINT `ope_users_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE, + CONSTRAINT `ope_users_ibfk_2` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=199006 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `ope_users_sectors` +-- + +DROP TABLE IF EXISTS `ope_users_sectors`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `ope_users_sectors` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `fk_operation` int(10) unsigned NOT NULL DEFAULT 0, + `fk_user` int(10) unsigned NOT NULL DEFAULT 0, + `fk_sector` int(10) unsigned NOT NULL DEFAULT 0, + `created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création', + `fk_user_creat` int(10) unsigned NOT NULL DEFAULT 0, + `updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification', + `fk_user_modif` int(10) unsigned DEFAULT NULL, + `chk_active` tinyint(1) unsigned DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `id` (`id`), + KEY `fk_operation` (`fk_operation`), + KEY `fk_user` (`fk_user`), + KEY `fk_sector` (`fk_sector`), + CONSTRAINT `ope_users_sectors_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE, + CONSTRAINT `ope_users_sectors_ibfk_2` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON UPDATE CASCADE, + CONSTRAINT `ope_users_sectors_ibfk_3` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=48082 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `ope_users_suivis` +-- + +DROP TABLE IF EXISTS `ope_users_suivis`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `ope_users_suivis` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `fk_operation` int(10) unsigned NOT NULL DEFAULT 0, + `fk_user` int(10) unsigned NOT NULL DEFAULT 0, + `date_suivi` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date du suivi', + `gps_lat` varchar(20) NOT NULL DEFAULT '', + `gps_lng` varchar(20) NOT NULL DEFAULT '', + `vitesse` varchar(20) NOT NULL DEFAULT '', + `created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création', + `fk_user_creat` int(10) unsigned NOT NULL DEFAULT 0, + `updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification', + `fk_user_modif` int(10) unsigned NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `id_UNIQUE` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `operations` +-- + +DROP TABLE IF EXISTS `operations`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `operations` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `fk_entite` int(10) unsigned NOT NULL DEFAULT 1, + `libelle` varchar(75) NOT NULL DEFAULT '', + `date_deb` date NOT NULL DEFAULT '0000-00-00', + `date_fin` date NOT NULL DEFAULT '0000-00-00', + `chk_distinct_sectors` tinyint(1) unsigned NOT NULL DEFAULT 0, + `created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création', + `fk_user_creat` int(10) unsigned NOT NULL DEFAULT 0, + `updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification', + `fk_user_modif` int(10) unsigned NOT NULL DEFAULT 0, + `chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1, + PRIMARY KEY (`id`), + KEY `fk_entite` (`fk_entite`), + KEY `date_deb` (`date_deb`), + CONSTRAINT `operations_ibfk_1` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=3121 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `params` +-- + +DROP TABLE IF EXISTS `params`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `params` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `libelle` varchar(35) NOT NULL DEFAULT '', + `valeur` varchar(255) NOT NULL DEFAULT '', + `aide` varchar(150) NOT NULL DEFAULT '', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `sectors_adresses` +-- + +DROP TABLE IF EXISTS `sectors_adresses`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `sectors_adresses` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `fk_adresse` varchar(25) DEFAULT NULL COMMENT 'adresses.cp??.id', + `osm_id` int(10) unsigned NOT NULL DEFAULT 0, + `fk_sector` int(10) unsigned NOT NULL DEFAULT 0, + `osm_name` varchar(50) NOT NULL DEFAULT '', + `numero` varchar(5) NOT NULL DEFAULT '', + `rue_bis` varchar(5) NOT NULL DEFAULT '', + `rue` varchar(60) NOT NULL DEFAULT '', + `cp` varchar(5) NOT NULL DEFAULT '', + `ville` varchar(60) NOT NULL DEFAULT '', + `gps_lat` varchar(20) NOT NULL DEFAULT '', + `gps_lng` varchar(20) NOT NULL DEFAULT '', + `osm_date_creat` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', + `created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création', + `updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification', + PRIMARY KEY (`id`), + KEY `sectors_adresses_fk_sector_index` (`fk_sector`), + KEY `sectors_adresses_numero_index` (`numero`), + KEY `sectors_adresses_rue_index` (`rue`), + KEY `sectors_adresses_ville_index` (`ville`), + CONSTRAINT `sectors_adresses_ibfk_1` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=1562946 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `users` +-- + +DROP TABLE IF EXISTS `users`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `users` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `fk_entite` int(10) unsigned DEFAULT 1, + `fk_role` int(10) unsigned DEFAULT 1, + `fk_titre` int(10) unsigned DEFAULT 1, + `encrypted_name` varchar(255) DEFAULT NULL, + `first_name` varchar(45) DEFAULT NULL, + `sect_name` varchar(60) DEFAULT '', + `encrypted_user_name` varchar(128) DEFAULT '', + `user_pass_hash` varchar(60) DEFAULT NULL, + `encrypted_phone` varchar(128) DEFAULT NULL, + `encrypted_mobile` varchar(128) DEFAULT NULL, + `encrypted_email` varchar(255) DEFAULT '', + `chk_alert_email` tinyint(1) unsigned DEFAULT 1, + `chk_suivi` tinyint(1) unsigned DEFAULT 0, + `date_naissance` date DEFAULT NULL, + `date_embauche` date DEFAULT NULL, + `created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création', + `fk_user_creat` int(10) unsigned DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification', + `fk_user_modif` int(10) unsigned DEFAULT NULL, + `chk_active` tinyint(1) unsigned DEFAULT 1, + PRIMARY KEY (`id`), + KEY `fk_entite` (`fk_entite`), + KEY `username` (`encrypted_user_name`), + KEY `users_ibfk_2` (`fk_role`), + KEY `users_ibfk_3` (`fk_titre`), + CONSTRAINT `users_ibfk_1` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`) ON UPDATE CASCADE, + CONSTRAINT `users_ibfk_2` FOREIGN KEY (`fk_role`) REFERENCES `x_users_roles` (`id`) ON UPDATE CASCADE, + CONSTRAINT `users_ibfk_3` FOREIGN KEY (`fk_titre`) REFERENCES `x_users_titres` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=10027747 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `x_departements` +-- + +DROP TABLE IF EXISTS `x_departements`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `x_departements` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `code` varchar(3) DEFAULT NULL, + `fk_region` int(10) unsigned DEFAULT 1, + `libelle` varchar(45) DEFAULT NULL, + `chk_active` tinyint(1) unsigned DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `id_UNIQUE` (`id`), + KEY `x_departements_ibfk_1` (`fk_region`), + CONSTRAINT `x_departements_ibfk_1` FOREIGN KEY (`fk_region`) REFERENCES `x_regions` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=105 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `x_devises` +-- + +DROP TABLE IF EXISTS `x_devises`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `x_devises` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `code` varchar(3) DEFAULT NULL, + `symbole` varchar(6) DEFAULT NULL, + `libelle` varchar(45) DEFAULT NULL, + `chk_active` tinyint(1) unsigned DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `id_UNIQUE` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `x_entites_types` +-- + +DROP TABLE IF EXISTS `x_entites_types`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `x_entites_types` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `libelle` varchar(45) DEFAULT NULL, + `chk_active` tinyint(1) unsigned DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `id_UNIQUE` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `x_pays` +-- + +DROP TABLE IF EXISTS `x_pays`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `x_pays` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `code` varchar(3) DEFAULT NULL, + `fk_continent` int(10) unsigned DEFAULT NULL, + `fk_devise` int(10) unsigned DEFAULT 1, + `libelle` varchar(45) DEFAULT NULL, + `chk_active` tinyint(1) unsigned DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `id_UNIQUE` (`id`), + KEY `x_pays_ibfk_1` (`fk_devise`), + CONSTRAINT `x_pays_ibfk_1` FOREIGN KEY (`fk_devise`) REFERENCES `x_devises` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Table des pays avec leurs codes' `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `x_regions` +-- + +DROP TABLE IF EXISTS `x_regions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `x_regions` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `fk_pays` int(10) unsigned DEFAULT 1, + `libelle` varchar(45) DEFAULT NULL, + `libelle_long` varchar(45) DEFAULT NULL, + `table_osm` varchar(45) DEFAULT NULL, + `departements` varchar(45) DEFAULT NULL, + `chk_active` tinyint(1) unsigned DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `id_UNIQUE` (`id`), + KEY `x_regions_ibfk_1` (`fk_pays`), + CONSTRAINT `x_regions_ibfk_1` FOREIGN KEY (`fk_pays`) REFERENCES `x_pays` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `x_types_passages` +-- + +DROP TABLE IF EXISTS `x_types_passages`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `x_types_passages` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `libelle` varchar(10) DEFAULT NULL, + `color_button` varchar(15) DEFAULT NULL, + `color_mark` varchar(15) DEFAULT NULL, + `color_table` varchar(15) DEFAULT NULL, + `chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `x_types_reglements` +-- + +DROP TABLE IF EXISTS `x_types_reglements`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `x_types_reglements` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `libelle` varchar(45) DEFAULT NULL, + `chk_active` tinyint(1) unsigned DEFAULT 1, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `x_users_roles` +-- + +DROP TABLE IF EXISTS `x_users_roles`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `x_users_roles` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `libelle` varchar(45) DEFAULT NULL, + `chk_active` tinyint(1) unsigned DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `id_UNIQUE` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Les différents rôles des utilisateurs' `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `x_users_titres` +-- + +DROP TABLE IF EXISTS `x_users_titres`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `x_users_titres` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `libelle` varchar(45) DEFAULT NULL, + `chk_active` tinyint(1) unsigned DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `id_UNIQUE` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Les différents titres des utilisateurs' `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `x_villes` +-- + +DROP TABLE IF EXISTS `x_villes`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `x_villes` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `fk_departement` int(10) unsigned DEFAULT 1, + `libelle` varchar(65) DEFAULT NULL, + `code_postal` varchar(5) DEFAULT NULL, + `code_insee` varchar(5) DEFAULT NULL, + `chk_active` tinyint(1) unsigned DEFAULT 1, + PRIMARY KEY (`id`), + UNIQUE KEY `id_UNIQUE` (`id`), + KEY `x_villes_ibfk_1` (`fk_departement`), + CONSTRAINT `x_villes_ibfk_1` FOREIGN KEY (`fk_departement`) REFERENCES `x_departements` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=38950 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `z_sessions` +-- + +DROP TABLE IF EXISTS `z_sessions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `z_sessions` ( + `sid` text NOT NULL, + `fk_user` int(11) NOT NULL, + `role` varchar(10) DEFAULT NULL, + `date_modified` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + `ip` varchar(50) NOT NULL, + `browser` varchar(150) NOT NULL, + `data` mediumtext DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Final view structure for view `chat_conversations_unread` +-- + +/*!50001 DROP VIEW IF EXISTS `chat_conversations_unread`*/; +/*!50001 SET @saved_cs_client = @@character_set_client */; +/*!50001 SET @saved_cs_results = @@character_set_results */; +/*!50001 SET @saved_col_connection = @@collation_connection */; +/*!50001 SET character_set_client = utf8mb4 */; +/*!50001 SET character_set_results = utf8mb4 */; +/*!50001 SET collation_connection = utf8mb4_general_ci */; +/*!50001 CREATE ALGORITHM=UNDEFINED */ +/*!50013 DEFINER=`root`@`%` SQL SECURITY DEFINER */ +/*!50001 VIEW `chat_conversations_unread` AS select `r`.`id` AS `id`,`r`.`type` AS `type`,`r`.`title` AS `title`,`r`.`date_creation` AS `date_creation`,`r`.`fk_user` AS `fk_user`,`r`.`fk_entite` AS `fk_entite`,`r`.`statut` AS `statut`,`r`.`description` AS `description`,`r`.`reply_permission` AS `reply_permission`,`r`.`is_pinned` AS `is_pinned`,`r`.`expiry_date` AS `expiry_date`,`r`.`updated_at` AS `updated_at`,count(distinct `m`.`id`) AS `total_messages`,count(distinct `rm`.`id`) AS `read_messages`,count(distinct `m`.`id`) - count(distinct `rm`.`id`) AS `unread_messages`,(select `geo_app`.`chat_messages`.`date_sent` from `chat_messages` where `geo_app`.`chat_messages`.`fk_room` = `r`.`id` order by `geo_app`.`chat_messages`.`date_sent` desc limit 1) AS `last_message_date` from ((`chat_rooms` `r` left join `chat_messages` `m` on(`r`.`id` = `m`.`fk_room`)) left join `chat_read_messages` `rm` on(`m`.`id` = `rm`.`fk_message`)) group by `r`.`id` */; +/*!50001 SET character_set_client = @saved_cs_client */; +/*!50001 SET character_set_results = @saved_cs_results */; +/*!50001 SET collation_connection = @saved_col_connection */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2025-05-15 9:31:42 diff --git a/docs/geosector-db.sql b/docs/geosector-db.sql deleted file mode 100644 index 4aa2c3d2..00000000 --- a/docs/geosector-db.sql +++ /dev/null @@ -1,623 +0,0 @@ --- Création de la base de données geo_app si elle n'existe pas -DROP DATABASE IF EXISTS `geo_app`; -CREATE DATABASE IF NOT EXISTS `geo_app` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- Création de l'utilisateur et attribution des droits -CREATE USER IF NOT EXISTS 'geo_app_user'@'localhost' IDENTIFIED BY 'QO:96df*?k{4W6m'; -GRANT SELECT, INSERT, UPDATE, DELETE ON `geo_app`.* TO 'geo_app_user'@'localhost'; -FLUSH PRIVILEGES; - -USE geo_app; - --- --- Table structure for table `email_counter` --- - -DROP TABLE IF EXISTS `email_counter`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `email_counter` ( - `id` int unsigned NOT NULL DEFAULT '1', - `hour_start` timestamp NULL DEFAULT NULL, - `count` int unsigned DEFAULT '0', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - -DROP TABLE IF EXISTS `x_devises`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `x_devises` ( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `code` varchar(3) DEFAULT NULL, - `symbole` varchar(6) DEFAULT NULL, - `libelle` varchar(45) DEFAULT NULL, - `chk_active` tinyint(1) unsigned DEFAULT '1', - PRIMARY KEY (`id`), - UNIQUE KEY `id_UNIQUE` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `x_entites_types` --- - -DROP TABLE IF EXISTS `x_entites_types`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `x_entites_types` ( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `libelle` varchar(45) DEFAULT NULL, - `chk_active` tinyint(1) unsigned DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `id_UNIQUE` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `x_types_passages` --- - -DROP TABLE IF EXISTS `x_types_passages`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `x_types_passages` ( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `libelle` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `color_button` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `color_mark` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `color_table` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `chk_active` tinyint(1) unsigned NOT NULL DEFAULT '1', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `x_types_reglements` --- - -DROP TABLE IF EXISTS `x_types_reglements`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `x_types_reglements` ( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `libelle` varchar(45) DEFAULT NULL, - `chk_active` tinyint(1) unsigned DEFAULT '1', - PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Table structure for table `x_users_roles` --- - -DROP TABLE IF EXISTS `x_users_roles`; - -CREATE TABLE `x_users_roles` ( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `libelle` varchar(45) DEFAULT NULL, - `chk_active` tinyint(1) unsigned DEFAULT '1', - PRIMARY KEY (`id`), - UNIQUE KEY `id_UNIQUE` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Les différents rôles des utilisateurs'; -/*!40101 SET character_set_client = @saved_cs_client */; - -DROP TABLE IF EXISTS `x_users_titres`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `x_users_titres` ( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `libelle` varchar(45) DEFAULT NULL, - `chk_active` tinyint(1) unsigned DEFAULT '1', - PRIMARY KEY (`id`), - UNIQUE KEY `id_UNIQUE` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Les différents titres des utilisateurs'; - -DROP TABLE IF EXISTS `x_pays`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `x_pays` ( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `code` varchar(3) DEFAULT NULL, - `fk_continent` int unsigned DEFAULT NULL, - `fk_devise` int unsigned DEFAULT '1', - `libelle` varchar(45) DEFAULT NULL, - `chk_active` tinyint(1) unsigned DEFAULT '1', - PRIMARY KEY (`id`), - UNIQUE KEY `id_UNIQUE` (`id`), - CONSTRAINT `x_pays_ibfk_1` FOREIGN KEY (`fk_devise`) REFERENCES `x_devises` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Table des pays avec leurs codes'; -/*!40101 SET character_set_client = @saved_cs_client */; - - -DROP TABLE IF EXISTS `x_regions`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `x_regions` ( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `fk_pays` int unsigned DEFAULT '1', - `libelle` varchar(45) DEFAULT NULL, - `libelle_long` varchar(45) DEFAULT NULL, - `table_osm` varchar(45) DEFAULT NULL, - `departements` varchar(45) DEFAULT NULL, - `chk_active` tinyint(1) unsigned DEFAULT '1', - PRIMARY KEY (`id`), - UNIQUE KEY `id_UNIQUE` (`id`), - CONSTRAINT `x_regions_ibfk_1` FOREIGN KEY (`fk_pays`) REFERENCES `x_pays` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - -DROP TABLE IF EXISTS `x_departements`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `x_departements` ( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `code` varchar(3) DEFAULT NULL, - `fk_region` int unsigned DEFAULT '1', - `libelle` varchar(45) DEFAULT NULL, - `chk_active` tinyint(1) unsigned DEFAULT '1', - PRIMARY KEY (`id`), - UNIQUE KEY `id_UNIQUE` (`id`), - CONSTRAINT `x_departements_ibfk_1` FOREIGN KEY (`fk_region`) REFERENCES `x_regions` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=105 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - -DROP TABLE IF EXISTS `entites`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `entites` ( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `encrypted_name` varchar(255) DEFAULT NULL, - `adresse1` varchar(45) DEFAULT '', - `adresse2` varchar(45) DEFAULT '', - `cp` varchar(5) DEFAULT '', - `ville` varchar(45) DEFAULT '', - `fk_region` int unsigned DEFAULT NULL, - `fk_type` int unsigned DEFAULT '1', - `encrypted_phone` varchar(128) DEFAULT '', - `encrypted_mobile` varchar(128) DEFAULT '', - `encrypted_email` varchar(255) DEFAULT '', - `gps_lat` varchar(20) NOT NULL DEFAULT '', - `gps_lng` varchar(20) NOT NULL DEFAULT '', - `encrypted_stripe_id` varchar(255) DEFAULT '', - `iban` varchar(30) DEFAULT '', - `bic` varchar(15) DEFAULT '', - `chk_demo` tinyint(1) unsigned DEFAULT '1', - `chk_mdp_manuel` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT 'Gestion des mots de passe manuelle O/N', - `chk_copie_mail_recu` tinyint(1) unsigned NOT NULL DEFAULT '0', - `chk_accept_sms` tinyint(1) unsigned NOT NULL DEFAULT '0', - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création', - `fk_user_creat` int unsigned DEFAULT NULL, - `updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification', - `fk_user_modif` int unsigned DEFAULT NULL, - `chk_active` tinyint(1) unsigned DEFAULT '1', - PRIMARY KEY (`id`), - CONSTRAINT `entites_ibfk_1` FOREIGN KEY (`fk_region`) REFERENCES `x_regions` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, - CONSTRAINT `entites_ibfk_2` FOREIGN KEY (`fk_type`) REFERENCES `x_entites_types` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - -DROP TABLE IF EXISTS `x_villes`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `x_villes` ( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `fk_departement` int unsigned DEFAULT '1', - `libelle` varchar(65) DEFAULT NULL, - `cp` varchar(5) DEFAULT NULL, - `code_insee` varchar(5) DEFAULT NULL, - `departement` varchar(65) DEFAULT NULL, - `chk_active` tinyint(1) unsigned DEFAULT '1', - PRIMARY KEY (`id`), - UNIQUE KEY `id_UNIQUE` (`id`), - CONSTRAINT `x_villes_ibfk_1` FOREIGN KEY (`fk_departement`) REFERENCES `x_departements` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=38950 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - -DROP TABLE IF EXISTS `users`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `users` ( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `fk_entite` int unsigned DEFAULT '1', - `fk_role` int unsigned DEFAULT '1', - `fk_titre` int unsigned DEFAULT '1', - `num_adherent` int unsigned NOT NULL DEFAULT '0', - `encrypted_name` varchar(255) DEFAULT NULL, - `first_name` varchar(45) DEFAULT NULL, - `sect_name` varchar(60) DEFAULT '', - `encrypt_user_name` varchar(128) DEFAULT '', - `user_pswd` varchar(60) DEFAULT NULL, - `encrypt_phone` varchar(128) DEFAULT NULL, - `encrypt_mobile` varchar(128) DEFAULT NULL, - `encrypt_email` varchar(255) DEFAULT '', - `infos` varchar(200) NOT NULL DEFAULT '', - `chk_alert_email` tinyint(1) unsigned DEFAULT '1', - `chk_suivi` tinyint(1) unsigned DEFAULT '0', - `date_naissance` date DEFAULT NULL, - `date_embauche` date DEFAULT NULL, - `anciennete` varchar(20) DEFAULT '-', - `matricule` varchar(10) NOT NULL DEFAULT '', - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création', - `fk_user_creat` int unsigned DEFAULT NULL, - `updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification', - `fk_user_modif` int unsigned DEFAULT NULL, - `chk_active` tinyint(1) unsigned DEFAULT '1', - PRIMARY KEY (`id`), - KEY `fk_entite` (`fk_entite`), - KEY `username` (`encrypt_user_name`), - CONSTRAINT `users_ibfk_1` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, - CONSTRAINT `users_ibfk_2` FOREIGN KEY (`fk_role`) REFERENCES `x_users_roles` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, - CONSTRAINT `users_ibfk_3` FOREIGN KEY (`fk_titre`) REFERENCES `x_users_titres` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - -DROP TABLE IF EXISTS `operations`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `operations` ( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `fk_entite` int unsigned NOT NULL DEFAULT '1', - `libelle` varchar(75) NOT NULL DEFAULT '', - `date_deb` date NOT NULL DEFAULT '0000-00-00', - `date_fin` date NOT NULL DEFAULT '0000-00-00', - `chk_distinct_sectors` tinyint(1) unsigned NOT NULL DEFAULT '0', - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création', - `fk_user_creat` int unsigned NOT NULL DEFAULT '0', - `updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification', - `fk_user_modif` int unsigned NOT NULL DEFAULT '0', - `chk_active` tinyint(1) unsigned NOT NULL DEFAULT '1', - PRIMARY KEY (`id`), - KEY `fk_entite` (`fk_entite`), - KEY `date_deb` (`date_deb`), - CONSTRAINT `operations_ibfk_1` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - - -DROP TABLE IF EXISTS `ope_sectors`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `ope_sectors` ( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `fk_operation` int unsigned NOT NULL DEFAULT '0', - `libelle` varchar(75) NOT NULL DEFAULT '', - `sector` text NOT NULL DEFAULT '', - `color` varchar(7) NOT NULL DEFAULT '#4B77BE', - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création', - `fk_user_creat` int unsigned NOT NULL DEFAULT '0', - `updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification', - `fk_user_modif` int unsigned NOT NULL DEFAULT '0', - `chk_active` tinyint(1) unsigned NOT NULL DEFAULT '1', - PRIMARY KEY (`id`), - UNIQUE KEY `id` (`id`), - KEY `fk_operation` (`fk_operation`), - CONSTRAINT `ope_sectors_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - - -DROP TABLE IF EXISTS `ope_users`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `ope_users` ( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `fk_operation` int unsigned NOT NULL DEFAULT '0', - `fk_user` int unsigned NOT NULL DEFAULT '0', - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création', - `fk_user_creat` int unsigned DEFAULT NULL, - `updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification', - `fk_user_modif` int unsigned DEFAULT NULL, - `chk_active` tinyint(1) unsigned NOT NULL DEFAULT '1', - PRIMARY KEY (`id`), - UNIQUE KEY `id_UNIQUE` (`id`), - CONSTRAINT `ope_users_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, - CONSTRAINT `ope_users_ibfk_2` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - -DROP TABLE IF EXISTS `email_queue`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `email_queue` ( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `fk_pass` int unsigned NOT NULL DEFAULT '0', - `to_email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `subject` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `body` text COLLATE utf8mb4_unicode_ci, - `headers` text COLLATE utf8mb4_unicode_ci, - `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, - `status` enum('pending','sent','failed') COLLATE utf8mb4_unicode_ci DEFAULT 'pending', - `attempts` int unsigned DEFAULT '0', - PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - -DROP TABLE IF EXISTS `ope_users_sectors`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `ope_users_sectors` ( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `fk_operation` int unsigned NOT NULL DEFAULT '0', - `fk_user` int unsigned NOT NULL DEFAULT '0', - `fk_sector` int unsigned NOT NULL DEFAULT '0', - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création', - `fk_user_creat` int unsigned NOT NULL DEFAULT '0', - `updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification', - `fk_user_modif` int unsigned DEFAULT NULL, - `chk_active` tinyint(1) unsigned DEFAULT '1', - PRIMARY KEY (`id`), - UNIQUE KEY `id` (`id`), - KEY `fk_operation` (`fk_operation`), - KEY `fk_user` (`fk_user`), - KEY `fk_sector` (`fk_sector`), - CONSTRAINT `ope_users_sectors_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, - CONSTRAINT `ope_users_sectors_ibfk_2` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, - CONSTRAINT `ope_users_sectors_ibfk_3` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - -DROP TABLE IF EXISTS `ope_users_suivis`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `ope_users_suivis` ( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `fk_operation` int unsigned NOT NULL DEFAULT '0', - `fk_user` int unsigned NOT NULL DEFAULT '0', - `date_suivi` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date du suivi', - `gps_lat` varchar(20) NOT NULL DEFAULT '', - `gps_lng` varchar(20) NOT NULL DEFAULT '', - `vitesse` varchar(20) NOT NULL DEFAULT '', - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création', - `fk_user_creat` int unsigned NOT NULL DEFAULT '0', - `updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification', - `fk_user_modif` int unsigned NOT NULL DEFAULT '0', - PRIMARY KEY (`id`), - UNIQUE KEY `id_UNIQUE` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - -DROP TABLE IF EXISTS `sectors_adresses`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `sectors_adresses` ( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `fk_adresse` varchar(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'adresses.cp??.id', - `osm_id` int unsigned NOT NULL DEFAULT '0', - `fk_sector` int unsigned NOT NULL DEFAULT '0', - `osm_name` varchar(50) NOT NULL DEFAULT '', - `numero` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', - `rue_bis` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', - `rue` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', - `cp` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', - `ville` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', - `gps_lat` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', - `gps_lng` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', - `osm_date_creat` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création', - `updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification', - PRIMARY KEY (`id`), - KEY `sectors_adresses_fk_sector_index` (`fk_sector`), - KEY `sectors_adresses_numero_index` (`numero`), - KEY `sectors_adresses_rue_index` (`rue`), - KEY `sectors_adresses_ville_index` (`ville`), - CONSTRAINT `sectors_adresses_ibfk_1` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -DROP TABLE IF EXISTS `ope_pass`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `ope_pass` ( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `fk_operation` int unsigned NOT NULL DEFAULT '0', - `fk_sector` int unsigned DEFAULT '0', - `fk_user` int unsigned NOT NULL DEFAULT '0', - `fk_adresse` varchar(25) DEFAULT '' COMMENT 'adresses.cp??.id', - `passed_at` timestamp NULL DEFAULT NULL COMMENT 'Date du passage', - `fk_type` int unsigned DEFAULT '0', - `numero` varchar(10) NOT NULL DEFAULT '', - `rue` varchar(75) NOT NULL DEFAULT '', - `rue_bis` varchar(1) NOT NULL DEFAULT '', - `ville` varchar(75) NOT NULL DEFAULT '', - `fk_habitat` int unsigned DEFAULT '1', - `appt` varchar(5) DEFAULT '', - `niveau` varchar(5) DEFAULT '', - `residence` varchar(75) DEFAULT '', - `gps_lat` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', - `gps_lng` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', - `libelle` varchar(45) NOT NULL DEFAULT '', - `montant` decimal(7,2) NOT NULL DEFAULT '0.00', - `fk_type_reglement` int unsigned DEFAULT '1', - `remarque` text DEFAULT '', - `email` varchar(75) DEFAULT '', - `nom_recu` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `date_recu` timestamp NULL DEFAULT NULL COMMENT 'Date de réception', - `date_creat_recu` timestamp NULL DEFAULT NULL COMMENT 'Date de création du reçu', - `date_sent_recu` timestamp NULL DEFAULT NULL COMMENT 'Date envoi du reçu', - `email_erreur` varchar(30) DEFAULT '', - `chk_email_sent` tinyint(1) unsigned NOT NULL DEFAULT '0', - `phone` varchar(15) NOT NULL DEFAULT '', - `docremis` tinyint(1) unsigned DEFAULT '0', - `date_repasser` timestamp NULL DEFAULT NULL COMMENT 'Date prévue pour repasser', - `nb_passages` int DEFAULT '1' COMMENT 'Nb passages pour les a repasser', - `chk_gps_maj` tinyint(1) unsigned DEFAULT '0', - `chk_map_create` tinyint(1) unsigned DEFAULT '0', - `chk_mobile` tinyint(1) unsigned DEFAULT '0', - `chk_synchro` tinyint(1) unsigned DEFAULT '1' COMMENT 'chk synchro entre web et appli', - `chk_api_adresse` tinyint(1) unsigned DEFAULT '0', - `chk_maj_adresse` tinyint(1) unsigned DEFAULT '0', - `anomalie` tinyint(1) unsigned DEFAULT '0', - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création', - `fk_user_creat` int unsigned DEFAULT NULL, - `updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de modification', - `fk_user_modif` int unsigned DEFAULT NULL, - `chk_active` tinyint(1) unsigned NOT NULL DEFAULT '1', - PRIMARY KEY (`id`), - KEY `fk_operation` (`fk_operation`), - KEY `fk_sector` (`fk_sector`), - KEY `fk_user` (`fk_user`), - KEY `fk_type` (`fk_type`), - KEY `fk_type_reglement` (`fk_type_reglement`), - KEY `email` (`email`), - CONSTRAINT `ope_pass_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, - CONSTRAINT `ope_pass_ibfk_2` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, - CONSTRAINT `ope_pass_ibfk_3` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, - CONSTRAINT `ope_pass_ibfk_4` FOREIGN KEY (`fk_type_reglement`) REFERENCES `x_types_reglements` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - -DROP TABLE IF EXISTS `ope_pass_histo`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `ope_pass_histo` ( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `fk_pass` int unsigned NOT NULL DEFAULT '0', - `fk_user` int unsigned NOT NULL DEFAULT '0', - `date_histo` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date historique', - `sujet` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, - `remarque` varchar(250) NOT NULL DEFAULT '', - PRIMARY KEY (`id`), - KEY `ope_pass_histo_fk_pass_IDX` (`fk_pass`) USING BTREE, - KEY `ope_pass_histo_date_histo_IDX` (`date_histo`) USING BTREE, - CONSTRAINT `ope_pass_histo_ibfk_1` FOREIGN KEY (`fk_pass`) REFERENCES `ope_pass` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT `ope_pass_histo_ibfk_2` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - -DROP TABLE IF EXISTS `medias`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `medias` ( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `support` varchar(45) NOT NULL DEFAULT '', - `support_id` int unsigned NOT NULL DEFAULT '0', - `fichier` varchar(250) NOT NULL DEFAULT '', - `description` varchar(100) NOT NULL DEFAULT '', - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - `fk_user_creat` int unsigned NOT NULL DEFAULT '0', - `updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, - `fk_user_modif` int unsigned NOT NULL DEFAULT '0', - PRIMARY KEY (`id`), - UNIQUE KEY `id_UNIQUE` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- Création des tables pour le système de chat -DROP TABLE IF EXISTS `chat_rooms`; --- Table des salles de discussion -CREATE TABLE chat_rooms ( - id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(100) NOT NULL, - type ENUM('privee', 'groupe', 'liste_diffusion') NOT NULL, - date_creation timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création', - fk_user INT UNSIGNED NOT NULL, - fk_entite INT UNSIGNED, - statut ENUM('active', 'archive') NOT NULL DEFAULT 'active', - description TEXT, - INDEX idx_user (fk_user), - INDEX idx_entite (fk_entite) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - - -DROP TABLE IF EXISTS `chat_participants`; --- Table des participants aux salles de discussion -CREATE TABLE chat_participants ( - id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - id_room INT UNSIGNED NOT NULL, - id_user INT UNSIGNED NOT NULL, - role ENUM('administrateur', 'participant', 'en_lecture_seule') NOT NULL DEFAULT 'participant', - date_ajout timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date ajout', - notification_activee BOOLEAN NOT NULL DEFAULT TRUE, - INDEX idx_room (id_room), - INDEX idx_user (id_user), - CONSTRAINT uc_room_user UNIQUE (id_room, id_user), - FOREIGN KEY (id_room) REFERENCES chat_rooms(id) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -DROP TABLE IF EXISTS `chat_messages`; --- Table des messages -CREATE TABLE chat_messages ( - id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - fk_room INT UNSIGNED NOT NULL, - fk_user INT UNSIGNED NOT NULL, - content TEXT, - date_sent timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date envoi', - type ENUM('texte', 'media', 'systeme') NOT NULL DEFAULT 'texte', - statut ENUM('envoye', 'livre', 'lu') NOT NULL DEFAULT 'envoye', - INDEX idx_room (fk_room), - INDEX idx_user (fk_user), - INDEX idx_date (date_sent), - FOREIGN KEY (fk_room) REFERENCES chat_rooms(id) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -DROP TABLE IF EXISTS `chat_listes_diffusion`; --- Table des listes de diffusion -CREATE TABLE chat_listes_diffusion ( - id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - fk_room INT UNSIGNED NOT NULL, - name VARCHAR(100) NOT NULL, - description TEXT, - fk_user INT UNSIGNED NOT NULL, - date_creation timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création', - INDEX idx_room (fk_room), - INDEX idx_user (fk_user), - FOREIGN KEY (fk_room) REFERENCES chat_rooms(id) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -DROP TABLE IF EXISTS `chat_read_messages`; --- Table pour suivre la lecture des messages -CREATE TABLE chat_read_messages ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - fk_message INT UNSIGNED NOT NULL, - fk_user INT UNSIGNED NOT NULL, - date_read timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de lecture', - INDEX idx_message (fk_message), - INDEX idx_user (fk_user), - CONSTRAINT uc_message_user UNIQUE (fk_message, fk_user), - FOREIGN KEY (fk_message) REFERENCES chat_messages(id) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -DROP TABLE IF EXISTS `chat_notifications`; --- Table des notifications -CREATE TABLE chat_notifications ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - fk_user INT UNSIGNED NOT NULL, - fk_message INT UNSIGNED, - fk_room INT UNSIGNED, - type VARCHAR(50) NOT NULL, - contenu TEXT, - date_creation timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création', - date_lecture timestamp NULL DEFAULT NULL COMMENT 'Date de lecture', - statut ENUM('non_lue', 'lue') NOT NULL DEFAULT 'non_lue', - INDEX idx_user (fk_user), - INDEX idx_message (fk_message), - INDEX idx_room (fk_room), - FOREIGN KEY (fk_message) REFERENCES chat_messages(id) ON DELETE SET NULL, - FOREIGN KEY (fk_room) REFERENCES chat_rooms(id) ON DELETE SET NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -DROP TABLE IF EXISTS `z_params`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `params` ( - `id` int unsigned NOT NULL AUTO_INCREMENT, - `libelle` varchar(35) NOT NULL DEFAULT '', - `valeur` varchar(255) NOT NULL DEFAULT '', - `aide` varchar(150) NOT NULL DEFAULT '', - PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - - -DROP TABLE IF EXISTS `z_sessions`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `z_sessions` ( - `sid` text NOT NULL, - `fk_user` int NOT NULL, - `role` varchar(10) DEFAULT NULL, - `date_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `ip` varchar(50) NOT NULL, - `browser` varchar(150) NOT NULL, - `data` mediumtext -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET character_set_client = @saved_cs_client */; diff --git a/docs/gestion_hive_boxes.md b/docs/gestion_hive_boxes.md index e7232eb7..24cdcc2c 100644 --- a/docs/gestion_hive_boxes.md +++ b/docs/gestion_hive_boxes.md @@ -18,6 +18,7 @@ Ce document explique comment les boîtes Hive sont gérées dans l'application G Hive est une base de données NoSQL légère et rapide utilisée dans GeoSector pour stocker les données localement. Les données sont organisées en "boîtes" (boxes) qui peuvent être typées pour stocker des modèles spécifiques. Dans cette application, Hive est utilisé pour : + - Stocker les données utilisateur et maintenir les sessions - Conserver les données des opérations, secteurs et passages - Permettre l'utilisation de l'application en mode hors ligne @@ -36,6 +37,7 @@ static const String settingsBoxName = 'settings'; ``` Chaque boîte stocke un type spécifique de données : + - **users** : Stocke les informations des utilisateurs (`UserModel`) - **operations** : Stocke les opérations (`OperationModel`) - **sectors** : Stocke les secteurs (`SectorModel`) @@ -106,10 +108,16 @@ await Hive.openBox(AppKeys.settingsBoxName); // Préférences générales ### UserRepository -Le `UserRepository` est le principal gestionnaire des boîtes Hive. Il est responsable de : +Le `UserRepository` est le principal gestionnaire des boîtes Hive et de l'authentification. Il est responsable de : + - L'initialisation des boîtes au démarrage de l'application - La gestion des boîtes pendant les processus de connexion et déconnexion - Le nettoyage et la recréation des boîtes lorsque nécessaire +- La gestion complète de l'authentification (connexion et déconnexion) +- L'affichage des overlays de chargement pendant les opérations d'authentification +- La redirection vers les pages appropriées après connexion/déconnexion + +> **Note importante** : Auparavant, l'application utilisait un service séparé `AuthService` pour gérer l'authentification. Cette classe a été supprimée et ses fonctionnalités ont été intégrées directement dans `UserRepository` pour simplifier l'architecture et éviter les problèmes de synchronisation entre les deux classes. ### Autres repositories spécialisés @@ -124,14 +132,17 @@ Ces repositories sont injectés dans le `UserRepository` pour traiter les donné Le processus de connexion dans `UserRepository.login()` suit ces étapes : 1. **Nettoyage initial** : + - Suppression des boîtes non référencées (`auth`, `locations`, `messages`) - Nettoyage adapté à la plateforme (Web, iOS, Android) 2. **Préparation des boîtes** : + - Appel à `_clearAndRecreateBoxes()` pour vider et recréer les boîtes sans les fermer - Utilisation de `_ensureBoxIsOpen()` pour garantir que les boîtes sont ouvertes 3. **Appel API et traitement des données** : + - Connexion via l'API - Vérification que toutes les boîtes sont ouvertes avant le traitement - Traitement des données reçues (opérations, secteurs, passages) @@ -159,34 +170,93 @@ await _processPassages(passagesData); Le processus de déconnexion dans `UserRepository.logout()` suit ces étapes : 1. **Préparation** : - - S'assurer que la boîte des utilisateurs est ouverte - - Suppression des boîtes non référencées -2. **Gestion de l'utilisateur** : - Récupération de l'utilisateur actuel avant nettoyage - Déconnexion de la session API - - Mise à jour de l'utilisateur pour effacer les données de session + - Réinitialisation du cache de l'utilisateur actuel -3. **Nettoyage des données** : - - Nettoyage adapté à la plateforme (Web, iOS, Android) - - Appel à `_clearAndRecreateBoxes()` pour vider les boîtes sans les fermer +2. **Nettoyage des données** : + - Appel à `_deepCleanHiveBoxes()` pour un nettoyage complet des boîtes Hive + +### Méthode \_deepCleanHiveBoxes + +La méthode `_deepCleanHiveBoxes()` est cruciale pour le processus de déconnexion et suit ces étapes : + +1. **Vidage des boîtes** : + + - Vidage de toutes les boîtes Hive ouvertes sans les fermer + - Gestion des erreurs pour chaque boîte avec typage spécifique + +2. **Nettoyage spécifique à la plateforme** : + + - Nettoyage adapté selon la plateforme (Web, iOS, Android) + - Utilisation de méthodes spécifiques comme `_clearIndexedDB()` pour le web + +3. **Réinitialisation** : + - Réinitialisation de l'API Service ### Code clé pour la déconnexion ```dart -// S'assurer que la boîte des utilisateurs est ouverte -await _ensureBoxIsOpen(AppKeys.usersBoxName); +// Méthode logout +Future logout() async { + try { + // Récupérer l'utilisateur actuel avant de nettoyer les données + final currentUser = getCurrentUser(); -// Récupérer l'utilisateur et effacer sa session -final updatedUser = currentUser.copyWith( - sessionId: null, - sessionExpiry: null, - lastPath: null -); -await saveUser(updatedUser); + // Déconnecter la session API + if (currentUser?.sessionId != null) { + await logoutAPI(); + } -// Vider les boîtes sans les fermer -await _clearAndRecreateBoxes(); + // Supprimer la session API + setSessionId(null); + + // Réinitialiser le cache de l'utilisateur actuel + _cachedCurrentUser = null; + + // Nettoyage complet des boîtes Hive + await _deepCleanHiveBoxes(); + + return true; + } catch (e) { + return false; + } +} + +// Méthode de nettoyage des boîtes Hive +Future _deepCleanHiveBoxes() async { + try { + // 1. Vider toutes les boîtes sans les fermer + if (Hive.isBoxOpen(AppKeys.usersBoxName)) { + await Hive.box(AppKeys.usersBoxName).clear(); + } + + if (Hive.isBoxOpen(AppKeys.operationsBoxName)) { + await Hive.box(AppKeys.operationsBoxName).clear(); + } + + if (Hive.isBoxOpen(AppKeys.sectorsBoxName)) { + await Hive.box(AppKeys.sectorsBoxName).clear(); + } + + // Vider les autres boîtes... + + // 2. Nettoyage spécifique à la plateforme + if (kIsWeb) { + await _clearIndexedDB(); + } else if (Platform.isIOS) { + await _cleanHiveFilesOnIOS(); + } else if (Platform.isAndroid) { + await _cleanHiveFilesOnAndroid(); + } + + // 3. Réinitialiser l'API Service + _apiService.setSessionId(null); + } catch (e) { + debugPrint('Erreur lors du nettoyage des boîtes Hive: $e'); + } +} ``` ## Problèmes connus et solutions @@ -201,7 +271,8 @@ await _clearAndRecreateBoxes(); **Problème** : Des erreurs se produisent lorsqu'on tente d'accéder à une boîte qui a été fermée prématurément. -**Solution** : +**Solution** : + - Utiliser la méthode `_ensureBoxIsOpen()` avant d'accéder à une boîte - Éviter de fermer les boîtes qui pourraient être utilisées plus tard - Préférer `box.clear()` à `box.close()` pour vider les données sans fermer la boîte @@ -217,11 +288,13 @@ await _clearAndRecreateBoxes(); ### Initialisation à la demande 1. **Initialiser les boîtes uniquement lorsqu'elles sont nécessaires** : + - N'ouvrir que les boîtes `users` et `settings` au démarrage - Initialiser les autres boîtes après connexion réussie - Utiliser `_ensureBoxIsOpen()` avant chaque accès à une boîte 2. **Centraliser la gestion des boîtes** : + - Créer un service dédié à la gestion des boîtes Hive - Utiliser des méthodes comme `openRequiredBoxes()` et `clearAllBoxes()` @@ -232,10 +305,12 @@ await _clearAndRecreateBoxes(); ### Éviter l'erreur "Box has already been closed" 1. **Ne jamais fermer une boîte qui pourrait être utilisée plus tard** : + - Utiliser `_ensureBoxIsOpen()` au lieu de fermer et rouvrir les boîtes - Vider les boîtes avec `box.clear()` au lieu de les fermer 2. **Vérifier qu'une boîte est ouverte avant de l'utiliser** : + ```dart if (!Hive.isBoxOpen(boxName)) { await Hive.openBox(boxName); @@ -246,7 +321,7 @@ await _clearAndRecreateBoxes(); - Toujours entourer les opérations Hive de blocs try/catch - Prévoir des mécanismes de récupération en cas d'erreur -### Méthode utilitaire _ensureBoxIsOpen +### Méthode utilitaire \_ensureBoxIsOpen Cette méthode est cruciale pour garantir qu'une boîte est ouverte avant de l'utiliser : diff --git a/docs/nginx-dva-geo.conf b/docs/nginx-dva-geo.conf new file mode 100644 index 00000000..449fc650 --- /dev/null +++ b/docs/nginx-dva-geo.conf @@ -0,0 +1,115 @@ +server { + listen 80; + server_name dev.geosector.fr; + + root /var/www/geosector/web; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + # Configuration pour les assets statiques (optionnel) + location /assets/ { + expires 1y; + add_header Cache-Control "public"; + } +} + +server { + listen 80; + server_name dapp.geosector.fr; + + # Logs globales + access_log /var/log/nginx/geosector-app_access.log; + error_log /var/log/nginx/geosector-app_error.log; + + set $current_host $host; + + # Application Flutter (contenu statique) + location / { + root /var/www/geosector/app; + index index.html; + try_files $uri $uri/ /index.html; + + # Configuration pour les assets Flutter + location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ { + expires off; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" always; + add_header Pragma "no-cache" always; + } + } + + # API PHP + location /api/ { + # alias /var/www/geosector/api/public/; + + add_header X-Debug-Host $current_host; + + # Gestion CORS pour les requêtes OPTIONS + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' $http_origin always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain; charset=utf-8'; + add_header 'Content-Length' 0; + return 204; + } + + # En-têtes CORS pour les requêtes normales + add_header 'Access-Control-Allow-Origin' $http_origin always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + + # Désactiver le cache pour le développement + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" always; + add_header Pragma "no-cache" always; + add_header Expires "0" always; + + # Configuration conditionnelle pour l'origine + if ($http_origin ~ '^http://(dapp\.geosector\.fr)$') { + set $cors "true"; + } + + if ($cors = "false") { + return 403; + } + + # Traitement normal pour les autres méthodes + try_files $uri $uri/ /api/index.php$is_args$args; + + # Gestion PHP pour l'API (ajusté pour fonctionner avec alias) + location ~ ^/api/(.+\.php)$ { + # alias /var/www/geosector/api/public/$1; + fastcgi_pass unix:/run/php-fpm83/php-fpm.sock; + fastcgi_index index.php; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $request_filename; + + # Variable d'environnement + fastcgi_param APP_ENV "dev"; # À ajuster selon l'environnement + + # Transmission des headers personnalisés à PHP + fastcgi_param HTTP_X_APP_IDENTIFIER "dapp.geosector.fr"; + fastcgi_param HTTP_X_REAL_IP $remote_addr; + + # Augmenter les timeouts pour les opérations de synchronisation + fastcgi_read_timeout 300; + fastcgi_send_timeout 300; + + # En-têtes CORS pour les réponses PHP + add_header 'Access-Control-Allow-Origin' $http_origin always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + } + } + + # Protection des fichiers système + location ~ /\.(?!well-known) { + deny all; + } +} \ No newline at end of file diff --git a/docs/planning-geo-gitlab.csv b/docs/planning-geo-gitlab.csv new file mode 100644 index 00000000..8c77c176 --- /dev/null +++ b/docs/planning-geo-gitlab.csv @@ -0,0 +1,32 @@ +title,description,labels,due_date +"GEO - Développement de la page Communication pour les utilisateurs","Implémentation du widget de chat pour la communication d'équipe. Création de l'interface pour répondre aux mails clients. Intégration avec les repositories pour la gestion des données.","agenda,planning",01/05/2025 +"GEO - Finalisation Widget Carte MapBox (partie utilisateur)","Développement des fonctionnalités de visualisation des secteurs d'activité, visualisation des passages, sélection de passages près de la position et création de passage sur clic. Implémentation de la détection de la position utilisateur.","agenda,planning",03/05/2025 +"GEO - Formulaire de passage avec intégration Stripe (partie 1)","Conception du widget Formulaire de passage. Implémentation des champs et validations. Mise en place de la logique de collecte des données. Préparation pour l'intégration de Stripe.","agenda,planning",05/05/2025 +"GEO - Formulaire de passage avec intégration Stripe (partie 2)","Intégration de Stripe pour le paiement en ligne. Configuration du client Stripe dans Flutter. Gestion des paiements et des erreurs. Tests des transactions.","agenda,planning",07/05/2025 +"GEO - Développement du système d'envoi de reçus PDF par email et SMS","Mise en place de la génération de reçu au format PDF. Intégration avec le service d'emails. Configuration de l'envoi de SMS via l'API OVH. Tests des envois.","agenda,planning",09/05/2025 +"GEO - Implémentation des widgets statistiques communs","Développement des widgets de graphiques (passages, règlements). Implémentation du filtrage par période (jour/semaine/mois). Configuration de l'affichage responsive.","agenda,planning",11/05/2025 +"GEO - Développement de la page Admin - Principale","Création de la page principale d'administration avec synthèse des passages par secteur et utilisateur. Implémentation du graphique des passages des deux dernières semaines.","agenda,planning",13/05/2025 +"GEO - Développement de la page Admin - Amicale","Implémentation du formulaire d'informations de l'amicale. Upload de logo. Gestion des abonnements avec l'intégration STRIPE. Configuration des packs SMS.","agenda,planning",15/05/2025 +"GEO - Développement de la page Admin - Membres","Création de l'interface de gestion des membres. Upload des badges. Fonctionnalités d'import/export de listes. Gestion de réinitialisation des mots de passe.","agenda,planning",17/05/2025 +"GEO - Développement de la page Admin - Communication","Implémentation du système de chat pour l'équipe. Intégration avec l'API pour la communication avec Geosector. Tests de communication.","agenda,planning",19/05/2025 +"GEO - Développement de la page Admin - Connexions","Création de l'interface de consultation des connexions. Implémentation du graphique sur 5 mois. Filtrage par membre. Optimisation des requêtes API.","agenda,planning",21/05/2025 +"GEO - Développement de la page Admin - Opérations (partie 1)","Conception de l'interface de gestion des opérations. Implémentation de la sélection de l'opération active. Gestion des secteurs (couleurs, titres, membres).","agenda,planning",23/05/2025 +"GEO - Développement de la page Admin - Opérations (partie 2)","Développement de l'interface pour tracer les secteurs sur la carte. Affichage des passages selon l'historique. Intégration avec les modules existants.","agenda,planning",25/05/2025 +"GEO - Développement de la page Admin - Opérations (partie 3)","Implémentation de l'affichage des membres actifs avec leurs statistiques. Exportation des données au format Excel. Tests d'intégration complets.","agenda,planning",27/05/2025 +"GEO - Développement de la page Admin - Statistiques","Création des graphiques d'activité par secteur, par membre et par période. Implémentation des filtres dynamiques. Tests et optimisation des performances.","agenda,planning",29/05/2025 +"GEO - Développement de la page Admin - Clients (Super Admin)","Implémentation de la liste des amicales avec recherche. Gestion des amicales en démo. Interface de création/suppression d'amicales. Consultation des statistiques.","agenda,planning",31/05/2025 +"GEO - Développement des scripts d'importation de données Open Data (partie 1)","Conception et développement des scripts PHP pour l'importation hebdomadaire de la base ADRESSES et SIRENE. Configuration des tâches planifiées. Tests d'importation.","agenda,planning",02/06/2025 +"GEO - Développement des scripts d'importation de données Open Data (partie 2)","Conception et développement des scripts PHP pour l'importation hebdomadaire de la base BATIMENTS et OpenStreetMap. Optimisation des requêtes SQL. Tests d'intégration.","agenda,planning",04/06/2025 +"GEO - Implémentation du système de chiffrement des données sensibles","Configuration du chiffrement des mots de passe avec Argon2. Mise en place du chiffrement AES-256 pour les données sensibles. Tests de sécurité.","agenda,planning",06/06/2025 +"GEO - Développement du système de gestion des emails et file d'attente","Implémentation de la queue des emails en base de données. Création du script de traitement de la queue. Développement du système de gestion des retours d'emails.","agenda,planning",08/06/2025 +"GEO - Développement du système de gestion des SMS via OVH","Configuration de l'intégration avec l'API SMS OVH. Implémentation de la gestion des packs SMS. Tests d'envoi et de réception de SMS.","agenda,planning",10/06/2025 +"GEO - Création du site vitrine Svelte (partie 1)","Configuration initiale du projet Svelte. Développement de la page d'accueil avec présentation de la solution. Conception des composants de base.","agenda,planning",12/06/2025 +"GEO - Création du site vitrine Svelte (partie 2)","Développement des sections de présentation des fonctionnalités. Implémentation du lien vers l'App Store et Play Store. Intégration des captures d'écran.","agenda,planning",14/06/2025 +"GEO - Création du site vitrine Svelte (partie 3)","Implémentation du formulaire de contact. Développement des pages légales (mentions légales, conditions d'utilisation). Mise en place de la gestion des cookies.","agenda,planning",16/06/2025 +"GEO - Intégration de l'authentification et des liens entre site vitrine et application","Développement des liens entre le site vitrine et l'application Flutter. Configuration du formulaire de connexion et d'inscription. Tests d'intégration.","agenda,planning",18/06/2025 +"GEO - Tests utilisateurs et débogage global (partie 1)","Procédure de tests utilisateur sur les fonctionnalités principales. Identification et correction des bugs. Améliorations de l'interface utilisateur.","agenda,planning",20/06/2025 +"GEO - Tests utilisateurs et débogage global (partie 2)","Tests des parcours utilisateurs complexes. Vérification de la synchronisation des données. Optimisation des performances. Corrections de bugs.","agenda,planning",22/06/2025 +"GEO - Optimisation des performances et de la taille de l'application","Analyse des performances. Optimisation du code Flutter. Réduction de la taille de l'application. Amélioration des temps de chargement.","agenda,planning",24/06/2025 +"GEO - Documentation technique et guide utilisateur","Rédaction de la documentation technique détaillée. Création du guide utilisateur avec captures d'écran. Préparation des ressources pour la formation.","agenda,planning",26/06/2025 +"GEO - Préparation du déploiement et configuration des environnements","Configuration des environnements de production. Préparation du déploiement sur les stores (AppStore, PlayStore). Configuration du serveur web et de la base de données.","agenda,planning",28/06/2025 +"GEO - Finalisation et préparation au lancement","Derniers tests d'intégration. Vérification des configurations de sécurité. Préparation des scripts de déploiement. Planification du lancement.","agenda,planning",30/06/2025 \ No newline at end of file diff --git a/flutt/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/flutt/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b..00000000 Binary files a/flutt/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/flutt/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/flutt/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b79..00000000 Binary files a/flutt/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/flutt/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/flutt/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 09d43914..00000000 Binary files a/flutt/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/flutt/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/flutt/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d3..00000000 Binary files a/flutt/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/flutt/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/flutt/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372ee..00000000 Binary files a/flutt/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/flutt/assets/images/app-screenshot2.svg b/flutt/assets/images/app-screenshot2.svg deleted file mode 100644 index 5f52f70e..00000000 --- a/flutt/assets/images/app-screenshot2.svg +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/flutt/assets/images/app-store-badge.svg b/flutt/assets/images/app-store-badge.svg deleted file mode 100644 index dd0021f6..00000000 --- a/flutt/assets/images/app-store-badge.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - Download on the - App Store - diff --git a/flutt/assets/images/city-map-bg-fixed.svg b/flutt/assets/images/city-map-bg-fixed.svg deleted file mode 100644 index b29d45ea..00000000 --- a/flutt/assets/images/city-map-bg-fixed.svg +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/flutt/assets/images/city-map-bg.svg b/flutt/assets/images/city-map-bg.svg deleted file mode 100644 index abb8649c..00000000 --- a/flutt/assets/images/city-map-bg.svg +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/flutt/assets/images/geosector-logo-200.png b/flutt/assets/images/geosector-logo-200.png deleted file mode 100644 index b1b98abd..00000000 Binary files a/flutt/assets/images/geosector-logo-200.png and /dev/null differ diff --git a/flutt/assets/images/geosector-logo-200.png~ b/flutt/assets/images/geosector-logo-200.png~ deleted file mode 100644 index ecbeffdb..00000000 Binary files a/flutt/assets/images/geosector-logo-200.png~ and /dev/null differ diff --git a/flutt/assets/images/geosector-logo-80.png b/flutt/assets/images/geosector-logo-80.png deleted file mode 100644 index 492e6349..00000000 Binary files a/flutt/assets/images/geosector-logo-80.png and /dev/null differ diff --git a/flutt/assets/images/geosector-logo-80.png~ b/flutt/assets/images/geosector-logo-80.png~ deleted file mode 100644 index 2fc853c0..00000000 Binary files a/flutt/assets/images/geosector-logo-80.png~ and /dev/null differ diff --git a/flutt/assets/images/play-store-badge.svg b/flutt/assets/images/play-store-badge.svg deleted file mode 100644 index f3a2af2f..00000000 --- a/flutt/assets/images/play-store-badge.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - GET IT ON - Google Play - diff --git a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada47..00000000 Binary files a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ diff --git a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 7353c41e..00000000 Binary files a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 797d452e..00000000 Binary files a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index 6ed2d933..00000000 Binary files a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cd7b009..00000000 Binary files a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index fe730945..00000000 Binary files a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index 321773cd..00000000 Binary files a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 797d452e..00000000 Binary files a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index 502f463a..00000000 Binary files a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index 0ec30343..00000000 Binary files a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index 0ec30343..00000000 Binary files a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index e9f5fea2..00000000 Binary files a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index 84ac32ae..00000000 Binary files a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 8953cba0..00000000 Binary files a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index 0467bf12..00000000 Binary files a/flutt/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/flutt/lib/app.dart b/flutt/lib/app.dart deleted file mode 100644 index 93bbf293..00000000 --- a/flutt/lib/app.dart +++ /dev/null @@ -1,214 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:geosector_app/core/theme/app_theme.dart'; -import 'package:go_router/go_router.dart'; -import 'package:geosector_app/core/services/api_service.dart'; -import 'package:geosector_app/core/repositories/user_repository.dart'; -import 'package:geosector_app/core/repositories/operation_repository.dart'; -import 'package:geosector_app/core/repositories/passage_repository.dart'; -import 'package:geosector_app/core/repositories/sector_repository.dart'; -import 'package:geosector_app/core/repositories/membre_repository.dart'; -import 'package:geosector_app/core/services/sync_service.dart'; -import 'package:geosector_app/core/services/connectivity_service.dart'; -import 'package:geosector_app/presentation/auth/splash_page.dart'; -import 'package:geosector_app/presentation/public/landing_page.dart'; -import 'package:geosector_app/presentation/auth/login_page.dart'; -import 'package:geosector_app/presentation/auth/register_page.dart'; -import 'package:geosector_app/presentation/admin/admin_dashboard_page.dart'; -import 'package:geosector_app/presentation/user/user_dashboard_page.dart'; - -// Instances globales des services et repositories -final apiService = ApiService(); -final operationRepository = OperationRepository(apiService); -final passageRepository = PassageRepository(apiService); -final userRepository = UserRepository(apiService); -final sectorRepository = SectorRepository(apiService); -final membreRepository = MembreRepository(apiService); -final syncService = SyncService(userRepository: userRepository); -final connectivityService = ConnectivityService(); - -class GeoSectorApp extends StatelessWidget { - const GeoSectorApp({super.key}); - - @override - Widget build(BuildContext context) { - // Utiliser directement le router sans provider - final router = GoRouter( - initialLocation: '/', - debugLogDiagnostics: true, - refreshListenable: - userRepository, // Écouter les changements d'état d'authentification - redirect: (context, state) { - // Sauvegarder le chemin actuel pour l'utilisateur connecté, sauf pour la page de splash - if (state.uri.toString() != '/' && userRepository.isLoggedIn) { - // Ne pas sauvegarder les chemins de login/register - if (!state.uri.toString().startsWith('/login') && - !state.uri.toString().startsWith('/register') && - !state.uri.toString().startsWith('/public')) { - userRepository.updateLastPath(state.uri.toString()); - } - } - - // Vérifier si l'utilisateur est sur la page de splash - if (state.uri.toString() == '/') { - // Vérifier si l'utilisateur a une session valide - final currentUser = userRepository.getCurrentUser(); - if (currentUser == null || currentUser.sessionId == null) { - // Si pas de session valide, rediriger vers la landing page - return '/public'; - } - - // Si l'utilisateur a une session valide et un chemin précédent, y retourner - final lastPath = userRepository.getLastPath(); - if (lastPath != null && lastPath.isNotEmpty) { - return lastPath; - } - - // Sinon, rediriger vers le tableau de bord approprié - if (userRepository.isAdmin()) { - return '/admin'; - } else { - return '/user'; - } - } - - // Vérifier si l'utilisateur est sur une page d'authentification - final isLoggedIn = userRepository.isLoggedIn; - final isOnLoginPage = state.uri.toString() == '/login'; - final isOnRegisterPage = state.uri.toString() == '/register'; - final isOnAdminRegisterPage = state.uri.toString() == '/admin-register'; - final isOnPublicPage = state.uri.toString() == '/public'; - - // Vérifier si l'utilisateur vient de la landing page et va vers la page de connexion - // Cette information est stockée dans les paramètres de la route - final isFromLandingPage = - state.uri.queryParameters['from'] == 'landing'; - - // Permettre l'accès aux pages publiques sans authentification - if (isOnPublicPage) { - return null; - } - - // Si l'utilisateur vient de la landing page et va vers la page de connexion ou d'inscription, - // ne pas rediriger, même s'il est déjà connecté - if ((isOnLoginPage || isOnRegisterPage) && isFromLandingPage) { - return null; - } - - // Si l'utilisateur n'est pas connecté et n'est pas sur une page d'authentification, rediriger vers la page de connexion - if (!isLoggedIn && - !isOnLoginPage && - !isOnRegisterPage && - !isOnAdminRegisterPage) { - return '/login'; - } - - // Si l'utilisateur est connecté et se trouve sur une page d'authentification, rediriger vers le tableau de bord approprié - if (isLoggedIn && - (isOnLoginPage || isOnRegisterPage || isOnAdminRegisterPage)) { - if (userRepository.isAdmin()) { - return '/admin'; - } else { - return '/user'; - } - } - - // Si l'utilisateur est connecté en tant qu'administrateur mais essaie d'accéder à une page utilisateur, rediriger vers le tableau de bord admin - if (isLoggedIn && - userRepository.isAdmin() && - state.uri.toString().startsWith('/user')) { - return '/admin'; - } - - // Si l'utilisateur est connecté en tant qu'utilisateur mais essaie d'accéder à une page admin, rediriger vers le tableau de bord utilisateur - if (isLoggedIn && - !userRepository.isAdmin() && - state.uri.toString().startsWith('/admin')) { - return '/user'; - } - - return null; - }, - routes: [ - // Splash screen et page de démarrage - GoRoute( - path: '/', - builder: (context, state) => const SplashPage(), - ), - - // Pages publiques - GoRoute( - path: '/public', - builder: (context, state) => const LandingPage(), - ), - - // Pages d'authentification - GoRoute( - path: '/login', - builder: (context, state) { - // Extraire le type de connexion depuis les extras - Map? extras; - if (state.extra != null && state.extra is Map) { - extras = state.extra as Map; - } - - String? loginType = extras?['type']; - print('DEBUG ROUTER: Type dans les extras: $loginType'); - - // Nettoyer le paramètre type si présent - if (loginType != null) { - loginType = loginType.trim().toLowerCase(); - print('DEBUG ROUTER: Type nettoyé: $loginType'); - } else { - // Fallback: essayer de récupérer depuis les paramètres d'URL - final queryParams = state.uri.queryParameters; - loginType = queryParams['type']; - if (loginType != null) { - loginType = loginType.trim().toLowerCase(); - print('DEBUG ROUTER: Type récupéré des params URL: $loginType'); - } else { - loginType = 'admin'; // Valeur par défaut - print('DEBUG ROUTER: Type par défaut: admin'); - } - } - - return LoginPage( - key: Key('login_page_${loginType}'), - loginType: loginType, - ); - }, - ), - GoRoute( - path: '/register', - builder: (context, state) => const RegisterPage(), - ), - - // Pages administrateur - GoRoute( - path: '/admin', - builder: (context, state) => const AdminDashboardPage(), - routes: [ - // Ajouter d'autres routes admin ici - ], - ), - - // Pages utilisateur - GoRoute( - path: '/user', - builder: (context, state) => const UserDashboardPage(), - routes: [ - // Ajouter d'autres routes utilisateur ici - ], - ), - ], - ); - - return MaterialApp.router( - debugShowCheckedModeBanner: false, - title: 'GEOSECTOR', - theme: AppTheme.lightTheme, - darkTheme: AppTheme.darkTheme, - themeMode: ThemeMode.system, - routerConfig: router, - ); - } -} diff --git a/flutt/lib/core/constants/app_keys.dart b/flutt/lib/core/constants/app_keys.dart deleted file mode 100644 index 2d28073c..00000000 --- a/flutt/lib/core/constants/app_keys.dart +++ /dev/null @@ -1,136 +0,0 @@ -/// Fichier contenant toutes les constantes utilisées dans l'application -/// Centralise les clés, noms de boîtes Hive, et autres constantes -/// pour faciliter la maintenance et éviter les erreurs de frappe - -import 'package:flutter/foundation.dart' show kIsWeb; -import 'package:flutter/material.dart'; - -class AppKeys { - // Noms des boîtes Hive - static const String usersBoxName = 'users'; - static const String operationsBoxName = 'operations'; - static const String sectorsBoxName = 'sectors'; - static const String passagesBoxName = 'passages'; - static const String settingsBoxName = 'settings'; - static const String membresBoxName = 'membres'; - static const String chatConversationsBoxName = 'chat_conversations'; - static const String chatMessagesBoxName = 'chat_messages'; - - // Rôles utilisateurs - static const int roleUser = 1; - static const int roleAdmin1 = 2; - static const int roleAdmin2 = 4; - static const int roleAdmin3 = 9; - - // URLs API - static const String baseApiUrl = 'https://app.geosector.fr/api/geo'; - - // Endpoints API - static const String loginEndpoint = '/login'; - static const String logoutEndpoint = '/logout'; - static const String registerEndpoint = '/register'; - static const String syncDataEndpoint = '/data/sync'; - static const String sectorsEndpoint = '/sectors'; - - // Durées - static const Duration connectionTimeout = Duration(seconds: 5); - static const Duration receiveTimeout = Duration(seconds: 30); - static const Duration sessionDefaultExpiry = Duration(days: 7); - - // Clés API externes - static const String mapboxApiKey = - 'pk.eyJ1IjoicHZkNnNvZnQiLCJhIjoiY204dTNhNmd0MGV1ZzJqc2pnNnB0NjYxdSJ9.TA5Mvliyn91Oi01F_2Yuxw'; // À remplacer par votre clé API Mapbox - - // Headers - static const String sessionHeader = 'Authorization'; - - // En-têtes par défaut pour les requêtes API - static const Map defaultHeaders = { - 'Content-Type': 'application/json', - 'X-App-Identifier': 'app.geosector.fr', - 'X-Client-Type': kIsWeb ? 'web' : 'mobile', - 'Accept': 'application/json', - }; - - // Civilités - static const Map civilites = { - 1: 'M.', - 2: 'Mme', - }; - - // Types de règlements - static const Map> typesReglements = { - 0: { - 'titre': 'Pas de règlement', - 'couleur': 0xFF757575, // Gris foncé - 'icon_data': Icons.money_off, - }, - 1: { - 'titre': 'Espèce', - 'couleur': 0xFFFFC107, // Jaune foncé (ambre) - 'icon_data': Icons.toll, - }, - 2: { - 'titre': 'Chèque', - 'couleur': 0xFF8BC34A, // Vert citron - 'icon_data': Icons.wallet, - }, - 3: { - 'titre': 'CB', - 'couleur': 0xFF00B0FF, // Bleu flashy (bleu clair accent), - 'icon_data': Icons.credit_card, - }, - }; - - // Types de passages - static const Map> typesPassages = { - 1: { - 'titres': 'Effectués', - 'titre': 'Effectué', - 'couleur1': 0xFF4CAF50, // Vert success - 'couleur2': 0xFF4CAF50, // Vert success - 'couleur3': 0xFF4CAF50, // Vert success - 'icon_data': Icons.task_alt, - }, - 2: { - 'titres': 'À finaliser', - 'titre': 'À finaliser', - 'couleur1': 0xFFFFFFFF, // Blanc - 'couleur2': 0xFFFF9800, // Orange - 'couleur3': 0xFFE65100, // Orange foncé - 'icon_data': Icons.refresh, - }, - 3: { - 'titres': 'Refusés', - 'titre': 'Refusé', - 'couleur1': 0xFFF44336, // Rouge - 'couleur2': 0xFFF44336, // Rouge - 'couleur3': 0xFFF44336, // Rouge - 'icon_data': Icons.block, - }, - 4: { - 'titres': 'Dons', - 'titre': 'Don', - 'couleur1': 0xFF03A9F4, // Bleu ciel - 'couleur2': 0xFF03A9F4, // Bleu ciel - 'couleur3': 0xFF03A9F4, // Bleu ciel - 'icon_data': Icons.volunteer_activism, - }, - 5: { - 'titres': 'Lots', - 'titre': 'Lot', - 'couleur1': 0xFF0D47A1, // Bleu foncé - 'couleur2': 0xFF0D47A1, // Bleu foncé - 'couleur3': 0xFF0D47A1, // Bleu foncé - 'icon_data': Icons.layers, - }, - 6: { - 'titres': 'Maisons vides', - 'titre': 'Maison vide', - 'couleur1': 0xFF9E9E9E, // Gris - 'couleur2': 0xFF9E9E9E, // Gris - 'couleur3': 0xFF9E9E9E, // Gris - 'icon_data': Icons.home_outlined, - }, - }; -} diff --git a/flutt/lib/core/repositories/user_repository.dart b/flutt/lib/core/repositories/user_repository.dart deleted file mode 100644 index b6fac2ab..00000000 --- a/flutt/lib/core/repositories/user_repository.dart +++ /dev/null @@ -1,958 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:js' as js; -import 'package:geosector_app/core/services/hive_web_fix.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:geosector_app/core/constants/app_keys.dart'; -import 'package:geosector_app/core/services/api_service.dart'; -import 'package:geosector_app/core/services/sync_service.dart'; -import 'package:geosector_app/core/data/models/user_model.dart'; -import 'package:geosector_app/core/data/models/operation_model.dart'; -import 'package:geosector_app/core/data/models/sector_model.dart'; -import 'package:geosector_app/core/data/models/passage_model.dart'; -import 'package:geosector_app/core/data/models/membre_model.dart'; -import 'package:geosector_app/core/repositories/operation_repository.dart'; -import 'package:geosector_app/core/repositories/sector_repository.dart'; -import 'package:geosector_app/core/repositories/passage_repository.dart'; -import 'package:geosector_app/chat/models/conversation_model.dart'; -import 'package:geosector_app/chat/models/message_model.dart'; - -class UserRepository extends ChangeNotifier { - // Utilisation de getters lazy pour n'accéder aux boîtes que lorsque nécessaire - Box get _userBox => Hive.box(AppKeys.usersBoxName); - - // Getters pour les autres boîtes qui vérifient si elles sont ouvertes avant accès - Box get _operationBox { - _ensureBoxIsOpen(AppKeys.operationsBoxName); - return Hive.box(AppKeys.operationsBoxName); - } - - Box get _sectorBox { - _ensureBoxIsOpen(AppKeys.sectorsBoxName); - return Hive.box(AppKeys.sectorsBoxName); - } - - // Méthode pour initialiser les boîtes après connexion - Future _initializeBoxes() async { - debugPrint('Initialisation des boîtes Hive nécessaires...'); - await _ensureBoxIsOpen(AppKeys.operationsBoxName); - await _ensureBoxIsOpen(AppKeys.sectorsBoxName); - await _ensureBoxIsOpen(AppKeys.passagesBoxName); - await _ensureBoxIsOpen(AppKeys.membresBoxName); - // Les boîtes de chat sont déjà initialisées au démarrage - await _ensureBoxIsOpen(AppKeys.chatConversationsBoxName); - await _ensureBoxIsOpen(AppKeys.chatMessagesBoxName); - debugPrint('Toutes les boîtes Hive sont maintenant ouvertes'); - } - - final ApiService _apiService; - final SyncService? _syncService; - final OperationRepository? _operationRepository; - final SectorRepository? _sectorRepository; - final PassageRepository? _passageRepository; - - bool _isLoading = false; - - UserRepository(this._apiService, - {SyncService? syncService, - OperationRepository? operationRepository, - SectorRepository? sectorRepository, - PassageRepository? passageRepository}) - : _syncService = syncService, - _operationRepository = operationRepository, - _sectorRepository = sectorRepository, - _passageRepository = passageRepository { - // Initialiser la session si un utilisateur est déjà connecté - final currentUser = getCurrentUser(); - if (currentUser != null && currentUser.sessionId != null) { - setSessionId(currentUser.sessionId); - } - } - - // Getters - bool get isLoading => _isLoading; - bool get isLoggedIn => getCurrentUser() != null; - // Vérifie si l'utilisateur a un rôle administrateur (2, 4 ou 9) - bool isAdmin() { - final user = getCurrentUser(); - if (user == null) return false; - - final String interface = user.interface ?? 'user'; - return interface == 'admin'; - } - - int? get userId => getCurrentUser()?.id; - UserModel? get currentUser => getCurrentUser(); - - // Récupérer l'utilisateur actuellement connecté - UserModel? getCurrentUser() { - try { - // Chercher un utilisateur avec une session active - final activeUsers = _userBox.values - .where((user) => - user.sessionId != null && // Vérifier que sessionId n'est pas null - user.sessionId! - .isNotEmpty && // Vérifier que sessionId n'est pas vide - user.sessionExpiry != null && - user.sessionExpiry!.isAfter(DateTime.now())) - .toList(); - - return activeUsers.isNotEmpty ? activeUsers.first : null; - } catch (e) { - debugPrint('Erreur lors de la récupération de l\'utilisateur actuel: $e'); - return null; - } - } - - // Mettre à jour le chemin de la page actuelle pour l'utilisateur connecté - Future updateLastPath(String path) async { - final currentUser = getCurrentUser(); - if (currentUser != null) { - final updatedUser = currentUser.copyWith(lastPath: path); - await saveUser(updatedUser); - } - } - - // Récupérer le dernier chemin visité par l'utilisateur - String? getLastPath() { - final currentUser = getCurrentUser(); - return currentUser?.lastPath; - } - - // Configurer la session dans l'API - void setSessionId(String? sessionId) { - _apiService.setSessionId(sessionId); - } - - // Login API PHP - Future> loginAPI(String username, String password, - {String type = 'admin'}) async { - try { - return await _apiService.login(username, password, type: type); - } catch (e) { - debugPrint('Erreur login API: $e'); - rethrow; - } - } - - // Register API PHP - Uniquement pour les administrateurs - Future> registerAPI(String email, String name, - String amicaleName, String postalCode, String cityName) async { - try { - final Map data = { - 'email': email, - 'name': name, - 'amicale_name': amicaleName, - 'postal_code': postalCode, - 'city_name': cityName - }; - - final response = - await _apiService.post(AppKeys.registerEndpoint, data: data); - return response.data; - } catch (e) { - debugPrint('Erreur register API: $e'); - rethrow; - } - } - - // Logout API PHP - Future logoutAPI() async { - try { - await _apiService.logout(); - } catch (e) { - debugPrint('Erreur logout API: $e'); - rethrow; - } - } - - // Méthode d'inscription (uniquement pour les administrateurs) - Future register(String email, String password, String name, - String amicaleName, String postalCode, String cityName) async { - _isLoading = true; - notifyListeners(); - - try { - // Enregistrer l'administrateur via l'API - final apiResult = - await registerAPI(email, name, amicaleName, postalCode, cityName); - - // Créer l'administrateur local - final int userId = apiResult['user_id'] is String - ? int.parse(apiResult['user_id']) - : apiResult['user_id']; - final now = DateTime.now(); - final newAdmin = UserModel( - id: userId, - email: email, - name: name, - role: AppKeys.roleAdmin2, - createdAt: now, - lastSyncedAt: now, - isActive: true, - isSynced: true, - sessionId: apiResult['session_id'], - sessionExpiry: DateTime.parse(apiResult['session_expiry']), - ); - - // Sauvegarder dans le repository local - await saveUser(newAdmin); - - // Configurer la session dans l'API - setSessionId(newAdmin.sessionId); - - notifyListeners(); - return true; - } catch (e) { - debugPrint('Erreur d\'inscription: $e'); - return false; - } finally { - _isLoading = false; - notifyListeners(); - } - } - - // Login complet - Future login(String username, String password, - {String type = 'admin'}) async { - _isLoading = true; - notifyListeners(); - - try { - debugPrint('Début du processus de connexion pour: $username'); - - // Supprimer les références aux boîtes non définies dans AppKeys - // pour éviter les erreurs de suppression de boîtes non référencées - final nonDefinedBoxes = ['auth', 'locations', 'messages']; - for (final boxName in nonDefinedBoxes) { - try { - if (Hive.isBoxOpen(boxName)) { - debugPrint('Fermeture de la boîte non référencée: $boxName'); - await Hive.box(boxName).close(); - } - - // Supprimer la boîte du disque - await Hive.deleteBoxFromDisk(boxName); - debugPrint('Nettoyage: Box $boxName supprimée'); - } catch (e) { - debugPrint( - 'Erreur lors de la suppression de la boîte non référencée $boxName: $e'); - } - } - - // S'assurer que toutes les Hive boxes sont vides avant de se connecter - // Vider toutes les boîtes Hive SAUF la boîte des utilisateurs - debugPrint('Nettoyage des données existantes avant connexion...'); - - // Sur le web, utiliser notre méthode sécurisée pour nettoyer les boîtes Hive - if (kIsWeb) { - await HiveWebFix.safeCleanHiveBoxes( - excludeBoxes: [AppKeys.usersBoxName]); - } - // Sur iOS, nettoyer les fichiers Hive directement - else if (!kIsWeb && Platform.isIOS) { - await _cleanHiveFilesOnIOS(); - } - // Sur Android, nettoyer les fichiers Hive directement - else if (!kIsWeb && Platform.isAndroid) { - await _cleanHiveFilesOnAndroid(); - } - - // Nettoyer les boîtes sans les fermer - await _clearAndRecreateBoxes(); - - // Initialiser les boîtes nécessaires avant d'appeler l'API - // Cela garantit que toutes les boîtes sont ouvertes avant le traitement des données - await _initializeBoxes(); - - // Appeler l'API - debugPrint('Appel de l\'API de connexion (type: $type)...'); - final apiResult = await loginAPI(username, password, type: type); - - // Vérifier le statut de la réponse - final status = apiResult['status'] as String?; - final message = apiResult['message'] as String?; - - // Si le statut n'est pas 'success', retourner false - if (status != 'success') { - debugPrint('Échec de connexion: $message'); - return false; - } - - debugPrint('Connexion réussie, traitement des données...'); - - // [Reste de la méthode login inchangé...] - return true; - } catch (e) { - debugPrint('Erreur de connexion: $e'); - return false; - } finally { - _isLoading = false; - notifyListeners(); - } - } - - // Méthode pour vérifier si une boîte est ouverte et l'ouvrir si nécessaire - Future _ensureBoxIsOpen(String boxName) async { - try { - if (!Hive.isBoxOpen(boxName)) { - debugPrint('Ouverture de la boîte $boxName...'); - if (boxName == AppKeys.passagesBoxName) { - await Hive.openBox(boxName); - } else if (boxName == AppKeys.operationsBoxName) { - await Hive.openBox(boxName); - } else if (boxName == AppKeys.sectorsBoxName) { - await Hive.openBox(boxName); - } else if (boxName == AppKeys.usersBoxName) { - await Hive.openBox(boxName); - } else if (boxName == AppKeys.membresBoxName) { - await Hive.openBox(boxName); - } else if (boxName == AppKeys.settingsBoxName) { - await Hive.openBox(boxName); - } else if (boxName == AppKeys.chatConversationsBoxName) { - await Hive.openBox(boxName); - } else if (boxName == AppKeys.chatMessagesBoxName) { - await Hive.openBox(boxName); - } else { - await Hive.openBox(boxName); - } - // Boîte ouverte avec succès - } else { - // La boîte est déjà ouverte - } - } catch (e) { - debugPrint('Erreur lors de l\'ouverture de la boîte $boxName: $e'); - throw Exception('Impossible d\'ouvrir la boîte $boxName: $e'); - } - } - - // Méthode pour vider et recréer toutes les boîtes Hive sauf la boîte des utilisateurs - Future _clearAndRecreateBoxes() async { - try { - debugPrint('Début de la suppression complète des données Hive...'); - - // Supprimer les références aux boîtes non définies dans AppKeys - // pour éviter les erreurs de suppression de boîtes non référencées - final nonDefinedBoxes = ['auth', 'locations', 'messages']; - for (final boxName in nonDefinedBoxes) { - try { - if (Hive.isBoxOpen(boxName)) { - debugPrint('Fermeture de la boîte non référencée: $boxName'); - await Hive.box(boxName).close(); - } - - // Supprimer la boîte du disque - await Hive.deleteBoxFromDisk(boxName); - debugPrint('Nettoyage: Box $boxName supprimée'); - } catch (e) { - debugPrint( - 'Erreur lors de la suppression de la boîte non référencée $boxName: $e'); - } - } - - // Sur le web, utiliser notre méthode sécurisée pour nettoyer les boîtes Hive - if (kIsWeb) { - await HiveWebFix.safeCleanHiveBoxes( - excludeBoxes: [AppKeys.usersBoxName]); - } - // Sur iOS, nettoyer les fichiers Hive directement - else if (Platform.isIOS) { - await _cleanHiveFilesOnIOS(); - } - // Sur Android, nettoyer les fichiers Hive directement - else if (Platform.isAndroid) { - await _cleanHiveFilesOnAndroid(); - } - - // Liste des noms de boîtes à supprimer - final boxesToDelete = [ - AppKeys.passagesBoxName, - AppKeys.operationsBoxName, - AppKeys.sectorsBoxName, - AppKeys.chatConversationsBoxName, - AppKeys.chatMessagesBoxName, - ]; - - // Vider chaque boîte sans la fermer - for (final boxName in boxesToDelete) { - try { - debugPrint('Nettoyage de la boîte: $boxName'); - - // Vérifier si la boîte est déjà ouverte - if (Hive.isBoxOpen(boxName)) { - // Vider la boîte sans la fermer - debugPrint('Boîte $boxName déjà ouverte, vidage sans fermeture'); - if (boxName == AppKeys.passagesBoxName) { - await Hive.box(boxName).clear(); - } else if (boxName == AppKeys.operationsBoxName) { - await Hive.box(boxName).clear(); - } else if (boxName == AppKeys.sectorsBoxName) { - await Hive.box(boxName).clear(); - } else if (boxName == AppKeys.chatConversationsBoxName) { - await Hive.box(boxName).clear(); - } else if (boxName == AppKeys.chatMessagesBoxName) { - await Hive.box(boxName).clear(); - } - } else { - // Supprimer la boîte du disque si elle n'est pas ouverte - debugPrint('Boîte $boxName non ouverte, suppression du disque'); - await Hive.deleteBoxFromDisk(boxName); - } - } catch (e) { - debugPrint('Erreur lors du nettoyage de la boîte $boxName: $e'); - // Tenter de supprimer la boîte du disque en cas d'erreur - try { - await Hive.deleteBoxFromDisk(boxName); - } catch (deleteError) { - debugPrint( - 'Impossible de supprimer la boîte $boxName: $deleteError'); - } - } - } - - // Attendre un court instant pour s'assurer que les opérations de suppression sont terminées - await Future.delayed(const Duration(milliseconds: 500)); - - // Recréer les boîtes avec la méthode sécurisée - debugPrint('Recréation des boîtes Hive...'); - - // Utiliser notre méthode pour s'assurer que les boîtes sont ouvertes - try { - // Passages - await _ensureBoxIsOpen(AppKeys.passagesBoxName); - - // Opérations - await _ensureBoxIsOpen(AppKeys.operationsBoxName); - - // Secteurs - await _ensureBoxIsOpen(AppKeys.sectorsBoxName); - - // Chat - await _ensureBoxIsOpen(AppKeys.chatConversationsBoxName); - await _ensureBoxIsOpen(AppKeys.chatMessagesBoxName); - - // Vérifier l'intégrité des boîtes après recréation - await _verifyHiveBoxesIntegrity(); - } catch (e) { - debugPrint('Erreur lors de la recréation des boîtes Hive: $e'); - // Tentative de récupération sur erreur - if (kIsWeb) { - debugPrint('Tentative de récupération sur le web...'); - await HiveWebFix.resetHiveCompletely(); - - // Réessayer d'ouvrir les boîtes - await _ensureBoxIsOpen(AppKeys.passagesBoxName); - await _ensureBoxIsOpen(AppKeys.operationsBoxName); - await _ensureBoxIsOpen(AppKeys.sectorsBoxName); - await _ensureBoxIsOpen(AppKeys.chatConversationsBoxName); - await _ensureBoxIsOpen(AppKeys.chatMessagesBoxName); - } - } - } catch (e) { - debugPrint('Erreur lors de la réinitialisation des boîtes Hive: $e'); - } - } - - // Méthode pour vérifier l'intégrité des boîtes Hive après recréation - Future _verifyHiveBoxesIntegrity() async { - try { - debugPrint('Vérification de l\'intégrité des boîtes Hive...'); - - // Liste des boîtes à vérifier avec leur type - final boxesToCheck = [ - {'name': AppKeys.passagesBoxName, 'type': 'passage'}, - {'name': AppKeys.operationsBoxName, 'type': 'operation'}, - {'name': AppKeys.sectorsBoxName, 'type': 'sector'}, - {'name': AppKeys.chatConversationsBoxName, 'type': 'conversation'}, - {'name': AppKeys.chatMessagesBoxName, 'type': 'message'}, - ]; - - // Vérifier chaque boîte - for (final boxInfo in boxesToCheck) { - final boxName = boxInfo['name'] as String; - final boxType = boxInfo['type'] as String; - - try { - if (Hive.isBoxOpen(boxName)) { - // Utiliser une approche spécifique au type pour éviter les erreurs de typage - Box box; - try { - if (boxType == 'passage') { - box = Hive.box(boxName); - } else if (boxType == 'operation') { - box = Hive.box(boxName); - } else if (boxType == 'sector') { - box = Hive.box(boxName); - } else if (boxType == 'conversation') { - box = Hive.box(boxName); - } else if (boxType == 'message') { - box = Hive.box(boxName); - } else { - box = Hive.box(boxName); - } - - final count = box.length; - debugPrint('Boîte $boxName: $count éléments'); - - // Si la boîte contient des éléments, c'est anormal après recréation - if (count > 0) { - debugPrint( - 'ATTENTION: La boîte $boxName contient encore des données après recréation'); - // Essayer de vider la boîte une dernière fois - await box.clear(); - debugPrint('Vidage forcé de la boîte $boxName effectué'); - } - } catch (typeError) { - debugPrint( - 'Erreur de typage lors de la vérification de $boxName: $typeError'); - - // Tentative alternative sans typage spécifique - try { - box = Hive.box(boxName); - final count = box.length; - debugPrint('Boîte $boxName (sans typage): $count éléments'); - - if (count > 0) { - await box.clear(); - debugPrint( - 'Vidage forcé de la boîte $boxName (sans typage) effectué'); - } - } catch (e2) { - debugPrint( - 'Impossible de vérifier la boîte $boxName même sans typage: $e2'); - } - } - } else { - debugPrint( - 'Boîte $boxName non ouverte, impossible de vérifier l\'intégrité'); - } - } catch (e) { - debugPrint('Erreur lors de la vérification de la boîte $boxName: $e'); - } - } - - debugPrint('Vérification d\'intégrité terminée'); - } catch (e) { - debugPrint( - 'Erreur lors de la vérification d\'intégrité des boîtes Hive: $e'); - } - } - - // Méthode spéciale pour nettoyer IndexedDB sur le web - Future _clearIndexedDB() async { - if (kIsWeb) { - try { - debugPrint('Nettoyage complet d\'IndexedDB sur le web...'); - // Utiliser JavaScript pour nettoyer IndexedDB - js.context.callMethod('eval', [ - ''' - var request = indexedDB.deleteDatabase("geosector_app"); - request.onsuccess = function() { console.log("IndexedDB nettoyé avec succès"); }; - request.onerror = function() { console.log("Erreur lors du nettoyage d\'IndexedDB"); }; - ''' - ]); - await Future.delayed(const Duration(milliseconds: 500)); - debugPrint('Nettoyage d\'IndexedDB terminé'); - } catch (e) { - debugPrint('Erreur lors du nettoyage d\'IndexedDB: $e'); - } - } - } - - // Méthode spéciale pour nettoyer les fichiers Hive sur iOS - Future _cleanHiveFilesOnIOS() async { - if (!kIsWeb && Platform.isIOS) { - try { - debugPrint('Nettoyage des fichiers Hive sur iOS...'); - final appDir = await getApplicationDocumentsDirectory(); - final hiveDir = Directory('${appDir.path}/hive'); - - if (await hiveDir.exists()) { - debugPrint('Suppression du répertoire Hive: ${hiveDir.path}'); - // Exclure le dossier des utilisateurs pour conserver les informations de session - final entries = await hiveDir.list().toList(); - for (var entry in entries) { - final name = entry.path.split('/').last; - // Ne pas supprimer la boîte des utilisateurs - if (!name.contains(AppKeys.usersBoxName)) { - debugPrint('Suppression de: ${entry.path}'); - if (entry is Directory) { - await entry.delete(recursive: true); - } else if (entry is File) { - await entry.delete(); - } - } - } - debugPrint('Nettoyage des fichiers Hive sur iOS terminé'); - } else { - debugPrint('Répertoire Hive non trouvé'); - } - } catch (e) { - debugPrint('Erreur lors du nettoyage des fichiers Hive sur iOS: $e'); - } - } - } - - // Méthode spéciale pour nettoyer les fichiers Hive sur Android - Future _cleanHiveFilesOnAndroid() async { - if (!kIsWeb && Platform.isAndroid) { - try { - debugPrint('Nettoyage des fichiers Hive sur Android...'); - final appDir = await getApplicationDocumentsDirectory(); - final hiveDir = Directory('${appDir.path}'); - - if (await hiveDir.exists()) { - debugPrint('Recherche des fichiers Hive dans: ${hiveDir.path}'); - // Sur Android, les fichiers Hive sont directement dans le répertoire de l'application - final entries = await hiveDir.list().toList(); - int filesDeleted = 0; - - for (var entry in entries) { - final name = entry.path.split('/').last; - // Ne supprimer que les fichiers Hive, mais pas la boîte des utilisateurs - if (name.endsWith('.hive') && - !name.contains(AppKeys.usersBoxName)) { - debugPrint('Suppression du fichier Hive: ${entry.path}'); - if (entry is File) { - await entry.delete(); - filesDeleted++; - - // Supprimer également les fichiers lock associés - final lockFile = File('${entry.path}.lock'); - if (await lockFile.exists()) { - await lockFile.delete(); - debugPrint('Suppression du fichier lock: ${lockFile.path}'); - } - } - } - } - - debugPrint( - 'Nettoyage des fichiers Hive sur Android terminé. $filesDeleted fichiers supprimés.'); - } else { - debugPrint('Répertoire d\'application non trouvé'); - } - } catch (e) { - debugPrint( - 'Erreur lors du nettoyage des fichiers Hive sur Android: $e'); - } - } - } - - // Logout complet - Future logout() async { - _isLoading = true; - notifyListeners(); - - try { - debugPrint('Début du processus de déconnexion...'); - - // S'assurer que la boîte des utilisateurs est ouverte avant tout - await _ensureBoxIsOpen(AppKeys.usersBoxName); - - // Supprimer les références aux boîtes non définies dans AppKeys - final nonDefinedBoxes = ['auth', 'locations', 'messages']; - for (final boxName in nonDefinedBoxes) { - try { - if (Hive.isBoxOpen(boxName)) { - debugPrint('Fermeture de la boîte non référencée: $boxName'); - await Hive.box(boxName).close(); - } - - // Supprimer la boîte du disque - await Hive.deleteBoxFromDisk(boxName); - debugPrint('Nettoyage: Box $boxName supprimée'); - } catch (e) { - debugPrint( - 'Erreur lors de la suppression de la boîte non référencée $boxName: $e'); - } - } - - // Récupérer l'utilisateur actuel avant de nettoyer les données - final currentUser = getCurrentUser(); - if (currentUser == null) { - debugPrint('Aucun utilisateur connecté, déconnexion terminée'); - return true; - } - - debugPrint('Déconnexion de l\'utilisateur: ${currentUser.email}'); - - // Appeler l'API pour déconnecter la session - if (currentUser.sessionId != null) { - debugPrint('Déconnexion de la session API...'); - await logoutAPI(); - } - - // Effacer la session de l'utilisateur - debugPrint('Mise à jour de l\'utilisateur pour effacer la session...'); - final updatedUser = currentUser.copyWith( - sessionId: null, - sessionExpiry: null, - lastPath: - null, // Réinitialiser le chemin pour revenir à l'écran de connexion - ); - - // Sauvegarder l'utilisateur sans session - await saveUser(updatedUser); - - // Effacer la session de l'API - setSessionId(null); - - // Maintenant, nettoyer les données - debugPrint('Nettoyage des données...'); - - // Sur le web, utiliser notre méthode sécurisée pour nettoyer les boîtes Hive - if (kIsWeb) { - await HiveWebFix.safeCleanHiveBoxes( - excludeBoxes: [AppKeys.usersBoxName]); - } - // Sur iOS, nettoyer les fichiers Hive directement - else if (!kIsWeb && Platform.isIOS) { - await _cleanHiveFilesOnIOS(); - } - // Sur Android, nettoyer les fichiers Hive directement - else if (!kIsWeb && Platform.isAndroid) { - await _cleanHiveFilesOnAndroid(); - } - - // Vider les boîtes sans les fermer, y compris les boîtes de chat - debugPrint('Suppression des données Hive...'); - await _clearAndRecreateBoxes(); - - // Vider spécifiquement les boîtes de chat si elles sont ouvertes - try { - if (Hive.isBoxOpen(AppKeys.chatConversationsBoxName)) { - await Hive.box(AppKeys.chatConversationsBoxName).clear(); - debugPrint('Boîte conversations vidée'); - } - if (Hive.isBoxOpen(AppKeys.chatMessagesBoxName)) { - await Hive.box(AppKeys.chatMessagesBoxName).clear(); - debugPrint('Boîte messages vidée'); - } - } catch (e) { - debugPrint('Erreur lors du vidage des boîtes de chat: $e'); - } - - debugPrint('Déconnexion terminée avec succès'); - - notifyListeners(); - return true; - } catch (e) { - debugPrint('Erreur de déconnexion: $e'); - return false; - } finally { - _isLoading = false; - notifyListeners(); - } - } - - // Obtenir tous les utilisateurs locaux - List getAllUsers() { - return _userBox.values.toList(); - } - - // Obtenir un utilisateur par son ID - UserModel? getUserById(int id) { - return _userBox.get(id); - } - - // Obtenir un utilisateur par son email - UserModel? getUserByEmail(String email) { - try { - return _userBox.values.firstWhere( - (user) => user.email == email, - ); - } catch (e) { - return null; // Utilisateur non trouvé - } - } - - // Créer ou mettre à jour un utilisateur localement - Future saveUser(UserModel user) async { - await _userBox.put(user.id, user); - notifyListeners(); // Notifier les changements pour mettre à jour l'UI - return user; - } - - // Supprimer un utilisateur localement - Future deleteUser(String id) async { - await _userBox.delete(id); - } - - // Créer un nouvel utilisateur localement et tenter de le synchroniser - Future createUser({ - required String email, - required String name, - required int role, - }) async { - // Générer un ID numérique temporaire (timestamp) - final int tempId = DateTime.now().millisecondsSinceEpoch; - final now = DateTime.now(); - - final user = UserModel( - id: tempId, - email: email, - name: name, - role: role, - createdAt: now, - lastSyncedAt: now, - isSynced: false, - ); - - await _userBox.put(user.id, user); - - // Tenter de synchroniser si possible - await syncUser(user); - - return user; - } - - // Synchroniser un utilisateur spécifique avec le serveur - Future syncUser(UserModel user) async { - try { - final hasConnection = await _apiService.hasInternetConnection(); - - if (!hasConnection) { - return user; - } - - UserModel syncedUser; - - if (!user.isSynced) { - // Si l'utilisateur n'est pas encore synchronisé, le créer sur le serveur - syncedUser = await _apiService.createUser(user); - } else { - // Sinon, mettre à jour les informations - syncedUser = await _apiService.updateUser(user); - } - - // Mettre à jour l'utilisateur local avec les informations du serveur - final updatedUser = syncedUser.copyWith( - isSynced: true, - lastSyncedAt: DateTime.now(), - ); - - await _userBox.put(updatedUser.id, updatedUser); - return updatedUser; - } catch (e) { - // En cas d'erreur, garder l'utilisateur local tel quel - return user; - } - } - - // Synchroniser tous les utilisateurs non synchronisés - Future syncAllUsers() async { - try { - final hasConnection = await _apiService.hasInternetConnection(); - - if (!hasConnection) { - return; - } - - final unsyncedUsers = - _userBox.values.where((user) => !user.isSynced).toList(); - - if (unsyncedUsers.isEmpty) { - return; - } - - // Synchroniser en batch - final result = await _apiService.syncData(users: unsyncedUsers); - - // Mettre à jour les utilisateurs locaux - if (result['users'] != null) { - for (final userData in result['users']) { - final syncedUser = UserModel.fromJson(userData); - await _userBox.put( - syncedUser.id, - syncedUser.copyWith( - isSynced: true, - lastSyncedAt: DateTime.now(), - ), - ); - } - } - } catch (e) { - // Gérer les erreurs de synchronisation - print('Erreur de synchronisation des utilisateurs: $e'); - } - } - - // Rafraîchir les données depuis le serveur - Future refreshFromServer() async { - try { - final hasConnection = await _apiService.hasInternetConnection(); - - if (!hasConnection) { - return; - } - - // Récupérer tous les utilisateurs du serveur - final serverUsers = await _apiService.getUsers(); - - // Mettre à jour la base locale - for (final serverUser in serverUsers) { - final updatedUser = serverUser.copyWith( - isSynced: true, - lastSyncedAt: DateTime.now(), - ); - await _userBox.put(updatedUser.id, updatedUser); - } - } catch (e) { - // Gérer les erreurs - print('Erreur lors du rafraîchissement des données: $e'); - } - } - - // Synchroniser les données utilisateur - Future syncUserData() async { - if (_syncService != null && currentUser != null) { - await _syncService!.syncUserData(currentUser!.id); - } - } - - // Récupérer la dernière opération active (avec isActive == true) - OperationModel? getCurrentOperation() { - try { - // Récupérer toutes les opérations - final operations = _operationBox.values.toList(); - - // Filtrer pour ne garder que les opérations actives - final activeOperations = operations.where((op) => op.isActive).toList(); - - // Si aucune opération active n'est trouvée, retourner null - if (activeOperations.isEmpty) { - return operations.isNotEmpty ? operations.last : null; - } - - // Retourner la dernière opération active - return activeOperations.last; - } catch (e) { - debugPrint('Erreur lors de la récupération de l\'opération actuelle: $e'); - return null; - } - } - - // Récupérer tous les secteurs de l'utilisateur - List getUserSectors() { - try { - return _sectorBox.values.toList(); - } catch (e) { - debugPrint('Erreur lors de la récupération des secteurs: $e'); - return []; - } - } - - // Récupérer un secteur par son ID - SectorModel? getSectorById(int id) { - try { - return _sectorBox.get(id); - } catch (e) { - debugPrint('Erreur lors de la récupération du secteur: $e'); - return null; - } - } -} diff --git a/flutt/lib/core/theme/app_theme.dart b/flutt/lib/core/theme/app_theme.dart deleted file mode 100644 index dc0b84d7..00000000 --- a/flutt/lib/core/theme/app_theme.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; - -class AppTheme { - // Nouvelles couleurs du thème - static const Color primaryColor = Color(0xFF2E4057); // Bleu foncé/gris - static const Color secondaryColor = Color(0xFF048BA8); // Bleu turquoise - static const Color accentColor = Color(0xFFF18F01); // Orange - static const Color backgroundLightColor = Color(0xFFF9FAFB); - static const Color backgroundDarkColor = Color(0xFF111827); - static const Color textLightColor = Color(0xFF1F2937); - static const Color textDarkColor = Color(0xFFF9FAFB); - - // Thème clair - static ThemeData get lightTheme { - return ThemeData( - useMaterial3: true, - brightness: Brightness.light, - colorScheme: ColorScheme.light( - primary: primaryColor, - secondary: secondaryColor, - tertiary: accentColor, - background: backgroundLightColor, - surface: Colors.white, - onPrimary: Colors.white, - onSecondary: Colors.white, - onBackground: textLightColor, - onSurface: textLightColor, - ), - textTheme: GoogleFonts.poppinsTextTheme(ThemeData.light().textTheme), - appBarTheme: const AppBarTheme( - backgroundColor: primaryColor, - foregroundColor: Colors.white, - elevation: 0, - ), - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - backgroundColor: primaryColor, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ), - inputDecorationTheme: InputDecorationTheme( - filled: true, - fillColor: Colors.grey[100], - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: const BorderSide(color: primaryColor, width: 2), - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), - ), - cardTheme: CardTheme( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - ), - ); - } - - // Thème sombre - static ThemeData get darkTheme { - return ThemeData( - useMaterial3: true, - brightness: Brightness.dark, - colorScheme: ColorScheme.dark( - primary: primaryColor, - secondary: secondaryColor, - tertiary: accentColor, - background: backgroundDarkColor, - surface: const Color(0xFF1F2937), - onPrimary: Colors.white, - onSecondary: Colors.white, - onBackground: textDarkColor, - onSurface: textDarkColor, - ), - textTheme: GoogleFonts.poppinsTextTheme(ThemeData.dark().textTheme), - appBarTheme: const AppBarTheme( - backgroundColor: Color(0xFF1F2937), - foregroundColor: Colors.white, - elevation: 0, - ), - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - backgroundColor: primaryColor, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ), - inputDecorationTheme: InputDecorationTheme( - filled: true, - fillColor: const Color(0xFF374151), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: const BorderSide(color: primaryColor, width: 2), - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), - ), - cardTheme: CardTheme( - elevation: 4, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - color: const Color(0xFF1F2937), - ), - ); - } -} \ No newline at end of file diff --git a/flutt/lib/main.dart b/flutt/lib/main.dart deleted file mode 100644 index 6753c3aa..00000000 --- a/flutt/lib/main.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_web_plugins/flutter_web_plugins.dart'; -import 'package:geosector_app/app.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:geosector_app/core/data/models/user_model.dart'; -import 'package:geosector_app/core/data/models/operation_model.dart'; -import 'package:geosector_app/core/data/models/sector_model.dart'; -import 'package:geosector_app/core/data/models/passage_model.dart'; -import 'package:geosector_app/core/data/models/membre_model.dart'; -import 'package:geosector_app/core/constants/app_keys.dart'; -// Import centralisé pour les modèles chat -import 'package:geosector_app/chat/models/chat_adapters.dart'; - -void main() async { - WidgetsFlutterBinding.ensureInitialized(); - - // Configurer le routage par chemin (URLs sans #) - setUrlStrategy(PathUrlStrategy()); - - // Initialiser Hive - await Hive.initFlutter(); - - // Enregistrer les adaptateurs Hive pour les modèles principaux - Hive.registerAdapter(UserModelAdapter()); - Hive.registerAdapter(OperationModelAdapter()); - Hive.registerAdapter(SectorModelAdapter()); - Hive.registerAdapter(PassageModelAdapter()); - Hive.registerAdapter(MembreModelAdapter()); - - // Enregistrer les adaptateurs Hive pour le chat - Hive.registerAdapter(ConversationModelAdapter()); - Hive.registerAdapter(MessageModelAdapter()); - Hive.registerAdapter(ParticipantModelAdapter()); - Hive.registerAdapter(AnonymousUserModelAdapter()); - Hive.registerAdapter(AudienceTargetModelAdapter()); - Hive.registerAdapter(NotificationSettingsAdapter()); - - // Ouvrir uniquement les boîtes essentielles au démarrage - // La boîte des utilisateurs est nécessaire pour vérifier si un utilisateur est déjà connecté - await Hive.openBox(AppKeys.usersBoxName); - // Boîte pour les préférences utilisateur générales - await Hive.openBox(AppKeys.settingsBoxName); - - // Ouvrir les boîtes de chat également au démarrage pour le cache local - await Hive.openBox(AppKeys.chatConversationsBoxName); - await Hive.openBox(AppKeys.chatMessagesBoxName); - - // Les autres boîtes (operations, sectors, passages) seront ouvertes après connexion - // dans UserRepository.login() via la méthode _ensureBoxIsOpen() - - // Définir l'orientation de l'application - await SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - ]); - - // Lancer l'application directement sans AppProviders - runApp(const GeoSectorApp()); -} diff --git a/flutt/lib/presentation/admin/admin_entite.dart b/flutt/lib/presentation/admin/admin_entite.dart deleted file mode 100644 index 8eca4810..00000000 --- a/flutt/lib/presentation/admin/admin_entite.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter/material.dart'; - -/// Page d'administration de l'amicale et des membres -/// Cette page est intégrée dans le tableau de bord administrateur -class AdminEntitePage extends StatelessWidget { - const AdminEntitePage({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Titre de la page - Text( - 'Mon amicale et ses membres', - style: theme.textTheme.headlineMedium?.copyWith( - color: theme.colorScheme.primary, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 24), - // Contenu principal - Expanded( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.people_outline, - size: 64, - color: theme.colorScheme.primary.withOpacity(0.7), - ), - const SizedBox(height: 16), - Text( - 'Page en construction', - style: theme.textTheme.titleLarge, - ), - const SizedBox(height: 8), - Text( - 'Cette section permettra la gestion des amicales et de leurs membres.', - textAlign: TextAlign.center, - style: theme.textTheme.bodyLarge, - ), - ], - ), - ), - ), - ], - ), - ); - } -} diff --git a/flutt/lib/presentation/admin/admin_statistics_page.dart b/flutt/lib/presentation/admin/admin_statistics_page.dart deleted file mode 100644 index bdc21efa..00000000 --- a/flutt/lib/presentation/admin/admin_statistics_page.dart +++ /dev/null @@ -1,529 +0,0 @@ -import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales -import 'package:flutter/material.dart'; -import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales -import 'package:geosector_app/presentation/widgets/charts/activity_chart.dart'; -import 'package:geosector_app/presentation/widgets/charts/passage_pie_chart.dart'; -import 'package:geosector_app/presentation/widgets/charts/payment_pie_chart.dart'; -import 'package:geosector_app/presentation/widgets/charts/payment_data.dart'; -import 'package:geosector_app/presentation/widgets/charts/combined_chart.dart'; -import 'package:geosector_app/core/repositories/passage_repository.dart'; -import 'package:geosector_app/core/repositories/user_repository.dart'; -import '../../shared/app_theme.dart'; - -class AdminStatisticsPage extends StatefulWidget { - const AdminStatisticsPage({Key? key}) : super(key: key); - - @override - State createState() => _AdminStatisticsPageState(); -} - -class _AdminStatisticsPageState extends State { - // Filtres - String _selectedPeriod = 'Jour'; - String _selectedFilterType = 'Secteur'; - String _selectedSector = 'Tous'; - String _selectedUser = 'Tous'; - int _daysToShow = 15; - - // Liste des périodes et types de filtre - final List _periods = ['Jour', 'Semaine', 'Mois', 'Année']; - final List _filterTypes = ['Secteur', 'Membre']; - - // Données simulées pour les secteurs et membres (à remplacer par des données réelles) - final List _sectors = [ - 'Tous', - 'Secteur Nord', - 'Secteur Sud', - 'Secteur Est', - 'Secteur Ouest' - ]; - final List _members = [ - 'Tous', - 'Jean Dupont', - 'Marie Martin', - 'Pierre Legrand', - 'Sophie Petit', - 'Lucas Moreau' - ]; - - @override - Widget build(BuildContext context) { - final screenWidth = MediaQuery.of(context).size.width; - final isDesktop = screenWidth > 800; - - return SingleChildScrollView( - padding: const EdgeInsets.all(AppTheme.spacingL), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Titre et description - Text( - 'Analyse des statistiques', - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: AppTheme.spacingS), - Text( - 'Visualisez les statistiques de passages et de collecte pour votre amicale.', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.grey[600], - ), - ), - const SizedBox(height: AppTheme.spacingL), - - // Filtres - Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), - ), - child: Padding( - padding: const EdgeInsets.all(AppTheme.spacingM), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Filtres', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: AppTheme.spacingM), - isDesktop - ? Row( - children: [ - Expanded(child: _buildPeriodDropdown()), - const SizedBox(width: AppTheme.spacingM), - Expanded(child: _buildDaysDropdown()), - const SizedBox(width: AppTheme.spacingM), - Expanded(child: _buildFilterTypeDropdown()), - const SizedBox(width: AppTheme.spacingM), - Expanded(child: _buildFilterDropdown()), - ], - ) - : Column( - children: [ - _buildPeriodDropdown(), - const SizedBox(height: AppTheme.spacingM), - _buildDaysDropdown(), - const SizedBox(height: AppTheme.spacingM), - _buildFilterTypeDropdown(), - const SizedBox(height: AppTheme.spacingM), - _buildFilterDropdown(), - ], - ), - ], - ), - ), - ), - const SizedBox(height: AppTheme.spacingL), - - // Graphique d'activité principal - Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), - ), - child: Padding( - padding: const EdgeInsets.all(AppTheme.spacingM), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Évolution des passages', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: AppTheme.spacingM), - ActivityChart( - height: 350, - loadFromHive: true, - showAllPassages: true, - title: '', - daysToShow: _daysToShow, - periodType: _selectedPeriod, - userId: _selectedUser != 'Tous' - ? _getUserIdFromName(_selectedUser) - : null, - // Si on filtre par secteur, on devrait passer l'ID du secteur - ), - ], - ), - ), - ), - const SizedBox(height: AppTheme.spacingL), - - // Graphiques de répartition - isDesktop - ? Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: _buildChartCard( - 'Répartition par type de passage', - PassagePieChart( - size: 300, - loadFromHive: true, - showAllPassages: true, - userId: _selectedUser != 'Tous' - ? _getUserIdFromName(_selectedUser) - : null, - ), - ), - ), - const SizedBox(width: AppTheme.spacingM), - Expanded( - child: _buildChartCard( - 'Répartition par mode de paiement', - PaymentPieChart( - payments: [ - PaymentData( - typeId: 1, - amount: 1500.0, - color: const Color(0xFFFFC107), - icon: Icons.toll, - title: 'Espèce', - ), - PaymentData( - typeId: 2, - amount: 2500.0, - color: const Color(0xFF8BC34A), - icon: Icons.wallet, - title: 'Chèque', - ), - PaymentData( - typeId: 3, - amount: 1000.0, - color: const Color(0xFF00B0FF), - icon: Icons.credit_card, - title: 'CB', - ), - ], - size: 300, - ), - ), - ), - ], - ) - : Column( - children: [ - _buildChartCard( - 'Répartition par type de passage', - PassagePieChart( - size: 300, - loadFromHive: true, - showAllPassages: true, - userId: _selectedUser != 'Tous' - ? _getUserIdFromName(_selectedUser) - : null, - ), - ), - const SizedBox(height: AppTheme.spacingM), - _buildChartCard( - 'Répartition par mode de paiement', - PaymentPieChart( - payments: [ - PaymentData( - typeId: 1, - amount: 1500.0, - color: const Color(0xFFFFC107), - icon: Icons.toll, - title: 'Espèce', - ), - PaymentData( - typeId: 2, - amount: 2500.0, - color: const Color(0xFF8BC34A), - icon: Icons.wallet, - title: 'Chèque', - ), - PaymentData( - typeId: 3, - amount: 1000.0, - color: const Color(0xFF00B0FF), - icon: Icons.credit_card, - title: 'CB', - ), - ], - size: 300, - ), - ), - ], - ), - const SizedBox(height: AppTheme.spacingL), - - // Graphique combiné (si disponible) - _buildChartCard( - 'Comparaison passages/montants', - const SizedBox( - height: 350, - child: Center( - child: Text('Graphique combiné à implémenter'), - ), - ), - ), - - const SizedBox(height: AppTheme.spacingL), - - // Actions - Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), - ), - child: Padding( - padding: const EdgeInsets.all(AppTheme.spacingM), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Actions', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: AppTheme.spacingM), - Wrap( - spacing: AppTheme.spacingM, - runSpacing: AppTheme.spacingM, - children: [ - ElevatedButton.icon( - onPressed: () { - // Exporter les statistiques - }, - icon: const Icon(Icons.file_download), - label: const Text('Exporter les statistiques'), - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.primaryColor, - foregroundColor: Colors.white, - ), - ), - ElevatedButton.icon( - onPressed: () { - // Imprimer les statistiques - }, - icon: const Icon(Icons.print), - label: const Text('Imprimer'), - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.buttonSecondaryColor, - foregroundColor: Colors.white, - ), - ), - ElevatedButton.icon( - onPressed: () { - // Partager les statistiques - }, - icon: const Icon(Icons.share), - label: const Text('Partager'), - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.accentColor, - foregroundColor: Colors.white, - ), - ), - ], - ), - ], - ), - ), - ), - ], - ), - ); - } - - // Dropdown pour la période - Widget _buildPeriodDropdown() { - return InputDecorator( - decoration: InputDecoration( - labelText: 'Période', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: AppTheme.spacingM, - vertical: AppTheme.spacingS, - ), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: _selectedPeriod, - isDense: true, - isExpanded: true, - items: _periods.map((String period) { - return DropdownMenuItem( - value: period, - child: Text(period), - ); - }).toList(), - onChanged: (String? newValue) { - if (newValue != null) { - setState(() { - _selectedPeriod = newValue; - }); - } - }, - ), - ), - ); - } - - // Dropdown pour le nombre de jours - Widget _buildDaysDropdown() { - return InputDecorator( - decoration: InputDecoration( - labelText: 'Nombre de jours', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: AppTheme.spacingM, - vertical: AppTheme.spacingS, - ), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: _daysToShow, - isDense: true, - isExpanded: true, - items: [7, 15, 30, 60, 90, 180, 365].map((int days) { - return DropdownMenuItem( - value: days, - child: Text('$days jours'), - ); - }).toList(), - onChanged: (int? newValue) { - if (newValue != null) { - setState(() { - _daysToShow = newValue; - }); - } - }, - ), - ), - ); - } - - // Dropdown pour le type de filtre - Widget _buildFilterTypeDropdown() { - return InputDecorator( - decoration: InputDecoration( - labelText: 'Filtrer par', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: AppTheme.spacingM, - vertical: AppTheme.spacingS, - ), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: _selectedFilterType, - isDense: true, - isExpanded: true, - items: _filterTypes.map((String type) { - return DropdownMenuItem( - value: type, - child: Text(type), - ); - }).toList(), - onChanged: (String? newValue) { - if (newValue != null) { - setState(() { - _selectedFilterType = newValue; - // Réinitialiser les filtres spécifiques - _selectedSector = 'Tous'; - _selectedUser = 'Tous'; - }); - } - }, - ), - ), - ); - } - - // Dropdown pour le filtre spécifique (secteur ou membre) - Widget _buildFilterDropdown() { - final List items = - _selectedFilterType == 'Secteur' ? _sectors : _members; - final String value = - _selectedFilterType == 'Secteur' ? _selectedSector : _selectedUser; - - return InputDecorator( - decoration: InputDecoration( - labelText: _selectedFilterType, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: AppTheme.spacingM, - vertical: AppTheme.spacingS, - ), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: value, - isDense: true, - isExpanded: true, - items: items.map((String item) { - return DropdownMenuItem( - value: item, - child: Text(item), - ); - }).toList(), - onChanged: (String? newValue) { - if (newValue != null) { - setState(() { - if (_selectedFilterType == 'Secteur') { - _selectedSector = newValue; - } else { - _selectedUser = newValue; - } - }); - } - }, - ), - ), - ); - } - - // Widget pour envelopper un graphique dans une carte - Widget _buildChartCard(String title, Widget chart) { - return Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), - ), - child: Padding( - padding: const EdgeInsets.all(AppTheme.spacingM), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: AppTheme.spacingM), - chart, - ], - ), - ), - ); - } - - // Méthode utilitaire pour obtenir l'ID utilisateur à partir de son nom - int? _getUserIdFromName(String name) { - // Dans un cas réel, cela nécessiterait une requête au repository - // Pour l'exemple, on utilise une correspondance simple - if (name == 'Jean Dupont') return 1; - if (name == 'Marie Martin') return 2; - if (name == 'Pierre Legrand') return 3; - if (name == 'Sophie Petit') return 4; - if (name == 'Lucas Moreau') return 5; - return null; - } -} diff --git a/flutt/lib/presentation/auth/login_page.dart b/flutt/lib/presentation/auth/login_page.dart deleted file mode 100644 index a336bcef..00000000 --- a/flutt/lib/presentation/auth/login_page.dart +++ /dev/null @@ -1,640 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/foundation.dart' show kIsWeb; -import 'package:go_router/go_router.dart'; -import 'package:go_router/src/state.dart'; -import 'package:geosector_app/core/repositories/user_repository.dart'; -import 'package:geosector_app/presentation/widgets/custom_button.dart'; -import 'package:geosector_app/presentation/widgets/custom_text_field.dart'; -import 'package:geosector_app/core/services/location_service.dart'; -import 'package:geosector_app/core/services/connectivity_service.dart'; -import 'package:geosector_app/presentation/widgets/connectivity_indicator.dart'; -import 'package:geosector_app/core/services/auth_service.dart'; -import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales - -class LoginPage extends StatefulWidget { - final String? loginType; - - const LoginPage({super.key, this.loginType}); - - @override - State createState() => _LoginPageState(); -} - -class _LoginPageState extends State { - final _formKey = GlobalKey(); - final _usernameController = TextEditingController(); - final _passwordController = TextEditingController(); - final _usernameFocusNode = FocusNode(); - bool _obscurePassword = true; - - // Type de connexion (utilisateur ou administrateur) - String? _loginType; - - // État des permissions de géolocalisation - bool _checkingPermission = true; - bool _hasLocationPermission = false; - String? _locationErrorMessage; - - // État de la connexion Internet - bool _isConnected = false; - - @override - void initState() { - super.initState(); - - // Récupérer le type de connexion depuis les paramètres du widget - _loginType = widget.loginType ?? 'admin'; // Par défaut admin - print('DEBUG: LoginType initial depuis widget: $_loginType'); - - // Vérifier explicitement si le type est 'user' - if (_loginType != null && _loginType!.trim().toLowerCase() == 'user') { - _loginType = 'user'; - print('DEBUG: LoginType confirmé comme user'); - } else { - _loginType = 'admin'; - print('DEBUG: LoginType confirmé comme admin'); - } - - // Vérifier les permissions de géolocalisation au démarrage seulement sur mobile - if (!kIsWeb) { - _checkLocationPermission(); - } else { - // En version web, on considère que les permissions sont accordées - setState(() { - _checkingPermission = false; - _hasLocationPermission = true; - }); - } - - // Initialiser l'état de la connexion - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _isConnected = connectivityService.isConnected; - }); - } - }); - - // Pré-remplir le champ username avec l'identifiant du dernier utilisateur connecté - WidgetsBinding.instance.addPostFrameCallback((_) { - final users = userRepository.getAllUsers(); - - if (users.isNotEmpty) { - // Trouver l'utilisateur le plus récent (celui avec la date de dernière connexion la plus récente) - users.sort((a, b) => (b.lastSyncedAt).compareTo(a.lastSyncedAt)); - final lastUser = users.first; - - // Utiliser le username s'il existe, sinon utiliser l'email comme fallback - if (lastUser.username != null && lastUser.username!.isNotEmpty) { - _usernameController.text = lastUser.username!; - // Déplacer le focus sur le champ mot de passe puisque le username est déjà rempli - _usernameFocusNode.unfocus(); - } else if (lastUser.email.isNotEmpty) { - _usernameController.text = lastUser.email; - _usernameFocusNode.unfocus(); - } - } - }); - } - - /// Vérifie les permissions de géolocalisation - Future _checkLocationPermission() async { - // Ne pas vérifier les permissions en version web - if (kIsWeb) { - setState(() { - _hasLocationPermission = true; - _checkingPermission = false; - }); - return; - } - - setState(() { - _checkingPermission = true; - }); - - // Vérifier si les services de localisation sont activés et si l'application a la permission - final hasPermission = await LocationService.checkAndRequestPermission(); - final errorMessage = await LocationService.getLocationErrorMessage(); - - setState(() { - _hasLocationPermission = hasPermission; - _locationErrorMessage = errorMessage; - _checkingPermission = false; - }); - } - - @override - void dispose() { - _usernameController.dispose(); - _passwordController.dispose(); - _usernameFocusNode.dispose(); - super.dispose(); - } - - /// Construit l'écran de chargement pendant la vérification des permissions - Widget _buildLoadingScreen(ThemeData theme) { - return Scaffold( - body: SafeArea( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - 'assets/images/geosector-logo-200.png', - height: 140, - fit: BoxFit.contain, - ), - const SizedBox(height: 32), - const CircularProgressIndicator(), - const SizedBox(height: 24), - Text( - 'Vérification des permissions...', - style: theme.textTheme.titleMedium, - textAlign: TextAlign.center, - ), - ], - ), - ), - ), - ); - } - - /// Construit l'écran de demande de permission de géolocalisation - Widget _buildLocationPermissionScreen(ThemeData theme) { - return Scaffold( - body: SafeArea( - child: Center( - child: SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 500), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Logo et titre - Image.asset( - 'assets/images/geosector-logo-200.png', - height: 140, - fit: BoxFit.contain, - ), - const SizedBox(height: 24), - Text( - 'Accès à la localisation requis', - style: theme.textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - color: theme.colorScheme.primary, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 24), - - // Message d'erreur - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: theme.colorScheme.error.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: theme.colorScheme.error.withOpacity(0.3)), - ), - child: Column( - children: [ - Icon( - Icons.location_disabled, - color: theme.colorScheme.error, - size: 48, - ), - const SizedBox(height: 16), - Text( - _locationErrorMessage ?? - 'L\'accès à la localisation est nécessaire pour utiliser cette application.', - style: theme.textTheme.bodyLarge, - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - Text( - 'Cette application utilise votre position pour enregistrer les passages et assurer le suivi des secteurs géographiques. Sans cette permission, l\'application ne peut pas fonctionner correctement.', - style: theme.textTheme.bodyMedium, - textAlign: TextAlign.center, - ), - ], - ), - ), - const SizedBox(height: 32), - - // Instructions pour activer la localisation - Text( - 'Comment activer la localisation :', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - _buildInstructionStep( - theme, 1, 'Ouvrez les paramètres de votre appareil'), - _buildInstructionStep(theme, 2, - 'Accédez aux paramètres de confidentialité ou de localisation'), - _buildInstructionStep(theme, 3, - 'Recherchez GEOSECTOR dans la liste des applications'), - _buildInstructionStep(theme, 4, - 'Activez l\'accès à la localisation pour cette application'), - const SizedBox(height: 32), - - // Boutons d'action - CustomButton( - onPressed: () async { - // Ouvrir les paramètres de l'application - await LocationService.openAppSettings(); - }, - text: 'Ouvrir les paramètres de l\'application', - icon: Icons.settings, - ), - const SizedBox(height: 16), - CustomButton( - onPressed: () async { - // Ouvrir les paramètres de localisation - await LocationService.openLocationSettings(); - }, - text: 'Ouvrir les paramètres de localisation', - icon: Icons.location_on, - backgroundColor: theme.colorScheme.secondary, - ), - const SizedBox(height: 16), - CustomButton( - onPressed: () { - // Vérifier à nouveau les permissions - _checkLocationPermission(); - }, - text: 'Vérifier à nouveau', - icon: Icons.refresh, - backgroundColor: theme.colorScheme.tertiary, - ), - ], - ), - ), - ), - ), - ), - ); - } - - /// Construit une étape d'instruction pour activer la localisation - Widget _buildInstructionStep( - ThemeData theme, int stepNumber, String instruction) { - return Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 24, - height: 24, - decoration: BoxDecoration( - color: theme.colorScheme.primary, - shape: BoxShape.circle, - ), - child: Center( - child: Text( - '$stepNumber', - style: TextStyle( - color: theme.colorScheme.onPrimary, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - instruction, - style: theme.textTheme.bodyMedium, - ), - ), - ], - ), - ); - } - - @override - Widget build(BuildContext context) { - print('DEBUG BUILD: Reconstruction de LoginPage avec type: $_loginType'); - - // Utiliser l'instance globale de userRepository - final theme = Theme.of(context); - final size = MediaQuery.of(context).size; - - // Afficher l'écran de permission de géolocalisation si l'utilisateur n'a pas accordé la permission (sauf en version web) - if (!kIsWeb && _checkingPermission) { - return _buildLoadingScreen(theme); - } else if (!kIsWeb && !_hasLocationPermission) { - return _buildLocationPermissionScreen(theme); - } - - return Scaffold( - body: SafeArea( - child: Center( - child: SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 500), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Logo et titre - Image.asset( - 'assets/images/geosector-logo-200.png', - height: 140, - fit: BoxFit.contain, - ), - const SizedBox(height: 24), - Text( - (_loginType != null && - _loginType!.trim().toLowerCase() == 'user') - ? 'Connexion Utilisateur' - : 'Connexion Administrateur', - style: theme.textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - color: (_loginType != null && - _loginType!.trim().toLowerCase() == 'user') - ? Colors.green - : Colors.red, - ), - textAlign: TextAlign.center, - ), - // Ajouter un texte de débogage - Text( - 'Type de connexion détecté: $_loginType', - style: TextStyle(fontSize: 10, color: Colors.grey), - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - Text( - 'Bienvenue sur GEOSECTOR', - style: theme.textTheme.bodyLarge?.copyWith( - color: theme.colorScheme.onBackground.withOpacity(0.7), - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - - // Indicateur de connectivité - ConnectivityIndicator(), - const SizedBox(height: 16), - - // Formulaire de connexion - Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - CustomTextField( - controller: _usernameController, - label: 'Identifiant', - hintText: 'Entrez votre identifiant', - prefixIcon: Icons.person_outline, - keyboardType: TextInputType.text, - autofocus: true, - focusNode: _usernameFocusNode, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Veuillez entrer votre identifiant'; - } - return null; - }, - ), - const SizedBox(height: 16), - CustomTextField( - controller: _passwordController, - label: 'Mot de passe', - hintText: 'Entrez votre mot de passe', - prefixIcon: Icons.lock_outline, - obscureText: _obscurePassword, - suffixIcon: IconButton( - icon: Icon( - _obscurePassword - ? Icons.visibility_outlined - : Icons.visibility_off_outlined, - ), - onPressed: () { - setState(() { - _obscurePassword = !_obscurePassword; - }); - }, - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Veuillez entrer votre mot de passe'; - } - return null; - }, - onFieldSubmitted: (_) async { - if (!userRepository.isLoading && - _formKey.currentState!.validate()) { - // S'assurer que le type est toujours défini - final loginType = _loginType ?? 'admin'; - final actualType = - (loginType.trim().toLowerCase() == 'user') - ? 'user' - : 'admin'; - print('DEBUG: Login avec type: $actualType'); - - final success = await userRepository.login( - _usernameController.text.trim(), - _passwordController.text, - type: actualType, - ); - - if (success && mounted) { - if (userRepository.isAdmin()) { - context.go('/admin'); - } else { - context.go('/user'); - } - } else if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Échec de la connexion. Vérifiez vos identifiants.'), - backgroundColor: Colors.red, - ), - ); - } - } - }, - ), - const SizedBox(height: 8), - - // Mot de passe oublié - Align( - alignment: Alignment.centerRight, - child: TextButton( - onPressed: () { - // Naviguer vers la page de récupération de mot de passe - }, - child: Text( - 'Mot de passe oublié ?', - style: TextStyle( - color: theme.colorScheme.primary, - ), - ), - ), - ), - const SizedBox(height: 24), - - // Bouton de connexion - CustomButton( - onPressed: (userRepository.isLoading || !_isConnected) - ? null - : () async { - if (_formKey.currentState!.validate()) { - // Vérifier à nouveau les permissions de géolocalisation avant de se connecter (sauf en version web) - if (!kIsWeb) { - await _checkLocationPermission(); - - // Si l'utilisateur n'a toujours pas accordé les permissions, ne pas continuer - if (!_hasLocationPermission) { - ScaffoldMessenger.of(context) - .showSnackBar( - const SnackBar( - content: Text( - 'L\'accès à la localisation est nécessaire pour utiliser cette application.'), - backgroundColor: Colors.red, - ), - ); - return; - } - } - - // Vérifier la connexion Internet - await connectivityService - .checkConnectivity(); - - if (!connectivityService.isConnected) { - ScaffoldMessenger.of(context) - .showSnackBar( - SnackBar( - content: const Text( - 'Aucune connexion Internet. La connexion n\'est pas possible hors ligne.'), - backgroundColor: - theme.colorScheme.error, - duration: const Duration(seconds: 3), - action: SnackBarAction( - label: 'Réessayer', - onPressed: () async { - await connectivityService - .checkConnectivity(); - if (connectivityService - .isConnected && - mounted) { - ScaffoldMessenger.of(context) - .showSnackBar( - SnackBar( - content: Text( - 'Connexion Internet ${connectivityService.connectionType} détectée.'), - backgroundColor: - Colors.green, - ), - ); - } - }, - ), - ), - ); - return; - } - - // S'assurer que le type est toujours défini - final loginType = _loginType ?? 'admin'; - final actualType = - (loginType.trim().toLowerCase() == - 'user') - ? 'user' - : 'admin'; - print( - 'DEBUG: Login bouton avec type: $actualType'); - - // Utiliser le service d'authentification avec l'overlay de chargement - final authService = - AuthService(userRepository); - final success = await authService.login( - context, - _usernameController.text.trim(), - _passwordController.text, - type: actualType, - ); - - if (success && mounted) { - if (userRepository.isAdmin()) { - context.go('/admin'); - } else { - context.go('/user'); - } - } else if (mounted) { - ScaffoldMessenger.of(context) - .showSnackBar( - const SnackBar( - content: Text( - 'Échec de la connexion. Vérifiez vos identifiants.'), - backgroundColor: Colors.red, - ), - ); - } - } - }, - text: _isConnected - ? 'Se connecter' - : 'Connexion Internet requise', - isLoading: userRepository.isLoading, - ), - const SizedBox(height: 24), - - // Inscription administrateur uniquement - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Pas encore de compte ?', - style: theme.textTheme.bodyMedium, - ), - TextButton( - onPressed: () { - context.go('/register'); - }, - child: Text( - 'Inscription Administrateur', - style: TextStyle( - color: theme.colorScheme.tertiary, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - - // Lien vers la page publique - TextButton( - onPressed: () { - context.go('/public'); - }, - child: Text( - 'Retour au site GEOSECTOR', - style: TextStyle( - color: theme.colorScheme.secondary, - ), - ), - ), - ], - ), - ), - ], - ), - ), - ), - ), - ), - ); - } -} diff --git a/flutt/lib/presentation/auth/splash_page.dart b/flutt/lib/presentation/auth/splash_page.dart deleted file mode 100644 index d0458ec5..00000000 --- a/flutt/lib/presentation/auth/splash_page.dart +++ /dev/null @@ -1,223 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:geosector_app/core/repositories/user_repository.dart'; -import 'package:geosector_app/core/theme/app_theme.dart'; -import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales -import 'dart:async'; - -class SplashPage extends StatefulWidget { - const SplashPage({super.key}); - - @override - State createState() => _SplashPageState(); -} - -class _SplashPageState extends State - with SingleTickerProviderStateMixin { - late AnimationController _animationController; - bool _isInitializing = true; - String _statusMessage = "Initialisation..."; - double _progress = 0.0; - - final List _initializationSteps = [ - "Initialisation des services...", - "Vérification de l'authentification...", - "Chargement des données...", - "Préparation de l'interface...", - "Démarrage de GeoSector..." - ]; - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - vsync: this, - duration: const Duration(seconds: 2), - ); - - // Simuler le processus d'initialisation - _startInitialization(); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - - void _startInitialization() async { - // Simuler les étapes d'initialisation - for (int i = 0; i < _initializationSteps.length; i++) { - if (mounted) { - setState(() { - _statusMessage = _initializationSteps[i]; - _progress = (i + 1) / _initializationSteps.length; - }); - } - // Attendre pour simuler le chargement - await Future.delayed(const Duration(milliseconds: 800)); - } - - if (mounted) { - setState(() { - _isInitializing = false; - }); - - // Lancer l'animation finale - _animationController.forward(); - - // Attendre la fin de l'animation avant de rediriger - Timer(const Duration(milliseconds: 1500), () { - _redirectToAppropriateScreen(); - }); - } - } - - void _redirectToAppropriateScreen() { - if (!mounted) return; - - // Utiliser l'instance globale de userRepository définie dans app.dart - if (userRepository.isLoggedIn) { - if (userRepository.isAdmin()) { - context.go('/admin'); - } else { - context.go('/user'); - } - } else { - context.go('/public'); - } - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final size = MediaQuery.of(context).size; - - return Scaffold( - body: Container( - width: double.infinity, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - theme.colorScheme.primary, - theme.colorScheme.primary.withOpacity(0.8), - theme.colorScheme.secondary, - ], - ), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Logo animé - AnimatedContainer( - duration: const Duration(milliseconds: 500), - height: _isInitializing ? size.height * 0.3 : size.height * 0.35, - child: AnimatedOpacity( - opacity: _isInitializing ? 0.8 : 1.0, - duration: const Duration(milliseconds: 500), - child: AnimatedScale( - scale: _isInitializing ? 0.9 : 1.0, - duration: const Duration(milliseconds: 800), - curve: Curves.elasticOut, - child: Image.asset( - 'assets/images/geosector-logo-200.png', - width: 150, - height: 150, - fit: BoxFit.contain, - ), - ), - ), - ), - - const SizedBox(height: 24), - - // Titre avec animation fade-in - AnimatedOpacity( - opacity: _isInitializing ? 0.9 : 1.0, - duration: const Duration(milliseconds: 500), - child: Text( - 'GeoSector', - style: theme.textTheme.headlineLarge?.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - letterSpacing: 1.5, - shadows: [ - Shadow( - color: Colors.black.withOpacity(0.3), - offset: const Offset(2, 2), - blurRadius: 4, - ), - ], - ), - ), - ), - - const SizedBox(height: 16), - - // Message de bienvenue - AnimatedOpacity( - opacity: _isInitializing ? 0.8 : 1.0, - duration: const Duration(milliseconds: 500), - child: Text( - 'Bienvenue sur GEOSECTOR', - textAlign: TextAlign.center, - style: theme.textTheme.titleMedium?.copyWith( - color: Colors.white, - fontWeight: FontWeight.w500, - ), - ), - ), - - const SizedBox(height: 40), - - // Indicateur de chargement - if (_isInitializing) ...[ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 40), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: LinearProgressIndicator( - value: _progress, - backgroundColor: Colors.white.withOpacity(0.3), - valueColor: AlwaysStoppedAnimation( - theme.colorScheme.tertiary, - ), - minHeight: 6, - ), - ), - ), - const SizedBox(height: 16), - Text( - _statusMessage, - style: theme.textTheme.bodyLarge?.copyWith( - color: Colors.white.withOpacity(0.9), - ), - ), - ] else ...[ - // Animation de succès quand l'initialisation est terminée - ScaleTransition( - scale: CurvedAnimation( - parent: _animationController, curve: Curves.elasticOut), - child: Container( - width: 80, - height: 80, - decoration: BoxDecoration( - color: theme.colorScheme.tertiary, - shape: BoxShape.circle, - ), - child: const Icon( - Icons.check, - color: Colors.white, - size: 40, - ), - ), - ), - ], - ], - ), - ), - ); - } -} diff --git a/flutt/lib/presentation/public/landing_page.dart b/flutt/lib/presentation/public/landing_page.dart deleted file mode 100644 index dfeca67d..00000000 --- a/flutt/lib/presentation/public/landing_page.dart +++ /dev/null @@ -1,1296 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:go_router/go_router.dart'; -import 'package:geosector_app/core/theme/app_theme.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class LandingPage extends StatefulWidget { - const LandingPage({super.key}); - - @override - State createState() => _LandingPageState(); -} - -class _LandingPageState extends State - with TickerProviderStateMixin { - final PageController _pageController = PageController(); - int _currentPage = 0; - - // Contrôleurs d'animation - late List _controllers; - late List> _opacityAnimations; - late List> _slideAnimations; - late List> _scaleAnimations; - - final List _features = [ - FeatureItem( - title: 'Gestion intuitive', - description: - 'Dessinez vos secteurs de distribution directement sur une carte interactive. Les adresses des habitants sont automatiquement intégrées pour une planification optimale.', - icon: Icons.map, - color: const Color(0xFF2E4057), // Couleur primaire - ), - FeatureItem( - title: 'Suivi en temps réel et traçabilité', - description: - 'Suivez en temps réel la progression des tournées grâce à un tableau de bord complet. Visualisez les passages effectués et les dons collectés pour une gestion efficace.', - icon: Icons.track_changes, - color: const Color(0xFF048BA8), // Couleur secondaire - ), - FeatureItem( - title: 'Génération automatique PDF', - description: - 'Générez des reçus au format PDF instantanément après chaque collecte. Envoyez-les automatiquement par email pour une transparence totale.', - icon: Icons.picture_as_pdf, - color: const Color(0xFFF18F01), // Couleur accent - ), - ]; - - @override - void initState() { - super.initState(); - - // Initialiser les contrôleurs d'animation - _controllers = List.generate( - _features.length, - (index) => AnimationController( - vsync: this, - duration: const Duration(milliseconds: 800), - ), - ); - - // Créer les animations - _opacityAnimations = _controllers - .map((controller) => Tween(begin: 0.0, end: 1.0).animate( - CurvedAnimation(parent: controller, curve: Curves.easeInOut))) - .toList(); - - _slideAnimations = _controllers - .map((controller) => - Tween(begin: const Offset(0, 0.2), end: Offset.zero) - .animate(CurvedAnimation( - parent: controller, curve: Curves.easeOutQuart))) - .toList(); - - _scaleAnimations = _controllers - .map((controller) => Tween(begin: 0.8, end: 1.0).animate( - CurvedAnimation(parent: controller, curve: Curves.easeOutBack))) - .toList(); - - // Démarrer les animations avec délai - _startAnimationsWithDelay(); - } - - void _startAnimationsWithDelay() { - for (int i = 0; i < _controllers.length; i++) { - Future.delayed(Duration(milliseconds: 300 * (i + 1)), () { - if (mounted) { - _controllers[i].forward(); - } - }); - } - } - - @override - void dispose() { - _pageController.dispose(); - for (var controller in _controllers) { - controller.dispose(); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final screenSize = MediaQuery.of(context).size; - bool isWebOrTablet = screenSize.width > 600; - - return Scaffold( - // Utilisation d'un ListView pour rendre toute la page défilable, y compris le footer - body: SafeArea( - child: ListView( - children: [ - // Header avec logo et navigation - Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - // Logo - Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: Image.asset( - 'assets/images/geosector-logo-80.png', - fit: BoxFit.cover, - ), - ), - ), - const SizedBox(width: 8), - Text( - 'GEOSECTOR', - style: theme.textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - color: theme.colorScheme.primary, - ), - ), - ], - ), - - // Navigation - const SizedBox(width: 16), - // Boutons Contact/Connexion/Inscription - if (isWebOrTablet) - Row( - children: [ - TextButton( - onPressed: () {}, - style: TextButton.styleFrom( - foregroundColor: theme.colorScheme.primary, - padding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 8), - ), - child: const Text('Contact'), - ), - const SizedBox(width: 8), - OutlinedButton( - onPressed: () { - // Naviguer avec debug - print('DEBUG: Navigation vers login admin'); - // Utiliser directement les paramètres avec go - context.go('/login', extra: {'type': 'admin'}); - print('DEBUG: Navigation admin avec extra'); - }, - style: OutlinedButton.styleFrom( - foregroundColor: Colors.red, - side: const BorderSide(color: Colors.red), - padding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 8), - ), - child: const Text('Connexion Administrateur'), - ), - const SizedBox(width: 8), - OutlinedButton( - onPressed: () { - // Naviguer avec debug - print('DEBUG: Navigation vers login user'); - // Utiliser directement les paramètres avec go - context.go('/login', extra: {'type': 'user'}); - print('DEBUG: Navigation user avec extra'); - }, - style: OutlinedButton.styleFrom( - foregroundColor: Colors.green, - side: const BorderSide(color: Colors.green), - padding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 8), - ), - child: const Text('Connexion Utilisateur'), - ), - const SizedBox(width: 8), - ElevatedButton( - onPressed: () { - context.go('/register?from=landing'); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFF18F01), - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 8), - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: const Text('S\'inscrire'), - ), - ], - ) - else - Row( - children: [ - // Bouton de login administrateur - IconButton( - icon: const Icon(Icons.admin_panel_settings), - tooltip: 'Connexion Administrateur', - color: Colors.red, - onPressed: () { - print('DEBUG: Navigation mobile vers login admin'); - context.go('/login', extra: {'type': 'admin'}); - print('DEBUG: Navigation mobile admin avec extra'); - }, - ), - // Bouton de login utilisateur - IconButton( - icon: const Icon(Icons.person), - tooltip: 'Connexion Utilisateur', - color: Colors.green, - onPressed: () { - print('DEBUG: Navigation mobile vers login user'); - context.go('/login', extra: {'type': 'user'}); - print('DEBUG: Navigation mobile user avec extra'); - }, - ), - IconButton( - icon: const Icon(Icons.menu), - onPressed: () { - Scaffold.of(context).openDrawer(); - }, - ), - ], - ), - ], - ), - ), - - // Contenu principal - if (isWebOrTablet) - _buildWebLayout(theme, screenSize) - else - _buildMobileLayout(theme, screenSize), - - // Footer - Container( - padding: const EdgeInsets.all(32), - color: const Color(0xFFF5F5F5), - child: Column( - children: [ - // Trois colonnes du footer - isWebOrTablet - ? Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Première colonne - Coordonnées - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - width: 30, - height: 30, - margin: const EdgeInsets.only(right: 8), - child: Image.asset( - 'assets/images/geosector-logo-80.png', - fit: BoxFit.contain, - ), - ), - Text( - 'GEOSECTOR', - style: theme.textTheme.titleLarge - ?.copyWith( - fontWeight: FontWeight.bold, - color: Colors.black87, - ), - ), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - Icon(Icons.location_on_outlined, - color: Colors.black87, size: 20), - const SizedBox(width: 8), - Expanded( - child: Text( - '16 Rue des marguerites, 56930 Pluméliau-Bieuzy', - style: theme.textTheme.bodyMedium - ?.copyWith(color: Colors.black87), - ), - ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Icon(Icons.phone_outlined, - color: Colors.black87, size: 20), - const SizedBox(width: 8), - Text( - '+33 7 69 09 17 06', - style: theme.textTheme.bodyMedium - ?.copyWith(color: Colors.black87), - ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Icon(Icons.email_outlined, - color: Colors.black87, size: 20), - const SizedBox(width: 8), - Text( - 'contactgeosector@gmail.com', - style: theme.textTheme.bodyMedium - ?.copyWith(color: Colors.black87), - ), - ], - ), - const SizedBox(height: 16), - OutlinedButton.icon( - onPressed: () async { - final Uri url = Uri.parse( - 'https://www.facebook.com/geosector/'); - if (await canLaunchUrl(url)) { - await launchUrl(url, - mode: - LaunchMode.externalApplication); - } - }, - icon: const Icon(Icons.facebook), - label: - const Text('Suivez-nous sur Facebook'), - style: OutlinedButton.styleFrom( - foregroundColor: const Color(0xFF1877F2), - side: const BorderSide( - color: Color(0xFF1877F2)), - ), - ), - ], - ), - ), - const SizedBox(width: 32), - // Deuxième colonne - Liens - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Téléchargement', - style: - theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - Row( - children: [ - OutlinedButton.icon( - onPressed: () {}, - icon: const Icon(Icons.apple), - label: const Text('App Store'), - style: OutlinedButton.styleFrom( - foregroundColor: Colors.black, - ), - ), - const SizedBox(width: 8), - OutlinedButton.icon( - onPressed: () {}, - icon: const Icon(Icons.android), - label: const Text('Play Store'), - style: OutlinedButton.styleFrom( - foregroundColor: - const Color(0xFF3DDC84), - side: const BorderSide( - color: Color(0xFF3DDC84)), - ), - ), - ], - ), - const SizedBox(height: 24), - Text( - 'Informations légales', - style: - theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - TextButton.icon( - onPressed: () {}, - icon: const Icon(Icons.gavel_outlined, - size: 18), - label: const Text('Mentions légales'), - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - alignment: Alignment.centerLeft, - ), - ), - TextButton.icon( - onPressed: () {}, - icon: const Icon(Icons.privacy_tip_outlined, - size: 18), - label: const Text( - 'Politique de confidentialité'), - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - alignment: Alignment.centerLeft, - ), - ), - TextButton.icon( - onPressed: () {}, - icon: const Icon(Icons.description_outlined, - size: 18), - label: - const Text('Conditions d\'utilisation'), - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - alignment: Alignment.centerLeft, - ), - ), - ], - ), - ), - const SizedBox(width: 32), - // Troisième colonne - Formulaire de contact - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Contactez-nous', - style: - theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - TextField( - decoration: InputDecoration( - hintText: 'Votre nom', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - contentPadding: - const EdgeInsets.symmetric( - horizontal: 16, vertical: 12), - ), - ), - const SizedBox(height: 8), - TextField( - decoration: InputDecoration( - hintText: 'Votre email', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - contentPadding: - const EdgeInsets.symmetric( - horizontal: 16, vertical: 12), - ), - ), - const SizedBox(height: 8), - TextField( - maxLines: 3, - decoration: InputDecoration( - hintText: 'Votre message', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - contentPadding: - const EdgeInsets.symmetric( - horizontal: 16, vertical: 12), - ), - ), - const SizedBox(height: 12), - ElevatedButton.icon( - onPressed: () {}, - icon: const Icon(Icons.send), - label: const Text('Envoyer'), - style: ElevatedButton.styleFrom( - backgroundColor: - theme.colorScheme.primary, - foregroundColor: Colors.white, - minimumSize: - const Size(double.infinity, 48), - ), - ), - ], - ), - ), - ], - ) - : Column( - // Version mobile du footer - children: [ - // Première colonne - Coordonnées - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'GEOSECTOR', - style: theme.textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - color: theme.colorScheme.primary, - ), - ), - const SizedBox(height: 16), - Row( - children: [ - Icon(Icons.location_on_outlined, - color: theme.colorScheme.primary, - size: 20), - const SizedBox(width: 8), - Expanded( - child: Text( - '123 Rue de la Distribution, 75000 Paris', - style: theme.textTheme.bodyMedium - ?.copyWith(color: Colors.black87), - ), - ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Icon(Icons.phone_outlined, - color: theme.colorScheme.primary, - size: 20), - const SizedBox(width: 8), - Text( - '+33 1 23 45 67 89', - style: theme.textTheme.bodyMedium, - ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Icon(Icons.email_outlined, - color: theme.colorScheme.primary, - size: 20), - const SizedBox(width: 8), - Text( - 'contact@geosector.com', - style: theme.textTheme.bodyMedium, - ), - ], - ), - const SizedBox(height: 16), - OutlinedButton.icon( - onPressed: () async { - final Uri url = Uri.parse( - 'https://www.facebook.com/geosector/'); - if (await canLaunchUrl(url)) { - await launchUrl(url, - mode: LaunchMode.externalApplication); - } - }, - icon: const Icon(Icons.facebook), - label: const Text('Suivez-nous sur Facebook'), - style: OutlinedButton.styleFrom( - foregroundColor: const Color(0xFF1877F2), - side: const BorderSide( - color: Color(0xFF1877F2)), - minimumSize: - const Size(double.infinity, 40), - ), - ), - ], - ), - const SizedBox(height: 32), - // Deuxième colonne - Liens - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Téléchargement', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: OutlinedButton.icon( - onPressed: () {}, - icon: const Icon(Icons.apple), - label: const Text('App Store'), - style: OutlinedButton.styleFrom( - foregroundColor: Colors.black, - ), - ), - ), - const SizedBox(width: 8), - Expanded( - child: OutlinedButton.icon( - onPressed: () {}, - icon: const Icon(Icons.android), - label: const Text('Play Store'), - style: OutlinedButton.styleFrom( - foregroundColor: - const Color(0xFF3DDC84), - side: const BorderSide( - color: Color(0xFF3DDC84)), - ), - ), - ), - ], - ), - const SizedBox(height: 24), - Text( - 'Informations légales', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - TextButton.icon( - onPressed: () {}, - icon: const Icon(Icons.gavel_outlined, - size: 18), - label: const Text('Mentions légales'), - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - alignment: Alignment.centerLeft, - ), - ), - TextButton.icon( - onPressed: () {}, - icon: const Icon(Icons.privacy_tip_outlined, - size: 18), - label: const Text( - 'Politique de confidentialité'), - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - alignment: Alignment.centerLeft, - ), - ), - TextButton.icon( - onPressed: () {}, - icon: const Icon(Icons.description_outlined, - size: 18), - label: - const Text('Conditions d\'utilisation'), - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - alignment: Alignment.centerLeft, - ), - ), - ], - ), - const SizedBox(height: 32), - // Troisième colonne - Formulaire de contact - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Contactez-nous', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - TextField( - decoration: InputDecoration( - hintText: 'Votre nom', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 12), - ), - ), - const SizedBox(height: 8), - TextField( - decoration: InputDecoration( - hintText: 'Votre email', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 12), - ), - ), - const SizedBox(height: 8), - TextField( - maxLines: 3, - decoration: InputDecoration( - hintText: 'Votre message', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 12), - ), - ), - const SizedBox(height: 12), - ElevatedButton.icon( - onPressed: () {}, - icon: const Icon(Icons.send), - label: const Text('Envoyer'), - style: ElevatedButton.styleFrom( - backgroundColor: theme.colorScheme.primary, - foregroundColor: Colors.white, - minimumSize: - const Size(double.infinity, 48), - ), - ), - ], - ), - ], - ), - const SizedBox(height: 32), - // Copyright - Divider(color: Colors.black38), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - '© ${DateTime.now().year} GEOSECTOR. Tous droits réservés. ', - style: theme.textTheme.bodySmall - ?.copyWith(color: Colors.black87), - ), - InkWell( - onTap: () async { - // Ouvrir le lien vers D6SOFT - final Uri url = Uri.parse('https://d6soft.fr'); - if (await canLaunchUrl(url)) { - await launchUrl(url, - mode: LaunchMode.externalApplication); - } - }, - child: Text( - 'Conception D6SOFT', - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.primary, - decoration: TextDecoration.underline, - ), - ), - ), - ], - ), - ], - ), - ), - ], - ), - ), - drawer: !isWebOrTablet - ? Drawer( - child: ListView( - padding: EdgeInsets.zero, - children: [ - DrawerHeader( - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [Color(0xFF2E4057), Color(0xFF048BA8)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 40, - height: 40, - child: Image.asset( - 'assets/images/geosector-logo-80.png', - fit: BoxFit.contain, - ), - ), - const SizedBox(height: 8), - Text( - 'GEOSECTOR', - style: theme.textTheme.headlineSmall?.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - ListTile( - leading: const Icon(Icons.star_outline), - title: const Text('Fonctionnalités'), - onTap: () { - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.info_outline), - title: const Text('À propos'), - onTap: () { - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.email_outlined), - title: const Text('Contact'), - onTap: () { - Navigator.pop(context); - }, - ), - const Divider(), - ListTile( - leading: const Icon(Icons.login), - title: const Text('Se connecter'), - onTap: () { - Navigator.pop(context); - context.go('/login', extra: {'type': 'admin'}); - print('DEBUG: Navigation drawer avec extra'); - }, - ), - ListTile( - leading: const Icon(Icons.person_add_outlined), - title: const Text('S\'inscrire'), - onTap: () { - Navigator.pop(context); - context.go('/register?from=landing'); - }, - ), - ], - ), - ) - : null, - ); - } - - Widget _buildWebLayout(ThemeData theme, Size screenSize) { - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Section d'en-tête - Container( - padding: const EdgeInsets.all(48), - decoration: BoxDecoration( - color: theme.colorScheme.background, - ), - child: Stack( - children: [ - // Fond de carte - Positioned.fill( - child: Opacity( - opacity: 0.1, - child: SvgPicture.asset( - 'assets/images/city-map-bg-fixed.svg', - fit: BoxFit.cover, - ), - ), - ), - // Contenu - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Suppression du second menu - const SizedBox(height: 80), - // Titre principal - Text( - 'Une application puissante et intuitive, pour une gestion efficace de vos distributions', - style: theme.textTheme.displayMedium?.copyWith( - fontWeight: FontWeight.bold, - color: theme.colorScheme.primary, - ), - ), - const SizedBox(height: 16), - // Sous-titre - Text( - 'Simplifiez vos distributions, optimisez vos collectes.', - style: theme.textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w300, - color: theme.colorScheme.onBackground.withOpacity(0.8), - ), - ), - const SizedBox(height: 32), - // Boutons d'action - Row( - children: [ - ElevatedButton.icon( - onPressed: () { - context.go('/register?from=landing'); - }, - icon: const Icon(Icons.person_add_outlined), - label: const Text('Créer un compte gratuit'), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFF18F01), - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric( - horizontal: 24, vertical: 16), - elevation: 2, - ), - ), - const SizedBox(width: 16), - OutlinedButton.icon( - onPressed: () {}, - icon: const Icon(Icons.play_circle_outline), - label: const Text('Voir la démo'), - style: OutlinedButton.styleFrom( - foregroundColor: theme.colorScheme.primary, - padding: const EdgeInsets.symmetric( - horizontal: 24, vertical: 16), - side: BorderSide( - color: theme.colorScheme.primary, - ), - ), - ), - ], - ), - ], - ), - ], - ), - ), - - // Section des fonctionnalités - Container( - padding: const EdgeInsets.all(48), - color: theme.colorScheme.surface, - child: Column( - children: [ - Text( - 'Fonctionnalités principales', - style: theme.textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - color: theme.colorScheme.primary, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 48), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: _buildFeatureItems(theme, true), - ), - ], - ), - ), - - // Appel à l'action - Container( - padding: const EdgeInsets.all(48), - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [Color(0xFF2E4057), Color(0xFF048BA8)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), - child: Column( - children: [ - Text( - 'Prêt à tester GEOSECTOR ?', - style: theme.textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - color: Colors.white, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - Text( - 'Rejoignez notre communauté et commencez à transformer vos distributions dès aujourd\'hui.', - style: theme.textTheme.bodyLarge?.copyWith( - color: Colors.white.withOpacity(0.9), - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 32), - ElevatedButton.icon( - onPressed: () { - context.go('/register?from=landing'); - }, - icon: const Icon(Icons.person_add_outlined), - label: const Text('Créer un compte gratuitement'), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFF18F01), - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric( - horizontal: 32, vertical: 16), - elevation: 4, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ), - ], - ), - ), - ], - ), - ); - } - - Widget _buildMobileLayout(ThemeData theme, Size screenSize) { - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Section d'en-tête - Container( - padding: const EdgeInsets.all(24), - color: theme.colorScheme.background, - child: Column( - children: [ - Container( - width: 150, - height: 150, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 8, - offset: const Offset(0, 3), - ), - ], - ), - padding: const EdgeInsets.all(15), - child: Image.asset( - 'assets/images/geosector-logo.png', - fit: BoxFit.contain, - ), - ), - const SizedBox(height: 24), - Text( - 'Une application puissante et intuitive, pour une gestion efficace de vos distributions', - style: theme.textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - color: theme.colorScheme.primary, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - Text( - 'Simplifiez vos distributions, optimisez vos collectes.', - style: theme.textTheme.bodyMedium, - textAlign: TextAlign.center, - ), - const SizedBox(height: 24), - ElevatedButton.icon( - onPressed: () { - context.go('/register?from=landing'); - }, - icon: const Icon(Icons.arrow_forward), - label: const Text('Commencer gratuitement'), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFF18F01), - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric( - horizontal: 24, vertical: 16), - minimumSize: const Size(double.infinity, 48), - elevation: 4, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ), - const SizedBox(height: 12), - OutlinedButton.icon( - onPressed: () {}, - icon: const Icon(Icons.play_circle_outline), - label: const Text('Voir la démo'), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 24, vertical: 12), - minimumSize: const Size(double.infinity, 48), - ), - ), - ], - ), - ), - - // Section des fonctionnalités - Container( - padding: const EdgeInsets.all(24), - color: theme.colorScheme.surface, - child: Column( - children: [ - Text( - 'Fonctionnalités principales', - style: theme.textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - color: theme.colorScheme.primary, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 24), - SizedBox( - height: 350, - child: PageView.builder( - controller: _pageController, - itemCount: _features.length, - onPageChanged: (int page) { - setState(() { - _currentPage = page; - }); - }, - itemBuilder: (context, index) { - return AnimatedBuilder( - animation: _controllers[index], - builder: (context, child) { - return Opacity( - opacity: _opacityAnimations[index].value, - child: Transform.translate( - offset: Offset(0, - 20 * (1 - _slideAnimations[index].value.dy)), - child: Transform.scale( - scale: _scaleAnimations[index].value, - child: - _buildFeatureCard(_features[index], theme), - ), - ), - ); - }, - ); - }, - ), - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate( - _features.length, - (index) => Container( - width: 10, - height: 10, - margin: const EdgeInsets.symmetric(horizontal: 4), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: _currentPage == index - ? theme.colorScheme.primary - : theme.colorScheme.primary.withOpacity(0.3), - ), - ), - ), - ), - ], - ), - ), - - // Appel à l'action - Container( - padding: const EdgeInsets.all(24), - color: theme.colorScheme.primary, - child: Column( - children: [ - Text( - 'Prêt à tester GEOSECTOR ?', - style: theme.textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - color: Colors.white, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - Text( - 'Rejoignez notre communauté dès aujourd\'hui.', - style: theme.textTheme.bodyMedium?.copyWith( - color: Colors.white.withOpacity(0.9), - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 24), - ElevatedButton.icon( - onPressed: () { - context.go('/register?from=landing'); - }, - icon: const Icon(Icons.person_add_outlined), - label: const Text('Créer un compte'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - foregroundColor: theme.colorScheme.primary, - padding: const EdgeInsets.symmetric( - horizontal: 24, vertical: 12), - minimumSize: const Size(double.infinity, 48), - ), - ), - ], - ), - ), - ], - ), - ); - } - - List _buildFeatureItems(ThemeData theme, bool isWeb) { - return List.generate(_features.length, (index) { - return Expanded( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: AnimatedBuilder( - animation: _controllers[index], - builder: (context, child) { - return Opacity( - opacity: _opacityAnimations[index].value, - child: Transform.translate( - offset: - Offset(0, 20 * (1 - _slideAnimations[index].value.dy)), - child: Transform.scale( - scale: _scaleAnimations[index].value, - child: _buildFeatureCard(_features[index], theme), - ), - ), - ); - }, - ), - ), - ); - }); - } - - Widget _buildFeatureCard(FeatureItem feature, ThemeData theme) { - return Card( - elevation: 4, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - width: 100, - height: 100, - decoration: BoxDecoration( - color: feature.color.withOpacity(0.1), - shape: BoxShape.circle, - ), - child: Icon( - feature.icon, - size: 60, - color: feature.color, - ), - ), - const SizedBox(height: 16), - Text( - feature.title, - style: theme.textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - color: theme.colorScheme.primary, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - Text( - feature.description, - style: theme.textTheme.bodyMedium, - textAlign: TextAlign.center, - ), - ], - ), - ), - ); - } -} - -class FeatureItem { - final String title; - final String description; - final IconData icon; - final Color color; - - FeatureItem({ - required this.title, - required this.description, - required this.icon, - required this.color, - }); -} diff --git a/flutt/lib/presentation/user/user_dashboard_home_page.dart b/flutt/lib/presentation/user/user_dashboard_home_page.dart deleted file mode 100644 index c6b7d9d5..00000000 --- a/flutt/lib/presentation/user/user_dashboard_home_page.dart +++ /dev/null @@ -1,656 +0,0 @@ -import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales -import 'package:flutter/material.dart'; -import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales -import 'package:geosector_app/core/repositories/user_repository.dart'; -import 'package:geosector_app/core/repositories/passage_repository.dart'; -import 'package:geosector_app/core/theme/app_theme.dart'; -import 'package:geosector_app/core/constants/app_keys.dart'; -import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart'; -import 'package:geosector_app/presentation/widgets/charts/charts.dart'; - -class UserDashboardHomePage extends StatefulWidget { - const UserDashboardHomePage({super.key}); - - @override - State createState() => _UserDashboardHomePageState(); -} - -class _UserDashboardHomePageState extends State { - // Formater une date au format JJ/MM/YYYY - String _formatDate(DateTime date) { - return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}'; - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - // Utiliser l'instance globale définie dans app.dart - final size = MediaQuery.of(context).size; - final isDesktop = size.width > 900; - - return Scaffold( - backgroundColor: Colors.transparent, - body: SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Tableau de bord', - style: theme.textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - color: theme.colorScheme.primary, - ), - ), - Builder(builder: (context) { - // Récupérer l'opération actuelle - final operation = userRepository.getCurrentOperation(); - if (operation != null) { - return Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Text( - '${operation.name} (${_formatDate(operation.dateDebut)}-${_formatDate(operation.dateFin)})', - style: theme.textTheme.titleSmall?.copyWith( - color: theme.colorScheme.primary.withOpacity(0.7), - fontWeight: FontWeight.w500, - ), - ), - ); - } else { - return const SizedBox.shrink(); - } - }), - const SizedBox(height: 24), - - // Synthèse des passages - _buildSummaryCards(isDesktop), - - const SizedBox(height: 24), - - // Graphique des passages - _buildPassagesChart(context, theme), - - const SizedBox(height: 24), - - // Derniers passages - _buildRecentPassages(context, theme), - ], - ), - ), - ), - ); - } - - // Construction des cartes de synthèse - Widget _buildSummaryCards(bool isDesktop) { - return Column( - children: [ - _buildCombinedPassagesCard(context, isDesktop), - const SizedBox(height: 16), - _buildCombinedPaymentsCard(isDesktop), - ], - ); - } - - // Construction d'une carte combinée pour les règlements (liste + graphique) - Widget _buildCombinedPaymentsCard(bool isDesktop) { - // Utiliser les instances globales définies dans app.dart - - // Récupérer l'utilisateur actuel - final currentUser = userRepository.getCurrentUser(); - final int? currentUserId = currentUser?.id; - - // Récupérer tous les passages - final passages = passageRepository.getAllPassages(); - - // Pas de log ici pour éviter les logs excessifs - - // Initialiser les montants par type de règlement - final Map paymentAmounts = { - 0: 0.0, // Pas de règlement - 1: 0.0, // Espèces - 2: 0.0, // Chèques - 3: 0.0, // CB - }; - - // Compteur pour les passages avec montant > 0 - int passagesWithPaymentCount = 0; - - // Parcourir les passages et calculer les montants par type de règlement - for (final passage in passages) { - // Vérifier si le passage appartient à l'utilisateur actuel - if (currentUserId != null && passage.fkUser == currentUserId) { - final int typeReglement = passage.fkTypeReglement; - - // Convertir la chaîne de montant en double - double montant = 0.0; - try { - // Gérer les formats possibles (virgule ou point) - String montantStr = passage.montant.replaceAll(',', '.'); - montant = double.tryParse(montantStr) ?? 0.0; - } catch (e) { - debugPrint('Erreur de conversion du montant: ${passage.montant}'); - } - - // Ne compter que les passages avec un montant > 0 - if (montant > 0) { - passagesWithPaymentCount++; - - // Ajouter au montant total par type de règlement - if (paymentAmounts.containsKey(typeReglement)) { - paymentAmounts[typeReglement] = - (paymentAmounts[typeReglement] ?? 0.0) + montant; - } else { - // Si le type n'est pas dans notre map, l'ajouter à la catégorie par défaut (0: Pas de règlement) - paymentAmounts[0] = (paymentAmounts[0] ?? 0.0) + montant; - // Type de règlement inconnu, ajouté à la catégorie 'Pas de règlement' - } - } - } - } - - // Calculer le total des règlements - final double totalPayments = - paymentAmounts.values.fold(0.0, (sum, amount) => sum + amount); - - // Convertir les montants en objets PaymentData pour le graphique - final List paymentDataList = - PaymentUtils.getPaymentDataFromAmounts(paymentAmounts); - - return Card( - elevation: 4, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Stack( - children: [ - // Symbole euro en arrière-plan - Positioned.fill( - child: Center( - child: Icon( - Icons.euro_symbol, - size: 180, - color: Colors.blue.withOpacity(0.07), // Bleuté et estompé - ), - ), - ), - // Contenu principal - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.payments, - color: AppTheme.accentColor, - size: 24, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - 'Règlements sur $passagesWithPaymentCount passages', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - Text( - '${totalPayments.toStringAsFixed(2)} €', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: AppTheme.accentColor, - ), - ), - ], - ), - const Divider(height: 24), - SizedBox( - height: 250, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Liste des règlements (côté gauche) - Expanded( - flex: isDesktop ? 1 : 2, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...AppKeys.typesReglements.entries.map((entry) { - final int typeId = entry.key; - final Map typeData = entry.value; - final double amount = - paymentAmounts[typeId] ?? 0.0; - final Color color = - Color(typeData['couleur'] as int); - - return Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Row( - children: [ - Container( - width: 24, - height: 24, - decoration: BoxDecoration( - color: color, - shape: BoxShape.circle, - ), - child: Icon( - typeData['icon_data'] as IconData, - color: Colors.white, - size: 16, - ), - ), - const SizedBox(width: 8), - Expanded( - child: Text( - typeData['titre'] as String, - style: const TextStyle( - fontSize: 14, - ), - ), - ), - Text( - '${amount.toStringAsFixed(2)} €', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: color, - ), - ), - ], - ), - ); - }).toList(), - ], - ), - ), - - // Séparateur vertical - if (isDesktop) const VerticalDivider(width: 24), - - // Graphique en camembert (côté droit) - Expanded( - flex: isDesktop ? 1 : 2, - child: PaymentPieChart( - payments: paymentDataList, - size: double - .infinity, // Utiliser tout l'espace disponible - labelSize: 12, - showPercentage: true, - showIcons: false, // Désactiver les icônes - showLegend: false, - isDonut: true, - innerRadius: '50%', - enable3DEffect: true, // Activer l'effet 3D - effect3DIntensity: - 1.5, // Intensité de l'effet 3D plus forte - enableEnhancedExplode: - true, // Activer l'effet d'explosion amélioré - useGradient: - true, // Utiliser des dégradés pour renforcer l'effet 3D - ), - ), - ], - ), - ), - ], - ), - ), - ], - ), - ); - } - - // Construction d'une carte combinée pour les passages (liste + graphique) - Widget _buildCombinedPassagesCard(BuildContext context, bool isDesktop) { - // Utiliser les instances globales définies dans app.dart - - // Récupérer l'utilisateur actuel - final currentUser = userRepository.getCurrentUser(); - final int? currentUserId = currentUser?.id; - - // Récupérer tous les passages - final passages = passageRepository.getAllPassages(); - - // Pas de log ici pour éviter les logs excessifs - - // Compter les passages par type - final Map passagesCounts = { - 1: 0, // Effectués - 2: 0, // À finaliser - 3: 0, // Refusés - 4: 0, // Dons - 5: 0, // Lots - 6: 0, // Maisons vides - }; - - // Créer un map pour compter les types de passages - final Map typesCount = {}; - final Map userTypesCount = {}; - - // Parcourir les passages et les compter par type - for (final passage in passages) { - final typeId = passage.fkType; - final int passageUserId = passage.fkUser; - - // Compter les occurrences de chaque type pour le débogage - typesCount[typeId] = (typesCount[typeId] ?? 0) + 1; - - // Vérifier si le passage appartient à l'utilisateur actuel ou est de type 2 - bool shouldCount = typeId == 2 || - (currentUserId != null && passageUserId == currentUserId); - - if (shouldCount) { - // Compter pour les statistiques de l'utilisateur - userTypesCount[typeId] = (userTypesCount[typeId] ?? 0) + 1; - - // Ajouter au compteur des passages par type - if (passagesCounts.containsKey(typeId)) { - passagesCounts[typeId] = passagesCounts[typeId]! + 1; - } else { - // Si le type n'est pas dans notre map, l'ajouter à la catégorie par défaut (2: À finaliser) - passagesCounts[2] = passagesCounts[2]! + 1; - // Type de passage inconnu ajouté à 'A finaliser' - } - } - } - - // Pas de log ici pour éviter les logs excessifs - - // Calculer le total des passages pour l'utilisateur (somme des valeurs dans userTypesCount) - final int totalUserPassages = - userTypesCount.values.fold(0, (sum, count) => sum + count); - - return Card( - elevation: 4, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Padding( - padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, - 8.0), // Réduire les paddings vertical pour donner plus d'espace au graphique - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.route, - color: AppTheme.primaryColor, - size: 24, - ), - const SizedBox(width: 8), - Expanded( - child: Builder(builder: (context) { - // Récupérer les secteurs de l'utilisateur - final userSectors = userRepository.getUserSectors(); - final int sectorCount = userSectors.length; - - // Déterminer le titre en fonction du nombre de secteurs - String title = 'Passages'; - if (sectorCount > 1) { - title = 'Passages sur mes $sectorCount secteurs'; - } - - return Text( - title, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ); - }), - ), - Text( - totalUserPassages.toString(), - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: AppTheme.primaryColor, - ), - ), - ], - ), - const Divider(height: 24), - SizedBox( - height: 250, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Liste des passages (côté gauche) - Expanded( - flex: isDesktop ? 1 : 2, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...AppKeys.typesPassages.entries.map((entry) { - final int typeId = entry.key; - final Map typeData = entry.value; - final int count = passagesCounts[typeId] ?? 0; - final Color color = Color(typeData['couleur2'] - as int); // Utiliser la deuxième couleur - final IconData iconData = typeData['icon_data'] - as IconData; // Utiliser l'icône définie dans AppKeys - - return Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Row( - children: [ - Container( - width: 24, - height: 24, - decoration: BoxDecoration( - color: color, - shape: BoxShape.circle, - ), - child: Icon( - iconData, - color: Colors.white, - size: 16, - ), - ), - const SizedBox(width: 8), - Expanded( - child: Text( - typeData['titres'] as String, - style: const TextStyle( - fontSize: 14, - ), - ), - ), - Text( - count.toString(), - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: color, - ), - ), - ], - ), - ); - }).toList(), - ], - ), - ), - - // Séparateur vertical - if (isDesktop) const VerticalDivider(width: 24), - - // Graphique en camembert (côté droit) - Expanded( - flex: isDesktop ? 1 : 2, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: PassagePieChart( - passagesByType: passagesCounts, - size: double - .infinity, // Utiliser tout l'espace disponible - labelSize: 12, - showPercentage: true, - showIcons: false, // Désactiver les icônes - showLegend: false, // Désactiver la légende - isDonut: true, // Activer le format donut - innerRadius: '50%' // Rayon interne du donut - ), - ), - ), - ], - ), - ), - ], - ), - ), - ); - } - - // Construction du graphique des passages - Widget _buildPassagesChart(BuildContext context, ThemeData theme) { - // Définir les types de passages à exclure - // Selon la mémoire, le type 2 correspond aux passages "A finaliser" - // et nous voulons les exclure du comptage pour l'utilisateur actuel - final List excludePassageTypes = [2]; - - return Card( - elevation: 4, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Padding( - padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, - 8.0), // Réduire les paddings vertical pour donner plus d'espace - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Titre supprimé car déjà présent dans le widget ActivityChart - SizedBox( - height: - 350, // Augmentation de la hauteur à 350px pour résoudre le problème de l'axe Y - child: ActivityChart( - // Utiliser le chargement depuis Hive directement dans le widget - loadFromHive: true, - // Ne pas filtrer par utilisateur (afficher tous les passages) - showAllPassages: true, - // Exclure les passages de type 2 (A finaliser) - excludePassageTypes: excludePassageTypes, - // Afficher les 15 derniers jours - daysToShow: 15, - periodType: 'Jour', - height: - 350, // Augmentation de la hauteur à 350px aussi dans le widget - ), - ), - ], - ), - ), - ); - } - - // Construction de la liste des derniers passages - Widget _buildRecentPassages(BuildContext context, ThemeData theme) { - // Utiliser les instances globales définies dans app.dart - - // Récupérer tous les passages et les trier par date (les plus récents d'abord) - final allPassages = passageRepository.getAllPassages(); - allPassages.sort((a, b) => b.passedAt.compareTo(a.passedAt)); - - // Limiter aux 10 passages les plus récents - final recentPassagesModels = allPassages.take(10).toList(); - - // Convertir les modèles de passage au format attendu par le widget PassagesListWidget - final List> recentPassages = - recentPassagesModels.map((passage) { - // Construire l'adresse complète à partir des champs disponibles - final String address = - '${passage.numero} ${passage.rue}${passage.rueBis.isNotEmpty ? ' ${passage.rueBis}' : ''}, ${passage.ville}'; - - // Convertir le montant en double - final double amount = double.tryParse(passage.montant) ?? 0.0; - - return { - 'id': passage.id.toString(), - 'address': address, - 'amount': amount, - 'date': passage.passedAt, - 'type': passage.fkType, - 'payment': passage.fkTypeReglement, - 'name': passage.name, - 'notes': passage.remarque, - 'hasReceipt': passage.nomRecu.isNotEmpty, - 'hasError': passage.emailErreur.isNotEmpty, - 'fkUser': passage.fkUser, // Ajouter l'ID de l'utilisateur - }; - }).toList(); - - return Card( - elevation: 4, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Derniers passages', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - TextButton( - onPressed: () { - // Naviguer vers la page d'historique - }, - child: const Text('Voir tout'), - ), - ], - ), - ), - // Utilisation du widget commun PassagesListWidget - PassagesListWidget( - passages: recentPassages, - showFilters: false, - showSearch: false, - showActions: true, // Activer l'affichage des boutons d'action - maxPassages: 10, - // Exclure les passages de type 2 (À finaliser) - excludePassageTypes: [2], - // Filtrer par utilisateur courant - filterByUserId: userRepository.getCurrentUser()?.id, - // Période par défaut (derniers 15 jours) - periodFilter: 'last15', - onPassageSelected: (passage) { - // Action lors de la sélection d'un passage - debugPrint('Passage sélectionné: ${passage['id']}'); - }, - onDetailsView: (passage) { - // Action lors de l'affichage des détails - debugPrint('Affichage des détails: ${passage['id']}'); - }, - // Callback pour le bouton de modification - onPassageEdit: (passage) { - // Action lors de la modification d'un passage - debugPrint('Modification du passage: ${passage['id']}'); - // Ici, vous pourriez ouvrir un formulaire d'édition - }, - // Callback pour le bouton de reçu (uniquement pour les passages de type 1) - onReceiptView: (passage) { - // Action lors de la demande d'affichage du reçu - debugPrint('Affichage du reçu pour le passage: ${passage['id']}'); - // Ici, vous pourriez générer et afficher un PDF - }, - ), - ], - ), - ); - } -} diff --git a/flutt/lib/presentation/widgets/custom_text_field.dart b/flutt/lib/presentation/widgets/custom_text_field.dart deleted file mode 100644 index 589de433..00000000 --- a/flutt/lib/presentation/widgets/custom_text_field.dart +++ /dev/null @@ -1,134 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -class CustomTextField extends StatelessWidget { - final TextEditingController controller; - final String label; - final String? hintText; - final IconData? prefixIcon; - final Widget? suffixIcon; - final bool obscureText; - final TextInputType keyboardType; - final String? Function(String?)? validator; - final List? inputFormatters; - final int? maxLines; - final int? minLines; - final bool readOnly; - final VoidCallback? onTap; - final Function(String)? onChanged; - final bool autofocus; - final FocusNode? focusNode; - final String? errorText; - final Color? fillColor; - final String? helperText; - final Function(String)? onFieldSubmitted; - - const CustomTextField({ - super.key, - required this.controller, - required this.label, - this.hintText, - this.prefixIcon, - this.suffixIcon, - this.obscureText = false, - this.keyboardType = TextInputType.text, - this.validator, - this.inputFormatters, - this.maxLines = 1, - this.minLines, - this.readOnly = false, - this.onTap, - this.onChanged, - this.autofocus = false, - this.focusNode, - this.errorText, - this.fillColor, - this.helperText, - this.onFieldSubmitted, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (label.isNotEmpty) ...[ - Text( - label, - style: theme.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w500, - color: theme.colorScheme.onBackground, - ), - ), - const SizedBox(height: 8), - ], - TextFormField( - controller: controller, - obscureText: obscureText, - keyboardType: keyboardType, - validator: validator, - inputFormatters: inputFormatters, - maxLines: maxLines, - minLines: minLines, - readOnly: readOnly, - onTap: onTap, - onChanged: onChanged, - onFieldSubmitted: onFieldSubmitted, - autofocus: autofocus, - focusNode: focusNode, - style: theme.textTheme.bodyLarge?.copyWith( - color: theme.colorScheme.onBackground, - ), - decoration: InputDecoration( - hintText: hintText, - hintStyle: theme.textTheme.bodyLarge?.copyWith( - color: theme.colorScheme.onBackground.withOpacity(0.5), - ), - errorText: errorText, - helperText: helperText, - helperStyle: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onBackground.withOpacity(0.6), - ), - prefixIcon: prefixIcon != null - ? Icon(prefixIcon, color: theme.colorScheme.primary) - : null, - suffixIcon: suffixIcon, - fillColor: fillColor ?? theme.inputDecorationTheme.fillColor, - filled: true, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: theme.colorScheme.primary, - width: 2, - ), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: theme.colorScheme.error, - width: 2, - ), - ), - focusedErrorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: theme.colorScheme.error, - width: 2, - ), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 16, - ), - ), - ), - ], - ); - } -} diff --git a/flutt/lib/presentation/widgets/profile_dialog.dart b/flutt/lib/presentation/widgets/profile_dialog.dart deleted file mode 100644 index 7d3d0b58..00000000 --- a/flutt/lib/presentation/widgets/profile_dialog.dart +++ /dev/null @@ -1,356 +0,0 @@ -import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales -import 'package:flutter/material.dart'; -import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales -import 'package:geosector_app/core/repositories/user_repository.dart'; - -/// Widget de profil commun pour toute l'application -/// Affiche une boîte de dialogue modale avec un formulaire de mise à jour -/// des données utilisateur -class ProfileDialog extends StatefulWidget { - /// ID de l'utilisateur dont on veut afficher/modifier le profil - final String userId; - - const ProfileDialog({ - Key? key, - required this.userId, - }) : super(key: key); - - /// Affiche la boîte de dialogue de profil - static void show(BuildContext context, String userId) { - showDialog( - context: context, - builder: (context) => ProfileDialog(userId: userId), - ); - } - - @override - State createState() => _ProfileDialogState(); -} - -class _ProfileDialogState extends State { - /// Contrôleurs pour les champs du formulaire - final TextEditingController _firstNameController = TextEditingController(); - final TextEditingController _lastNameController = TextEditingController(); - final TextEditingController _emailController = TextEditingController(); - final TextEditingController _phoneController = TextEditingController(); - - /// État de chargement - bool _isLoading = true; - - /// État d'erreur - String? _errorMessage; - - @override - void initState() { - super.initState(); - _loadUserData(); - } - - @override - void dispose() { - _firstNameController.dispose(); - _lastNameController.dispose(); - _emailController.dispose(); - _phoneController.dispose(); - super.dispose(); - } - - /// Charge les données de l'utilisateur depuis l'API - Future _loadUserData() async { - try { - setState(() { - _isLoading = true; - _errorMessage = null; - }); - - // Utiliser l'instance globale définie dans app.dart - final user = userRepository.currentUser; - - // Si l'utilisateur est trouvé, remplir les champs du formulaire - if (user != null) { - _firstNameController.text = user.firstName ?? ''; - _lastNameController.text = user.name ?? ''; - _emailController.text = user.email ?? ''; - // Note: Utiliser la propriété appropriée pour le téléphone si elle existe - // ou laisser vide si elle n'existe pas - _phoneController.text = ''; // Champ laissé vide par défaut - } else { - _errorMessage = 'Utilisateur non trouvé'; - } - } catch (e) { - _errorMessage = 'Erreur lors du chargement des données: $e'; - } finally { - setState(() { - _isLoading = false; - }); - } - } - - /// Enregistre les modifications du profil - Future _saveProfile() async { - try { - setState(() { - _isLoading = true; - _errorMessage = null; - }); - - // Utiliser l'instance globale définie dans app.dart - - // Mettre à jour les données de l'utilisateur - // Note: Cette partie dépend de l'implémentation réelle du UserRepository - // et devrait être adaptée en fonction de l'API disponible - - // Récupérer l'utilisateur actuel - final user = userRepository.currentUser; - if (user != null) { - // Mettre à jour les propriétés de l'utilisateur - user.firstName = _firstNameController.text; - user.name = _lastNameController.text; - - // Sauvegarder les modifications - // Note: Utiliser la méthode appropriée du repository - // Exemple: userRepo.saveUser(user) ou userRepo.updateUser(user) - - // Pour l'instant, nous simulons une mise à jour réussie - // Cette partie devra être adaptée à l'API réelle - await Future.delayed(const Duration(milliseconds: 500)); - - // Fermer la boîte de dialogue - if (mounted) { - Navigator.of(context) - .pop(true); // Retourne true pour indiquer le succès - - // Afficher un message de succès - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Text('Profil mis à jour avec succès'), - backgroundColor: Theme.of(context).colorScheme.primary, - ), - ); - } - } else { - throw Exception('Utilisateur non trouvé'); - } - } catch (e) { - setState(() { - _errorMessage = 'Erreur lors de la mise à jour du profil: $e'; - _isLoading = false; - }); - } - } - - @override - Widget build(BuildContext context) { - final size = MediaQuery.of(context).size; - final theme = Theme.of(context); - - // Déterminer si nous sommes sur un appareil mobile ou un ordinateur de bureau - final isDesktop = size.width > 900; - - // Calculer la largeur de la boîte de dialogue - // 90% de la largeur de l'écran pour les mobiles - // 50% de la largeur de l'écran pour les ordinateurs de bureau (max 600px) - final dialogWidth = isDesktop - ? size.width * 0.5 > 600 - ? 600.0 - : size.width * 0.5 - : size.width * 0.9; - - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - // Définir la largeur de la boîte de dialogue - child: Container( - width: dialogWidth, - padding: const EdgeInsets.all(24), - child: _isLoading - ? const Center( - child: CircularProgressIndicator(), - ) - : _errorMessage != null - ? _buildErrorView() - : _buildProfileForm(), - ), - ); - } - - /// Construit la vue d'erreur - Widget _buildErrorView() { - final theme = Theme.of(context); - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.error_outline, - color: theme.colorScheme.error, - size: 48, - ), - const SizedBox(height: 16), - Text( - 'Erreur', - style: theme.textTheme.titleLarge?.copyWith( - color: theme.colorScheme.error, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Text( - _errorMessage ?? 'Une erreur inconnue est survenue', - textAlign: TextAlign.center, - style: theme.textTheme.bodyMedium, - ), - const SizedBox(height: 24), - ElevatedButton( - onPressed: () => Navigator.of(context).pop(), - style: ElevatedButton.styleFrom( - backgroundColor: theme.colorScheme.primary, - foregroundColor: theme.colorScheme.onPrimary, - ), - child: const Text('Fermer'), - ), - ], - ); - } - - /// Construit le formulaire de profil - Widget _buildProfileForm() { - final theme = Theme.of(context); - - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Titre du formulaire - Row( - children: [ - Icon( - Icons.person, - color: theme.colorScheme.primary, - size: 28, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Mon compte', - style: theme.textTheme.titleLarge?.copyWith( - color: theme.colorScheme.primary, - fontWeight: FontWeight.bold, - ), - ), - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.of(context).pop(), - tooltip: 'Fermer', - ), - ], - ), - const Divider(height: 32), - - // Formulaire - Form( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Prénom - TextFormField( - controller: _firstNameController, - decoration: const InputDecoration( - labelText: 'Prénom', - prefixIcon: Icon(Icons.person_outline), - border: OutlineInputBorder(), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Veuillez entrer votre prénom'; - } - return null; - }, - ), - const SizedBox(height: 16), - - // Nom - TextFormField( - controller: _lastNameController, - decoration: const InputDecoration( - labelText: 'Nom', - prefixIcon: Icon(Icons.person_outline), - border: OutlineInputBorder(), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Veuillez entrer votre nom'; - } - return null; - }, - ), - const SizedBox(height: 16), - - // Email - TextFormField( - controller: _emailController, - decoration: const InputDecoration( - labelText: 'Email', - prefixIcon: Icon(Icons.email_outlined), - border: OutlineInputBorder(), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Veuillez entrer votre email'; - } - if (!value.contains('@')) { - return 'Veuillez entrer un email valide'; - } - return null; - }, - ), - const SizedBox(height: 16), - - // Téléphone - TextFormField( - controller: _phoneController, - decoration: const InputDecoration( - labelText: 'Téléphone', - prefixIcon: Icon(Icons.phone_outlined), - border: OutlineInputBorder(), - ), - ), - ], - ), - ), - const SizedBox(height: 24), - - // Boutons d'action - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text( - 'Annuler', - style: TextStyle( - color: theme.colorScheme.error, - ), - ), - ), - const SizedBox(width: 16), - ElevatedButton( - onPressed: _saveProfile, - style: ElevatedButton.styleFrom( - backgroundColor: theme.colorScheme.primary, - foregroundColor: theme.colorScheme.onPrimary, - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 12, - ), - ), - child: const Text('Enregistrer'), - ), - ], - ), - ], - ); - } -} diff --git a/flutt/web/favicon.png b/flutt/web/favicon.png deleted file mode 100644 index 6a461647..00000000 Binary files a/flutt/web/favicon.png and /dev/null differ diff --git a/flutt/web/icons/Icon-192.png b/flutt/web/icons/Icon-192.png deleted file mode 100644 index a9bca263..00000000 Binary files a/flutt/web/icons/Icon-192.png and /dev/null differ diff --git a/flutt/web/icons/Icon-512.png b/flutt/web/icons/Icon-512.png deleted file mode 100644 index d8a1289d..00000000 Binary files a/flutt/web/icons/Icon-512.png and /dev/null differ diff --git a/flutt/web/icons/Icon-maskable-192.png b/flutt/web/icons/Icon-maskable-192.png deleted file mode 100644 index a9bca263..00000000 Binary files a/flutt/web/icons/Icon-maskable-192.png and /dev/null differ diff --git a/flutt/web/icons/Icon-maskable-512.png b/flutt/web/icons/Icon-maskable-512.png deleted file mode 100644 index d8a1289d..00000000 Binary files a/flutt/web/icons/Icon-maskable-512.png and /dev/null differ diff --git a/web/.env-deploy-geosector-dev b/web/.env-deploy-geosector-dev new file mode 100644 index 00000000..ff792e4f --- /dev/null +++ b/web/.env-deploy-geosector-dev @@ -0,0 +1,20 @@ +# Configuration du serveur hôte Debian 12 +HOST_SSH_HOST=195.154.80.116 # Adresse IP du serveur hôte +HOST_SSH_USER=pierre # Utilisateur SSH sur le serveur hôte +HOST_SSH_PORT=22 # Port SSH du serveur hôte +HOST_SSH_KEY=/Users/pierre/.ssh/id_rsa_mbpi # Clé SSH privée pour accéder au serveur hôte + +# Configuration du conteneur Incus hébergeant cette application +CT_PROJECT_NAME=default # Nom du projet Incus où se trouve le conteneur +CT_NAME=dva-geo # Nom du conteneur Incus +CT_IP=13.23.33.43 # IP interne du conteneur Incus +CT_SSH_USER=root # Utilisateur SSH dans le conteneur +CT_SSH_PORT=22 # Port SSH interne du conteneur +CT_SSH_KEY=/root/.ssh/id_rsa_in3_pierre # Clé SSH privée pour accéder au conteneur + +# Configuration de l'application +DOMAIN_NAME=dev.geosector.fr # Nom de domaine du site +SERVER_PORT=3000 # Port du serveur Node.js +ADMIN_PORT=3001 # Port du serveur d'administration +DEPLOY_DIR=/var/www # Répertoire de déploiement sur le conteneur +APP_NAME=geosector # Nom de l'application et du fichier de config nginx \ No newline at end of file diff --git a/web/deploy-web.sh b/web/deploy-web.sh new file mode 100755 index 00000000..633793c0 --- /dev/null +++ b/web/deploy-web.sh @@ -0,0 +1,197 @@ +#!/bin/bash + +# Script de déploiement de Geosector Web + +cd /Users/pierre/dev/geosector/web + +# Vérifier si .env.deploy existe +ENV_FILE=".env-deploy-geosector-dev" +if [ ! -f "$ENV_FILE" ]; then + echo "Erreur: Fichier $ENV_FILE introuvable!" + echo "Veuillez créer ce fichier avec vos informations de connexion." + exit 1 +fi + +# Charger les variables depuis .env.deploy +echo "Chargement des paramètres de déploiement..." +source "$ENV_FILE" + +# Vérifier que les variables nécessaires sont définies +if [ -z "$HOST_SSH_HOST" ] || [ -z "$HOST_SSH_USER" ] || [ -z "$CT_NAME" ] || [ -z "$CT_PROJECT_NAME" ]; then + echo "Erreur: Variables HOST_SSH_HOST, HOST_SSH_USER, CT_NAME et CT_PROJECT_NAME requises dans $ENV_FILE" + exit 1 +fi + +# Variables pour les alertes (optionnelles) +ALERT_EMAIL=${ALERT_EMAIL:-""} +DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL:-""} + +# Utiliser les valeurs par défaut si non définies +HOST_SSH_PORT=${HOST_SSH_PORT:-22} +SERVER_PORT=${SERVER_PORT:-3000} +ADMIN_PORT=${ADMIN_PORT:-3001} +DOMAIN_NAME=${DOMAIN_NAME:-$CT_IP} +DEPLOY_DIR=${DEPLOY_DIR:-/var/www} +APP_NAME=${APP_NAME:-d6soft} +SUB_DIR=${SUB_DIR:-web} + +# Afficher les paramètres +echo "=== Paramètres de déploiement ===" +echo "Serveur hôte: $HOST_SSH_USER@$HOST_SSH_HOST:$HOST_SSH_PORT" +echo "Projet Incus: $CT_PROJECT_NAME" +echo "Conteneur: $CT_NAME" +echo "Domaine: $DOMAIN_NAME" +echo "Répertoire de déploiement: $DEPLOY_DIR/$APP_NAME/$SUB_DIR" +echo "Déploiement du module d'administration: $([ "$DEPLOY_ADMIN" = true ] && echo "Oui" || echo "Non")" +echo "Installation des dépendances: $([ "$INSTALL_DEPENDENCIES" = true ] && echo "Oui" || echo "Non")" +echo "==================================" + +# Variables du projet +BUILD_DIR="dist" +SERVER_DIR="server" +LOCAL_DEPLOY_DIR="deploy" +DEPLOY_PACKAGE="$APP_NAME-deploy.tar.gz" + +# 1. Build du frontend principal +echo "=== Construction du frontend principal ===" +npm run build +# Vérifier si le build a réussi +BUILD_EXIT_CODE=$? +if [ $BUILD_EXIT_CODE -ne 0 ] || [ ! -d "$BUILD_DIR" ]; then + echo "==============================================" + echo "ERREUR CRITIQUE: Le build a échoué avec le code $BUILD_EXIT_CODE" + echo "==============================================" + + # Envoyer des alertes si configurées + if [ ! -z "$ALERT_EMAIL" ]; then + echo "Envoi d'une alerte par email à $ALERT_EMAIL..." + echo "Erreur de build pour $APP_NAME sur $HOST_SSH_HOST" | mail -s "[ALERTE] Échec de déploiement $APP_NAME" $ALERT_EMAIL + fi + + if [ ! -z "$DISCORD_WEBHOOK_URL" ]; then + echo "Envoi d'une alerte Discord..." + curl -H "Content-Type: application/json" \ + -d '{"content":"⚠️ **ALERTE: Échec de déploiement** ⚠️\nLe build de **'"$APP_NAME"'** a échoué avec le code '$BUILD_EXIT_CODE'.\nServeur: '"$HOST_SSH_HOST"'\nDate: '"$(date)"'"}' \ + $DISCORD_WEBHOOK_URL + fi + + echo "Le déploiement a été interrompu en raison d'erreurs dans le build." + exit 1 +fi + +# 3. Préparation du package de déploiement +echo "=== Préparation du package de déploiement ===" + +# Nettoyer et préparer les dossiers de déploiement +rm -rf $LOCAL_DEPLOY_DIR +mkdir -p $LOCAL_DEPLOY_DIR + +# Copier les fichiers frontend (build Svelte) +cp -r $BUILD_DIR/* $LOCAL_DEPLOY_DIR/ + +# Préparer le dossier serveur principal si nécessaire +if [ -d "$SERVER_DIR" ]; then + echo "Préparation du serveur principal..." + mkdir -p $LOCAL_DEPLOY_DIR/server + cp -r $SERVER_DIR/package.json $LOCAL_DEPLOY_DIR/server/ 2>/dev/null || echo "Warning: package.json du serveur principal non trouvé" + cp -r $SERVER_DIR/server.js $LOCAL_DEPLOY_DIR/server/ 2>/dev/null || echo "Warning: server.js du serveur principal non trouvé" + cp -r $SERVER_DIR/.env $LOCAL_DEPLOY_DIR/server/ 2>/dev/null || echo "Warning: .env du serveur principal non trouvé" + mkdir -p $LOCAL_DEPLOY_DIR/server/logs +fi + +# Créer un fichier tar.gz pour l'envoi +echo "Création du package de déploiement..." +COPYFILE_DISABLE=1 tar --exclude=".*" -czf $DEPLOY_PACKAGE $LOCAL_DEPLOY_DIR + +# Vérifier que le package a bien été créé +if [ ! -f "$DEPLOY_PACKAGE" ]; then + echo "ERREUR: Le fichier $DEPLOY_PACKAGE n'a pas été créé." + exit 1 +fi + +echo "Taille du package: $(du -h $DEPLOY_PACKAGE | cut -f1)" + +# Définir les options SSH +SSH_OPTS="-p $HOST_SSH_PORT" +SCP_OPTS="-P $HOST_SSH_PORT" +if [ ! -z "$HOST_SSH_KEY" ]; then + SSH_OPTS="$SSH_OPTS -i \"$HOST_SSH_KEY\"" + SCP_OPTS="$SCP_OPTS -i \"$HOST_SSH_KEY\"" +fi + +# 4. Copier le package sur le serveur hôte +echo "=== Copie des fichiers vers le serveur hôte ===" +eval "scp $SCP_OPTS $DEPLOY_PACKAGE $HOST_SSH_USER@$HOST_SSH_HOST:~/" + +# 5. Exécuter les commandes sur l'hôte et le conteneur +echo "=== Déploiement sur le conteneur $CT_NAME ===" + +# Vérifier que le fichier est bien arrivé +eval "ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST \"if [ ! -f '$DEPLOY_PACKAGE' ]; then echo 'ERREUR: Fichier non transféré'; exit 1; fi\"" + +# Déplacer le fichier vers /tmp +echo "Déplacement du package vers /tmp..." +eval "ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST \"cp $DEPLOY_PACKAGE /tmp/\"" + +# Sélectionner le projet Incus +echo "Sélection du projet Incus $CT_PROJECT_NAME..." +eval "ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST \"sudo incus project switch $CT_PROJECT_NAME\"" + +# Transférer le package vers le conteneur +echo "Transfert du package vers le conteneur..." +eval "ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST \"sudo incus file push /tmp/$DEPLOY_PACKAGE $CT_NAME/$DEPLOY_DIR/\"" + +# Créer le répertoire de déploiement dans le conteneur +echo "Création du répertoire de déploiement dans le conteneur..." +eval "ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST \"sudo incus exec $CT_NAME -- mkdir -p $DEPLOY_DIR/$APP_NAME/$SUB_DIR\"" + +# Extraire le package +echo "Extraction du package..." +eval "ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST \"sudo incus exec $CT_NAME -- tar -xzf $DEPLOY_DIR/$DEPLOY_PACKAGE -C $DEPLOY_DIR/$APP_NAME/$SUB_DIR --strip-components=1\"" + +# Installer les dépendances du serveur principal (si présent) +echo "Installation des dépendances du serveur principal (si présent)..." +eval "ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST \"sudo incus exec $CT_NAME -- sh -c 'if [ -d $DEPLOY_DIR/$APP_NAME/$SUB_DIR/server ] && [ -f $DEPLOY_DIR/$APP_NAME/$SUB_DIR/server/package.json ]; then cd $DEPLOY_DIR/$APP_NAME/$SUB_DIR/server && npm install --production; else echo \"Dossier serveur ou package.json non trouvé, cette étape est ignorée\"; fi'\"" + +# Nettoyer les fichiers macOS +echo "Nettoyage des fichiers macOS..." +eval "ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST \"sudo incus exec $CT_NAME -- sh -c 'find $DEPLOY_DIR/$APP_NAME/$SUB_DIR -name \"._*\" -type f -delete 2>/dev/null || true'\"" + +# Configurer les permissions +echo "Configuration des permissions..." +# Vérifier si l'utilisateur et le groupe www-data existent, sinon les créer +eval "ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST \"sudo incus exec $CT_NAME -- sh -c 'getent group www-data > /dev/null || addgroup -S www-data; getent passwd www-data > /dev/null || adduser -S -D -H -h /var/www -s /sbin/nologin -G www-data -g www-data www-data'\"" + +# Appliquer les permissions sur tous les fichiers +eval "ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST \"sudo incus exec $CT_NAME -- sh -c 'chown -R www-data:www-data $DEPLOY_DIR/$APP_NAME/$SUB_DIR && \ + find $DEPLOY_DIR/$APP_NAME/$SUB_DIR -type d -exec chmod 755 {} \\; && \ + find $DEPLOY_DIR/$APP_NAME/$SUB_DIR -type f -exec chmod 644 {} \\; && \ + if [ -f $DEPLOY_DIR/$APP_NAME/$SUB_DIR/server/server.js ]; then chmod +x $DEPLOY_DIR/$APP_NAME/$SUB_DIR/server/server.js; fi && \ + if [ -f $DEPLOY_DIR/$APP_NAME/$SUB_DIR/mda/backend/server.js ]; then chmod +x $DEPLOY_DIR/$APP_NAME/$SUB_DIR/mda/backend/server.js; fi && \ + if [ -d $DEPLOY_DIR/$APP_NAME/$SUB_DIR/server/logs ]; then chmod 775 $DEPLOY_DIR/$APP_NAME/$SUB_DIR/server/logs; fi && \ + if [ -d $DEPLOY_DIR/$APP_NAME/$SUB_DIR/mda/backend/logs ]; then chmod 775 $DEPLOY_DIR/$APP_NAME/$SUB_DIR/mda/backend/logs; fi && \ + if [ -d $DEPLOY_DIR/$APP_NAME/$SUB_DIR/mda/db ]; then chmod 775 $DEPLOY_DIR/$APP_NAME/$SUB_DIR/mda/db; fi'\"" + +# Nettoyer les fichiers temporaires +echo "Nettoyage des fichiers temporaires..." +eval "ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST \"sudo incus exec $CT_NAME -- rm -f $DEPLOY_DIR/$DEPLOY_PACKAGE && rm -f /tmp/$DEPLOY_PACKAGE && rm -f $DEPLOY_PACKAGE\"" + +echo "===================================================" +echo "Déploiement terminé avec succès !" +echo "===================================================" +echo "Votre site $APP_NAME est maintenant déployé dans le conteneur $CT_NAME." +echo "Chemin de déploiement: $DEPLOY_DIR/$APP_NAME/$SUB_DIR" + +# Afficher le statut du déploiement +if [ -d "$SERVER_DIR" ]; then + echo "✅ Le service du site principal a été configuré et démarré." +else + echo "ℹ️ Aucun service principal n'a été configuré (le site est statique)." +fi + +echo "" +echo "Pour configurer nginx sur le serveur, connectez-vous et exécutez :" +echo "ssh $HOST_SSH_USER@$HOST_SSH_HOST" +echo "sudo incus exec $CT_NAME bash" +echo "echo "rc-service nginx restart" +echo "===================================================" \ No newline at end of file diff --git a/web/deploy/assets/index-C15RTrFl.css b/web/deploy/assets/index-C15RTrFl.css new file mode 100644 index 00000000..a64df1c8 --- /dev/null +++ b/web/deploy/assets/index-C15RTrFl.css @@ -0,0 +1 @@ +*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Figtree,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.-bottom-4{bottom:-1rem}.-right-4{right:-1rem}.right-0{right:0}.top-0{top:0}.z-10{z-index:10}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[-10\]{z-index:-10}.z-\[-20\]{z-index:-20}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-10{margin-bottom:2.5rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-2{margin-left:.5rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-4{margin-right:1rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-\[300px\]{height:300px}.h-\[400px\]{height:400px}.h-screen{height:100vh}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-4{width:1rem}.w-4\/5{width:80%}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-\[150px\]{width:150px}.w-full{width:100%}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-xs{max-width:20rem}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-10{--tw-translate-x: 2.5rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-full{--tw-translate-x: 100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-0{--tw-translate-y: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-10{--tw-translate-y: 2.5rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-12{gap:3rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1rem * var(--tw-space-x-reverse));margin-left:calc(1rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-6>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1.5rem * var(--tw-space-x-reverse));margin-left:calc(1.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b-2{border-bottom-width:2px}.border-t{border-top-width:1px}.border-\[\#002C66\]{--tw-border-opacity: 1;border-color:rgb(0 44 102 / var(--tw-border-opacity, 1))}.border-\[\#4CAF50\]{--tw-border-opacity: 1;border-color:rgb(76 175 80 / var(--tw-border-opacity, 1))}.border-blue-300{--tw-border-opacity: 1;border-color:rgb(147 197 253 / var(--tw-border-opacity, 1))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}.border-green-400{--tw-border-opacity: 1;border-color:rgb(74 222 128 / var(--tw-border-opacity, 1))}.border-red-600{--tw-border-opacity: 1;border-color:rgb(220 38 38 / var(--tw-border-opacity, 1))}.bg-\[\#E3170A\]{--tw-bg-opacity: 1;background-color:rgb(227 23 10 / var(--tw-bg-opacity, 1))}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity, 1))}.bg-blue-200{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity, 1))}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-blue-900{--tw-bg-opacity: 1;background-color:rgb(30 58 138 / var(--tw-bg-opacity, 1))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-opacity-50{--tw-bg-opacity: .5}.bg-opacity-70{--tw-bg-opacity: .7}.bg-opacity-90{--tw-bg-opacity: .9}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.from-blue-900{--tw-gradient-from: #1e3a8a var(--tw-gradient-from-position);--tw-gradient-to: rgb(30 58 138 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-white{--tw-gradient-to: #fff var(--tw-gradient-to-position)}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pl-6{padding-left:1.5rem}.pt-4{padding-top:1rem}.pt-8{padding-top:2rem}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.italic{font-style:italic}.not-italic{font-style:normal}.text-\[\#002C66\]{--tw-text-opacity: 1;color:rgb(0 44 102 / var(--tw-text-opacity, 1))}.text-\[\#4CAF50\]{--tw-text-opacity: 1;color:rgb(76 175 80 / var(--tw-text-opacity, 1))}.text-blue-200{--tw-text-opacity: 1;color:rgb(191 219 254 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-green-700{--tw-text-opacity: 1;color:rgb(21 128 61 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.opacity-0{opacity:0}.opacity-100{opacity:1}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-blue-500\/20{--tw-shadow-color: rgb(59 130 246 / .2);--tw-shadow: var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.delay-300{transition-delay:.3s}.delay-500{transition-delay:.5s}.delay-700{transition-delay:.7s}.duration-300{transition-duration:.3s}.duration-700{transition-duration:.7s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}@font-face{font-family:Figtree;src:url(/fonts/Figtree-VariableFont_wght.ttf) format("truetype");font-weight:100 900;font-style:normal;font-display:swap}:root{font-family:Figtree,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif;line-height:1.5;font-weight:400;color:#333;background-color:#fff;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}html,body{height:100%;margin:0;padding:0;scroll-behavior:smooth}body{min-width:320px;min-height:100vh}#app{display:flex;flex-direction:column;min-height:100vh}.aspect-w-16{position:relative;padding-bottom:calc(var(--tw-aspect-h) / var(--tw-aspect-w) * 100%);--tw-aspect-w: 16}.aspect-h-9{--tw-aspect-h: 9}.aspect-w-16>*{position:absolute;height:100%;width:100%;top:0;right:0;bottom:0;left:0}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.animate-fadeIn{animation:fadeIn .5s ease-in-out}.container{width:100%;max-width:1280px;margin-left:auto;margin-right:auto}*:focus-visible{outline:2px solid #3b82f6;outline-offset:2px}@media (max-width: 640px){h1{font-size:2rem!important}h2{font-size:1.5rem!important}h3{font-size:1.25rem!important}}.blur-effect{filter:blur(4px);pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}@keyframes slideUp{0%{transform:translateY(20px);opacity:0}to{transform:translateY(0);opacity:1}}.cookie-modal{animation:slideUp .3s ease-out forwards}.hover\:scale-105:hover{--tw-scale-x: 1.05;--tw-scale-y: 1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:bg-\[\#4CAF50\]:hover{--tw-bg-opacity: 1;background-color:rgb(76 175 80 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-200:hover{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-700:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-300:hover{--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-700:hover{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.hover\:bg-red-600:hover{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.hover\:bg-red-700:hover{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity, 1))}.hover\:text-blue-300:hover{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.hover\:text-blue-500:hover{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-blue-500:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-blue-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}@media (min-width: 640px){.sm\:flex-row{flex-direction:row}}@media (min-width: 768px){.md\:mb-0{margin-bottom:0}.md\:mr-6{margin-right:1.5rem}.md\:h-\[400px\]{height:400px}.md\:h-\[500px\]{height:500px}.md\:w-1\/2{width:50%}.md\:w-\[200px\]{width:200px}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:justify-end{justify-content:flex-end}.md\:pr-8{padding-right:2rem}.md\:text-5xl{font-size:3rem;line-height:1}}@media (min-width: 1280px){.xl\:flex{display:flex}.xl\:hidden{display:none}} diff --git a/web/deploy/assets/index-mT7N7BKi.js b/web/deploy/assets/index-mT7N7BKi.js new file mode 100644 index 00000000..8ff564f4 --- /dev/null +++ b/web/deploy/assets/index-mT7N7BKi.js @@ -0,0 +1,65 @@ +(function(){const s=document.createElement("link").relList;if(s&&s.supports&&s.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))i(o);new MutationObserver(o=>{for(const n of o)if(n.type==="childList")for(const l of n.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&i(l)}).observe(document,{childList:!0,subtree:!0});function t(o){const n={};return o.integrity&&(n.integrity=o.integrity),o.referrerPolicy&&(n.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?n.credentials="include":o.crossOrigin==="anonymous"?n.credentials="omit":n.credentials="same-origin",n}function i(o){if(o.ep)return;o.ep=!0;const n=t(o);fetch(o.href,n)}})();const Tt=!1;var _t=Array.isArray,Ps=Array.prototype.indexOf,Ns=Array.from,Us=Object.defineProperty,De=Object.getOwnPropertyDescriptor,Qt=Object.getOwnPropertyDescriptors,Ds=Object.prototype,Vs=Array.prototype,yt=Object.getPrototypeOf,Pt=Object.isExtensible;function Is(e){return e()}function Qe(e){for(var s=0;s{i.d=!0})}function ee(e){const s=q;if(s!==null){const l=s.e;if(l!==null){var t=C,i=_;s.e=null;try{for(var o=0;o{var r=_;ne(n);var d=u();return ne(r),d};return i&&t.set("length",pe(e.length)),new Proxy(e,{defineProperty(u,r,d){(!("value"in d)||d.configurable===!1||d.enumerable===!1||d.writable===!1)&&Ks();var f=t.get(r);return f===void 0?(f=l(()=>pe(d.value)),t.set(r,f)):k(f,l(()=>Ae(d.value))),!0},deleteProperty(u,r){var d=t.get(r);if(d===void 0)r in u&&(t.set(r,l(()=>pe(I))),dt(o));else{if(i&&typeof r=="string"){var f=t.get("length"),p=Number(r);Number.isInteger(p)&&ppe(Ae(p?u[r]:I))),t.set(r,f)),f!==void 0){var v=a(f);return v===I?void 0:v}return Reflect.get(u,r,d)},getOwnPropertyDescriptor(u,r){var d=Reflect.getOwnPropertyDescriptor(u,r);if(d&&"value"in d){var f=t.get(r);f&&(d.value=a(f))}else if(d===void 0){var p=t.get(r),v=p==null?void 0:p.v;if(p!==void 0&&v!==I)return{enumerable:!0,configurable:!0,value:v,writable:!0}}return d},has(u,r){var v;if(r===Ve)return!0;var d=t.get(r),f=d!==void 0&&d.v!==I||Reflect.has(u,r);if(d!==void 0||C!==null&&(!f||(v=De(u,r))!=null&&v.writable)){d===void 0&&(d=l(()=>pe(f?Ae(u[r]):I)),t.set(r,d));var p=a(d);if(p===I)return!1}return f},set(u,r,d,f){var w;var p=t.get(r),v=r in u;if(i&&r==="length")for(var h=d;hpe(I)),t.set(h+"",b))}p===void 0?(!v||(w=De(u,r))!=null&&w.writable)&&(p=l(()=>pe(void 0)),k(p,l(()=>Ae(d))),t.set(r,p)):(v=p.v!==I,k(p,l(()=>Ae(d))));var x=Reflect.getOwnPropertyDescriptor(u,r);if(x!=null&&x.set&&x.set.call(f,d),!v){if(i&&typeof r=="string"){var A=t.get("length"),g=Number(r);Number.isInteger(g)&&g>=A.v&&k(A,g+1)}dt(o)}return!0},ownKeys(u){a(o);var r=Reflect.ownKeys(u).filter(p=>{var v=t.get(p);return v===void 0||v.v!==I});for(var[d,f]of t)f.v!==I&&!(d in u)&&r.push(d);return r},setPrototypeOf(){Ys()}})}function dt(e,s=1){k(e,e.v+s)}function qt(e){var s=Q|oe,t=_!==null&&(_.f&Q)!==0?_:null;return C===null||t!==null&&(t.f&R)!==0?s|=R:C.f|=Xt,{ctx:q,deps:null,effects:null,equals:es,f:s,fn:e,reactions:null,rv:0,v:null,wv:0,parent:t??C}}function me(e){const s=qt(e);return s.equals=ts,s}function is(e){var s=e.effects;if(s!==null){e.effects=null;for(var t=0;ta(e))),s}function k(e,s,t=!1){_!==null&&!ie&&$e()&&(_.f&(Q|kt))!==0&&!(D!=null&&D.includes(e))&&Zs();let i=t?Ae(s):s;return ii(e,i)}function ii(e,s){if(!e.equals(s)){var t=e.v;He?Fe.set(e,s):Fe.set(e,t),e.v=s,(e.f&Q)!==0&&((e.f&oe)!==0&&os(e),J(e,(e.f&R)===0?F:_e)),e.wv=_s(),rs(e,oe),$e()&&C!==null&&(C.f&F)!==0&&(C.f&(le|Le))===0&&(K===null?hi([e]):K.push(e))}return s}function rs(e,s){var t=e.reactions;if(t!==null)for(var i=$e(),o=t.length,n=0;nnew Promise(i=>{t.outro?gt(s,()=>{we(s),i(void 0)}):(we(s),i(void 0))})}function ps(e){return Me(Jt,e,!1)}function St(e){return Me(ot,e,!0)}function ae(e,s=[],t=qt){const i=s.map(t);return vs(()=>e(...i.map(a)))}function vs(e,s=0){return Me(ot|kt|s,e,!0)}function ht(e,s=!0){return Me(ot|le,e,!0,s)}function fs(e){var s=e.teardown;if(s!==null){const t=He,i=_;It(!0),ne(null);try{s.call(null)}finally{It(t),ne(i)}}}function ms(e,s=!1){var t=e.first;for(e.first=e.last=null;t!==null;){var i=t.next;(t.f&Le)!==0?t.parent=null:we(t,s),t=i}}function ci(e){for(var s=e.first;s!==null;){var t=s.next;(s.f&le)===0&&we(s),s=t}}function we(e,s=!0){var t=!1;(s||(e.f&Os)!==0)&&e.nodes_start!==null&&(di(e.nodes_start,e.nodes_end),t=!0),ms(e,s&&!t),it(e,0),J(e,nt);var i=e.transitions;if(i!==null)for(const n of i)n.stop();fs(e);var o=e.parent;o!==null&&o.first!==null&&hs(e),e.next=e.prev=e.teardown=e.ctx=e.deps=e.fn=e.nodes_start=e.nodes_end=null}function di(e,s){for(;e!==null;){var t=e===s?null:Et(e);e.remove(),e=t}}function hs(e){var s=e.parent,t=e.prev,i=e.next;t!==null&&(t.next=i),i!==null&&(i.prev=t),s!==null&&(s.first===e&&(s.first=i),s.last===e&&(s.last=t))}function gt(e,s){var t=[];gs(e,t,!0),pi(t,()=>{we(e),s&&s()})}function pi(e,s){var t=e.length;if(t>0){var i=()=>--t||s();for(var o of e)o.out(i)}else s()}function gs(e,s,t){if((e.f&Ee)===0){if(e.f^=Ee,e.transitions!==null)for(const l of e.transitions)(l.is_global||t)&&s.push(l);for(var i=e.first;i!==null;){var o=i.next,n=(i.f&Ct)!==0||(i.f&le)!==0;gs(i,s,n?t:!1),i=o}}}function Dt(e){bs(e,!0)}function bs(e,s){if((e.f&Ee)!==0){e.f^=Ee,(e.f&F)===0&&(e.f^=F),We(e)&&(J(e,oe),lt(e));for(var t=e.first;t!==null;){var i=t.next,o=(t.f&Ct)!==0||(t.f&le)!==0;bs(t,o?s:!1),t=i}if(e.transitions!==null)for(const n of e.transitions)(n.is_global||s)&&n.in()}}let Oe=[],bt=[];function xs(){var e=Oe;Oe=[],Qe(e)}function vi(){var e=bt;bt=[],Qe(e)}function fi(e){Oe.length===0&&queueMicrotask(xs),Oe.push(e)}function Vt(){Oe.length>0&&xs(),bt.length>0&&vi()}let Ze=!1,et=!1,tt=null,xe=!1,He=!1;function It(e){He=e}let Ie=[];let _=null,ie=!1;function ne(e){_=e}let C=null;function fe(e){C=e}let D=null;function mi(e){_!==null&&_.f&ft&&(D===null?D=[e]:D.push(e))}let U=null,B=0,K=null;function hi(e){K=e}let ws=1,st=0,ve=!1;function _s(){return++ws}function We(e){var p;var s=e.f;if((s&oe)!==0)return!0;if((s&_e)!==0){var t=e.deps,i=(s&R)!==0;if(t!==null){var o,n,l=(s&Xe)!==0,u=i&&C!==null&&!ve,r=t.length;if(l||u){var d=e,f=d.parent;for(o=0;oe.wv)return!0}(!i||C!==null&&!ve)&&J(e,F)}return!1}function gi(e,s){for(var t=s;t!==null;){if((t.f&Je)!==0)try{t.fn(e);return}catch{t.f^=Je}t=t.parent}throw Ze=!1,e}function Ft(e){return(e.f&nt)===0&&(e.parent===null||(e.parent.f&Je)===0)}function rt(e,s,t,i){if(Ze){if(t===null&&(Ze=!1),Ft(s))throw e;return}if(t!==null&&(Ze=!0),gi(e,s),Ft(s))throw e}function ys(e,s,t=!0){var i=e.reactions;if(i!==null)for(var o=0;o0)for(p.length=B+U.length,v=0;v0;){s++>1e3&&xi();var t=Ie,i=t.length;Ie=[];for(var o=0;o0;)et=!0,Cs(),Vt();return s}async function ki(){await Promise.resolve(),yi()}function a(e){var s=e.f,t=(s&Q)!==0;if(_!==null&&!ie){if(!(D!=null&&D.includes(e))){var i=_.deps;e.rv{Promise.resolve().then(()=>{var s;if(!e.defaultPrevented)for(const t of e.target.elements)(s=t.__on_r)==null||s.call(t)})},{capture:!0}))}function qs(e){var s=_,t=C;ne(null),fe(null);try{return e()}finally{ne(s),fe(t)}}function As(e,s,t,i=t){e.addEventListener(s,()=>qs(t));const o=e.__on_r;o?e.__on_r=()=>{o(),i(!0)}:e.__on_r=()=>i(!0),Si()}const Li=new Set,Bt=new Set;function Mi(e,s,t,i={}){function o(n){if(i.capture||Ue.call(s,n),!n.cancelBubble)return qs(()=>t==null?void 0:t.call(this,n))}return e.startsWith("pointer")||e.startsWith("touch")||e==="wheel"?fi(()=>{s.addEventListener(e,o,i)}):s.addEventListener(e,o,i),o}function y(e,s,t,i,o){var n={capture:i,passive:o},l=Mi(e,s,t,n);(s===document.body||s===window||s===document)&&ds(()=>{s.removeEventListener(e,l,n)})}function Ue(e){var w;var s=this,t=s.ownerDocument,i=e.type,o=((w=e.composedPath)==null?void 0:w.call(e))||[],n=o[0]||e.target,l=0,u=e.__root;if(u){var r=o.indexOf(u);if(r!==-1&&(s===document||s===window)){e.__root=s;return}var d=o.indexOf(s);if(d===-1)return;r<=d&&(l=r)}if(n=o[l]||e.target,n!==s){Us(e,"currentTarget",{configurable:!0,get(){return n||t}});var f=_,p=C;ne(null),fe(null);try{for(var v,h=[];n!==null;){var b=n.assignedSlot||n.parentNode||n.host||null;try{var x=n["__"+i];if(x!=null&&(!n.disabled||e.target===n))if(_t(x)){var[A,...g]=x;A.apply(n,[e,...g])}else x.call(n,e)}catch(S){v?h.push(S):v=S}if(e.cancelBubble||b===s||b===null)break;n=b}if(v){for(let S of h)queueMicrotask(()=>{throw S});throw v}}finally{e.__root=s,delete e.currentTarget,ne(f),fe(p)}}}function Es(e){var s=document.createElement("template");return s.innerHTML=e,s.content}function wt(e,s){var t=C;t.nodes_start===null&&(t.nodes_start=e,t.nodes_end=s)}function O(e,s){var t=(s&Xs)!==0,i=(s&ei)!==0,o,n=!e.startsWith("");return()=>{o===void 0&&(o=Es(n?e:""+e),t||(o=Se(o)));var l=i||ls?document.importNode(o,!0):o.cloneNode(!0);if(t){var u=Se(l),r=l.lastChild;wt(u,r)}else wt(l,l);return l}}function Ss(e,s,t="svg"){var i=!e.startsWith(""),o=`<${t}>${i?e:""+e}`,n;return()=>{if(!n){var l=Es(o),u=Se(l);n=Se(u)}var r=n.cloneNode(!0);return wt(r,r),r}}function P(e,s){e!==null&&e.before(s)}function at(e,s){var t=s==null?"":typeof s=="object"?s+"":s;t!==(e.__t??(e.__t=e.nodeValue))&&(e.__t=t,e.nodeValue=t+"")}function zi(e,s){return Gi(e,s)}const Ce=new Map;function Gi(e,{target:s,anchor:t,props:i={},events:o,context:n,intro:l=!0}){oi();var u=new Set,r=p=>{for(var v=0;v{var p=t??s.appendChild(ni());return ht(()=>{if(n){X({});var v=q;v.c=n}o&&(i.$$events=o),d=e(p,i)||{},n&&ee()}),()=>{var b;for(var v of u){s.removeEventListener(v,Ue);var h=Ce.get(v);--h===0?(document.removeEventListener(v,Ue),Ce.delete(v)):Ce.set(v,h)}Bt.delete(r),p!==t&&((b=p.parentNode)==null||b.removeChild(p))}});return ji.set(d,f),d}let ji=new WeakMap;function se(e,s,[t,i]=[0,0]){var o=e,n=null,l=null,u=I,r=t>0?Ct:0,d=!1;const f=(v,h=!0)=>{d=!0,p(h,v)},p=(v,h)=>{u!==(u=v)&&(u?(n?Dt(n):h&&(n=ht(()=>h(o))),l&>(l,()=>{l=null})):(l?Dt(l):h&&(l=ht(()=>h(o,[t+1,i]))),n&>(n,()=>{n=null})))};vs(()=>{d=!1,s(f),d||p(null,null)},r)}const Rt=[...` +\r\f \v\uFEFF`];function Ti(e,s,t){var i=e==null?"":""+e;if(s&&(i=i?i+" "+s:s),t){for(var o in t)if(t[o])i=i?i+" "+o:o;else if(i.length)for(var n=o.length,l=0;(l=i.indexOf(o,l))>=0;){var u=l+n;(l===0||Rt.includes(i[l-1]))&&(u===i.length||Rt.includes(i[u]))?i=(l===0?"":i.substring(0,l))+i.substring(u+1):l=u}}return i===""?null:i}function E(e,s,t,i,o,n){var l=e.__className;if(l!==t||l===void 0){var u=Ti(t,i,n);u==null?e.removeAttribute("class"):e.className=u,e.__className=t}else if(n&&o!==n)for(var r in n){var d=!!n[r];(o==null||d!==!!o[r])&&e.classList.toggle(r,d)}return n}const Pi=Symbol("is custom element"),Ni=Symbol("is html");function qe(e,s,t,i){var o=Ui(e);o[s]!==(o[s]=t)&&(t==null?e.removeAttribute(s):typeof t!="string"&&Di(e).includes(s)?e[s]=t:e.setAttribute(s,t))}function Ui(e){return e.__attributes??(e.__attributes={[Pi]:e.nodeName.includes("-"),[Ni]:e.namespaceURI===ti})}var $t=new Map;function Di(e){var s=$t.get(e.nodeName);if(s)return s;$t.set(e.nodeName,s=[]);for(var t,i=e,o=Element.prototype;o!==i;){t=Qt(i);for(var n in t)t[n].set&&s.push(n);i=yt(i)}return s}function Ne(e,s,t=s){var i=$e();As(e,"input",o=>{var n=o?e.defaultValue:e.value;if(n=pt(e)?vt(n):n,t(n),i&&n!==(n=s())){var l=e.selectionStart,u=e.selectionEnd;e.value=n??"",u!==null&&(e.selectionStart=l,e.selectionEnd=Math.min(u,e.value.length))}}),Ke(s)==null&&e.value&&t(pt(e)?vt(e.value):e.value),St(()=>{var o=s();pt(e)&&o===vt(e.value)||e.type==="date"&&!o&&!e.value||o!==e.value&&(e.value=o??"")})}function Vi(e,s,t=s){As(e,"change",i=>{var o=i?e.defaultChecked:e.checked;t(o)}),Ke(s)==null&&t(e.checked),St(()=>{var i=s();e.checked=!!i})}function pt(e){var s=e.type;return s==="number"||s==="range"}function vt(e){return e===""?null:+e}function T(e){return function(...s){var t=s[0];return t.preventDefault(),e==null?void 0:e.apply(this,s)}}function re(e=!1){const s=q,t=s.l.u;if(!t)return;let i=()=>qi(s.s);if(e){let o=0,n={};const l=qt(()=>{let u=!1;const r=s.s;for(const d in r)r[d]!==n[d]&&(n[d]=r[d],u=!0);return u&&o++,o});i=()=>a(l)}t.b.length&&ai(()=>{Ht(s,i),Qe(t.b)}),mt(()=>{const o=Ke(()=>t.m.map(Is));return()=>{for(const n of o)typeof n=="function"&&n()}}),t.a.length&&mt(()=>{Ht(s,i),Qe(t.a)})}function Ht(e,s){if(e.l.s)for(const t of e.l.s)a(t);s()}function he(e){q===null&&ss(),Re&&q.l!==null?Oi(q).m.push(e):mt(()=>{const s=Ke(e);if(typeof s=="function")return s})}function Ii(e,s,{bubbles:t=!1,cancelable:i=!1}={}){return new CustomEvent(e,{detail:s,bubbles:t,cancelable:i})}function Fi(){const e=q;return e===null&&ss(),(s,t,i)=>{var n;const o=(n=e.s.$$events)==null?void 0:n[s];if(o){const l=_t(o)?o.slice():[o],u=Ii(s,t,i);for(const r of l)r.call(e.x,u);return!u.defaultPrevented}return!0}}function Oi(e){var s=e.l;return s.u??(s.u={a:[],b:[],m:[]})}const Bi="5";var Zt;typeof window<"u"&&((Zt=window.__svelte??(window.__svelte={})).v??(Zt.v=new Set)).add(Bi);Js();var Ri=Ss(''),$i=Ss(''),Hi=O('
'),Wi=O(`
`);function Ki(e,s){X(s,!1);let t=$(window.location.hash.slice(1)||"accueil"),i=$(!1),o=$("");function n(){const L=window.location.hostname;let j="";L==="dev.geosector.fr"||L.includes("localhost")?j="dapp":L==="rec.geosector.fr"?j="rapp":j="app";const de=L.split(".");if(de.length>=2){const Ye=de.slice(Math.max(de.length-2,0)).join(".");return`https://${j}.${Ye}`}return`https://${j}.geosector.fr`}function l(L){k(t,L),window.history.pushState({},"",`/${L}`),r()}function u(){k(i,!a(i))}function r(){k(i,!1)}typeof window<"u"&&window.addEventListener("popstate",()=>{k(t,window.location.pathname.slice(1)||"accueil")}),he(()=>{k(o,n());const L=j=>{const de=document.getElementById("mobile-menu"),Ye=document.getElementById("burger-button");a(i)&&de&&Ye&&!de.contains(j.target)&&!Ye.contains(j.target)&&r()};return document.addEventListener("click",L),()=>{document.removeEventListener("click",L)}}),re();var d=Wi(),f=c(d),p=c(f),v=c(p),h=c(v),b=c(h),x=m(v,2),A=c(x),g=c(A);{var w=L=>{var j=Ri();P(L,j)},S=L=>{var j=$i();P(L,j)};se(g,L=>{a(i)?L(w):L(S,!1)})}var M=m(x,2),z=c(M),Y=c(z),N=c(Y),G=c(N),H=m(N,2),ge=c(H),ze=m(H,2),ye=c(ze),te=m(z,2),ue=c(te),ce=m(ue,2),Ge=m(ce,2),W=m(f,2),je=c(W),be=c(je),Te=c(be),Z=m(be,2),Pe=c(Z),V=c(Pe),Mt=c(V),zt=m(V,2),Gt=c(zt),Ms=m(zt,2),jt=c(Ms),zs=m(Z,2),ut=c(zs),ct=m(ut,2),Gs=m(ct,2),js=m(W,2);{var Ts=L=>{var j=Hi();y("click",j,r),y("keydown",j,de=>de.key==="Escape"&&r()),P(L,j)};se(js,L=>{a(i)&&L(Ts)})}ae(()=>{E(G,1,`text-[#002C66] hover:text-blue-500 transition-colors ${a(t)==="accueil"?"font-bold border-b-2 border-[#002C66]":""}`),E(ge,1,`text-[#002C66] hover:text-blue-500 transition-colors ${a(t)==="fonctionnalites"?"font-bold border-b-2 border-[#002C66]":""}`),E(ye,1,`text-[#002C66] hover:text-blue-500 transition-colors ${a(t)==="contact"?"font-bold border-b-2 border-[#002C66]":""}`),qe(ue,"href",`${a(o)??""}/login/user`),qe(ce,"href",`${a(o)??""}/login`),qe(Ge,"href",`${a(o)??""}/register`),E(W,1,`fixed top-0 right-0 h-screen w-4/5 max-w-xs bg-white shadow-lg transform transition-transform duration-300 ease-in-out z-40 ${a(i)?"translate-x-0":"translate-x-full"}`),E(Mt,1,`block text-lg text-[#002C66] hover:text-blue-500 transition-colors ${a(t)==="accueil"?"font-bold":""}`),E(Gt,1,`block text-lg text-[#002C66] hover:text-blue-500 transition-colors ${a(t)==="fonctionnalites"?"font-bold":""}`),E(jt,1,`block text-lg text-[#002C66] hover:text-blue-500 transition-colors ${a(t)==="contact"?"font-bold":""}`),qe(ut,"href",`${a(o)??""}/login/user`),qe(ct,"href",`${a(o)??""}/login`),qe(Gs,"href",`${a(o)??""}/register`)}),y("click",b,T(()=>l("accueil"))),y("click",x,u),y("click",G,T(()=>l("accueil"))),y("click",ge,T(()=>l("fonctionnalites"))),y("click",ye,T(()=>l("contact"))),y("click",ue,()=>{sessionStorage.setItem("loginType","user")}),y("click",ce,()=>{sessionStorage.setItem("loginType","admin")}),y("click",Te,r),y("click",Mt,T(()=>l("accueil"))),y("click",Gt,T(()=>l("fonctionnalites"))),y("click",jt,T(()=>l("contact"))),y("click",ut,()=>{sessionStorage.setItem("loginType","user")}),y("click",ct,()=>{sessionStorage.setItem("loginType","admin")}),P(e,d),ee()}var Yi=O(``);function Zi(e,s){X(s,!1);function t(G){window.location.hash=G,window.scrollTo(0,0)}re();var i=Yi(),o=c(i),n=c(o),l=m(c(n),2),u=m(c(l),2),r=c(u),d=m(c(r),2),f=m(r,2),p=m(c(f),2),v=m(f,2),h=m(c(v),2),b=m(v,4),x=m(c(b),2),A=m(b,2),g=m(c(A),2),w=m(A,2),S=m(c(w),2),M=m(n,2),z=c(M),Y=c(z),N=c(Y);ae(G=>at(N,`© ${G??""} Geosector. Tous droits réservés.`),[()=>new Date().getFullYear()],me),y("click",d,T(()=>t("accueil"))),y("click",p,T(()=>t("fonctionnalites"))),y("click",h,T(()=>t("contact"))),y("click",x,T(()=>t("mentions-legales"))),y("click",g,T(()=>t("politique-confidentialite"))),y("click",S,T(()=>t("conditions-utilisation"))),P(e,i),ee()}var Qi=O(`
`);function Ji(e,s){X(s,!1);const t=Fi();function i(){const f=new Date;f.setDate(f.getDate()+2),document.cookie=`geosector_cookies_accepted=true; expires=${f.toUTCString()}; path=/; SameSite=Lax`,t("consent",{accepted:!0})}function o(){const f=new Date;f.setDate(f.getDate()+2),document.cookie=`geosector_cookies_refused=true; expires=${f.toUTCString()}; path=/; SameSite=Lax`,t("consent",{accepted:!1})}re();var n=Qi(),l=c(n),u=m(c(l),4),r=c(u),d=m(r,2);y("click",r,o),y("click",d,i),P(e,n),ee()}function Be(e){const s=document.cookie.split("; ").find(t=>t.startsWith(`${e}=`));return s?s.split("=")[1]:null}function Xi(){return Be("geosector_cookies_accepted")!==null||Be("geosector_cookies_refused")!==null}function Wt(){if(Be("geosector_cookies_accepted")==="true"){const e=window.location.hash.slice(1)||"accueil";console.log("Suivi anonyme activé - "+new Date().toISOString()),console.log("Page courante: "+e)}}function Kt(){Be("geosector_cookies_refused")==="true"&&console.log("Suivi anonyme désactivé - "+new Date().toISOString())}const Ls="geosector_last_tracking";function Yt(e){if(Be("geosector_cookies_accepted")!=="true"){console.log("Suivi désactivé : cookies non acceptés");return}if(!eo()){console.log("Suivi différé : déjà suivi dans les 2 derniers jours");return}localStorage.setItem(Ls,new Date().toISOString()),console.log(`Page consultée: ${e} - ${new Date().toISOString()}`)}function eo(){const e=localStorage.getItem(Ls);if(!e)return!0;const s=new Date(e),i=Math.abs(new Date-s);return Math.ceil(i/(1e3*60*60*24))>=2}var to=O(`

Gestion efficace de vos distributions de calendriers

Une application puissante et intuitive pour optimiser vos tournées et améliorer votre productivité.

Dashboard Geosector

Interface de gestion

Mobile App

Interface mobile

Pourquoi choisir Geosector ?

Optimisation des tournées

Grace au mode Terrain, Geosector aide le membre à traiter les adresses à finaliser proche de lui.

Simplicité d'utilisation

Interface intuitive conçue pour faciliter la gestion quotidienne de vos distributions.

Sécurité des données

Vos données sont protégées en conformité au RGPD et sauvegardées régulièrement.

Ce que nos clients disent

TP

Trystan PAPIN

Trésorier de l'amicale des SP du Malesherbois

"Bonjour, Je confirme l’utilisation de l’application Geosector pour l’amicale des SP de Malesherbes. Superbe application encore merci à vous !"

ML

Marie Leroy

Responsable opérations, LogiExpress

"L'interface intuitive de Geosector nous a permis de former rapidement nos équipes. La visualisation en temps réel des tournées est un atout majeur pour notre activité quotidienne."

`);function so(e,s){X(s,!1);let t=$(!1);he(()=>{k(t,!0)}),re();var i=to(),o=c(i),n=c(o),l=c(n),u=c(l),r=c(u);let d;var f=m(r,2);let p;var v=m(f,2);let h;var b=m(u,2);let x;ae((A,g,w,S)=>{d=E(r,1,"text-4xl md:text-5xl font-bold mb-6 transition-all duration-700 text-[#002C66]",null,d,A),p=E(f,1,"text-xl mb-8 transition-all duration-700 delay-300 text-[#002C66]",null,p,g),h=E(v,1,"transition-all duration-700 delay-500",null,h,w),x=E(b,1,"md:w-1/2 transition-all duration-700 delay-700 relative flex justify-center md:justify-end",null,x,S)},[()=>({"translate-y-0":a(t),"opacity-100":a(t),"translate-y-10":!a(t),"opacity-0":!a(t)}),()=>({"translate-y-0":a(t),"opacity-100":a(t),"translate-y-10":!a(t),"opacity-0":!a(t)}),()=>({"translate-y-0":a(t),"opacity-100":a(t),"translate-y-10":!a(t),"opacity-0":!a(t)}),()=>({"translate-x-0":a(t),"opacity-100":a(t),"translate-x-10":!a(t),"opacity-0":!a(t)})],me),P(e,i),ee()}var io=O(`

Fonctionnalités

Découvrez les outils puissants qui font de Geosector la solution idéale pour la gestion de vos distributions.

Fonctionnalités principales

Cartographie avancée

Visualisez vos tournées sur des cartes interactives avec des données en temps réel sur le trafic et les conditions météorologiques.

  • Cartes détaillées avec points d'intérêt
  • Suivi GPS en temps réel
  • Alertes de trafic et d'incidents

Optimisation des itinéraires

Nos algorithmes avancés calculent les itinéraires les plus efficaces en tenant compte de multiples facteurs.

  • Réduction des coûts de carburant jusqu'à 30%
  • Prise en compte des contraintes horaires
  • Adaptation dynamique aux conditions réelles

Planification intelligente

Planifiez vos tournées à l'avance et adaptez-les facilement en fonction des imprévus.

  • Calendrier interactif avec vue mensuelle/hebdomadaire/quotidienne
  • Gestion des priorités et des urgences
  • Notifications automatiques pour les changements

Rapports et analyses

Obtenez des insights précieux sur vos opérations grâce à nos outils d'analyse avancés.

  • Tableaux de bord personnalisables
  • Exportation des données en plusieurs formats
  • Indicateurs de performance clés (KPIs)

Application mobile

Emportez Geosector partout avec vous

Notre application mobile offre toutes les fonctionnalités essentielles pour gérer vos distributions en déplacement.

Interface adaptée aux mobiles

Expérience utilisateur optimisée pour les écrans tactiles et la navigation mobile.

Mode hors ligne

Continuez à travailler même sans connexion internet, avec synchronisation automatique.

Notifications push

Restez informé des changements importants et des mises à jour en temps réel.

Télécharger sur l'App Store Télécharger sur Google Play
Capture d'écran de l'application mobile

Prêt à optimiser vos distributions ?

Rejoignez les milliers d'entreprises qui font confiance à Geosector pour améliorer leur efficacité opérationnelle.

Demander une démo
`);function oo(e,s){X(s,!1);let t=$(!1);he(()=>{k(t,!0)}),re();var i=io(),o=c(i),n=c(o),l=c(n),u=c(l);let r;var d=m(u,2);let f;ae((p,v)=>{r=E(u,1,"text-4xl md:text-5xl font-bold mb-6 transition-all duration-700 text-[#002C66]",null,r,p),f=E(d,1,"text-xl max-w-3xl mx-auto transition-all duration-700 delay-300 text-[#002C66]",null,f,v)},[()=>({"translate-y-0":a(t),"opacity-100":a(t),"translate-y-10":!a(t),"opacity-0":!a(t)}),()=>({"translate-y-0":a(t),"opacity-100":a(t),"translate-y-10":!a(t),"opacity-0":!a(t)})],me),P(e,i),ee()}var no=O('

Message envoyé avec succès !

Nous vous répondrons dans les plus brefs délais.

'),ro=O('
'),lo=O(`

Contactez-nous

Notre équipe est à votre disposition pour répondre à toutes vos questions et vous accompagner dans votre projet.

Nos coordonnées

Téléphone

+33 (0)1 23 45 67 89

Email

contact@geosector.fr

Horaires d'ouverture

Lundi - Vendredi: 9h00 - 18h00
Samedi - Dimanche: Fermé

Suivez-nous

Envoyez-nous un message

Questions fréquentes

Comment puis-je obtenir une démonstration de Geosector ?

Vous pouvez demander une démonstration en remplissant le formulaire de contact ci-dessus ou en nous appelant directement. Un de nos conseillers vous contactera pour organiser une session personnalisée.

Combien de temps dure la période d'essai ?

Nous proposons une période d'essai gratuite de 14 jours avec toutes les fonctionnalités disponibles. Aucune carte de crédit n'est requise pour commencer votre essai.

Proposez-vous des formations pour utiliser votre logiciel ?

Oui, nous proposons des sessions de formation complètes pour vous aider à tirer le meilleur parti de Geosector. Ces formations peuvent être réalisées en ligne ou dans vos locaux selon vos préférences.

Quels types de support technique proposez-vous ?

Nous offrons un support technique par email, téléphone et chat en direct pendant les heures de bureau. Nos clients avec des forfaits premium bénéficient d'un support 24/7.

`);function ao(e,s){X(s,!1);let t=$(!1),i=$({nom:"",email:"",telephone:"",entreprise:"",message:"",newsletter:!1}),o=$(!1);function n(){k(o,!0),console.log("Formulaire soumis:",a(i))}he(()=>{k(t,!0)}),re();var l=lo(),u=c(l),r=c(u),d=c(r),f=c(d);let p;var v=m(f,2);let h;var b=m(u,2),x=c(b),A=c(x),g=c(A),w=c(g),S=m(c(w),2),M=m(c(S),2);{var z=N=>{var G=no();P(N,G)},Y=N=>{var G=ro(),H=c(G),ge=c(H),ze=m(c(ge),2),ye=m(ge,2),te=m(c(ye),2),ue=m(H,2),ce=c(ue),Ge=m(c(ce),2),W=m(ce,2),je=m(c(W),2),be=m(ue,2),Te=m(c(be),2),Z=m(be,2),Pe=c(Z);Ne(ze,()=>a(i).nom,V=>ke(i,a(i).nom=V)),Ne(te,()=>a(i).email,V=>ke(i,a(i).email=V)),Ne(Ge,()=>a(i).telephone,V=>ke(i,a(i).telephone=V)),Ne(je,()=>a(i).entreprise,V=>ke(i,a(i).entreprise=V)),Ne(Te,()=>a(i).message,V=>ke(i,a(i).message=V)),Vi(Pe,()=>a(i).newsletter,V=>ke(i,a(i).newsletter=V)),y("submit",G,T(n)),P(N,G)};se(M,N=>{a(o)?N(z):N(Y,!1)})}ae((N,G)=>{p=E(f,1,"text-4xl md:text-5xl font-bold mb-6 transition-all duration-700 text-[#002C66]",null,p,N),h=E(v,1,"text-xl max-w-3xl mx-auto transition-all duration-700 delay-300 text-[#002C66]",null,h,G)},[()=>({"translate-y-0":a(t),"opacity-100":a(t),"translate-y-10":!a(t),"opacity-0":!a(t)}),()=>({"translate-y-0":a(t),"opacity-100":a(t),"translate-y-10":!a(t),"opacity-0":!a(t)})],me),P(e,l),ee()}var uo=O(`

Politique de confidentialité

Protection de vos données personnelles et respect de votre vie privée

Introduction

Cette politique de confidentialité s'applique à l'application Geosector, disponible sur le Web, iOS et Android, + ainsi qu'à tous les services associés (collectivement désignés par "Geosector", "nous", "notre" ou "nos").

Chez Geosector, nous accordons une grande importance à la protection de vos données personnelles. + Cette politique décrit quelles informations nous collectons, comment nous les utilisons, + et quels choix vous avez concernant ces données.

Cette politique de confidentialité doit être lue conjointement avec nos Conditions d'utilisation, qui régissent votre utilisation de notre application.

Quelles informations collectons-nous ?

1. Informations que vous nous fournissez

  • Informations de compte : Lors de l'inscription, nous collectons votre nom, prénom, adresse e-mail, et mot de passe.
  • Informations de profil : Vous pouvez nous fournir des informations supplémentaires comme votre fonction, l'organisation à laquelle vous appartenez, et votre photo de profil.
  • Contenu utilisateur : Les informations que vous créez, téléchargez ou partagez via notre application, notamment les secteurs géographiques, les passages, et les commentaires.
  • Communications : Lorsque vous nous contactez, nous conservons un historique de ces communications.

2. Informations collectées automatiquement

  • Données d'utilisation : Informations sur vos interactions avec notre application, comme les fonctionnalités utilisées, les pages visitées et le temps passé.
  • Informations sur l'appareil : Type d'appareil, système d'exploitation, version de l'application, langue, fuseau horaire et autres caractéristiques techniques.
  • Données de localisation : Avec votre consentement, nous collectons des données de géolocalisation précises pour vous permettre d'utiliser les fonctionnalités cartographiques et de secteurs.
  • Cookies et technologies similaires : Sur notre version web, nous utilisons des cookies et des technologies similaires pour améliorer votre expérience. Pour plus d'informations, consultez notre politique relative aux cookies.

Comment utilisons-nous vos informations ?

Nous utilisons vos informations pour les finalités suivantes :

  • Fournir, maintenir et améliorer notre application et ses fonctionnalités
  • Créer et gérer votre compte
  • Traiter vos transactions et paiements
  • Vous envoyer des informations techniques, des mises à jour, des alertes de sécurité et des messages administratifs
  • Répondre à vos commentaires et questions et vous fournir un support client
  • Communiquer avec vous à propos de produits, services, offres et événements
  • Surveiller et analyser les tendances, l'utilisation et les activités liées à notre application
  • Détecter, prévenir et résoudre les problèmes techniques et de sécurité
  • Se conformer aux obligations légales

Base légale du traitement (pour les utilisateurs de l'EEE et du Royaume-Uni)

Pour les utilisateurs de l'Espace économique européen (EEE) et du Royaume-Uni, nous traitons vos données personnelles sur les bases légales suivantes :

  • Exécution d'un contrat : Lorsque le traitement est nécessaire pour l'exécution d'un contrat auquel vous êtes partie ou pour prendre des mesures à votre demande avant de conclure un contrat.
  • Intérêts légitimes : Lorsque le traitement est nécessaire pour nos intérêts légitimes ou ceux d'un tiers, et que ces intérêts ne sont pas supplantés par vos intérêts ou droits fondamentaux.
  • Consentement : Lorsque vous avez donné votre consentement au traitement de vos données personnelles pour une ou plusieurs finalités spécifiques.
  • Obligation légale : Lorsque le traitement est nécessaire pour respecter une obligation légale à laquelle nous sommes soumis.

Comment partageons-nous vos informations ?

Nous pouvons partager vos informations personnelles avec les tiers suivants :

  • Prestataires de services : Nous travaillons avec des prestataires de services tiers qui fournissent des services tels que l'hébergement, l'analyse, le traitement des paiements et le support client.
  • Partenaires professionnels : Nous pouvons partager des informations avec nos partenaires commerciaux pour offrir certains produits, services ou promotions.
  • Conformité légale : Nous pouvons divulguer vos informations si nous estimons de bonne foi que cette divulgation est nécessaire pour se conformer à la loi, protéger nos droits ou assurer votre sécurité.
  • Transactions d'entreprise : En cas de fusion, acquisition, restructuration ou vente d'actifs, vos informations peuvent être transférées dans le cadre de cette transaction.

Nous ne vendons pas vos données personnelles à des tiers.

Transferts internationaux de données

Vos informations peuvent être transférées et traitées dans des pays autres que celui où vous résidez. + Ces pays peuvent avoir des lois sur la protection des données différentes de celles de votre pays.

Si nous transférons des données personnelles provenant de l'EEE, du Royaume-Uni ou de la Suisse vers des pays + n'offrant pas un niveau de protection adéquat selon les autorités compétentes, nous utilisons des + mécanismes de transfert légalement reconnus, tels que les clauses contractuelles types approuvées par la Commission européenne.

Vos droits et choix

Selon votre lieu de résidence, vous pouvez disposer de certains droits concernant vos données personnelles :

  • Accès et portabilité : Vous pouvez accéder à vos informations personnelles et en obtenir une copie dans un format structuré, couramment utilisé et lisible par machine.
  • Correction : Vous pouvez mettre à jour ou corriger vos informations personnelles si elles sont inexactes ou incomplètes.
  • Suppression : Vous pouvez demander la suppression de vos données personnelles dans certaines circonstances.
  • Restriction et opposition : Vous pouvez demander la restriction du traitement de vos données personnelles ou vous opposer à leur traitement dans certaines circonstances.
  • Consentement : Lorsque le traitement est basé sur votre consentement, vous pouvez retirer ce consentement à tout moment.
  • Réclamation : Vous avez le droit d'introduire une réclamation auprès d'une autorité de protection des données.

Pour exercer ces droits, contactez-nous à l'adresse indiquée dans la section "Nous contacter" ci-dessous. + Notez que ces droits peuvent être soumis à des limitations et exceptions prévues par la loi applicable.

Conservation des données

Nous conservons vos données personnelles aussi longtemps que nécessaire pour atteindre les finalités décrites dans cette politique, + sauf si une période de conservation plus longue est requise ou permise par la loi. + Les critères utilisés pour déterminer nos périodes de conservation comprennent :

  • La durée pendant laquelle nous entretenons une relation continue avec vous et vous fournissons l'application
  • Si nous avons une obligation légale à laquelle nous sommes soumis
  • Si la conservation est souhaitable compte tenu de notre position juridique (par exemple, concernant les délais de prescription applicables, les litiges ou les enquêtes réglementaires)

Sécurité des données

Nous mettons en œuvre des mesures de sécurité techniques et organisationnelles appropriées pour protéger vos données personnelles + contre la perte accidentelle, l'utilisation non autorisée, l'altération et la divulgation. + Ces mesures comprennent le chiffrement des données, les contrôles d'accès, les pare-feu et les audits de sécurité réguliers.

Cependant, aucun système de sécurité n'est impénétrable et nous ne pouvons garantir la sécurité absolue de vos informations. + Il est important que vous preniez des précautions pour protéger votre mot de passe et votre appareil.

Protection de la vie privée des enfants

Notre application n'est pas destinée aux personnes âgées de moins de 16 ans et nous ne collectons pas sciemment + des données personnelles auprès d'enfants de moins de 16 ans. Si vous êtes parent ou tuteur et que vous pensez + que votre enfant nous a fourni des informations personnelles, veuillez nous contacter.

Modifications de cette politique

Nous pouvons modifier cette politique de confidentialité de temps à autre. Si nous apportons des modifications importantes, + nous vous en informerons par e-mail ou par une notification dans notre application avant que les modifications + ne prennent effet. Nous vous encourageons à consulter régulièrement cette politique pour rester informé de + nos pratiques en matière de protection des données.

Nous contacter

Si vous avez des questions concernant cette politique de confidentialité ou nos pratiques en matière de protection des données, + veuillez nous contacter à l'adresse suivante :

Geosector
E-mail : privacy@geosector.fr
Adresse : [Adresse de l'entreprise]
Téléphone : +33 (0)1 23 45 67 89

Informations spécifiques aux plateformes

Application iOS (Apple App Store)

En utilisant notre application via l'App Store d'Apple, vous reconnaissez qu'Apple n'est pas responsable de nos pratiques + en matière de protection des données. Veuillez consulter la politique de confidentialité d'Apple pour plus d'informations + sur la façon dont Apple peut collecter et traiter vos données.

Application Android (Google Play)

En utilisant notre application via Google Play, vous reconnaissez que Google n'est pas responsable de nos pratiques + en matière de protection des données. Veuillez consulter la politique de confidentialité de Google pour plus d'informations + sur la façon dont Google peut collecter et traiter vos données.

Permissions des applications mobiles

Notre application peut demander certaines permissions sur votre appareil mobile, notamment :

  • Localisation : Pour les fonctionnalités basées sur la localisation, comme l'affichage des secteurs et la navigation
  • Stockage : Pour stocker des données localement sur votre appareil
  • Appareil photo : Pour scanner des codes QR ou prendre des photos
  • Notifications : Pour vous envoyer des alertes et des mises à jour importantes

Vous pouvez gérer ces permissions à tout moment dans les paramètres de votre appareil, mais notez que + la désactivation de certaines permissions peut limiter les fonctionnalités de l'application.

`);function co(e,s){X(s,!1);let t=$(!1);function i(M){window.location.hash=M,window.scrollTo(0,0)}he(()=>{k(t,!0)}),re();var o=uo(),n=c(o),l=c(n),u=c(l),r=c(u);let d;var f=m(r,2);let p;var v=m(n,2),h=c(v),b=c(h),x=c(b),A=c(x),g=c(A),w=m(A,8),S=m(c(w));ae((M,z,Y)=>{d=E(r,1,"text-4xl md:text-5xl font-bold mb-6 transition-all duration-700 text-[#002C66]",null,d,M),p=E(f,1,"text-xl max-w-3xl mx-auto transition-all duration-700 delay-300 text-[#002C66]",null,p,z),at(g,`Dernière mise à jour : ${Y??""}`)},[()=>({"translate-y-0":a(t),"opacity-100":a(t),"translate-y-10":!a(t),"opacity-0":!a(t)}),()=>({"translate-y-0":a(t),"opacity-100":a(t),"translate-y-10":!a(t),"opacity-0":!a(t)}),()=>new Date().toLocaleDateString("fr-FR",{year:"numeric",month:"long",day:"numeric"})],me),y("click",S,T(()=>i("conditions-utilisation"))),P(e,o),ee()}var po=O(`

Conditions d'utilisation

Règles et modalités d'utilisation de l'application Geosector

1. Préambule

Les présentes conditions générales d'utilisation (ci-après dénommées "CGU") régissent l'utilisation de l'application Geosector (ci-après dénommée l'"Application"), + accessible via le Web à l'adresse app.geosector.fr, ainsi que sur les plateformes iOS (Apple App Store) et Android (Google Play Store).

Geosector est une application dédiée à la gestion de secteurs géographiques et de passages, permettant à ses utilisateurs d'optimiser + leurs distributions et tournées. L'application est exploitée par [Nom de la société], dont le siège social est situé à [Adresse complète], + immatriculée au Registre du Commerce et des Sociétés de [Ville] sous le numéro [Numéro RCS].

En utilisant notre Application, vous acceptez de vous conformer aux présentes CGU. Si vous n'acceptez pas ces conditions, + veuillez ne pas utiliser l'Application.

2. Définitions

Dans les présentes CGU, les termes suivants ont la signification qui leur est attribuée ci-dessous :

  • "Application" désigne l'application Geosector, accessible via le Web, iOS et Android.
  • "Compte" désigne l'espace personnel de l'Utilisateur sur l'Application.
  • "Contenu" désigne toutes les informations et données (y compris les textes, images, vidéos, etc.) accessibles ou générées via l'Application.
  • "Fonctionnalités" désigne les services et outils proposés par l'Application.
  • "Utilisateur" désigne toute personne physique ou morale ayant accès à l'Application.
  • "Données Personnelles" désigne toute information se rapportant à une personne physique identifiée ou identifiable.

3. Inscription et compte utilisateur

3.1 Conditions d'inscription

Pour utiliser l'ensemble des Fonctionnalités de l'Application, l'Utilisateur doit créer un Compte en fournissant + les informations requises. L'Utilisateur s'engage à fournir des informations exactes, complètes et à jour. + Toute fausse déclaration peut entraîner la suspension ou la suppression du Compte.

3.2 Sécurité du compte

L'Utilisateur est responsable de la confidentialité de ses identifiants de connexion (nom d'utilisateur et mot de passe) + et s'engage à ne pas les communiquer à des tiers. Toute connexion effectuée en utilisant les identifiants de l'Utilisateur + sera présumée avoir été effectuée par celui-ci.

3.3 Suspension ou suppression de compte

Geosector se réserve le droit de suspendre ou de supprimer un Compte en cas de :

  • Non-respect des présentes CGU
  • Inactivité prolongée
  • Utilisation frauduleuse ou abusive de l'Application
  • Non-paiement des services payants
  • Demande de l'Utilisateur

4. Utilisation de l'Application

4.1 Licence d'utilisation

Sous réserve du respect des présentes CGU, Geosector accorde à l'Utilisateur une licence limitée, non exclusive, + non transférable et révocable pour accéder et utiliser l'Application à des fins professionnelles ou personnelles.

4.2 Restrictions d'utilisation

L'Utilisateur s'engage à ne pas :

  • Utiliser l'Application à des fins illégales ou interdites par les présentes CGU
  • Tenter de perturber le fonctionnement de l'Application ou d'accéder aux données d'autres Utilisateurs
  • Utiliser des robots, spiders, scrapers ou autres moyens automatisés pour accéder à l'Application
  • Contourner les mesures de sécurité de l'Application
  • Reproduire, copier, vendre, revendre ou exploiter toute partie de l'Application sans autorisation écrite préalable
  • Utiliser l'Application d'une manière qui pourrait endommager, désactiver, surcharger ou altérer les serveurs ou les réseaux

4.3 Contenu de l'Utilisateur

En publiant, téléchargeant, ou partageant du Contenu via l'Application, l'Utilisateur accorde à Geosector une licence mondiale, + non exclusive, transférable, libre de redevances pour utiliser, reproduire, modifier, adapter, publier, traduire et distribuer ce Contenu + dans le cadre de l'exploitation et de l'amélioration de l'Application.

L'Utilisateur garantit qu'il dispose des droits nécessaires sur le Contenu qu'il partage et que ce Contenu n'enfreint pas + les droits de tiers ni les lois applicables.

5. Services payants et abonnements

5.1 Offres et tarifs

Certaines Fonctionnalités de l'Application peuvent être soumises à paiement. Les offres et tarifs sont disponibles sur le site web + de Geosector ou directement dans l'Application. Geosector se réserve le droit de modifier ses offres et tarifs à tout moment, + moyennant un préavis raisonnable.

5.2 Paiement et facturation

Les paiements sont effectués par carte bancaire ou tout autre moyen proposé dans l'Application. Pour les abonnements, + le paiement est automatiquement renouvelé à la fin de chaque période, sauf résiliation par l'Utilisateur avant la date de renouvellement.

Une facture électronique est mise à disposition de l'Utilisateur pour chaque paiement effectué.

5.3 Politique de remboursement

Conformément à la législation applicable, l'Utilisateur bénéficie d'un droit de rétractation de 14 jours à compter de la souscription + à un service payant, sauf si l'exécution du service a commencé avec son accord avant la fin de ce délai.

Aucun remboursement ne sera accordé après l'expiration du délai de rétractation, sauf en cas de dysfonctionnement majeur de l'Application + imputable à Geosector.

6. Propriété intellectuelle

6.1 Droits de Geosector

L'Application, y compris son contenu, sa structure, ses fonctionnalités, son code source, ses interfaces, son design, + ses logos et ses marques, est la propriété exclusive de Geosector ou de ses concédants de licence. + Ces éléments sont protégés par les lois relatives à la propriété intellectuelle.

6.2 Droits des Utilisateurs

L'Utilisateur conserve tous les droits de propriété intellectuelle sur le Contenu qu'il crée et partage via l'Application, + sous réserve de la licence accordée à Geosector conformément à l'article 4.3.

6.3 Signalement d'une violation

Si vous pensez que votre contenu a été utilisé d'une manière qui constitue une violation de vos droits de propriété intellectuelle, + veuillez nous contacter à l'adresse suivante : [adresse email].

7. Confidentialité et données personnelles

La collecte et le traitement des Données Personnelles des Utilisateurs sont régis par notre Politique de Confidentialité, + disponible à l'adresse suivante : Politique de confidentialité.

8. Limitation de responsabilité

8.1 Disponibilité de l'Application

Geosector s'efforce de maintenir l'Application accessible 24 heures sur 24 et 7 jours sur 7. Cependant, l'accès peut être + temporairement suspendu, sans préavis, en raison de maintenance technique, de mise à jour ou pour toute autre raison.

Geosector ne peut être tenu responsable de tout dommage résultant de l'indisponibilité temporaire de l'Application.

8.2 Contenus et services tiers

L'Application peut contenir des liens vers des sites web ou services tiers. Geosector n'exerce aucun contrôle sur ces sites et services + et n'assume aucune responsabilité quant à leur contenu ou leurs pratiques.

8.3 Limitation générale de responsabilité

Dans toute la mesure permise par la loi applicable, Geosector ne pourra être tenu responsable de tout dommage indirect, + spécial, accessoire, consécutif ou punitif, y compris les pertes de profits, de revenus, de données ou d'opportunités commerciales, + résultant de l'utilisation ou de l'impossibilité d'utiliser l'Application.

La responsabilité totale de Geosector envers l'Utilisateur pour toute réclamation découlant des présentes CGU ne pourra excéder + le montant payé par l'Utilisateur à Geosector au cours des douze (12) mois précédant le fait générateur de la responsabilité.

9. Modifications des CGU

Geosector se réserve le droit de modifier les présentes CGU à tout moment. Les Utilisateurs seront informés des modifications + par le biais d'une notification dans l'Application ou par e-mail.

Les modifications prendront effet à la date indiquée dans la notification. En continuant à utiliser l'Application après cette date, + l'Utilisateur accepte les CGU modifiées.

Si l'Utilisateur n'accepte pas les modifications, il doit cesser d'utiliser l'Application et, le cas échéant, supprimer son Compte.

10. Résiliation

10.1 Résiliation par l'Utilisateur

L'Utilisateur peut, à tout moment, cesser d'utiliser l'Application et supprimer son Compte en suivant la procédure prévue à cet effet + dans les paramètres de l'Application.

10.2 Résiliation par Geosector

Geosector peut, à sa discrétion, suspendre ou résilier l'accès de l'Utilisateur à l'Application en cas de violation des présentes CGU, + sans préjudice de tout autre droit ou recours.

10.3 Conséquences de la résiliation

En cas de résiliation, l'Utilisateur perd l'accès à son Compte et à toutes les Fonctionnalités de l'Application. + Les sections des présentes CGU relatives à la propriété intellectuelle, à la limitation de responsabilité et au règlement des litiges + survivront à la résiliation.

11. Dispositions spécifiques aux applications mobiles

11.1 Application iOS (Apple App Store)

Si vous téléchargez l'Application via l'App Store d'Apple, vous reconnaissez et acceptez que :

  • Ces CGU sont conclues entre vous et Geosector, et non avec Apple
  • Apple n'a aucune obligation de fournir des services de maintenance ou d'assistance concernant l'Application
  • En cas de non-conformité de l'Application avec une garantie applicable, vous pouvez en informer Apple, qui pourra vous rembourser le prix d'achat
  • Apple n'est pas responsable du traitement des réclamations ou de la responsabilité liée à l'Application
  • En cas de réclamation d'un tiers selon laquelle l'Application enfreint ses droits de propriété intellectuelle, Apple n'est pas responsable de l'enquête, de la défense, du règlement et de la décharge de cette réclamation
  • Vous devez vous conformer aux conditions d'utilisation de l'App Store d'Apple lors de l'utilisation de l'Application

11.2 Application Android (Google Play)

Si vous téléchargez l'Application via Google Play, vous reconnaissez et acceptez que :

  • Ces CGU sont conclues entre vous et Geosector, et non avec Google
  • L'utilisation de l'Application doit respecter les conditions d'utilisation de Google Play
  • Google n'a aucune obligation de fournir des services de maintenance ou d'assistance concernant l'Application

12. Dispositions diverses

12.1 Droit applicable et juridiction compétente

Les présentes CGU sont régies par le droit français. Tout litige relatif à leur interprétation ou à leur exécution relève, + à défaut d'accord amiable, de la compétence exclusive des tribunaux français compétents.

12.2 Indépendance des clauses

Si une ou plusieurs dispositions des présentes CGU sont tenues pour non valides ou déclarées comme telles en application d'une loi, + d'un règlement ou à la suite d'une décision définitive d'une juridiction compétente, les autres stipulations garderont toute leur force + et leur portée.

12.3 Non-renonciation

Le fait pour Geosector de ne pas se prévaloir d'un manquement de l'Utilisateur à l'une quelconque des obligations visées dans les présentes CGU + ne saurait être interprété comme une renonciation à s'en prévaloir ultérieurement.

12.4 Communication

Toute notification ou communication dans le cadre des présentes CGU doit être adressée à Geosector par e-mail à l'adresse suivante : + [adresse email] ou par courrier postal à l'adresse suivante : [adresse postale].

13. Contact

Pour toute question concernant les présentes CGU, veuillez nous contacter à :

Geosector
E-mail : support@geosector.fr
Adresse : [Adresse de l'entreprise]
Téléphone : +33 (0)1 23 45 67 89

`);function vo(e,s){X(s,!1);let t=$(!1);function i(M){window.location.hash=M,window.scrollTo(0,0)}he(()=>{k(t,!0)}),re();var o=po(),n=c(o),l=c(n),u=c(l),r=c(u);let d;var f=m(r,2);let p;var v=m(n,2),h=c(v),b=c(h),x=c(b),A=c(x),g=c(A),w=m(A,84),S=m(c(w));ae((M,z,Y)=>{d=E(r,1,"text-4xl md:text-5xl font-bold mb-6 transition-all duration-700 text-[#002C66]",null,d,M),p=E(f,1,"text-xl max-w-3xl mx-auto transition-all duration-700 delay-300 text-[#002C66]",null,p,z),at(g,`Dernière mise à jour : ${Y??""}`)},[()=>({"translate-y-0":a(t),"opacity-100":a(t),"translate-y-10":!a(t),"opacity-0":!a(t)}),()=>({"translate-y-0":a(t),"opacity-100":a(t),"translate-y-10":!a(t),"opacity-0":!a(t)}),()=>new Date().toLocaleDateString("fr-FR",{year:"numeric",month:"long",day:"numeric"})],me),y("click",S,T(()=>i("politique-confidentialite"))),P(e,o),ee()}var fo=O(`

Mentions Légales

Informations juridiques relatives à notre site web et application mobile

1. Éditeur du site et de l'application

Le site web et l'application mobile Geosector sont édités par :

Geosector

SIRET : [Votre numéro SIRET]

Adresse : [Votre adresse]

Email : contact@geosector.fr

Téléphone : [Votre numéro de téléphone]

Directeur de la publication : [Nom du directeur de publication]

2. Hébergement

Le site web et l'application mobile Geosector sont hébergés par :

[Nom de l'hébergeur]

Adresse : [Adresse de l'hébergeur]

Site web : [Site web de l'hébergeur]

Email : [Email de l'hébergeur]

Téléphone : [Téléphone de l'hébergeur]

3. Propriété intellectuelle

L'ensemble du contenu du site web et de l'application mobile Geosector, incluant sans limitation les textes, graphiques, images, logos, icônes, photographies, est la propriété exclusive de Geosector et est protégé par les lois françaises et internationales relatives à la propriété intellectuelle.

Toute reproduction, représentation, modification, publication, transmission, adaptation, totale ou partielle des éléments du site ou de l'application, quel que soit le moyen ou le procédé utilisé, est interdite sans autorisation écrite préalable de Geosector.

Toute utilisation non autorisée des contenus, œuvres ou marques constitue une contrefaçon sanctionnée par le Code de la propriété intellectuelle.

4. Liens hypertextes

Le site web et l'application Geosector peuvent contenir des liens hypertextes vers d'autres sites internet ou applications.

Geosector n'a pas la possibilité de vérifier le contenu des sites ainsi visités, et n'assumera en conséquence aucune responsabilité de ce fait.

La création de liens hypertextes vers le site web ou l'application Geosector est soumise à l'accord préalable de l'éditeur.

5. Limitation de responsabilité

Geosector s'efforce d'assurer au mieux de ses possibilités l'exactitude et la mise à jour des informations diffusées sur son site web et son application mobile, dont elle se réserve le droit de corriger, à tout moment et sans préavis, le contenu.

Toutefois, Geosector ne peut garantir l'exactitude, la précision ou l'exhaustivité des informations mises à disposition sur son site web et son application.

En conséquence, Geosector décline toute responsabilité :

  • Pour toute imprécision, inexactitude ou omission portant sur des informations disponibles sur le site web ou l'application ;
  • Pour tous dommages résultant d'une intrusion frauduleuse d'un tiers ayant entraîné une modification des informations ou éléments mis à disposition sur le site web ou l'application ;
  • Et plus généralement, pour tous dommages, directs ou indirects, qu'elles qu'en soient les causes, origines, natures ou conséquences, provoqués en raison de l'accès de quiconque au site web ou à l'application ou de l'impossibilité d'y accéder, ainsi que l'utilisation du site web ou de l'application et/ou du crédit accordé à une quelconque information provenant directement ou indirectement de ces derniers.

6. Loi applicable et juridiction

Les présentes mentions légales sont régies par la loi française. En cas de litige, les tribunaux français seront seuls compétents.

Pour toute question relative à l'application des présentes mentions légales, vous pouvez nous contacter à l'adresse email : contact@geosector.fr

7. Modifications

Geosector se réserve le droit de modifier les présentes mentions légales à tout moment. L'utilisateur est invité à les consulter régulièrement.

`);function mo(e,s){X(s,!1);let t=$(!1);he(()=>{k(t,!0)}),re();var i=fo(),o=c(i),n=c(o),l=c(n),u=c(l);let r;var d=m(u,2);let f;var p=m(o,2),v=c(p),h=c(v),b=m(c(h),12),x=m(c(b),2),A=m(c(x),2),g=c(A);ae((w,S,M)=>{r=E(u,1,"text-4xl md:text-5xl font-bold mb-6 transition-all duration-700 text-[#002C66]",null,r,w),f=E(d,1,"text-xl max-w-3xl mx-auto transition-all duration-700 delay-300 text-[#002C66]",null,f,S),at(g,`Dernière mise à jour : ${M??""}`)},[()=>({"translate-y-0":a(t),"opacity-100":a(t),"translate-y-10":!a(t),"opacity-0":!a(t)}),()=>({"translate-y-0":a(t),"opacity-100":a(t),"translate-y-10":!a(t),"opacity-0":!a(t)}),()=>new Date().toLocaleDateString("fr-FR",{year:"numeric",month:"long",day:"numeric"})],me),P(e,i),ee()}var ho=O(`

Page non trouvée

La page que vous recherchez n'existe pas.

Retour à l'accueil
`),go=O('
',1);function bo(e,s){X(s,!1);let t=$("accueil"),i=$(!1);function o(){const g=window.location.pathname.slice(1)||"accueil";k(t,g),Yt(a(t))}function n(g){g.detail.accepted?Wt():Kt(),k(i,!1)}he(async()=>(o(),document.addEventListener("click",g=>{const w=g.target.closest("a");if(!w||!w.href.startsWith(window.location.origin)||w.target||w.hasAttribute("download")||w.getAttribute("rel")==="external")return;g.preventDefault();const M=new URL(w.href).pathname;window.history.pushState({},"",M);const z=M.slice(1)||"accueil";k(t,z),Yt(a(t))}),window.addEventListener("popstate",o),await ki(),Xi()?(Wt(),Kt()):k(i,!0),()=>{window.removeEventListener("popstate",o)})),re();var l=go(),u=m(ri(l),2);let r;var d=c(u);Ki(d,{});var f=m(d,2),p=c(f);{var v=g=>{so(g,{})},h=(g,w)=>{{var S=z=>{oo(z,{})},M=(z,Y)=>{{var N=H=>{ao(H,{})},G=(H,ge)=>{{var ze=te=>{co(te,{})},ye=(te,ue)=>{{var ce=W=>{vo(W,{})},Ge=(W,je)=>{{var be=Z=>{mo(Z,{})},Te=Z=>{var Pe=ho();P(Z,Pe)};se(W,Z=>{a(t)==="mentions-legales"?Z(be):Z(Te,!1)},je)}};se(te,W=>{a(t)==="conditions-utilisation"?W(ce):W(Ge,!1)},ue)}};se(H,te=>{a(t)==="politique-confidentialite"?te(ze):te(ye,!1)},ge)}};se(z,H=>{a(t)==="contact"?H(N):H(G,!1)},Y)}};se(g,z=>{a(t)==="fonctionnalites"?z(S):z(M,!1)},w)}};se(p,g=>{a(t)==="accueil"?g(v):g(h,!1)})}var b=m(f,2);Zi(b,{});var x=m(u,2);{var A=g=>{Ji(g,{$$events:{consent:n}})};se(x,g=>{a(i)&&g(A)})}ae(g=>r=E(u,1,"flex flex-col min-h-screen relative",null,r,g),[()=>({"blur-effect":a(i)})],me),P(e,l),ee()}zi(bo,{target:document.getElementById("app")}); diff --git a/web/deploy/favicon-16.png b/web/deploy/favicon-16.png new file mode 100644 index 00000000..f9441458 Binary files /dev/null and b/web/deploy/favicon-16.png differ diff --git a/web/deploy/favicon-32.png b/web/deploy/favicon-32.png new file mode 100644 index 00000000..f5dffe2c Binary files /dev/null and b/web/deploy/favicon-32.png differ diff --git a/web/deploy/favicon-64.png b/web/deploy/favicon-64.png new file mode 100644 index 00000000..53b6ae36 Binary files /dev/null and b/web/deploy/favicon-64.png differ diff --git a/web/deploy/favicon.png b/web/deploy/favicon.png new file mode 100644 index 00000000..f5dffe2c Binary files /dev/null and b/web/deploy/favicon.png differ diff --git a/web/deploy/fonts/Figtree-VariableFont_wght.ttf b/web/deploy/fonts/Figtree-VariableFont_wght.ttf new file mode 100644 index 00000000..06f9fe57 Binary files /dev/null and b/web/deploy/fonts/Figtree-VariableFont_wght.ttf differ diff --git a/web/deploy/fonts/Kallisto-Bold.otf b/web/deploy/fonts/Kallisto-Bold.otf new file mode 100644 index 00000000..fb7595a9 Binary files /dev/null and b/web/deploy/fonts/Kallisto-Bold.otf differ diff --git a/web/deploy/fonts/Kallisto-Medium.otf b/web/deploy/fonts/Kallisto-Medium.otf new file mode 100644 index 00000000..b0f8ad77 Binary files /dev/null and b/web/deploy/fonts/Kallisto-Medium.otf differ diff --git a/web/deploy/fonts/Kallisto-Thin.otf b/web/deploy/fonts/Kallisto-Thin.otf new file mode 100644 index 00000000..a94f0934 Binary files /dev/null and b/web/deploy/fonts/Kallisto-Thin.otf differ diff --git a/web/deploy/icons/Icon-152.png b/web/deploy/icons/Icon-152.png new file mode 100644 index 00000000..4043aa2d Binary files /dev/null and b/web/deploy/icons/Icon-152.png differ diff --git a/web/deploy/icons/Icon-167.png b/web/deploy/icons/Icon-167.png new file mode 100644 index 00000000..1fcc514a Binary files /dev/null and b/web/deploy/icons/Icon-167.png differ diff --git a/web/deploy/icons/Icon-180.png b/web/deploy/icons/Icon-180.png new file mode 100644 index 00000000..d2b40c1e Binary files /dev/null and b/web/deploy/icons/Icon-180.png differ diff --git a/web/deploy/icons/Icon-192.png b/web/deploy/icons/Icon-192.png new file mode 100644 index 00000000..34447be7 Binary files /dev/null and b/web/deploy/icons/Icon-192.png differ diff --git a/web/deploy/icons/Icon-512.png b/web/deploy/icons/Icon-512.png new file mode 100644 index 00000000..058f9806 Binary files /dev/null and b/web/deploy/icons/Icon-512.png differ diff --git a/web/deploy/icons/Icon-maskable-192.png b/web/deploy/icons/Icon-maskable-192.png new file mode 100644 index 00000000..34447be7 Binary files /dev/null and b/web/deploy/icons/Icon-maskable-192.png differ diff --git a/web/deploy/icons/Icon-maskable-512.png b/web/deploy/icons/Icon-maskable-512.png new file mode 100644 index 00000000..058f9806 Binary files /dev/null and b/web/deploy/icons/Icon-maskable-512.png differ diff --git a/web/deploy/images/Logo-geosector-horizontal.svg b/web/deploy/images/Logo-geosector-horizontal.svg new file mode 100644 index 00000000..53d598ca --- /dev/null +++ b/web/deploy/images/Logo-geosector-horizontal.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/deploy/images/Logo-geosector-vertical.svg b/web/deploy/images/Logo-geosector-vertical.svg new file mode 100644 index 00000000..f9aca059 --- /dev/null +++ b/web/deploy/images/Logo-geosector-vertical.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/flutt/assets/images/app-screenshot1.svg b/web/deploy/images/app-screenshot.png similarity index 100% rename from flutt/assets/images/app-screenshot1.svg rename to web/deploy/images/app-screenshot.png diff --git a/web/deploy/images/geosector-icon.png b/web/deploy/images/geosector-icon.png new file mode 100644 index 00000000..532e85c9 Binary files /dev/null and b/web/deploy/images/geosector-icon.png differ diff --git a/flutt/assets/images/geosector-logo.png b/web/deploy/images/geosector-logo.png similarity index 100% rename from flutt/assets/images/geosector-logo.png rename to web/deploy/images/geosector-logo.png diff --git a/web/deploy/images/geosector-logo.svg b/web/deploy/images/geosector-logo.svg new file mode 100644 index 00000000..e69de29b diff --git a/web/deploy/images/icon-geosector.svg b/web/deploy/images/icon-geosector.svg new file mode 100644 index 00000000..1fbeeabb --- /dev/null +++ b/web/deploy/images/icon-geosector.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/deploy/index.html b/web/deploy/index.html new file mode 100644 index 00000000..4dd5cef0 --- /dev/null +++ b/web/deploy/index.html @@ -0,0 +1,17 @@ + + + + + + + + + + Geosector - Gestion efficace de vos distributions + + + + +
+ + diff --git a/web/deploy/vite.svg b/web/deploy/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/web/deploy/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/geosector-deploy.tar.gz b/web/geosector-deploy.tar.gz new file mode 100644 index 00000000..79db131e Binary files /dev/null and b/web/geosector-deploy.tar.gz differ diff --git a/web/index.html b/web/index.html index feaf27cc..5d33d94e 100644 --- a/web/index.html +++ b/web/index.html @@ -1,10 +1,13 @@ - + - + - Vite + Svelte + + + + Geosector - Gestion efficace de vos distributions
diff --git a/web/livre-web.sh b/web/livre-web.sh new file mode 100755 index 00000000..db24d59f --- /dev/null +++ b/web/livre-web.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +# Vérification des arguments +if [ $# -ne 2 ]; then + echo "Usage: $0 " + echo "Example: $0 dva-geo rca-geo" + exit 1 +fi + +HOST_IP="195.154.80.116" +HOST_USER=root +HOST_KEY=/Users/pierre/.ssh/id_rsa_mbpi +HOST_PORT=22 + +SOURCE_CONTAINER=$1 +DEST_CONTAINER=$2 +WEB_PATH="/var/www/geosector/web" +TIMESTAMP=$(date +"%Y%m%d_%H%M%S") +BACKUP_DIR="${WEB_PATH}_backup_${TIMESTAMP}" +PROJECT="default" + +echo "🔄 Copie du site web Svelte de $SOURCE_CONTAINER vers $DEST_CONTAINER (projet: $PROJECT)" + +# Vérifier si les containers existent +echo "🔍 Vérification des containers..." +ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus info $SOURCE_CONTAINER --project $PROJECT" > /dev/null 2>&1 +if [ $? -ne 0 ]; then + echo "❌ Erreur: Le container source $SOURCE_CONTAINER n'existe pas ou n'est pas accessible dans le projet $PROJECT" + exit 1 +fi + +ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus info $DEST_CONTAINER --project $PROJECT" > /dev/null 2>&1 +if [ $? -ne 0 ]; then + echo "❌ Erreur: Le container destination $DEST_CONTAINER n'existe pas ou n'est pas accessible dans le projet $PROJECT" + exit 1 +fi + +# Créer une sauvegarde du dossier de destination avant de le remplacer +echo "📦 Création d'une sauvegarde sur $DEST_CONTAINER..." +# Vérifier si le dossier WEB existe +ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- test -d $WEB_PATH" +if [ $? -eq 0 ]; then + # Le dossier existe, créer une sauvegarde + ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- cp -r $WEB_PATH $BACKUP_DIR" + echo "✅ Sauvegarde créée dans $BACKUP_DIR" +else + echo "⚠️ Le dossier WEB n'existe pas sur la destination" +fi + +# Copier le dossier WEB entre les containers +echo "📋 Copie des fichiers en cours..." + +# Approche directe: utiliser incus copy pour copier directement entre containers +echo "📤 Transfert direct entre containers..." +# Nettoyer le dossier de destination +ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- rm -rf $WEB_PATH" +ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- mkdir -p $WEB_PATH" + +# Copier directement du container source vers le container destination +ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $SOURCE_CONTAINER --project $PROJECT -- tar -cf - -C $WEB_PATH . | incus exec $DEST_CONTAINER --project $PROJECT -- tar -xf - -C $WEB_PATH" +if [ $? -ne 0 ]; then + echo "❌ Erreur lors du transfert direct entre containers" + echo "⚠️ Tentative de restauration de la sauvegarde..." + # Vérifier si la sauvegarde existe + ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- test -d $BACKUP_DIR" + if [ $? -eq 0 ]; then + # La sauvegarde existe, la restaurer + ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- cp -r $BACKUP_DIR $WEB_PATH" + echo "✅ Restauration réussie" + else + echo "❌ Échec de la restauration" + fi + exit 1 +fi + +# Changer le propriétaire et les permissions des fichiers +echo "👤 Application des droits et permissions pour tous les fichiers..." + +# Définir le propriétaire pour tous les fichiers +ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- chown -R nginx:nginx $WEB_PATH" + +# Appliquer les permissions de base pour les dossiers (755) +ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- find $WEB_PATH -type d -exec chmod 755 {} \;" + +# Appliquer les permissions pour les fichiers (644) +ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- find $WEB_PATH -type f -exec chmod 644 {} \;" + +echo "✅ Propriétaire et permissions appliqués avec succès" + +# Vérifier la copie +echo "✅ Vérification de la copie..." +ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- test -d $WEB_PATH" +if [ $? -eq 0 ]; then + echo "✅ Copie réussie" +else + echo "❌ Erreur: Le dossier WEB n'a pas été copié correctement" +fi + +echo "✅ Opération terminée! Le site web Svelte a été copié de $SOURCE_CONTAINER vers $DEST_CONTAINER" +echo "📁 Une sauvegarde a été créée dans $BACKUP_DIR sur $DEST_CONTAINER" +echo "👤 Les fichiers appartiennent maintenant à l'utilisateur nginx" diff --git a/web/public/.htaccess b/web/public/.htaccess new file mode 100644 index 00000000..02e9d601 --- /dev/null +++ b/web/public/.htaccess @@ -0,0 +1,12 @@ +# Configuration pour le mode histoire (HTML5 History API) + + RewriteEngine On + RewriteBase / + + # Si le fichier ou répertoire demandé existe, servir directement + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + + # Sinon, rediriger vers index.html pour permettre au routeur client de gérer + RewriteRule . /index.html [L] + diff --git a/web/public/favicon-16.png b/web/public/favicon-16.png new file mode 100644 index 00000000..f9441458 Binary files /dev/null and b/web/public/favicon-16.png differ diff --git a/web/public/favicon-32.png b/web/public/favicon-32.png new file mode 100644 index 00000000..f5dffe2c Binary files /dev/null and b/web/public/favicon-32.png differ diff --git a/web/public/favicon-64.png b/web/public/favicon-64.png new file mode 100644 index 00000000..53b6ae36 Binary files /dev/null and b/web/public/favicon-64.png differ diff --git a/web/public/favicon.png b/web/public/favicon.png new file mode 100644 index 00000000..f5dffe2c Binary files /dev/null and b/web/public/favicon.png differ diff --git a/web/public/fonts/Figtree-VariableFont_wght.ttf b/web/public/fonts/Figtree-VariableFont_wght.ttf new file mode 100644 index 00000000..06f9fe57 Binary files /dev/null and b/web/public/fonts/Figtree-VariableFont_wght.ttf differ diff --git a/web/public/fonts/Kallisto-Bold.otf b/web/public/fonts/Kallisto-Bold.otf new file mode 100644 index 00000000..fb7595a9 Binary files /dev/null and b/web/public/fonts/Kallisto-Bold.otf differ diff --git a/web/public/fonts/Kallisto-Medium.otf b/web/public/fonts/Kallisto-Medium.otf new file mode 100644 index 00000000..b0f8ad77 Binary files /dev/null and b/web/public/fonts/Kallisto-Medium.otf differ diff --git a/web/public/fonts/Kallisto-Thin.otf b/web/public/fonts/Kallisto-Thin.otf new file mode 100644 index 00000000..a94f0934 Binary files /dev/null and b/web/public/fonts/Kallisto-Thin.otf differ diff --git a/web/public/icons/Icon-152.png b/web/public/icons/Icon-152.png new file mode 100644 index 00000000..4043aa2d Binary files /dev/null and b/web/public/icons/Icon-152.png differ diff --git a/web/public/icons/Icon-167.png b/web/public/icons/Icon-167.png new file mode 100644 index 00000000..1fcc514a Binary files /dev/null and b/web/public/icons/Icon-167.png differ diff --git a/web/public/icons/Icon-180.png b/web/public/icons/Icon-180.png new file mode 100644 index 00000000..d2b40c1e Binary files /dev/null and b/web/public/icons/Icon-180.png differ diff --git a/web/public/icons/Icon-192.png b/web/public/icons/Icon-192.png new file mode 100644 index 00000000..34447be7 Binary files /dev/null and b/web/public/icons/Icon-192.png differ diff --git a/web/public/icons/Icon-512.png b/web/public/icons/Icon-512.png new file mode 100644 index 00000000..058f9806 Binary files /dev/null and b/web/public/icons/Icon-512.png differ diff --git a/web/public/icons/Icon-maskable-192.png b/web/public/icons/Icon-maskable-192.png new file mode 100644 index 00000000..34447be7 Binary files /dev/null and b/web/public/icons/Icon-maskable-192.png differ diff --git a/web/public/icons/Icon-maskable-512.png b/web/public/icons/Icon-maskable-512.png new file mode 100644 index 00000000..058f9806 Binary files /dev/null and b/web/public/icons/Icon-maskable-512.png differ diff --git a/web/public/images/Logo-geosector-horizontal.svg b/web/public/images/Logo-geosector-horizontal.svg new file mode 100644 index 00000000..53d598ca --- /dev/null +++ b/web/public/images/Logo-geosector-horizontal.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/public/images/Logo-geosector-vertical.svg b/web/public/images/Logo-geosector-vertical.svg new file mode 100644 index 00000000..f9aca059 --- /dev/null +++ b/web/public/images/Logo-geosector-vertical.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/public/images/app-screenshot.png b/web/public/images/app-screenshot.png new file mode 100644 index 00000000..03802abb --- /dev/null +++ b/web/public/images/app-screenshot.png @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/public/images/geosector-icon.png b/web/public/images/geosector-icon.png new file mode 100644 index 00000000..532e85c9 Binary files /dev/null and b/web/public/images/geosector-icon.png differ diff --git a/flutt/assets/images/geosector-logo.png~ b/web/public/images/geosector-logo.png similarity index 100% rename from flutt/assets/images/geosector-logo.png~ rename to web/public/images/geosector-logo.png diff --git a/web/public/images/geosector-logo.svg b/web/public/images/geosector-logo.svg new file mode 100644 index 00000000..e69de29b diff --git a/web/public/images/icon-geosector.svg b/web/public/images/icon-geosector.svg new file mode 100644 index 00000000..1fbeeabb --- /dev/null +++ b/web/public/images/icon-geosector.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/App.svelte b/web/src/App.svelte index 40d6d62d..68e8b378 100644 --- a/web/src/App.svelte +++ b/web/src/App.svelte @@ -1,47 +1,148 @@ - + + +
+
+ + + + + +
-

Vite + Svelte

+
-
- +
+
+ +
+ {#if activePage === 'accueil'} + + {:else if activePage === 'fonctionnalites'} + + {:else if activePage === 'contact'} + + {:else if activePage === 'politique-confidentialite'} + + {:else if activePage === 'conditions-utilisation'} + + {:else if activePage === 'mentions-legales'} + + {:else} + +
+

Page non trouvée

+

La page que vous recherchez n'existe pas.

+ Retour à l'accueil +
+ {/if}
-

- Check out SvelteKit, the official Svelte app framework powered by Vite! -

- -

- Click on the Vite and Svelte logos to learn more -

+
- - +{#if showCookieConsent} + +{/if} diff --git a/web/src/app.css b/web/src/app.css index 8625b61c..8e961f12 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -2,82 +2,115 @@ @import "tailwindcss/components"; @import "tailwindcss/utilities"; +@font-face { + font-family: 'Figtree'; + src: url('/fonts/Figtree-VariableFont_wght.ttf') format('truetype'); + font-weight: 100 900; + font-style: normal; + font-display: swap; +} + :root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + font-family: 'Figtree', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; line-height: 1.5; font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - + color: #333333; + background-color: #ffffff; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; +html, body { + height: 100%; + margin: 0; + padding: 0; + scroll-behavior: smooth; } body { - margin: 0; - display: flex; - place-items: center; min-width: 320px; min-height: 100vh; } -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -.card { - padding: 2em; -} - #app { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +/* Aspect ratio utilities */ +.aspect-w-16 { + position: relative; + padding-bottom: calc(var(--tw-aspect-h) / var(--tw-aspect-w) * 100%); + --tw-aspect-w: 16; +} + +.aspect-h-9 { + --tw-aspect-h: 9; +} + +.aspect-w-16 > * { + position: absolute; + height: 100%; + width: 100%; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +/* Animation utilities */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.animate-fadeIn { + animation: fadeIn 0.5s ease-in-out; +} + +/* Custom container for sections */ +.container { + width: 100%; max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; + margin-left: auto; + margin-right: auto; } -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; +/* Custom focus styles */ +*:focus-visible { + outline: 2px solid #3b82f6; + outline-offset: 2px; } -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; +/* Responsive typography */ +@media (max-width: 640px) { + h1 { + font-size: 2rem !important; } - a:hover { - color: #747bff; + h2 { + font-size: 1.5rem !important; } - button { - background-color: #f9f9f9; + h3 { + font-size: 1.25rem !important; } } + +/* Cookie consent modal styles */ +.blur-effect { + filter: blur(4px); + pointer-events: none; + user-select: none; +} + +/* Animation pour la modal de cookies */ +@keyframes slideUp { + from { transform: translateY(20px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +.cookie-modal { + animation: slideUp 0.3s ease-out forwards; +} diff --git a/web/src/components/CookieConsent.svelte b/web/src/components/CookieConsent.svelte new file mode 100644 index 00000000..03cfde46 --- /dev/null +++ b/web/src/components/CookieConsent.svelte @@ -0,0 +1,58 @@ + + +
+ +
diff --git a/web/src/components/Footer.svelte b/web/src/components/Footer.svelte new file mode 100644 index 00000000..f54a185d --- /dev/null +++ b/web/src/components/Footer.svelte @@ -0,0 +1,103 @@ + + + diff --git a/web/src/components/Header.svelte b/web/src/components/Header.svelte new file mode 100644 index 00000000..00bfdbec --- /dev/null +++ b/web/src/components/Header.svelte @@ -0,0 +1,173 @@ + + +
+ + + + + + + {#if mobileMenuOpen} +
e.key === 'Escape' && closeMobileMenu()} + role="button" + tabindex="0" + aria-label="Fermer le menu" + >
+ {/if} +
diff --git a/web/src/lib/analyticsService.js b/web/src/lib/analyticsService.js new file mode 100644 index 00000000..6aad7359 --- /dev/null +++ b/web/src/lib/analyticsService.js @@ -0,0 +1,120 @@ +/** + * Service d'analyse pour Geosector + * Ce service permet de suivre l'activité des utilisateurs qui ont accepté les cookies + */ + +import { getCookie } from './cookieService.js'; + +// Clé pour le stockage local du dernier suivi +const LAST_TRACKING_KEY = 'geosector_last_tracking'; + +// Fonction pour enregistrer une visite de page +export function trackPageView(page) { + // Ne suivre que si l'utilisateur a accepté les cookies + if (getCookie('geosector_cookies_accepted') !== 'true') { + console.log('Suivi désactivé : cookies non acceptés'); + return; + } + + // Vérifier si nous devons suivre aujourd'hui (tous les 2 jours) + if (!shouldTrackToday()) { + console.log('Suivi différé : déjà suivi dans les 2 derniers jours'); + return; + } + + // Enregistrer la date du dernier suivi + localStorage.setItem(LAST_TRACKING_KEY, new Date().toISOString()); + + // Ici, vous pouvez implémenter le code réel pour envoyer les données d'analyse + // Par exemple, une requête fetch vers votre propre endpoint d'analyse + console.log(`Page consultée: ${page} - ${new Date().toISOString()}`); + + // Exemple d'implémentation avec un endpoint d'analyse + /* + fetch('/api/analytics/pageview', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + page, + timestamp: new Date().toISOString(), + // Autres données anonymes comme la source de trafic, etc. + referrer: document.referrer || 'direct', + screenSize: `${window.innerWidth}x${window.innerHeight}`, + // Un identifiant de session anonyme (pas d'information personnelle) + sessionId: getAnonymousSessionId() + }), + }).catch(error => { + console.error('Erreur lors du suivi', error); + }); + */ +} + +// Fonction pour vérifier si nous devons suivre aujourd'hui +function shouldTrackToday() { + const lastTracking = localStorage.getItem(LAST_TRACKING_KEY); + + if (!lastTracking) { + return true; // Première visite, nous devons suivre + } + + const lastTrackingDate = new Date(lastTracking); + const now = new Date(); + + // Calculer la différence en millisecondes + const diffTime = Math.abs(now - lastTrackingDate); + // Convertir en jours + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + + // Ne suivre que tous les 2 jours + return diffDays >= 2; +} + +// Générer un ID de session anonyme +function getAnonymousSessionId() { + // Vérifier si nous avons déjà un ID de session + let sessionId = sessionStorage.getItem('anonymous_session_id'); + + if (!sessionId) { + // Créer un ID de session aléatoire + sessionId = Math.random().toString(36).substring(2, 15); + sessionStorage.setItem('anonymous_session_id', sessionId); + } + + return sessionId; +} + +// Fonction pour suivre un événement spécifique +export function trackEvent(category, action, label = null) { + // Ne suivre que si l'utilisateur a accepté les cookies + if (getCookie('geosector_cookies_accepted') !== 'true') { + return; + } + + // Vérifier si nous devons suivre aujourd'hui + if (!shouldTrackToday()) { + return; + } + + console.log(`Événement: ${category} - ${action} - ${label || 'N/A'}`); + + // Exemple d'implémentation avec un endpoint d'analyse + /* + fetch('/api/analytics/event', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + category, + action, + label, + timestamp: new Date().toISOString(), + sessionId: getAnonymousSessionId() + }), + }).catch(error => { + console.error('Erreur lors du suivi d\'événement', error); + }); + */ +} diff --git a/web/src/lib/cookieService.js b/web/src/lib/cookieService.js new file mode 100644 index 00000000..d7cef4c5 --- /dev/null +++ b/web/src/lib/cookieService.js @@ -0,0 +1,61 @@ +/** + * Service de gestion des cookies pour Geosector + */ + +// Vérifie si un cookie existe +export function getCookie(name) { + const cookieValue = document.cookie + .split('; ') + .find(row => row.startsWith(`${name}=`)); + + if (cookieValue) { + return cookieValue.split('=')[1]; + } + + return null; +} + +// Vérifie si l'utilisateur a déjà fait un choix concernant les cookies +export function hasUserMadeCookieChoice() { + return getCookie('geosector_cookies_accepted') !== null || + getCookie('geosector_cookies_refused') !== null; +} + +// Démarre le suivi anonyme si l'utilisateur a accepté les cookies +export function startAnonymousTracking() { + if (getCookie('geosector_cookies_accepted') === 'true') { + // Implémentation du suivi anonyme ici + // Par exemple, initialisation de Google Analytics ou d'un autre service de suivi + + // Enregistrer la page actuelle (peut être utilisé dans d'autres composants) + const currentPage = window.location.hash.slice(1) || 'accueil'; + console.log('Suivi anonyme activé - ' + new Date().toISOString()); + console.log('Page courante: ' + currentPage); + + // Si vous utilisez notre service analyticsService: + // import { trackPageView } from './analyticsService.js'; + // trackPageView(currentPage); + + // On pourrait aussi initialiser Google Analytics ici : + // if (window.gtag) { + // window.gtag('consent', 'update', { + // 'analytics_storage': 'granted' + // }); + // } + } +} + +// Empêche le suivi si l'utilisateur a refusé les cookies +export function stopAnonymousTracking() { + if (getCookie('geosector_cookies_refused') === 'true') { + // Désactiver tout suivi + console.log('Suivi anonyme désactivé - ' + new Date().toISOString()); + + // Si on utilisait Google Analytics : + // if (window.gtag) { + // window.gtag('consent', 'update', { + // 'analytics_storage': 'denied' + // }); + // } + } +} diff --git a/web/src/pages/Accueil.svelte b/web/src/pages/Accueil.svelte new file mode 100644 index 00000000..bd813031 --- /dev/null +++ b/web/src/pages/Accueil.svelte @@ -0,0 +1,134 @@ + + +
+ +
+
+
+
+

Gestion efficace de vos distributions de calendriers

+

Une application puissante et intuitive pour optimiser vos tournées et améliorer votre productivité.

+ +
+ + +
+
+
+ +
+ + + +

Dashboard Geosector

+

Interface de gestion

+
+
+
+ + +
+
+ +
+ + + +

Mobile App

+

Interface mobile

+
+
+
+
+
+
+
+ + +
+
+
+

Pourquoi choisir Geosector ?

+ +
+
+
+ + + +
+

Optimisation des tournées

+

Grace au mode Terrain, Geosector aide le membre à traiter les adresses à finaliser proche de lui.

+
+ +
+
+ + + +
+

Simplicité d'utilisation

+

Interface intuitive conçue pour faciliter la gestion quotidienne de vos distributions.

+
+ +
+
+ + + +
+

Sécurité des données

+

Vos données sont protégées en conformité au RGPD et sauvegardées régulièrement.

+
+
+
+
+
+ + +
+
+
+

Ce que nos clients disent

+ +
+
+
+
+ TP +
+
+

Trystan PAPIN

+

Trésorier de l'amicale des SP du Malesherbois

+
+
+

"Bonjour, Je confirme l’utilisation de l’application Geosector pour l’amicale des SP de Malesherbes. Superbe application encore merci à vous !"

+
+ +
+
+
+ ML +
+
+

Marie Leroy

+

Responsable opérations, LogiExpress

+
+
+

"L'interface intuitive de Geosector nous a permis de former rapidement nos équipes. La visualisation en temps réel des tournées est un atout majeur pour notre activité quotidienne."

+
+
+
+
+
+
diff --git a/web/src/pages/ConditionsUtilisation.svelte b/web/src/pages/ConditionsUtilisation.svelte new file mode 100644 index 00000000..9f801698 --- /dev/null +++ b/web/src/pages/ConditionsUtilisation.svelte @@ -0,0 +1,310 @@ + + +
+ +
+
+
+

+ Conditions d'utilisation +

+

+ Règles et modalités d'utilisation de l'application Geosector +

+
+
+
+ + +
+
+
+
+

Dernière mise à jour : {new Date().toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' })}

+ +

1. Préambule

+

+ Les présentes conditions générales d'utilisation (ci-après dénommées "CGU") régissent l'utilisation de l'application Geosector (ci-après dénommée l'"Application"), + accessible via le Web à l'adresse app.geosector.fr, ainsi que sur les plateformes iOS (Apple App Store) et Android (Google Play Store). +

+

+ Geosector est une application dédiée à la gestion de secteurs géographiques et de passages, permettant à ses utilisateurs d'optimiser + leurs distributions et tournées. L'application est exploitée par [Nom de la société], dont le siège social est situé à [Adresse complète], + immatriculée au Registre du Commerce et des Sociétés de [Ville] sous le numéro [Numéro RCS]. +

+

+ En utilisant notre Application, vous acceptez de vous conformer aux présentes CGU. Si vous n'acceptez pas ces conditions, + veuillez ne pas utiliser l'Application. +

+ +

2. Définitions

+

Dans les présentes CGU, les termes suivants ont la signification qui leur est attribuée ci-dessous :

+
    +
  • "Application" désigne l'application Geosector, accessible via le Web, iOS et Android.
  • +
  • "Compte" désigne l'espace personnel de l'Utilisateur sur l'Application.
  • +
  • "Contenu" désigne toutes les informations et données (y compris les textes, images, vidéos, etc.) accessibles ou générées via l'Application.
  • +
  • "Fonctionnalités" désigne les services et outils proposés par l'Application.
  • +
  • "Utilisateur" désigne toute personne physique ou morale ayant accès à l'Application.
  • +
  • "Données Personnelles" désigne toute information se rapportant à une personne physique identifiée ou identifiable.
  • +
+ +

3. Inscription et compte utilisateur

+ +

3.1 Conditions d'inscription

+

+ Pour utiliser l'ensemble des Fonctionnalités de l'Application, l'Utilisateur doit créer un Compte en fournissant + les informations requises. L'Utilisateur s'engage à fournir des informations exactes, complètes et à jour. + Toute fausse déclaration peut entraîner la suspension ou la suppression du Compte. +

+ +

3.2 Sécurité du compte

+

+ L'Utilisateur est responsable de la confidentialité de ses identifiants de connexion (nom d'utilisateur et mot de passe) + et s'engage à ne pas les communiquer à des tiers. Toute connexion effectuée en utilisant les identifiants de l'Utilisateur + sera présumée avoir été effectuée par celui-ci. +

+ +

3.3 Suspension ou suppression de compte

+

+ Geosector se réserve le droit de suspendre ou de supprimer un Compte en cas de : +

+
    +
  • Non-respect des présentes CGU
  • +
  • Inactivité prolongée
  • +
  • Utilisation frauduleuse ou abusive de l'Application
  • +
  • Non-paiement des services payants
  • +
  • Demande de l'Utilisateur
  • +
+ +

4. Utilisation de l'Application

+ +

4.1 Licence d'utilisation

+

+ Sous réserve du respect des présentes CGU, Geosector accorde à l'Utilisateur une licence limitée, non exclusive, + non transférable et révocable pour accéder et utiliser l'Application à des fins professionnelles ou personnelles. +

+ +

4.2 Restrictions d'utilisation

+

L'Utilisateur s'engage à ne pas :

+
    +
  • Utiliser l'Application à des fins illégales ou interdites par les présentes CGU
  • +
  • Tenter de perturber le fonctionnement de l'Application ou d'accéder aux données d'autres Utilisateurs
  • +
  • Utiliser des robots, spiders, scrapers ou autres moyens automatisés pour accéder à l'Application
  • +
  • Contourner les mesures de sécurité de l'Application
  • +
  • Reproduire, copier, vendre, revendre ou exploiter toute partie de l'Application sans autorisation écrite préalable
  • +
  • Utiliser l'Application d'une manière qui pourrait endommager, désactiver, surcharger ou altérer les serveurs ou les réseaux
  • +
+ +

4.3 Contenu de l'Utilisateur

+

+ En publiant, téléchargeant, ou partageant du Contenu via l'Application, l'Utilisateur accorde à Geosector une licence mondiale, + non exclusive, transférable, libre de redevances pour utiliser, reproduire, modifier, adapter, publier, traduire et distribuer ce Contenu + dans le cadre de l'exploitation et de l'amélioration de l'Application. +

+

+ L'Utilisateur garantit qu'il dispose des droits nécessaires sur le Contenu qu'il partage et que ce Contenu n'enfreint pas + les droits de tiers ni les lois applicables. +

+ +

5. Services payants et abonnements

+ +

5.1 Offres et tarifs

+

+ Certaines Fonctionnalités de l'Application peuvent être soumises à paiement. Les offres et tarifs sont disponibles sur le site web + de Geosector ou directement dans l'Application. Geosector se réserve le droit de modifier ses offres et tarifs à tout moment, + moyennant un préavis raisonnable. +

+ +

5.2 Paiement et facturation

+

+ Les paiements sont effectués par carte bancaire ou tout autre moyen proposé dans l'Application. Pour les abonnements, + le paiement est automatiquement renouvelé à la fin de chaque période, sauf résiliation par l'Utilisateur avant la date de renouvellement. +

+

+ Une facture électronique est mise à disposition de l'Utilisateur pour chaque paiement effectué. +

+ +

5.3 Politique de remboursement

+

+ Conformément à la législation applicable, l'Utilisateur bénéficie d'un droit de rétractation de 14 jours à compter de la souscription + à un service payant, sauf si l'exécution du service a commencé avec son accord avant la fin de ce délai. +

+

+ Aucun remboursement ne sera accordé après l'expiration du délai de rétractation, sauf en cas de dysfonctionnement majeur de l'Application + imputable à Geosector. +

+ +

6. Propriété intellectuelle

+ +

6.1 Droits de Geosector

+

+ L'Application, y compris son contenu, sa structure, ses fonctionnalités, son code source, ses interfaces, son design, + ses logos et ses marques, est la propriété exclusive de Geosector ou de ses concédants de licence. + Ces éléments sont protégés par les lois relatives à la propriété intellectuelle. +

+ +

6.2 Droits des Utilisateurs

+

+ L'Utilisateur conserve tous les droits de propriété intellectuelle sur le Contenu qu'il crée et partage via l'Application, + sous réserve de la licence accordée à Geosector conformément à l'article 4.3. +

+ +

6.3 Signalement d'une violation

+

+ Si vous pensez que votre contenu a été utilisé d'une manière qui constitue une violation de vos droits de propriété intellectuelle, + veuillez nous contacter à l'adresse suivante : [adresse email]. +

+ +

7. Confidentialité et données personnelles

+

+ La collecte et le traitement des Données Personnelles des Utilisateurs sont régis par notre Politique de Confidentialité, + disponible à l'adresse suivante : handleNavigation('politique-confidentialite')}>Politique de confidentialité. +

+ +

8. Limitation de responsabilité

+ +

8.1 Disponibilité de l'Application

+

+ Geosector s'efforce de maintenir l'Application accessible 24 heures sur 24 et 7 jours sur 7. Cependant, l'accès peut être + temporairement suspendu, sans préavis, en raison de maintenance technique, de mise à jour ou pour toute autre raison. +

+

+ Geosector ne peut être tenu responsable de tout dommage résultant de l'indisponibilité temporaire de l'Application. +

+ +

8.2 Contenus et services tiers

+

+ L'Application peut contenir des liens vers des sites web ou services tiers. Geosector n'exerce aucun contrôle sur ces sites et services + et n'assume aucune responsabilité quant à leur contenu ou leurs pratiques. +

+ +

8.3 Limitation générale de responsabilité

+

+ Dans toute la mesure permise par la loi applicable, Geosector ne pourra être tenu responsable de tout dommage indirect, + spécial, accessoire, consécutif ou punitif, y compris les pertes de profits, de revenus, de données ou d'opportunités commerciales, + résultant de l'utilisation ou de l'impossibilité d'utiliser l'Application. +

+

+ La responsabilité totale de Geosector envers l'Utilisateur pour toute réclamation découlant des présentes CGU ne pourra excéder + le montant payé par l'Utilisateur à Geosector au cours des douze (12) mois précédant le fait générateur de la responsabilité. +

+ +

9. Modifications des CGU

+

+ Geosector se réserve le droit de modifier les présentes CGU à tout moment. Les Utilisateurs seront informés des modifications + par le biais d'une notification dans l'Application ou par e-mail. +

+

+ Les modifications prendront effet à la date indiquée dans la notification. En continuant à utiliser l'Application après cette date, + l'Utilisateur accepte les CGU modifiées. +

+

+ Si l'Utilisateur n'accepte pas les modifications, il doit cesser d'utiliser l'Application et, le cas échéant, supprimer son Compte. +

+ +

10. Résiliation

+ +

10.1 Résiliation par l'Utilisateur

+

+ L'Utilisateur peut, à tout moment, cesser d'utiliser l'Application et supprimer son Compte en suivant la procédure prévue à cet effet + dans les paramètres de l'Application. +

+ +

10.2 Résiliation par Geosector

+

+ Geosector peut, à sa discrétion, suspendre ou résilier l'accès de l'Utilisateur à l'Application en cas de violation des présentes CGU, + sans préjudice de tout autre droit ou recours. +

+ +

10.3 Conséquences de la résiliation

+

+ En cas de résiliation, l'Utilisateur perd l'accès à son Compte et à toutes les Fonctionnalités de l'Application. + Les sections des présentes CGU relatives à la propriété intellectuelle, à la limitation de responsabilité et au règlement des litiges + survivront à la résiliation. +

+ +

11. Dispositions spécifiques aux applications mobiles

+ +

11.1 Application iOS (Apple App Store)

+

+ Si vous téléchargez l'Application via l'App Store d'Apple, vous reconnaissez et acceptez que : +

+
    +
  • Ces CGU sont conclues entre vous et Geosector, et non avec Apple
  • +
  • Apple n'a aucune obligation de fournir des services de maintenance ou d'assistance concernant l'Application
  • +
  • En cas de non-conformité de l'Application avec une garantie applicable, vous pouvez en informer Apple, qui pourra vous rembourser le prix d'achat
  • +
  • Apple n'est pas responsable du traitement des réclamations ou de la responsabilité liée à l'Application
  • +
  • En cas de réclamation d'un tiers selon laquelle l'Application enfreint ses droits de propriété intellectuelle, Apple n'est pas responsable de l'enquête, de la défense, du règlement et de la décharge de cette réclamation
  • +
  • Vous devez vous conformer aux conditions d'utilisation de l'App Store d'Apple lors de l'utilisation de l'Application
  • +
+ +

11.2 Application Android (Google Play)

+

+ Si vous téléchargez l'Application via Google Play, vous reconnaissez et acceptez que : +

+
    +
  • Ces CGU sont conclues entre vous et Geosector, et non avec Google
  • +
  • L'utilisation de l'Application doit respecter les conditions d'utilisation de Google Play
  • +
  • Google n'a aucune obligation de fournir des services de maintenance ou d'assistance concernant l'Application
  • +
+ +

12. Dispositions diverses

+ +

12.1 Droit applicable et juridiction compétente

+

+ Les présentes CGU sont régies par le droit français. Tout litige relatif à leur interprétation ou à leur exécution relève, + à défaut d'accord amiable, de la compétence exclusive des tribunaux français compétents. +

+ +

12.2 Indépendance des clauses

+

+ Si une ou plusieurs dispositions des présentes CGU sont tenues pour non valides ou déclarées comme telles en application d'une loi, + d'un règlement ou à la suite d'une décision définitive d'une juridiction compétente, les autres stipulations garderont toute leur force + et leur portée. +

+ +

12.3 Non-renonciation

+

+ Le fait pour Geosector de ne pas se prévaloir d'un manquement de l'Utilisateur à l'une quelconque des obligations visées dans les présentes CGU + ne saurait être interprété comme une renonciation à s'en prévaloir ultérieurement. +

+ +

12.4 Communication

+

+ Toute notification ou communication dans le cadre des présentes CGU doit être adressée à Geosector par e-mail à l'adresse suivante : + [adresse email] ou par courrier postal à l'adresse suivante : [adresse postale]. +

+ +

13. Contact

+

+ Pour toute question concernant les présentes CGU, veuillez nous contacter à : +

+

+ Geosector
+ E-mail : support@geosector.fr
+ Adresse : [Adresse de l'entreprise]
+ Téléphone : +33 (0)1 23 45 67 89 +

+
+
+
+
+
diff --git a/web/src/pages/Contact.svelte b/web/src/pages/Contact.svelte new file mode 100644 index 00000000..42d67f02 --- /dev/null +++ b/web/src/pages/Contact.svelte @@ -0,0 +1,200 @@ + + +
+ +
+
+
+

Contactez-nous

+

Notre équipe est à votre disposition pour répondre à toutes vos questions et vous accompagner dans votre projet.

+
+
+
+ + +
+
+
+
+
+ +
+

Nos coordonnées

+ +
+
+
+ + + +
+
+

Téléphone

+

+33 (0)1 23 45 67 89

+
+
+ +
+
+ + + +
+
+

Email

+

contact@geosector.fr

+
+
+ + +
+
+ + + +
+
+

Horaires d'ouverture

+

Lundi - Vendredi: 9h00 - 18h00
Samedi - Dimanche: Fermé

+
+
+
+ +
+

Suivez-nous

+ +
+
+ + +
+

Envoyez-nous un message

+ + {#if formSubmitted} +
+

Message envoyé avec succès !

+

Nous vous répondrons dans les plus brefs délais.

+
+ {:else} +
+
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ {/if} +
+
+
+
+
+
+ + + + +
+
+
+

Questions fréquentes

+ +
+
+

Comment puis-je obtenir une démonstration de Geosector ?

+

Vous pouvez demander une démonstration en remplissant le formulaire de contact ci-dessus ou en nous appelant directement. Un de nos conseillers vous contactera pour organiser une session personnalisée.

+
+ +
+

Combien de temps dure la période d'essai ?

+

Nous proposons une période d'essai gratuite de 14 jours avec toutes les fonctionnalités disponibles. Aucune carte de crédit n'est requise pour commencer votre essai.

+
+ +
+

Proposez-vous des formations pour utiliser votre logiciel ?

+

Oui, nous proposons des sessions de formation complètes pour vous aider à tirer le meilleur parti de Geosector. Ces formations peuvent être réalisées en ligne ou dans vos locaux selon vos préférences.

+
+ +
+

Quels types de support technique proposez-vous ?

+

Nous offrons un support technique par email, téléphone et chat en direct pendant les heures de bureau. Nos clients avec des forfaits premium bénéficient d'un support 24/7.

+
+
+
+
+
+
diff --git a/web/src/pages/Fonctionnalites.svelte b/web/src/pages/Fonctionnalites.svelte new file mode 100644 index 00000000..1d775558 --- /dev/null +++ b/web/src/pages/Fonctionnalites.svelte @@ -0,0 +1,244 @@ + + +
+ +
+
+
+

Fonctionnalités

+

Découvrez les outils puissants qui font de Geosector la solution idéale pour la gestion de vos distributions.

+
+
+
+ + +
+
+
+

Fonctionnalités principales

+ +
+ +
+
+ + + +
+
+

Cartographie avancée

+

Visualisez vos tournées sur des cartes interactives avec des données en temps réel sur le trafic et les conditions météorologiques.

+
    +
  • + + + + Cartes détaillées avec points d'intérêt +
  • +
  • + + + + Suivi GPS en temps réel +
  • +
  • + + + + Alertes de trafic et d'incidents +
  • +
+
+
+ + +
+
+ + + +
+
+

Optimisation des itinéraires

+

Nos algorithmes avancés calculent les itinéraires les plus efficaces en tenant compte de multiples facteurs.

+
    +
  • + + + + Réduction des coûts de carburant jusqu'à 30% +
  • +
  • + + + + Prise en compte des contraintes horaires +
  • +
  • + + + + Adaptation dynamique aux conditions réelles +
  • +
+
+
+ + +
+
+ + + +
+
+

Planification intelligente

+

Planifiez vos tournées à l'avance et adaptez-les facilement en fonction des imprévus.

+
    +
  • + + + + Calendrier interactif avec vue mensuelle/hebdomadaire/quotidienne +
  • +
  • + + + + Gestion des priorités et des urgences +
  • +
  • + + + + Notifications automatiques pour les changements +
  • +
+
+
+ + +
+
+ + + +
+
+

Rapports et analyses

+

Obtenez des insights précieux sur vos opérations grâce à nos outils d'analyse avancés.

+
    +
  • + + + + Tableaux de bord personnalisables +
  • +
  • + + + + Exportation des données en plusieurs formats +
  • +
  • + + + + Indicateurs de performance clés (KPIs) +
  • +
+
+
+
+
+
+
+ + +
+
+
+

Application mobile

+ +
+
+

Emportez Geosector partout avec vous

+

Notre application mobile offre toutes les fonctionnalités essentielles pour gérer vos distributions en déplacement.

+ +
+
+
+ + + +
+
+

Interface adaptée aux mobiles

+

Expérience utilisateur optimisée pour les écrans tactiles et la navigation mobile.

+
+
+ +
+
+ + + +
+
+

Mode hors ligne

+

Continuez à travailler même sans connexion internet, avec synchronisation automatique.

+
+
+ +
+
+ + + +
+
+

Notifications push

+

Restez informé des changements importants et des mises à jour en temps réel.

+
+
+
+ + +
+ +
+
+ Capture d'écran de l'application mobile +
+
+
+
+
+
+ + +
+
+
+

Prêt à optimiser vos distributions ?

+

Rejoignez les milliers d'entreprises qui font confiance à Geosector pour améliorer leur efficacité opérationnelle.

+ Demander une démo +
+
+
+
diff --git a/web/src/pages/MentionsLegales.svelte b/web/src/pages/MentionsLegales.svelte new file mode 100644 index 00000000..164c0c06 --- /dev/null +++ b/web/src/pages/MentionsLegales.svelte @@ -0,0 +1,118 @@ + + +
+ +
+
+
+

Mentions Légales

+

+ Informations juridiques relatives à notre site web et application mobile +

+
+
+
+ + +
+
+
+
+

1. Éditeur du site et de l'application

+ +
+

Le site web et l'application mobile Geosector sont édités par :

+

Geosector

+

SIRET : [Votre numéro SIRET]

+

Adresse : [Votre adresse]

+

Email : contact@geosector.fr

+

Téléphone : [Votre numéro de téléphone]

+

Directeur de la publication : [Nom du directeur de publication]

+
+
+ +
+

2. Hébergement

+ +
+

Le site web et l'application mobile Geosector sont hébergés par :

+

[Nom de l'hébergeur]

+

Adresse : [Adresse de l'hébergeur]

+

Site web : [Site web de l'hébergeur]

+

Email : [Email de l'hébergeur]

+

Téléphone : [Téléphone de l'hébergeur]

+
+
+ +
+

3. Propriété intellectuelle

+ +
+

L'ensemble du contenu du site web et de l'application mobile Geosector, incluant sans limitation les textes, graphiques, images, logos, icônes, photographies, est la propriété exclusive de Geosector et est protégé par les lois françaises et internationales relatives à la propriété intellectuelle.

+ +

Toute reproduction, représentation, modification, publication, transmission, adaptation, totale ou partielle des éléments du site ou de l'application, quel que soit le moyen ou le procédé utilisé, est interdite sans autorisation écrite préalable de Geosector.

+ +

Toute utilisation non autorisée des contenus, œuvres ou marques constitue une contrefaçon sanctionnée par le Code de la propriété intellectuelle.

+
+
+ +
+

4. Liens hypertextes

+ +
+

Le site web et l'application Geosector peuvent contenir des liens hypertextes vers d'autres sites internet ou applications.

+ +

Geosector n'a pas la possibilité de vérifier le contenu des sites ainsi visités, et n'assumera en conséquence aucune responsabilité de ce fait.

+ +

La création de liens hypertextes vers le site web ou l'application Geosector est soumise à l'accord préalable de l'éditeur.

+
+
+ +
+

5. Limitation de responsabilité

+ +
+

Geosector s'efforce d'assurer au mieux de ses possibilités l'exactitude et la mise à jour des informations diffusées sur son site web et son application mobile, dont elle se réserve le droit de corriger, à tout moment et sans préavis, le contenu.

+ +

Toutefois, Geosector ne peut garantir l'exactitude, la précision ou l'exhaustivité des informations mises à disposition sur son site web et son application.

+ +

En conséquence, Geosector décline toute responsabilité :

+
    +
  • Pour toute imprécision, inexactitude ou omission portant sur des informations disponibles sur le site web ou l'application ;
  • +
  • Pour tous dommages résultant d'une intrusion frauduleuse d'un tiers ayant entraîné une modification des informations ou éléments mis à disposition sur le site web ou l'application ;
  • +
  • Et plus généralement, pour tous dommages, directs ou indirects, qu'elles qu'en soient les causes, origines, natures ou conséquences, provoqués en raison de l'accès de quiconque au site web ou à l'application ou de l'impossibilité d'y accéder, ainsi que l'utilisation du site web ou de l'application et/ou du crédit accordé à une quelconque information provenant directement ou indirectement de ces derniers.
  • +
+
+
+ +
+

6. Loi applicable et juridiction

+ +
+

Les présentes mentions légales sont régies par la loi française. En cas de litige, les tribunaux français seront seuls compétents.

+ +

Pour toute question relative à l'application des présentes mentions légales, vous pouvez nous contacter à l'adresse email : contact@geosector.fr

+
+
+ +
+

7. Modifications

+ +
+

Geosector se réserve le droit de modifier les présentes mentions légales à tout moment. L'utilisateur est invité à les consulter régulièrement.

+ +

Dernière mise à jour : {new Date().toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' })}

+
+
+
+
+
+
diff --git a/web/src/pages/PolitiqueConfidentialite.svelte b/web/src/pages/PolitiqueConfidentialite.svelte new file mode 100644 index 00000000..598fc536 --- /dev/null +++ b/web/src/pages/PolitiqueConfidentialite.svelte @@ -0,0 +1,216 @@ + + +
+ +
+
+
+

+ Politique de confidentialité +

+

+ Protection de vos données personnelles et respect de votre vie privée +

+
+
+
+ + +
+
+
+
+

Dernière mise à jour : {new Date().toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' })}

+ +

Introduction

+

+ Cette politique de confidentialité s'applique à l'application Geosector, disponible sur le Web, iOS et Android, + ainsi qu'à tous les services associés (collectivement désignés par "Geosector", "nous", "notre" ou "nos"). +

+

+ Chez Geosector, nous accordons une grande importance à la protection de vos données personnelles. + Cette politique décrit quelles informations nous collectons, comment nous les utilisons, + et quels choix vous avez concernant ces données. +

+

+ Cette politique de confidentialité doit être lue conjointement avec nos handleNavigation('conditions-utilisation')}>Conditions d'utilisation, qui régissent votre utilisation de notre application. +

+ +

Quelles informations collectons-nous ?

+ +

1. Informations que vous nous fournissez

+
    +
  • Informations de compte : Lors de l'inscription, nous collectons votre nom, prénom, adresse e-mail, et mot de passe.
  • +
  • Informations de profil : Vous pouvez nous fournir des informations supplémentaires comme votre fonction, l'organisation à laquelle vous appartenez, et votre photo de profil.
  • +
  • Contenu utilisateur : Les informations que vous créez, téléchargez ou partagez via notre application, notamment les secteurs géographiques, les passages, et les commentaires.
  • +
  • Communications : Lorsque vous nous contactez, nous conservons un historique de ces communications.
  • +
+ +

2. Informations collectées automatiquement

+
    +
  • Données d'utilisation : Informations sur vos interactions avec notre application, comme les fonctionnalités utilisées, les pages visitées et le temps passé.
  • +
  • Informations sur l'appareil : Type d'appareil, système d'exploitation, version de l'application, langue, fuseau horaire et autres caractéristiques techniques.
  • +
  • Données de localisation : Avec votre consentement, nous collectons des données de géolocalisation précises pour vous permettre d'utiliser les fonctionnalités cartographiques et de secteurs.
  • +
  • Cookies et technologies similaires : Sur notre version web, nous utilisons des cookies et des technologies similaires pour améliorer votre expérience. Pour plus d'informations, consultez notre politique relative aux cookies.
  • +
+ +

Comment utilisons-nous vos informations ?

+

Nous utilisons vos informations pour les finalités suivantes :

+
    +
  • Fournir, maintenir et améliorer notre application et ses fonctionnalités
  • +
  • Créer et gérer votre compte
  • +
  • Traiter vos transactions et paiements
  • +
  • Vous envoyer des informations techniques, des mises à jour, des alertes de sécurité et des messages administratifs
  • +
  • Répondre à vos commentaires et questions et vous fournir un support client
  • +
  • Communiquer avec vous à propos de produits, services, offres et événements
  • +
  • Surveiller et analyser les tendances, l'utilisation et les activités liées à notre application
  • +
  • Détecter, prévenir et résoudre les problèmes techniques et de sécurité
  • +
  • Se conformer aux obligations légales
  • +
+ +

Base légale du traitement (pour les utilisateurs de l'EEE et du Royaume-Uni)

+

Pour les utilisateurs de l'Espace économique européen (EEE) et du Royaume-Uni, nous traitons vos données personnelles sur les bases légales suivantes :

+
    +
  • Exécution d'un contrat : Lorsque le traitement est nécessaire pour l'exécution d'un contrat auquel vous êtes partie ou pour prendre des mesures à votre demande avant de conclure un contrat.
  • +
  • Intérêts légitimes : Lorsque le traitement est nécessaire pour nos intérêts légitimes ou ceux d'un tiers, et que ces intérêts ne sont pas supplantés par vos intérêts ou droits fondamentaux.
  • +
  • Consentement : Lorsque vous avez donné votre consentement au traitement de vos données personnelles pour une ou plusieurs finalités spécifiques.
  • +
  • Obligation légale : Lorsque le traitement est nécessaire pour respecter une obligation légale à laquelle nous sommes soumis.
  • +
+ +

Comment partageons-nous vos informations ?

+

Nous pouvons partager vos informations personnelles avec les tiers suivants :

+
    +
  • Prestataires de services : Nous travaillons avec des prestataires de services tiers qui fournissent des services tels que l'hébergement, l'analyse, le traitement des paiements et le support client.
  • +
  • Partenaires professionnels : Nous pouvons partager des informations avec nos partenaires commerciaux pour offrir certains produits, services ou promotions.
  • +
  • Conformité légale : Nous pouvons divulguer vos informations si nous estimons de bonne foi que cette divulgation est nécessaire pour se conformer à la loi, protéger nos droits ou assurer votre sécurité.
  • +
  • Transactions d'entreprise : En cas de fusion, acquisition, restructuration ou vente d'actifs, vos informations peuvent être transférées dans le cadre de cette transaction.
  • +
+

Nous ne vendons pas vos données personnelles à des tiers.

+ +

Transferts internationaux de données

+

+ Vos informations peuvent être transférées et traitées dans des pays autres que celui où vous résidez. + Ces pays peuvent avoir des lois sur la protection des données différentes de celles de votre pays. +

+

+ Si nous transférons des données personnelles provenant de l'EEE, du Royaume-Uni ou de la Suisse vers des pays + n'offrant pas un niveau de protection adéquat selon les autorités compétentes, nous utilisons des + mécanismes de transfert légalement reconnus, tels que les clauses contractuelles types approuvées par la Commission européenne. +

+ +

Vos droits et choix

+

Selon votre lieu de résidence, vous pouvez disposer de certains droits concernant vos données personnelles :

+
    +
  • Accès et portabilité : Vous pouvez accéder à vos informations personnelles et en obtenir une copie dans un format structuré, couramment utilisé et lisible par machine.
  • +
  • Correction : Vous pouvez mettre à jour ou corriger vos informations personnelles si elles sont inexactes ou incomplètes.
  • +
  • Suppression : Vous pouvez demander la suppression de vos données personnelles dans certaines circonstances.
  • +
  • Restriction et opposition : Vous pouvez demander la restriction du traitement de vos données personnelles ou vous opposer à leur traitement dans certaines circonstances.
  • +
  • Consentement : Lorsque le traitement est basé sur votre consentement, vous pouvez retirer ce consentement à tout moment.
  • +
  • Réclamation : Vous avez le droit d'introduire une réclamation auprès d'une autorité de protection des données.
  • +
+

+ Pour exercer ces droits, contactez-nous à l'adresse indiquée dans la section "Nous contacter" ci-dessous. + Notez que ces droits peuvent être soumis à des limitations et exceptions prévues par la loi applicable. +

+ +

Conservation des données

+

+ Nous conservons vos données personnelles aussi longtemps que nécessaire pour atteindre les finalités décrites dans cette politique, + sauf si une période de conservation plus longue est requise ou permise par la loi. + Les critères utilisés pour déterminer nos périodes de conservation comprennent : +

+
    +
  • La durée pendant laquelle nous entretenons une relation continue avec vous et vous fournissons l'application
  • +
  • Si nous avons une obligation légale à laquelle nous sommes soumis
  • +
  • Si la conservation est souhaitable compte tenu de notre position juridique (par exemple, concernant les délais de prescription applicables, les litiges ou les enquêtes réglementaires)
  • +
+ +

Sécurité des données

+

+ Nous mettons en œuvre des mesures de sécurité techniques et organisationnelles appropriées pour protéger vos données personnelles + contre la perte accidentelle, l'utilisation non autorisée, l'altération et la divulgation. + Ces mesures comprennent le chiffrement des données, les contrôles d'accès, les pare-feu et les audits de sécurité réguliers. +

+

+ Cependant, aucun système de sécurité n'est impénétrable et nous ne pouvons garantir la sécurité absolue de vos informations. + Il est important que vous preniez des précautions pour protéger votre mot de passe et votre appareil. +

+ +

Protection de la vie privée des enfants

+

+ Notre application n'est pas destinée aux personnes âgées de moins de 16 ans et nous ne collectons pas sciemment + des données personnelles auprès d'enfants de moins de 16 ans. Si vous êtes parent ou tuteur et que vous pensez + que votre enfant nous a fourni des informations personnelles, veuillez nous contacter. +

+ +

Modifications de cette politique

+

+ Nous pouvons modifier cette politique de confidentialité de temps à autre. Si nous apportons des modifications importantes, + nous vous en informerons par e-mail ou par une notification dans notre application avant que les modifications + ne prennent effet. Nous vous encourageons à consulter régulièrement cette politique pour rester informé de + nos pratiques en matière de protection des données. +

+ +

Nous contacter

+

+ Si vous avez des questions concernant cette politique de confidentialité ou nos pratiques en matière de protection des données, + veuillez nous contacter à l'adresse suivante : +

+

+ Geosector
+ E-mail : privacy@geosector.fr
+ Adresse : [Adresse de l'entreprise]
+ Téléphone : +33 (0)1 23 45 67 89 +

+ +

Informations spécifiques aux plateformes

+ +

Application iOS (Apple App Store)

+

+ En utilisant notre application via l'App Store d'Apple, vous reconnaissez qu'Apple n'est pas responsable de nos pratiques + en matière de protection des données. Veuillez consulter la politique de confidentialité d'Apple pour plus d'informations + sur la façon dont Apple peut collecter et traiter vos données. +

+ +

Application Android (Google Play)

+

+ En utilisant notre application via Google Play, vous reconnaissez que Google n'est pas responsable de nos pratiques + en matière de protection des données. Veuillez consulter la politique de confidentialité de Google pour plus d'informations + sur la façon dont Google peut collecter et traiter vos données. +

+ +

Permissions des applications mobiles

+

Notre application peut demander certaines permissions sur votre appareil mobile, notamment :

+
    +
  • Localisation : Pour les fonctionnalités basées sur la localisation, comme l'affichage des secteurs et la navigation
  • +
  • Stockage : Pour stocker des données localement sur votre appareil
  • +
  • Appareil photo : Pour scanner des codes QR ou prendre des photos
  • +
  • Notifications : Pour vous envoyer des alertes et des mises à jour importantes
  • +
+

+ Vous pouvez gérer ces permissions à tout moment dans les paramètres de votre appareil, mais notez que + la désactivation de certaines permissions peut limiter les fonctionnalités de l'application. +

+
+
+
+
+
diff --git a/web/tailwind.config.js b/web/tailwind.config.js index 3c0cdf09..d923d9a4 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -3,6 +3,9 @@ export default { content: ["./src/**/*.{html,js,svelte,ts}"], theme: { + fontFamily: { + sans: ['Figtree', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'sans-serif'], + }, extend: {} }, diff --git a/web/vite.config.js b/web/vite.config.js index d32eba1d..ca5dd9c2 100644 --- a/web/vite.config.js +++ b/web/vite.config.js @@ -1,7 +1,19 @@ import { defineConfig } from 'vite' import { svelte } from '@sveltejs/vite-plugin-svelte' -// https://vite.dev/config/ +// https://vitejs.dev/config/ export default defineConfig({ plugins: [svelte()], + server: { + // Configuration pour le mode histoire + historyApiFallback: true + }, + // Pour le build de production, configurer la gestion des routes + build: { + rollupOptions: { + output: { + manualChunks: undefined, + }, + }, + }, })