feat: création branche singletons - début refactorisation

- Sauvegarde des fichiers critiques
- Préparation transformation ApiService en singleton
- Préparation création CurrentUserService et CurrentAmicaleService
- Objectif: renommer Box users -> user
This commit is contained in:
d6soft
2025-06-05 15:22:29 +02:00
parent ef83b258d9
commit 95e9af23e2
41 changed files with 68682 additions and 65048 deletions

90
.vscode/settings.json vendored
View File

@@ -1,90 +0,0 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": null,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
},
"[php]": {
"editor.defaultFormatter": "bmewburn.vscode-intelephense-client",
"editor.tabSize": 4,
"editor.insertSpaces": true
},
"[dart]": {
"editor.defaultFormatter": "Dart-Code.dart-code",
"editor.formatOnSave": true,
"editor.formatOnType": true,
"editor.rulers": [
80
],
"editor.selectionHighlight": false,
"editor.suggest.snippetsPreventQuickSuggestions": false,
"editor.suggestSelection": "first",
"editor.tabCompletion": "onlySnippets",
"editor.wordBasedSuggestions": "off"
},
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode",
"editor.tabSize": 2
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.wordWrap": "on"
},
"php.validate.executablePath": "/usr/bin/php",
"php.suggest.basic": false,
"intelephense.environment.phpVersion": "8.3.0",
"dart.flutterSdkPath": "/Users/pierre/dev/flutter",
"dart.lineLength": 80,
"svelte.enable-ts-plugin": true,
"files.exclude": {
"**/.git": true,
"**/.DS_Store": true,
"**/node_modules": true,
"**/build": true,
"**/.dart_tool": true,
"**/.flutter-plugins": true,
"**/.flutter-plugins-dependencies": true
},
"files.associations": {
"*.php": "php",
"*.dart": "dart",
"*.svelte": "svelte"
},
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/build": true,
"**/.dart_tool": true,
"**/vendor": true
},
"cline.autoApproveRequests": true,
"cline.enableMemoryBank": true,
"cline.includeSnippetsFromMemory": true,
"cline.contextLength": 10000,
"cline.autoFormat": true,
"cline.primaryDocumentationFile": "CONTEXT-AI.md",
"cline.gitIntegration": true,
"cline.projectStructure": {
"api": "php",
"app": "flutter",
"web": "svelte"
},
"cline.referenceFiles": {
"database": "docs/geo_app.dump",
"apiEndpoints": "docs/api_endpoints.md",
"architecture": "docs/architecture.md"
},
"cline.databaseSchema": "docs/geo_app.dump"
}

View File

@@ -1,153 +0,0 @@
{
"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": 1,
"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,
"php.validate.executablePath": "/opt/homebrew/opt/php@8.3/bin/php",
"php.executablePath": "/opt/homebrew/opt/php@8.3/bin/php",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.formatOnPaste": true
},
"prettier.printWidth": 360,
"prettier.semi": true,
"prettier.singleQuote": true,
"prettier.tabWidth": 2,
"prettier.trailingComma": "es5",
"explorer.autoReveal": false,
"explorer.confirmDragAndDrop": false,
"emmet.triggerExpansionOnTab": true,
"emmet.includeLanguages": {
"javascript": "javascript",
"php": "php",
"svelte": "html",
"dart": "dart"
},
"problems.decorations.enabled": true,
"explorer.decorations.colors": true,
"explorer.decorations.badges": true,
"php.validate.enable": true,
"php.suggest.basic": false,
"dart.analysisExcludedFolders": [],
"dart.enableSdkFormatter": 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": "web/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,
"js/ts.implicitProjectConfig.checkJs": false,
"svelte.enable-ts-plugin": false,
"cline.autoApproveLimit": 100,
"cline.autoApproveRequests": true,
"cline.enableMemoryBank": true,
"cline.includeSnippetsFromMemory": true,
"cline.contextLength": 10000,
"cline.autoFormat": true,
"cline.primaryDocumentationFile": ".cline",
"cline.gitIntegration": true,
"cline.projectStructure": {
"api": "php",
"app": "flutter",
"web": "svelte"
},
"cline.referenceFiles": {
"database": "docs/db-resalice.dump",
"apiEndpoints": "docs/api_endpoints.md",
"architecture": "docs/architecture.md"
},
"cline.databaseSchema": "docs/db-resalice.dump",
"peacock.color": "#dd0531",
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#fa1b49",
"activityBar.background": "#fa1b49",
"activityBar.foreground": "#e7e7e7",
"activityBar.inactiveForeground": "#e7e7e799",
"activityBarBadge.background": "#155e02",
"activityBarBadge.foreground": "#e7e7e7",
"commandCenter.border": "#e7e7e799",
"sash.hoverBorder": "#fa1b49",
"statusBar.background": "#dd0531",
"statusBar.foreground": "#e7e7e7",
"statusBarItem.hoverBackground": "#fa1b49",
"statusBarItem.remoteBackground": "#dd0531",
"statusBarItem.remoteForeground": "#e7e7e7",
"titleBar.activeBackground": "#dd0531",
"titleBar.activeForeground": "#e7e7e7",
"titleBar.inactiveBackground": "#dd053199",
"titleBar.inactiveForeground": "#e7e7e799"
}
}

View File

@@ -155,7 +155,8 @@ class LoginController {
'email' => $email, 'email' => $email,
'name' => $decryptedName, 'name' => $decryptedName,
'first_name' => $user['first_name'] ?? '', 'first_name' => $user['first_name'] ?? '',
'fk_role' => $user['fk_role'] ?? '0' 'fk_role' => $user['fk_role'] ?? '0',
'fk_entite' => $user['fk_entite'] ?? '0',
// 'interface' supprimée pour se baser uniquement sur le rôle // 'interface' supprimée pour se baser uniquement sur le rôle
]; ];
Session::login($sessionData); Session::login($sessionData);
@@ -406,7 +407,7 @@ class LoginController {
// 6. Récupérer les membres (users de l'entité du user) si nécessaire // 6. Récupérer les membres (users de l'entité du user) si nécessaire
if ($interface === 'admin' && $user['fk_role'] == 2 && !empty($user['fk_entite'])) { if ($interface === 'admin' && $user['fk_role'] == 2 && !empty($user['fk_entite'])) {
$membresStmt = $this->db->prepare( $membresStmt = $this->db->prepare(
'SELECT id, fk_role, fk_titre, encrypted_name, first_name, sect_name, 'SELECT id, fk_role, fk_entite, fk_titre, encrypted_name, first_name, sect_name,
encrypted_user_name, encrypted_phone, encrypted_mobile, encrypted_email, encrypted_user_name, encrypted_phone, encrypted_mobile, encrypted_email,
date_naissance, date_embauche, chk_active date_naissance, date_embauche, chk_active
FROM users FROM users
@@ -422,6 +423,7 @@ class LoginController {
$membreItem = [ $membreItem = [
'id' => $membre['id'], 'id' => $membre['id'],
'fk_role' => $membre['fk_role'], 'fk_role' => $membre['fk_role'],
'fk_entite' => $membre['fk_entite'],
'fk_titre' => $membre['fk_titre'], 'fk_titre' => $membre['fk_titre'],
'first_name' => $membre['first_name'] ?? '', 'first_name' => $membre['first_name'] ?? '',
'sect_name' => $membre['sect_name'] ?? '', 'sect_name' => $membre['sect_name'] ?? '',

File diff suppressed because one or more lines are too long

View File

@@ -24,13 +24,14 @@ class MembreModelAdapter extends TypeAdapter<MembreModel> {
name: fields[8] as String, name: fields[8] as String,
username: fields[9] as String, username: fields[9] as String,
email: fields[10] as String, email: fields[10] as String,
fkEntite: fields[11] as int,
); );
} }
@override @override
void write(BinaryWriter writer, MembreModel obj) { void write(BinaryWriter writer, MembreModel obj) {
writer writer
..writeByte(11) ..writeByte(12)
..writeByte(0) ..writeByte(0)
..write(obj.id) ..write(obj.id)
..writeByte(1) ..writeByte(1)
@@ -52,7 +53,9 @@ class MembreModelAdapter extends TypeAdapter<MembreModel> {
..writeByte(9) ..writeByte(9)
..write(obj.username) ..write(obj.username)
..writeByte(10) ..writeByte(10)
..write(obj.email); ..write(obj.email)
..writeByte(11)
..write(obj.fkEntite);
} }
@override @override

View File

@@ -1,31 +0,0 @@
Extension Discovery Cache
=========================
This folder is used by `package:extension_discovery` to cache lists of
packages that contains extensions for other packages.
DO NOT USE THIS FOLDER
----------------------
* Do not read (or rely) the contents of this folder.
* Do write to this folder.
If you're interested in the lists of extensions stored in this folder use the
API offered by package `extension_discovery` to get this information.
If this package doesn't work for your use-case, then don't try to read the
contents of this folder. It may change, and will not remain stable.
Use package `extension_discovery`
---------------------------------
If you want to access information from this folder.
Feel free to delete this folder
-------------------------------
Files in this folder act as a cache, and the cache is discarded if the files
are older than the modification time of `.dart_tool/package_config.json`.
Hence, it should never be necessary to clear this cache manually, if you find a
need to do please file a bug.

View File

@@ -1 +0,0 @@
{"version":2,"entries":[{"package":"geosector_app","rootUri":"../","packageUri":"lib/"}]}

File diff suppressed because one or more lines are too long

View File

@@ -552,7 +552,6 @@ file:///Users/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/classes/b
file:///Users/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/classes/lonlat.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/classes/lonlat.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/classes/utm.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/classes/utm.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/mgrs.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/mgrs.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/nested-1.0.0/lib/nested.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/package_info_plus.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/package_info_plus.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/src/package_info_plus_linux.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/src/package_info_plus_linux.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/src/package_info_plus_web.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/src/package_info_plus_web.dart
@@ -639,19 +638,6 @@ file:///Users/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projectio
file:///Users/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/tmerc.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/tmerc.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/utm.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/utm.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/vandg.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/vandg.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/provider.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/async_provider.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/change_notifier_provider.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/consumer.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/deferred_inherited_provider.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/devtool.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/inherited_provider.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/listenable_provider.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/provider.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/proxy_provider.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/reassemble_handler.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/selector.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/value_listenable_provider.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/retry-3.1.2/lib/retry.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/retry-3.1.2/lib/retry.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/source_span.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/source_span.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/charcode.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/charcode.dart
@@ -1801,10 +1787,10 @@ file:///Users/pierre/dev/geosector/app/lib/core/services/location_service.dart
file:///Users/pierre/dev/geosector/app/lib/core/services/sync_service.dart file:///Users/pierre/dev/geosector/app/lib/core/services/sync_service.dart
file:///Users/pierre/dev/geosector/app/lib/core/theme/app_theme.dart file:///Users/pierre/dev/geosector/app/lib/core/theme/app_theme.dart
file:///Users/pierre/dev/geosector/app/lib/main.dart file:///Users/pierre/dev/geosector/app/lib/main.dart
file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_amicale_page.dart
file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_communication_page.dart file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_communication_page.dart
file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_dashboard_home_page.dart file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_dashboard_home_page.dart
file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_dashboard_page.dart file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_dashboard_page.dart
file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_entite.dart
file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_history_page.dart file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_history_page.dart
file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_map_page.dart file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_map_page.dart
file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_statistics_page.dart file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_statistics_page.dart

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -552,7 +552,6 @@ file:///Users/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/classes/b
file:///Users/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/classes/lonlat.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/classes/lonlat.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/classes/utm.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/classes/utm.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/mgrs.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/mgrs.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/nested-1.0.0/lib/nested.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/package_info_plus.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/package_info_plus.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/src/package_info_plus_linux.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/src/package_info_plus_linux.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/src/package_info_plus_web.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/src/package_info_plus_web.dart
@@ -639,19 +638,6 @@ file:///Users/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projectio
file:///Users/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/tmerc.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/tmerc.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/utm.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/utm.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/vandg.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0/lib/src/projections/vandg.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/provider.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/async_provider.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/change_notifier_provider.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/consumer.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/deferred_inherited_provider.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/devtool.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/inherited_provider.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/listenable_provider.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/provider.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/proxy_provider.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/reassemble_handler.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/selector.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/provider-6.1.5/lib/src/value_listenable_provider.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/retry-3.1.2/lib/retry.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/retry-3.1.2/lib/retry.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/source_span.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/source_span.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/charcode.dart file:///Users/pierre/.pub-cache/hosted/pub.dev/source_span-1.10.1/lib/src/charcode.dart
@@ -1800,10 +1786,10 @@ file:///Users/pierre/dev/geosector/app/lib/core/services/location_service.dart
file:///Users/pierre/dev/geosector/app/lib/core/services/sync_service.dart file:///Users/pierre/dev/geosector/app/lib/core/services/sync_service.dart
file:///Users/pierre/dev/geosector/app/lib/core/theme/app_theme.dart file:///Users/pierre/dev/geosector/app/lib/core/theme/app_theme.dart
file:///Users/pierre/dev/geosector/app/lib/main.dart file:///Users/pierre/dev/geosector/app/lib/main.dart
file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_amicale_page.dart
file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_communication_page.dart file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_communication_page.dart
file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_dashboard_home_page.dart file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_dashboard_home_page.dart
file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_dashboard_page.dart file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_dashboard_page.dart
file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_entite.dart
file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_history_page.dart file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_history_page.dart
file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_map_page.dart file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_map_page.dart
file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_statistics_page.dart file:///Users/pierre/dev/geosector/app/lib/presentation/admin/admin_statistics_page.dart

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@
"packages": [ "packages": [
{ {
"name": "geosector_app", "name": "geosector_app",
"version": "0.3.2", "version": "0.3.3",
"dependencies": [ "dependencies": [
"connectivity_plus", "connectivity_plus",
"cupertino_icons", "cupertino_icons",

File diff suppressed because one or more lines are too long

View File

@@ -1,156 +0,0 @@
{
"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": 1,
"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": true,
// -- 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": [300],
"editor.wordWrapColumn": 300,
"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,
"php.validate.executablePath": "/opt/homebrew/opt/php@8.3/bin/php",
"php.executablePath": "/opt/homebrew/opt/php@8.3/bin/php",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.formatOnPaste": true
},
"prettier.printWidth": 360,
"prettier.semi": true,
"prettier.singleQuote": true,
"prettier.tabWidth": 2,
"prettier.trailingComma": "es5",
"explorer.autoReveal": false,
"explorer.confirmDragAndDrop": false,
"emmet.triggerExpansionOnTab": true,
"emmet.includeLanguages": {
"javascript": "javascript",
"php": "php",
"svelte": "html",
"dart": "dart"
},
"problems.decorations.enabled": true,
"explorer.decorations.colors": true,
"explorer.decorations.badges": true,
"php.validate.enable": true,
"php.suggest.basic": false,
"dart.analysisExcludedFolders": [],
"dart.enableSdkFormatter": 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": "web/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,
"js/ts.implicitProjectConfig.checkJs": false,
"svelte.enable-ts-plugin": false,
"cline.autoApproveLimit": 100,
"cline.autoApproveRequests": true,
"cline.enableMemoryBank": true,
"cline.includeSnippetsFromMemory": true,
"cline.contextLength": 10000,
"cline.autoFormat": true,
"cline.primaryDocumentationFile": ".cline",
"cline.gitIntegration": true,
"cline.projectStructure": {
"api": "php",
"app": "flutter",
"web": "svelte"
},
"cline.referenceFiles": {
"database": "docs/db-resalice.dump",
"apiEndpoints": "docs/api_endpoints.md",
"architecture": "docs/architecture.md"
},
"cline.databaseSchema": "docs/db-resalice.dump",
"peacock.color": "#42b883",
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#65c89b",
"activityBar.background": "#65c89b",
"activityBar.foreground": "#15202b",
"activityBar.inactiveForeground": "#15202b99",
"activityBarBadge.background": "#945bc4",
"activityBarBadge.foreground": "#e7e7e7",
"commandCenter.border": "#15202b99",
"sash.hoverBorder": "#65c89b",
"statusBar.background": "#42b883",
"statusBar.foreground": "#15202b",
"statusBarItem.hoverBackground": "#359268",
"statusBarItem.remoteBackground": "#42b883",
"statusBarItem.remoteForeground": "#15202b",
"titleBar.activeBackground": "#42b883",
"titleBar.activeForeground": "#15202b",
"titleBar.inactiveBackground": "#42b88399",
"titleBar.inactiveForeground": "#15202b99"
}
}

1537
app/PLAN2-APP.md Normal file

File diff suppressed because it is too large Load Diff

2853
app/README2-APP.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -37,6 +37,9 @@ class MembreModel extends HiveObject {
@HiveField(10) @HiveField(10)
final String email; final String email;
@HiveField(11)
final int fkEntite;
MembreModel({ MembreModel({
required this.id, required this.id,
required this.fkRole, required this.fkRole,
@@ -49,6 +52,7 @@ class MembreModel extends HiveObject {
required this.name, required this.name,
required this.username, required this.username,
required this.email, required this.email,
required this.fkEntite,
}); });
// Factory pour convertir depuis JSON (API) // Factory pour convertir depuis JSON (API)
@@ -63,13 +67,15 @@ class MembreModel extends HiveObject {
// Convertir le titre en int, qu'il soit déjà int ou string // Convertir le titre en int, qu'il soit déjà int ou string
final dynamic rawTitre = json['fk_titre']; final dynamic rawTitre = json['fk_titre'];
final int fkTitre = final int fkTitre = rawTitre is String ? int.parse(rawTitre) : rawTitre as int;
rawTitre is String ? int.parse(rawTitre) : rawTitre as int;
// Convertir le chkActive en int, qu'il soit déjà int ou string // Convertir le chkActive en int, qu'il soit déjà int ou string
final dynamic rawActive = json['chk_active']; final dynamic rawActive = json['chk_active'];
final int chkActive = final int chkActive = rawActive is String ? int.parse(rawActive) : rawActive as int;
rawActive is String ? int.parse(rawActive) : rawActive as int;
// Convertir le fkEntite en int, qu'il soit déjà int ou string
final dynamic rawEntite = json['fk_entite'];
final int fkEntite = rawEntite is String ? int.parse(rawEntite) : rawEntite as int;
return MembreModel( return MembreModel(
id: id, id: id,
@@ -77,16 +83,13 @@ class MembreModel extends HiveObject {
fkTitre: fkTitre, fkTitre: fkTitre,
firstName: json['first_name'] ?? '', firstName: json['first_name'] ?? '',
sectName: json['sect_name'], sectName: json['sect_name'],
dateNaissance: json['date_naissance'] != null dateNaissance: json['date_naissance'] != null ? DateTime.parse(json['date_naissance']) : null,
? DateTime.parse(json['date_naissance']) dateEmbauche: json['date_embauche'] != null ? DateTime.parse(json['date_embauche']) : null,
: null,
dateEmbauche: json['date_embauche'] != null
? DateTime.parse(json['date_embauche'])
: null,
chkActive: chkActive, chkActive: chkActive,
name: json['name'] ?? '', name: json['name'] ?? '',
username: json['username'] ?? '', username: json['username'] ?? '',
email: json['email'] ?? '', email: json['email'] ?? '',
fkEntite: fkEntite,
); );
} }
@@ -104,6 +107,7 @@ class MembreModel extends HiveObject {
'name': name, 'name': name,
'username': username, 'username': username,
'email': email, 'email': email,
'fk_entite': fkEntite,
}; };
} }
@@ -119,9 +123,10 @@ class MembreModel extends HiveObject {
String? name, String? name,
String? username, String? username,
String? email, String? email,
int? fkEntite,
}) { }) {
return MembreModel( return MembreModel(
id: this.id, id: id,
fkRole: fkRole ?? this.fkRole, fkRole: fkRole ?? this.fkRole,
fkTitre: fkTitre ?? this.fkTitre, fkTitre: fkTitre ?? this.fkTitre,
firstName: firstName ?? this.firstName, firstName: firstName ?? this.firstName,
@@ -132,6 +137,7 @@ class MembreModel extends HiveObject {
name: name ?? this.name, name: name ?? this.name,
username: username ?? this.username, username: username ?? this.username,
email: email ?? this.email, email: email ?? this.email,
fkEntite: fkEntite ?? this.fkEntite,
); );
} }
} }

View File

@@ -28,13 +28,14 @@ class MembreModelAdapter extends TypeAdapter<MembreModel> {
name: fields[8] as String, name: fields[8] as String,
username: fields[9] as String, username: fields[9] as String,
email: fields[10] as String, email: fields[10] as String,
fkEntite: fields[11] as int,
); );
} }
@override @override
void write(BinaryWriter writer, MembreModel obj) { void write(BinaryWriter writer, MembreModel obj) {
writer writer
..writeByte(11) ..writeByte(12)
..writeByte(0) ..writeByte(0)
..write(obj.id) ..write(obj.id)
..writeByte(1) ..writeByte(1)
@@ -56,7 +57,9 @@ class MembreModelAdapter extends TypeAdapter<MembreModel> {
..writeByte(9) ..writeByte(9)
..write(obj.username) ..write(obj.username)
..writeByte(10) ..writeByte(10)
..write(obj.email); ..write(obj.email)
..writeByte(11)
..write(obj.fkEntite);
} }
@override @override

View File

@@ -7,8 +7,7 @@ import 'package:geosector_app/core/data/models/amicale_model.dart';
class AmicaleRepository extends ChangeNotifier { class AmicaleRepository extends ChangeNotifier {
// Utilisation de getters lazy pour n'accéder à la boîte que lorsque nécessaire // Utilisation de getters lazy pour n'accéder à la boîte que lorsque nécessaire
Box<AmicaleModel> get _amicaleBox => Box<AmicaleModel> get _amicaleBox => Hive.box<AmicaleModel>(AppKeys.amicaleBoxName);
Hive.box<AmicaleModel>(AppKeys.amicaleBoxName);
final ApiService _apiService; final ApiService _apiService;
bool _isLoading = false; bool _isLoading = false;
@@ -18,6 +17,19 @@ class AmicaleRepository extends ChangeNotifier {
// Getters // Getters
bool get isLoading => _isLoading; bool get isLoading => _isLoading;
// Méthode pour exposer la Box Hive (nécessaire pour ValueListenableBuilder)
Box<AmicaleModel> getAmicalesBox() {
try {
if (!Hive.isBoxOpen(AppKeys.amicaleBoxName)) {
throw Exception('La boîte amicales n\'est pas ouverte');
}
return _amicaleBox;
} catch (e) {
debugPrint('Erreur lors de l\'accès à la boîte amicales: $e');
rethrow;
}
}
// Méthode pour vérifier si une boîte est ouverte et l'ouvrir si nécessaire // Méthode pour vérifier si une boîte est ouverte et l'ouvrir si nécessaire
Future<void> _ensureBoxIsOpen() async { Future<void> _ensureBoxIsOpen() async {
try { try {
@@ -59,8 +71,7 @@ class AmicaleRepository extends ChangeNotifier {
_ensureBoxIsOpen(); _ensureBoxIsOpen();
return _amicaleBox.get(fkEntite); return _amicaleBox.get(fkEntite);
} catch (e) { } catch (e) {
debugPrint( debugPrint('Erreur lors de la récupération de l\'amicale de l\'utilisateur: $e');
'Erreur lors de la récupération de l\'amicale de l\'utilisateur: $e');
return null; return null;
} }
} }
@@ -146,8 +157,7 @@ class AmicaleRepository extends ChangeNotifier {
final amicale = AmicaleModel.fromJson(amicaleMap); final amicale = AmicaleModel.fromJson(amicaleMap);
await _amicaleBox.put(amicale.id, amicale); await _amicaleBox.put(amicale.id, amicale);
count++; count++;
debugPrint( debugPrint('Amicale unique traitée: ${amicale.name} (ID: ${amicale.id})');
'Amicale unique traitée: ${amicale.name} (ID: ${amicale.id})');
} catch (e) { } catch (e) {
debugPrint('Erreur lors du traitement de l\'amicale unique: $e'); debugPrint('Erreur lors du traitement de l\'amicale unique: $e');
debugPrint('Exception détaillée: $e'); debugPrint('Exception détaillée: $e');
@@ -177,8 +187,7 @@ class AmicaleRepository extends ChangeNotifier {
await processAmicalesData(amicalesData); await processAmicalesData(amicalesData);
return getAllAmicales(); return getAllAmicales();
} else { } else {
debugPrint( debugPrint('Erreur lors de la récupération des amicales: ${response.statusCode}');
'Erreur lors de la récupération des amicales: ${response.statusCode}');
return []; return [];
} }
} catch (e) { } catch (e) {
@@ -204,8 +213,7 @@ class AmicaleRepository extends ChangeNotifier {
await saveAmicale(amicale); await saveAmicale(amicale);
return amicale; return amicale;
} else { } else {
debugPrint( debugPrint('Erreur lors de la récupération de l\'amicale: ${response.statusCode}');
'Erreur lors de la récupération de l\'amicale: ${response.statusCode}');
return null; return null;
} }
} catch (e) { } catch (e) {
@@ -234,8 +242,7 @@ class AmicaleRepository extends ChangeNotifier {
await saveAmicale(updatedAmicale); await saveAmicale(updatedAmicale);
return updatedAmicale; return updatedAmicale;
} else { } else {
debugPrint( debugPrint('Erreur lors de la mise à jour de l\'amicale: ${response.statusCode}');
'Erreur lors de la mise à jour de l\'amicale: ${response.statusCode}');
return null; return null;
} }
} catch (e) { } catch (e) {
@@ -254,23 +261,17 @@ class AmicaleRepository extends ChangeNotifier {
} }
final lowercaseQuery = query.toLowerCase(); final lowercaseQuery = query.toLowerCase();
return _amicaleBox.values return _amicaleBox.values.where((amicale) => amicale.name.toLowerCase().contains(lowercaseQuery)).toList();
.where((amicale) => amicale.name.toLowerCase().contains(lowercaseQuery))
.toList();
} }
// Filtrer les amicales par type // Filtrer les amicales par type
List<AmicaleModel> getAmicalesByType(int type) { List<AmicaleModel> getAmicalesByType(int type) {
return _amicaleBox.values return _amicaleBox.values.where((amicale) => amicale.fkType == type).toList();
.where((amicale) => amicale.fkType == type)
.toList();
} }
// Filtrer les amicales par région // Filtrer les amicales par région
List<AmicaleModel> getAmicalesByRegion(int regionId) { List<AmicaleModel> getAmicalesByRegion(int regionId) {
return _amicaleBox.values return _amicaleBox.values.where((amicale) => amicale.fkRegion == regionId).toList();
.where((amicale) => amicale.fkRegion == regionId)
.toList();
} }
// Filtrer les amicales actives // Filtrer les amicales actives
@@ -280,16 +281,12 @@ class AmicaleRepository extends ChangeNotifier {
// Filtrer les amicales par code postal // Filtrer les amicales par code postal
List<AmicaleModel> getAmicalesByPostalCode(String postalCode) { List<AmicaleModel> getAmicalesByPostalCode(String postalCode) {
return _amicaleBox.values return _amicaleBox.values.where((amicale) => amicale.codePostal == postalCode).toList();
.where((amicale) => amicale.codePostal == postalCode)
.toList();
} }
// Filtrer les amicales par ville // Filtrer les amicales par ville
List<AmicaleModel> getAmicalesByCity(String city) { List<AmicaleModel> getAmicalesByCity(String city) {
final lowercaseCity = city.toLowerCase(); final lowercaseCity = city.toLowerCase();
return _amicaleBox.values return _amicaleBox.values.where((amicale) => amicale.ville.toLowerCase().contains(lowercaseCity)).toList();
.where((amicale) => amicale.ville.toLowerCase().contains(lowercaseCity))
.toList();
} }
} }

View File

@@ -8,8 +8,7 @@ import 'package:geosector_app/core/data/models/membre_model.dart';
class MembreRepository extends ChangeNotifier { class MembreRepository extends ChangeNotifier {
// Utilisation de getters lazy pour n'accéder à la boîte que lorsque nécessaire // Utilisation de getters lazy pour n'accéder à la boîte que lorsque nécessaire
Box<MembreModel> get _membreBox => Box<MembreModel> get _membreBox => Hive.box<MembreModel>(AppKeys.membresBoxName);
Hive.box<MembreModel>(AppKeys.membresBoxName);
final ApiService _apiService; final ApiService _apiService;
bool _isLoading = false; bool _isLoading = false;
@@ -20,6 +19,19 @@ class MembreRepository extends ChangeNotifier {
bool get isLoading => _isLoading; bool get isLoading => _isLoading;
List<MembreModel> get membres => getAllMembres(); List<MembreModel> get membres => getAllMembres();
// Méthode pour exposer la Box Hive (nécessaire pour ValueListenableBuilder)
Box<MembreModel> getMembresBox() {
try {
if (!Hive.isBoxOpen(AppKeys.membresBoxName)) {
throw Exception('La boîte membres n\'est pas ouverte');
}
return Hive.box<MembreModel>(AppKeys.membresBoxName);
} catch (e) {
debugPrint('Erreur lors de l\'accès à la boîte membres: $e');
rethrow;
}
}
// Méthode pour vérifier si une boîte est ouverte et l'ouvrir si nécessaire // Méthode pour vérifier si une boîte est ouverte et l'ouvrir si nécessaire
Future<void> _ensureBoxIsOpen() async { Future<void> _ensureBoxIsOpen() async {
try { try {
@@ -29,10 +41,37 @@ class MembreRepository extends ChangeNotifier {
debugPrint('Boîte ${AppKeys.membresBoxName} ouverte avec succès'); debugPrint('Boîte ${AppKeys.membresBoxName} ouverte avec succès');
} }
} catch (e) { } catch (e) {
debugPrint( debugPrint('Erreur lors de l\'ouverture de la boîte ${AppKeys.membresBoxName}: $e');
'Erreur lors de l\'ouverture de la boîte ${AppKeys.membresBoxName}: $e'); throw Exception('Impossible d\'ouvrir la boîte ${AppKeys.membresBoxName}: $e');
throw Exception( }
'Impossible d\'ouvrir la boîte ${AppKeys.membresBoxName}: $e'); }
List<MembreModel> getMembresByAmicale(int fkEntite) {
try {
return _membreBox.values.where((membre) => membre.fkEntite == fkEntite).toList();
} catch (e) {
debugPrint('Erreur lors de la récupération des membres par amicale: $e');
return [];
}
}
// Récupérer les membres actifs par amicale
List<MembreModel> getActiveMembresByAmicale(int fkEntite) {
try {
return _membreBox.values.where((membre) => membre.fkEntite == fkEntite && membre.chkActive == 1).toList();
} catch (e) {
debugPrint('Erreur lors de la récupération des membres actifs par amicale: $e');
return [];
}
}
// Compter les membres par amicale
int countMembresByAmicale(int fkEntite) {
try {
return _membreBox.values.where((membre) => membre.fkEntite == fkEntite).length;
} catch (e) {
debugPrint('Erreur lors du comptage des membres par amicale: $e');
return 0;
} }
} }
@@ -79,8 +118,7 @@ class MembreRepository extends ChangeNotifier {
try { try {
final hasConnection = await _apiService.hasInternetConnection(); final hasConnection = await _apiService.hasInternetConnection();
if (!hasConnection) { if (!hasConnection) {
debugPrint( debugPrint('Pas de connexion Internet, utilisation des données locales');
'Pas de connexion Internet, utilisation des données locales');
return getAllMembres(); return getAllMembres();
} }
@@ -107,8 +145,7 @@ class MembreRepository extends ChangeNotifier {
notifyListeners(); notifyListeners();
return membres; return membres;
} catch (e) { } catch (e) {
debugPrint( debugPrint('Erreur lors de la récupération des membres depuis l\'API: $e');
'Erreur lors de la récupération des membres depuis l\'API: $e');
return getAllMembres(); return getAllMembres();
} finally { } finally {
_isLoading = false; _isLoading = false;
@@ -129,8 +166,7 @@ class MembreRepository extends ChangeNotifier {
} }
// Endpoint à adapter selon votre API // Endpoint à adapter selon votre API
final response = final response = await _apiService.post('/membres', data: membre.toJson());
await _apiService.post('/membres', data: membre.toJson());
final membreData = response.data['membre']; final membreData = response.data['membre'];
final newMembre = MembreModel.fromJson(membreData); final newMembre = MembreModel.fromJson(membreData);
@@ -154,14 +190,12 @@ class MembreRepository extends ChangeNotifier {
try { try {
final hasConnection = await _apiService.hasInternetConnection(); final hasConnection = await _apiService.hasInternetConnection();
if (!hasConnection) { if (!hasConnection) {
debugPrint( debugPrint('Pas de connexion Internet, impossible de mettre à jour le membre');
'Pas de connexion Internet, impossible de mettre à jour le membre');
return null; return null;
} }
// Endpoint à adapter selon votre API // Endpoint à adapter selon votre API
final response = final response = await _apiService.put('/membres/${membre.id}', data: membre.toJson());
await _apiService.put('/membres/${membre.id}', data: membre.toJson());
final membreData = response.data['membre']; final membreData = response.data['membre'];
final updatedMembre = MembreModel.fromJson(membreData); final updatedMembre = MembreModel.fromJson(membreData);
@@ -185,8 +219,7 @@ class MembreRepository extends ChangeNotifier {
try { try {
final hasConnection = await _apiService.hasInternetConnection(); final hasConnection = await _apiService.hasInternetConnection();
if (!hasConnection) { if (!hasConnection) {
debugPrint( debugPrint('Pas de connexion Internet, impossible de supprimer le membre');
'Pas de connexion Internet, impossible de supprimer le membre');
return false; return false;
} }

View File

@@ -203,9 +203,7 @@ class ApiService {
retryIf: (e) => e is SocketException || e is TimeoutException, retryIf: (e) => e is SocketException || e is TimeoutException,
); );
return (response.data as List) return (response.data as List).map((json) => UserModel.fromJson(json)).toList();
.map((json) => UserModel.fromJson(json))
.toList();
} catch (e) { } catch (e) {
// Gérer les erreurs // Gérer les erreurs
rethrow; rethrow;

View File

@@ -1,26 +0,0 @@
# Structure de présentation
Ce dossier contient tous les éléments liés à l'interface utilisateur de l'application, organisés comme suit :
## Sous-dossiers
- `/admin` : Pages et widgets spécifiques à l'interface administrateur
- `/user` : Pages et widgets spécifiques à l'interface utilisateur
- `/auth` : Pages et widgets liés à l'authentification
- `/public` : Pages et widgets accessibles sans authentification
- `/widgets` : Widgets partagés utilisés dans plusieurs parties de l'application
## Organisation des fichiers
Chaque sous-dossier peut contenir :
- Des pages (écrans complets)
- Des widgets spécifiques à cette section
- Des modèles de données d'UI
- Des utilitaires d'UI spécifiques
## Bonnes pratiques
- Les widgets réutilisables dans plusieurs sections doivent être placés dans `/widgets`
- Les widgets spécifiques à une section doivent être placés dans le sous-dossier correspondant
- Utiliser des imports relatifs pour les fichiers du même module
- Utiliser des imports absolus pour les fichiers d'autres modules

View File

@@ -0,0 +1,370 @@
import 'package:flutter/material.dart';
import 'dart:math' as math;
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/core/data/models/amicale_model.dart';
import 'package:geosector_app/core/data/models/membre_model.dart';
import 'package:geosector_app/core/data/models/user_model.dart';
import 'package:geosector_app/core/repositories/user_repository.dart';
import 'package:geosector_app/core/repositories/amicale_repository.dart';
import 'package:geosector_app/core/repositories/membre_repository.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 AdminAmicalePage extends StatefulWidget {
final UserRepository userRepository;
final AmicaleRepository amicaleRepository;
final MembreRepository membreRepository;
const AdminAmicalePage({
super.key,
required this.userRepository,
required this.amicaleRepository,
required this.membreRepository,
});
@override
State<AdminAmicalePage> createState() => _AdminAmicalePageState();
}
class _AdminAmicalePageState extends State<AdminAmicalePage> {
UserModel? _currentUser;
String? _errorMessage;
@override
void initState() {
super.initState();
_loadCurrentUser();
}
void _loadCurrentUser() {
final currentUser = widget.userRepository.getCurrentUser();
if (currentUser == null) {
setState(() {
_errorMessage = 'Utilisateur non connecté';
});
return;
}
if (currentUser.fkEntite == null) {
setState(() {
_errorMessage = 'Utilisateur non associé à une amicale';
});
return;
}
setState(() {
_currentUser = currentUser;
_errorMessage = null;
});
}
void _handleEditAmicale(AmicaleModel amicale) {
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();
// TODO: Naviguer vers la page de modification
// Navigator.of(context).push(
// MaterialPageRoute(
// builder: (context) => EditAmicalePage(
// amicale: amicale,
// amicaleRepository: widget.amicaleRepository,
// ),
// ),
// );
},
child: const Text('Modifier'),
),
],
),
);
}
void _handleEditMembre(MembreModel membre) {
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();
// TODO: Naviguer vers la page de modification
// Navigator.of(context).push(
// MaterialPageRoute(
// builder: (context) => EditMembrePage(
// membre: membre,
// membreRepository: widget.membreRepository,
// ),
// ),
// );
},
child: const Text('Modifier'),
),
],
),
);
}
void _handleAddMembre() {
if (_currentUser?.fkEntite == null) return;
// TODO: Naviguer vers la page d'ajout de membre
// Navigator.of(context).push(
// MaterialPageRoute(
// builder: (context) => AddMembrePage(
// amicaleId: _currentUser!.fkEntite!,
// membreRepository: widget.membreRepository,
// ),
// ),
// );
}
@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: const SizedBox(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 avec ValueListenableBuilder
if (_currentUser != null && _currentUser!.fkEntite != null)
Expanded(
child: ValueListenableBuilder<Box<AmicaleModel>>(
valueListenable: widget.amicaleRepository.getAmicalesBox().listenable(),
builder: (context, amicalesBox, child) {
final amicale = amicalesBox.get(_currentUser!.fkEntite!);
if (amicale == null) {
return 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(
'Amicale non trouvée',
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'L\'amicale associée à votre compte n\'existe plus.',
textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge,
),
],
),
);
}
return ValueListenableBuilder<Box<MembreModel>>(
valueListenable: widget.membreRepository.getMembresBox().listenable(),
builder: (context, membresBox, child) {
// Filtrer les membres par amicale
// Note: Il faudra ajouter le champ fkEntite au modèle MembreModel
final membres = membresBox.values.where((membre) => membre.fkEntite == _currentUser!.fkEntite).toList();
return 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],
onEdit: null,
onDelete: null,
amicaleRepository: widget.amicaleRepository,
userRepository: widget.userRepository,
apiService: null, // Ou passez l'ApiService si vous l'avez disponible
showActionsColumn: false,
),
),
const SizedBox(height: 32),
// Section Membres
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Membres de l\'amicale (${membres.length})',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
ElevatedButton.icon(
onPressed: _handleAddMembre,
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,
onDelete: null, // Géré par l'admin principal
membreRepository: widget.membreRepository,
),
),
),
],
);
},
);
},
),
),
// Message si pas d'utilisateur connecté
if (_currentUser == null)
const Expanded(
child: Center(
child: CircularProgressIndicator(),
),
),
],
),
),
],
);
}
}

View File

@@ -11,7 +11,7 @@ import 'admin_statistics_page.dart';
import 'admin_history_page.dart'; import 'admin_history_page.dart';
import 'admin_communication_page.dart'; import 'admin_communication_page.dart';
import 'admin_map_page.dart'; import 'admin_map_page.dart';
import 'admin_entite.dart'; import 'admin_amicale_page.dart';
/// Class pour dessiner les petits points blancs sur le fond /// Class pour dessiner les petits points blancs sur le fond
class DotsPainter extends CustomPainter { class DotsPainter extends CustomPainter {
@@ -37,14 +37,13 @@ class DotsPainter extends CustomPainter {
} }
class AdminDashboardPage extends StatefulWidget { class AdminDashboardPage extends StatefulWidget {
const AdminDashboardPage({Key? key}) : super(key: key); const AdminDashboardPage({super.key});
@override @override
State<AdminDashboardPage> createState() => _AdminDashboardPageState(); State<AdminDashboardPage> createState() => _AdminDashboardPageState();
} }
class _AdminDashboardPageState extends State<AdminDashboardPage> class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBindingObserver {
with WidgetsBindingObserver {
int _selectedIndex = 0; int _selectedIndex = 0;
// Liste des pages à afficher // Liste des pages à afficher
@@ -59,31 +58,31 @@ class _AdminDashboardPageState extends State<AdminDashboardPage>
label: 'Tableau de bord', label: 'Tableau de bord',
icon: Icons.dashboard_outlined, icon: Icons.dashboard_outlined,
selectedIcon: Icons.dashboard, selectedIcon: Icons.dashboard,
page: AdminDashboardHomePage(), pageType: _PageType.dashboardHome,
), ),
const _NavigationItem( const _NavigationItem(
label: 'Statistiques', label: 'Statistiques',
icon: Icons.bar_chart_outlined, icon: Icons.bar_chart_outlined,
selectedIcon: Icons.bar_chart, selectedIcon: Icons.bar_chart,
page: AdminStatisticsPage(), pageType: _PageType.statistics,
), ),
const _NavigationItem( const _NavigationItem(
label: 'Historique', label: 'Historique',
icon: Icons.history_outlined, icon: Icons.history_outlined,
selectedIcon: Icons.history, selectedIcon: Icons.history,
page: AdminHistoryPage(), pageType: _PageType.history,
), ),
const _NavigationItem( const _NavigationItem(
label: 'Messages', label: 'Messages',
icon: Icons.chat_outlined, icon: Icons.chat_outlined,
selectedIcon: Icons.chat, selectedIcon: Icons.chat,
page: AdminCommunicationPage(), pageType: _PageType.communication,
), ),
const _NavigationItem( const _NavigationItem(
label: 'Carte', label: 'Carte',
icon: Icons.map_outlined, icon: Icons.map_outlined,
selectedIcon: Icons.map, selectedIcon: Icons.map,
page: AdminMapPage(), pageType: _PageType.map,
), ),
]; ];
@@ -93,18 +92,42 @@ class _AdminDashboardPageState extends State<AdminDashboardPage>
label: 'Amicale & membres', label: 'Amicale & membres',
icon: Icons.business_outlined, icon: Icons.business_outlined,
selectedIcon: Icons.business, selectedIcon: Icons.business,
page: AdminEntitePage(), pageType: _PageType.amicale,
requiredRole: 2, requiredRole: 2,
), ),
const _NavigationItem( const _NavigationItem(
label: 'Opérations', label: 'Opérations',
icon: Icons.calendar_today_outlined, icon: Icons.calendar_today_outlined,
selectedIcon: Icons.calendar_today, selectedIcon: Icons.calendar_today,
page: Scaffold(body: Center(child: Text('Page Opérations'))), pageType: _PageType.operations,
requiredRole: 2, requiredRole: 2,
), ),
]; ];
// Construire la page basée sur le type
Widget _buildPage(_PageType pageType) {
switch (pageType) {
case _PageType.dashboardHome:
return const AdminDashboardHomePage();
case _PageType.statistics:
return const AdminStatisticsPage();
case _PageType.history:
return const AdminHistoryPage();
case _PageType.communication:
return const AdminCommunicationPage();
case _PageType.map:
return const AdminMapPage();
case _PageType.amicale:
return AdminAmicalePage(
userRepository: userRepository,
amicaleRepository: amicaleRepository,
membreRepository: membreRepository,
);
case _PageType.operations:
return const Scaffold(body: Center(child: Text('Page Opérations')));
}
}
// Construire la liste des destinations de navigation en fonction du rôle // Construire la liste des destinations de navigation en fonction du rôle
List<NavigationDestination> _buildNavigationDestinations() { List<NavigationDestination> _buildNavigationDestinations() {
final destinations = <NavigationDestination>[]; final destinations = <NavigationDestination>[];
@@ -145,13 +168,15 @@ class _AdminDashboardPageState extends State<AdminDashboardPage>
final currentUser = userRepository.getCurrentUser(); final currentUser = userRepository.getCurrentUser();
// Ajouter les pages de base // Ajouter les pages de base
pages.addAll(_baseNavigationItems.map((item) => item.page)); for (final item in _baseNavigationItems) {
pages.add(_buildPage(item.pageType));
}
// Ajouter les pages admin si l'utilisateur a le rôle requis // Ajouter les pages admin si l'utilisateur a le rôle requis
if (currentUser?.role == 2) { if (currentUser?.role == 2) {
for (final item in _adminNavigationItems) { for (final item in _adminNavigationItems) {
if (item.requiredRole == null || item.requiredRole == 2) { if (item.requiredRole == null || item.requiredRole == 2) {
pages.add(item.page); pages.add(_buildPage(item.pageType));
} }
} }
} }
@@ -171,11 +196,9 @@ class _AdminDashboardPageState extends State<AdminDashboardPage>
debugPrint('userRepository est correctement initialisé'); debugPrint('userRepository est correctement initialisé');
final currentUser = userRepository.getCurrentUser(); final currentUser = userRepository.getCurrentUser();
if (currentUser == null) { if (currentUser == null) {
debugPrint( debugPrint('ERREUR: Aucun utilisateur connecté dans AdminDashboardPage');
'ERREUR: Aucun utilisateur connecté dans AdminDashboardPage');
} else { } else {
debugPrint( debugPrint('Utilisateur connecté: ${currentUser.username} (${currentUser.id})');
'Utilisateur connecté: ${currentUser.username} (${currentUser.id})');
} }
userRepository.addListener(_handleUserRepositoryChanges); userRepository.addListener(_handleUserRepositoryChanges);
@@ -276,7 +299,7 @@ class _AdminDashboardPageState extends State<AdminDashboardPage>
), ),
child: CustomPaint( child: CustomPaint(
painter: DotsPainter(), painter: DotsPainter(),
child: Container(width: double.infinity, height: double.infinity), child: const SizedBox(width: double.infinity, height: double.infinity),
), ),
), ),
// Contenu de la page // Contenu de la page
@@ -299,19 +322,30 @@ class _AdminDashboardPageState extends State<AdminDashboardPage>
} }
} }
// Enum pour les types de pages
enum _PageType {
dashboardHome,
statistics,
history,
communication,
map,
amicale,
operations,
}
// Classe pour représenter une destination de navigation avec sa page associée // Classe pour représenter une destination de navigation avec sa page associée
class _NavigationItem { class _NavigationItem {
final String label; final String label;
final IconData icon; final IconData icon;
final IconData selectedIcon; final IconData selectedIcon;
final Widget page; final _PageType pageType;
final int? requiredRole; // null si accessible à tous les rôles final int? requiredRole; // null si accessible à tous les rôles
const _NavigationItem({ const _NavigationItem({
required this.label, required this.label,
required this.icon, required this.icon,
required this.selectedIcon, required this.selectedIcon,
required this.page, required this.pageType,
this.requiredRole, this.requiredRole,
}); });
} }

View File

@@ -1,361 +0,0 @@
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<AdminEntitePage> createState() => _AdminEntitePageState();
}
class _AdminEntitePageState extends State<AdminEntitePage> {
bool _isLoading = true;
AmicaleModel? _amicale;
List<MembreModel> _membres = [];
String? _errorMessage;
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _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,
),
),
),
],
),
),
],
),
),
],
);
}
}

View File

@@ -78,9 +78,7 @@ class _LoginPageState extends State<LoginPage> {
// Fallback sur la version du AppInfoService si elle existe // Fallback sur la version du AppInfoService si elle existe
if (mounted) { if (mounted) {
setState(() { setState(() {
_appVersion = AppInfoService.fullVersion _appVersion = AppInfoService.fullVersion.split(' ').last; // Extraire juste le numéro
.split(' ')
.last; // Extraire juste le numéro
}); });
} }
} }
@@ -103,8 +101,7 @@ class _LoginPageState extends State<LoginPage> {
// Vérification du type de connexion // Vérification du type de connexion
if (widget.loginType == null) { if (widget.loginType == null) {
// Si aucun type n'est spécifié, naviguer vers la splash page // Si aucun type n'est spécifié, naviguer vers la splash page
print( print('LoginPage: Aucun type de connexion spécifié, navigation vers splash page');
'LoginPage: Aucun type de connexion spécifié, navigation vers splash page');
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
GoRouter.of(context).go('/'); GoRouter.of(context).go('/');
}); });
@@ -157,13 +154,10 @@ class _LoginPageState extends State<LoginPage> {
''' '''
]); ]);
if (result != null && if (result != null && result is String && result.toLowerCase() == 'user') {
result is String &&
result.toLowerCase() == 'user') {
setState(() { setState(() {
_loginType = 'user'; _loginType = 'user';
print( print('LoginPage: Type détecté depuis sessionStorage: $_loginType');
'LoginPage: Type détecté depuis sessionStorage: $_loginType');
}); });
} }
} catch (e) { } catch (e) {
@@ -217,7 +211,7 @@ class _LoginPageState extends State<LoginPage> {
if (lastUser.role is String) { if (lastUser.role is String) {
roleValue = int.tryParse(lastUser.role as String) ?? 0; roleValue = int.tryParse(lastUser.role as String) ?? 0;
} else { } else {
roleValue = lastUser.role as int; roleValue = lastUser.role;
} }
// Vérifier si le rôle correspond au type de login // Vérifier si le rôle correspond au type de login
@@ -227,8 +221,7 @@ class _LoginPageState extends State<LoginPage> {
debugPrint('Rôle utilisateur (1) correspond au type de login (user)'); debugPrint('Rôle utilisateur (1) correspond au type de login (user)');
} else if (_loginType == 'admin' && roleValue > 1) { } else if (_loginType == 'admin' && roleValue > 1) {
roleMatches = true; roleMatches = true;
debugPrint( debugPrint('Rôle administrateur ($roleValue) correspond au type de login (admin)');
'Rôle administrateur (${roleValue}) correspond au type de login (admin)');
} }
// Pré-remplir le champ username seulement si le rôle correspond // Pré-remplir le champ username seulement si le rôle correspond
@@ -242,12 +235,10 @@ class _LoginPageState extends State<LoginPage> {
} else if (lastUser.email.isNotEmpty) { } else if (lastUser.email.isNotEmpty) {
_usernameController.text = lastUser.email; _usernameController.text = lastUser.email;
_usernameFocusNode.unfocus(); _usernameFocusNode.unfocus();
debugPrint( debugPrint('Champ username pré-rempli avec email: ${lastUser.email}');
'Champ username pré-rempli avec email: ${lastUser.email}');
} }
} else { } else {
debugPrint( debugPrint('Le rôle ($roleValue) ne correspond pas au type de login ($_loginType), champ username non pré-rempli');
'Le rôle (${roleValue}) ne correspond pas au type de login ($_loginType), champ username non pré-rempli');
} }
} }
}); });
@@ -327,14 +318,12 @@ class _LoginPageState extends State<LoginPage> {
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: _loginType == 'user' colors: _loginType == 'user' ? [Colors.white, Colors.green.shade300] : [Colors.white, Colors.red.shade300],
? [Colors.white, Colors.green.shade300]
: [Colors.white, Colors.red.shade300],
), ),
), ),
child: CustomPaint( child: CustomPaint(
painter: DotsPainter(), painter: DotsPainter(),
child: Container(width: double.infinity, height: double.infinity), child: const SizedBox(width: double.infinity, height: double.infinity),
), ),
), ),
SafeArea( SafeArea(
@@ -345,11 +334,8 @@ class _LoginPageState extends State<LoginPage> {
constraints: const BoxConstraints(maxWidth: 500), constraints: const BoxConstraints(maxWidth: 500),
child: Card( child: Card(
elevation: 8, elevation: 8,
shadowColor: _loginType == 'user' shadowColor: _loginType == 'user' ? Colors.green.withOpacity(0.5) : Colors.red.withOpacity(0.5),
? Colors.green.withOpacity(0.5) shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)),
: Colors.red.withOpacity(0.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0)),
child: Padding( child: Padding(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
child: Column( child: Column(
@@ -378,9 +364,7 @@ class _LoginPageState extends State<LoginPage> {
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.error.withOpacity(0.1), color: theme.colorScheme.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all( border: Border.all(color: theme.colorScheme.error.withOpacity(0.3)),
color:
theme.colorScheme.error.withOpacity(0.3)),
), ),
child: Column( child: Column(
children: [ children: [
@@ -391,8 +375,7 @@ class _LoginPageState extends State<LoginPage> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
_locationErrorMessage ?? _locationErrorMessage ?? 'L\'accès à la localisation est nécessaire pour utiliser cette application.',
'L\'accès à la localisation est nécessaire pour utiliser cette application.',
style: theme.textTheme.bodyLarge, style: theme.textTheme.bodyLarge,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@@ -416,14 +399,10 @@ class _LoginPageState extends State<LoginPage> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildInstructionStep(theme, 1, _buildInstructionStep(theme, 1, 'Ouvrez les paramètres de votre appareil'),
'Ouvrez les paramètres de votre appareil'), _buildInstructionStep(theme, 2, 'Accédez aux paramètres de confidentialité ou de localisation'),
_buildInstructionStep(theme, 2, _buildInstructionStep(theme, 3, 'Recherchez GEOSECTOR dans la liste des applications'),
'Accédez aux paramètres de confidentialité ou de localisation'), _buildInstructionStep(theme, 4, 'Activez l\'accès à la localisation pour cette application'),
_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), const SizedBox(height: 32),
// Boutons d'action // Boutons d'action
@@ -469,8 +448,7 @@ class _LoginPageState extends State<LoginPage> {
} }
/// Construit une étape d'instruction pour activer la localisation /// Construit une étape d'instruction pour activer la localisation
Widget _buildInstructionStep( Widget _buildInstructionStep(ThemeData theme, int stepNumber, String instruction) {
ThemeData theme, int stepNumber, String instruction) {
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 8.0), padding: const EdgeInsets.only(bottom: 8.0),
child: Row( child: Row(
@@ -530,14 +508,12 @@ class _LoginPageState extends State<LoginPage> {
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: _loginType == 'user' colors: _loginType == 'user' ? [Colors.white, Colors.green.shade300] : [Colors.white, Colors.red.shade300],
? [Colors.white, Colors.green.shade300]
: [Colors.white, Colors.red.shade300],
), ),
), ),
child: CustomPaint( child: CustomPaint(
painter: DotsPainter(), painter: DotsPainter(),
child: Container(width: double.infinity, height: double.infinity), child: const SizedBox(width: double.infinity, height: double.infinity),
), ),
), ),
SafeArea( SafeArea(
@@ -548,11 +524,8 @@ class _LoginPageState extends State<LoginPage> {
constraints: const BoxConstraints(maxWidth: 500), constraints: const BoxConstraints(maxWidth: 500),
child: Card( child: Card(
elevation: 8, elevation: 8,
shadowColor: _loginType == 'user' shadowColor: _loginType == 'user' ? Colors.green.withOpacity(0.5) : Colors.red.withOpacity(0.5),
? Colors.green.withOpacity(0.5) shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)),
: Colors.red.withOpacity(0.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0)),
child: Padding( child: Padding(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
child: Column( child: Column(
@@ -566,39 +539,26 @@ class _LoginPageState extends State<LoginPage> {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
_loginType == 'user' _loginType == 'user' ? 'Connexion Utilisateur' : 'Connexion Administrateur',
? 'Connexion Utilisateur'
: 'Connexion Administrateur',
style: theme.textTheme.headlineMedium?.copyWith( style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: _loginType == 'user' color: _loginType == 'user' ? Colors.green : Colors.red,
? Colors.green
: Colors.red,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(
AppInfoService.fullVersion,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.primary.withOpacity(0.7),
),
),
const SizedBox(height: 8),
// Ajouter un texte de débogage uniquement en mode développement // Ajouter un texte de débogage uniquement en mode développement
if (kDebugMode) if (kDebugMode)
Text( Text(
'Type de connexion: $_loginType', 'Type de connexion: $_loginType',
style: style: const TextStyle(fontSize: 10, color: Colors.grey),
TextStyle(fontSize: 10, color: Colors.grey),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Bienvenue sur GEOSECTOR', 'Bienvenue sur GEOSECTOR',
style: theme.textTheme.bodyLarge?.copyWith( style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onBackground color: theme.colorScheme.onSurface.withOpacity(0.7),
.withOpacity(0.7),
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@@ -616,23 +576,17 @@ class _LoginPageState extends State<LoginPage> {
color: theme.colorScheme.error.withOpacity(0.1), color: theme.colorScheme.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all( border: Border.all(
color: color: theme.colorScheme.error.withOpacity(0.3),
theme.colorScheme.error.withOpacity(0.3),
), ),
), ),
child: Column( child: Column(
children: [ children: [
Icon(Icons.signal_wifi_off, Icon(Icons.signal_wifi_off, color: theme.colorScheme.error, size: 32),
color: theme.colorScheme.error, size: 32),
const SizedBox(height: 8), const SizedBox(height: 8),
Text('Connexion Internet requise', Text('Connexion Internet requise',
style: theme.textTheme.titleMedium style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold, color: theme.colorScheme.error)),
?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.error)),
const SizedBox(height: 8), const SizedBox(height: 8),
const Text( const Text('Veuillez vous connecter à Internet (WiFi ou données mobiles) pour pouvoir vous connecter.'),
'Veuillez vous connecter à Internet (WiFi ou données mobiles) pour pouvoir vous connecter.'),
], ],
), ),
), ),
@@ -669,9 +623,7 @@ class _LoginPageState extends State<LoginPage> {
obscureText: _obscurePassword, obscureText: _obscurePassword,
suffixIcon: IconButton( suffixIcon: IconButton(
icon: Icon( icon: Icon(
_obscurePassword _obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
), ),
onPressed: () { onPressed: () {
setState(() { setState(() {
@@ -686,21 +638,17 @@ class _LoginPageState extends State<LoginPage> {
return null; return null;
}, },
onFieldSubmitted: (_) async { onFieldSubmitted: (_) async {
if (!userRepository.isLoading && if (!userRepository.isLoading && _formKey.currentState!.validate()) {
_formKey.currentState!.validate()) {
// Vérifier que le type de connexion est spécifié // Vérifier que le type de connexion est spécifié
if (_loginType.isEmpty) { if (_loginType.isEmpty) {
print( print('Login: Type non spécifié, redirection vers la page de démarrage');
'Login: Type non spécifié, redirection vers la page de démarrage');
context.go('/'); context.go('/');
return; return;
} }
print( print('Login: Tentative avec type: $_loginType');
'Login: Tentative avec type: $_loginType');
final success = final success = await userRepository.login(
await userRepository.login(
_usernameController.text.trim(), _usernameController.text.trim(),
_passwordController.text, _passwordController.text,
type: _loginType, type: _loginType,
@@ -708,16 +656,12 @@ class _LoginPageState extends State<LoginPage> {
if (success && mounted) { if (success && mounted) {
// Récupérer directement le rôle de l'utilisateur // Récupérer directement le rôle de l'utilisateur
final user = final user = userRepository.getCurrentUser();
userRepository.getCurrentUser();
if (user == null) { if (user == null) {
debugPrint( debugPrint('ERREUR: Utilisateur non trouvé après connexion réussie');
'ERREUR: Utilisateur non trouvé après connexion réussie'); ScaffoldMessenger.of(context).showSnackBar(
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar( const SnackBar(
content: Text( content: Text('Erreur de connexion. Veuillez réessayer.'),
'Erreur de connexion. Veuillez réessayer.'),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
@@ -727,32 +671,25 @@ class _LoginPageState extends State<LoginPage> {
// Convertir le rôle en int si nécessaire // Convertir le rôle en int si nécessaire
int roleValue; int roleValue;
if (user.role is String) { if (user.role is String) {
roleValue = int.tryParse( roleValue = int.tryParse(user.role as String) ?? 1;
user.role as String) ??
1;
} else { } else {
roleValue = user.role as int; roleValue = user.role;
} }
debugPrint( debugPrint('Role de l\'utilisateur: $roleValue');
'Role de l\'utilisateur: $roleValue');
// Redirection simple basée sur le rôle // Redirection simple basée sur le rôle
if (roleValue > 1) { if (roleValue > 1) {
debugPrint( debugPrint('Redirection vers /admin (rôle > 1)');
'Redirection vers /admin (rôle > 1)');
context.go('/admin'); context.go('/admin');
} else { } else {
debugPrint( debugPrint('Redirection vers /user (rôle = 1)');
'Redirection vers /user (rôle = 1)');
context.go('/user'); context.go('/user');
} }
} else if (mounted) { } else if (mounted) {
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context).showSnackBar(
.showSnackBar(
const SnackBar( const SnackBar(
content: Text( content: Text('Échec de la connexion. Vérifiez vos identifiants.'),
'Échec de la connexion. Vérifiez vos identifiants.'),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
@@ -781,23 +718,19 @@ class _LoginPageState extends State<LoginPage> {
// Bouton de connexion // Bouton de connexion
CustomButton( CustomButton(
onPressed: (userRepository.isLoading || onPressed: (userRepository.isLoading || !_isConnected)
!_isConnected)
? null ? null
: () async { : () async {
if (_formKey.currentState! if (_formKey.currentState!.validate()) {
.validate()) {
// Vérifier à nouveau les permissions de géolocalisation avant de se connecter (sauf en version web) // Vérifier à nouveau les permissions de géolocalisation avant de se connecter (sauf en version web)
if (!kIsWeb) { if (!kIsWeb) {
await _checkLocationPermission(); await _checkLocationPermission();
// Si l'utilisateur n'a toujours pas accordé les permissions, ne pas continuer // Si l'utilisateur n'a toujours pas accordé les permissions, ne pas continuer
if (!_hasLocationPermission) { if (!_hasLocationPermission) {
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context).showSnackBar(
.showSnackBar(
const SnackBar( const SnackBar(
content: Text( content: Text('L\'accès à la localisation est nécessaire pour utiliser cette application.'),
'L\'accès à la localisation est nécessaire pour utiliser cette application.'),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
@@ -806,36 +739,23 @@ class _LoginPageState extends State<LoginPage> {
} }
// Vérifier la connexion Internet // Vérifier la connexion Internet
await connectivityService await connectivityService.checkConnectivity();
.checkConnectivity();
if (!connectivityService if (!connectivityService.isConnected) {
.isConnected) { ScaffoldMessenger.of(context).showSnackBar(
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar( SnackBar(
content: const Text( content: const Text('Aucune connexion Internet. La connexion n\'est pas possible hors ligne.'),
'Aucune connexion Internet. La connexion n\'est pas possible hors ligne.'), backgroundColor: theme.colorScheme.error,
backgroundColor: duration: const Duration(seconds: 3),
theme.colorScheme.error,
duration: const Duration(
seconds: 3),
action: SnackBarAction( action: SnackBarAction(
label: 'Réessayer', label: 'Réessayer',
onPressed: () async { onPressed: () async {
await connectivityService await connectivityService.checkConnectivity();
.checkConnectivity(); if (connectivityService.isConnected && mounted) {
if (connectivityService ScaffoldMessenger.of(context).showSnackBar(
.isConnected &&
mounted) {
ScaffoldMessenger.of(
context)
.showSnackBar(
SnackBar( SnackBar(
content: Text( content: Text('Connexion Internet ${connectivityService.connectionType} détectée.'),
'Connexion Internet ${connectivityService.connectionType} détectée.'), backgroundColor: Colors.green,
backgroundColor:
Colors.green,
), ),
); );
} }
@@ -848,18 +768,15 @@ class _LoginPageState extends State<LoginPage> {
// Vérifier que le type de connexion est spécifié // Vérifier que le type de connexion est spécifié
if (_loginType.isEmpty) { if (_loginType.isEmpty) {
print( print('Login: Type non spécifié, redirection vers la page de démarrage');
'Login: Type non spécifié, redirection vers la page de démarrage');
context.go('/'); context.go('/');
return; return;
} }
print( print('Login: Tentative avec type: $_loginType');
'Login: Tentative avec type: $_loginType');
// Utiliser directement userRepository avec l'overlay de chargement // Utiliser directement userRepository avec l'overlay de chargement
final success = await userRepository final success = await userRepository.loginWithUI(
.loginWithUI(
context, context,
_usernameController.text.trim(), _usernameController.text.trim(),
_passwordController.text, _passwordController.text,
@@ -867,20 +784,15 @@ class _LoginPageState extends State<LoginPage> {
); );
if (success && mounted) { if (success && mounted) {
debugPrint( debugPrint('Connexion réussie, tentative de redirection...');
'Connexion réussie, tentative de redirection...');
// Récupérer directement le rôle de l'utilisateur // Récupérer directement le rôle de l'utilisateur
final user = userRepository final user = userRepository.getCurrentUser();
.getCurrentUser();
if (user == null) { if (user == null) {
debugPrint( debugPrint('ERREUR: Utilisateur non trouvé après connexion réussie');
'ERREUR: Utilisateur non trouvé après connexion réussie'); ScaffoldMessenger.of(context).showSnackBar(
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar( const SnackBar(
content: Text( content: Text('Erreur de connexion. Veuillez réessayer.'),
'Erreur de connexion. Veuillez réessayer.'),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
@@ -890,41 +802,32 @@ class _LoginPageState extends State<LoginPage> {
// Convertir le rôle en int si nécessaire // Convertir le rôle en int si nécessaire
int roleValue; int roleValue;
if (user.role is String) { if (user.role is String) {
roleValue = int.tryParse( roleValue = int.tryParse(user.role as String) ?? 1;
user.role as String) ??
1;
} else { } else {
roleValue = user.role as int; roleValue = user.role;
} }
debugPrint( debugPrint('Role de l\'utilisateur: $roleValue');
'Role de l\'utilisateur: $roleValue');
// Redirection simple basée sur le rôle // Redirection simple basée sur le rôle
if (roleValue > 1) { if (roleValue > 1) {
debugPrint( debugPrint('Redirection vers /admin (rôle > 1)');
'Redirection vers /admin (rôle > 1)');
context.go('/admin'); context.go('/admin');
} else { } else {
debugPrint( debugPrint('Redirection vers /user (rôle = 1)');
'Redirection vers /user (rôle = 1)');
context.go('/user'); context.go('/user');
} }
} else if (mounted) { } else if (mounted) {
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context).showSnackBar(
.showSnackBar(
const SnackBar( const SnackBar(
content: Text( content: Text('Échec de la connexion. Vérifiez vos identifiants.'),
'Échec de la connexion. Vérifiez vos identifiants.'),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
} }
} }
}, },
text: _isConnected text: _isConnected ? 'Se connecter' : 'Connexion Internet requise',
? 'Se connecter'
: 'Connexion Internet requise',
isLoading: userRepository.isLoading, isLoading: userRepository.isLoading,
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
@@ -1047,8 +950,7 @@ class _LoginPageState extends State<LoginPage> {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
return 'Veuillez entrer votre email'; return 'Veuillez entrer votre email';
} }
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$') if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
.hasMatch(value)) {
return 'Veuillez entrer un email valide'; return 'Veuillez entrer un email valide';
} }
return null; return null;
@@ -1105,10 +1007,8 @@ class _LoginPageState extends State<LoginPage> {
// Si la réponse est 404, c'est peut-être un problème de route // Si la réponse est 404, c'est peut-être un problème de route
if (response.statusCode == 404) { if (response.statusCode == 404) {
// Essayer avec une URL alternative // Essayer avec une URL alternative
final alternativeUrl = final alternativeUrl = '$baseUrl/api/index.php/lostpassword';
'$baseUrl/api/index.php/lostpassword'; print('Tentative avec URL alternative: $alternativeUrl');
print(
'Tentative avec URL alternative: $alternativeUrl');
final alternativeResponse = await http.post( final alternativeResponse = await http.post(
Uri.parse(alternativeUrl), Uri.parse(alternativeUrl),
@@ -1118,10 +1018,8 @@ class _LoginPageState extends State<LoginPage> {
}), }),
); );
print( print('Réponse alternative reçue: ${alternativeResponse.statusCode}');
'Réponse alternative reçue: ${alternativeResponse.statusCode}'); print('Corps de la réponse alternative: ${alternativeResponse.body}');
print(
'Corps de la réponse alternative: ${alternativeResponse.body}');
// Si la réponse alternative est un succès, utiliser cette réponse // Si la réponse alternative est un succès, utiliser cette réponse
if (alternativeResponse.statusCode == 200) { if (alternativeResponse.statusCode == 200) {
@@ -1129,14 +1027,12 @@ class _LoginPageState extends State<LoginPage> {
} }
} }
} catch (e) { } catch (e) {
print( print('Erreur lors de l\'envoi de la requête: $e');
'Erreur lors de l\'envoi de la requête: $e');
throw Exception('Erreur de connexion: $e'); throw Exception('Erreur de connexion: $e');
} }
// Traiter la réponse // Traiter la réponse
if (response != null && if (response.statusCode == 200) {
response.statusCode == 200) {
// Modifier le contenu de la boîte de dialogue pour afficher le message de succès // Modifier le contenu de la boîte de dialogue pour afficher le message de succès
setState(() { setState(() {
isLoading = false; isLoading = false;
@@ -1148,7 +1044,7 @@ class _LoginPageState extends State<LoginPage> {
barrierDismissible: false, barrierDismissible: false,
builder: (BuildContext context) { builder: (BuildContext context) {
// Fermer automatiquement la boîte de dialogue après 2 secondes // Fermer automatiquement la boîte de dialogue après 2 secondes
Future.delayed(Duration(seconds: 2), () { Future.delayed(const Duration(seconds: 2), () {
if (Navigator.of(context).canPop()) { if (Navigator.of(context).canPop()) {
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
@@ -1180,16 +1076,13 @@ class _LoginPageState extends State<LoginPage> {
// Afficher un message d'erreur // Afficher un message d'erreur
final responseData = json.decode(response.body); final responseData = json.decode(response.body);
throw Exception(responseData['message'] ?? throw Exception(responseData['message'] ?? 'Erreur lors de la récupération du mot de passe');
'Erreur lors de la récupération du mot de passe');
} }
} catch (e) { } catch (e) {
// Afficher un message d'erreur // Afficher un message d'erreur
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(e content: Text(e.toString().contains('Exception:')
.toString()
.contains('Exception:')
? e.toString().split('Exception: ')[1] ? e.toString().split('Exception: ')[1]
: 'Erreur lors de la récupération du mot de passe'), : 'Erreur lors de la récupération du mot de passe'),
backgroundColor: Colors.red, backgroundColor: Colors.red,
@@ -1204,21 +1097,20 @@ class _LoginPageState extends State<LoginPage> {
} }
} }
}, },
child: isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text('Recevoir un nouveau mot de passe'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue, backgroundColor: Colors.blue,
foregroundColor: Colors.white, foregroundColor: Colors.white,
), ),
child: isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Text('Recevoir un nouveau mot de passe'),
), ),
], ],
); );

View File

@@ -73,10 +73,8 @@ class _RegisterPageState extends State<RegisterPage> {
final String _hiddenToken = DateTime.now().millisecondsSinceEpoch.toString(); final String _hiddenToken = DateTime.now().millisecondsSinceEpoch.toString();
// Valeurs pour le captcha simple // Valeurs pour le captcha simple
final int _captchaNum1 = final int _captchaNum1 = 2 + (DateTime.now().second % 5); // Nombre entre 2 et 6
2 + (DateTime.now().second % 5); // Nombre entre 2 et 6 final int _captchaNum2 = 3 + (DateTime.now().minute % 4); // Nombre entre 3 et 6
final int _captchaNum2 =
3 + (DateTime.now().minute % 4); // Nombre entre 3 et 6
// État de la connexion Internet et de la plateforme // État de la connexion Internet et de la plateforme
bool _isConnected = false; bool _isConnected = false;
@@ -102,9 +100,7 @@ class _RegisterPageState extends State<RegisterPage> {
// Fallback sur la version du AppInfoService si elle existe // Fallback sur la version du AppInfoService si elle existe
if (mounted) { if (mounted) {
setState(() { setState(() {
_appVersion = AppInfoService.fullVersion _appVersion = AppInfoService.fullVersion.split(' ').last; // Extraire juste le numéro
.split(' ')
.last; // Extraire juste le numéro
}); });
} }
} }
@@ -168,8 +164,7 @@ class _RegisterPageState extends State<RegisterPage> {
try { try {
// Utiliser l'API interne de geosector pour récupérer les villes par code postal // Utiliser l'API interne de geosector pour récupérer les villes par code postal
final baseUrl = Uri final baseUrl = Uri.base.origin; // Récupère l'URL de base (ex: https://app.geosector.fr)
.base.origin; // Récupère l'URL de base (ex: https://app.geosector.fr)
final apiUrl = '$baseUrl/api/villes?code_postal=$postalCode'; final apiUrl = '$baseUrl/api/villes?code_postal=$postalCode';
final response = await http.get( final response = await http.get(
@@ -251,7 +246,7 @@ class _RegisterPageState extends State<RegisterPage> {
), ),
child: CustomPaint( child: CustomPaint(
painter: DotsPainter(), painter: DotsPainter(),
child: Container(width: double.infinity, height: double.infinity), child: const SizedBox(width: double.infinity, height: double.infinity),
), ),
), ),
SafeArea( SafeArea(
@@ -282,8 +277,7 @@ class _RegisterPageState extends State<RegisterPage> {
Text( Text(
'Enregistrez votre amicale sur GeoSector', 'Enregistrez votre amicale sur GeoSector',
style: theme.textTheme.bodyLarge?.copyWith( style: theme.textTheme.bodyLarge?.copyWith(
color: color: theme.colorScheme.onSurface.withOpacity(0.7),
theme.colorScheme.onBackground.withOpacity(0.7),
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@@ -295,8 +289,7 @@ class _RegisterPageState extends State<RegisterPage> {
if (mounted && _isConnected != isConnected) { if (mounted && _isConnected != isConnected) {
setState(() { setState(() {
_isConnected = isConnected; _isConnected = isConnected;
_connectionType = _connectionType = connectivityService.connectionType;
connectivityService.connectionType;
}); });
} }
}, },
@@ -343,8 +336,7 @@ class _RegisterPageState extends State<RegisterPage> {
if (_isConnected && mounted) { if (_isConnected && mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text( content: Text('Connexion Internet $_connectionType détectée.'),
'Connexion Internet $_connectionType détectée.'),
backgroundColor: Colors.green, backgroundColor: Colors.green,
), ),
); );
@@ -396,8 +388,7 @@ class _RegisterPageState extends State<RegisterPage> {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
return 'Veuillez entrer votre email'; return 'Veuillez entrer votre email';
} }
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$') if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
.hasMatch(value)) {
return 'Veuillez entrer un email valide'; return 'Veuillez entrer un email valide';
} }
return null; return null;
@@ -424,8 +415,7 @@ class _RegisterPageState extends State<RegisterPage> {
CustomTextField( CustomTextField(
controller: _postalCodeController, controller: _postalCodeController,
label: 'Code postal de l\'amicale', label: 'Code postal de l\'amicale',
hintText: hintText: 'Entrez le code postal de votre amicale',
'Entrez le code postal de votre amicale',
prefixIcon: Icons.location_on_outlined, prefixIcon: Icons.location_on_outlined,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
isRequired: true, isRequired: true,
@@ -453,13 +443,12 @@ class _RegisterPageState extends State<RegisterPage> {
children: [ children: [
Text( Text(
'Commune de l\'amicale', 'Commune de l\'amicale',
style: style: theme.textTheme.titleSmall?.copyWith(
theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: theme.colorScheme.onBackground, color: theme.colorScheme.onSurface,
), ),
), ),
Text( const Text(
'', '',
style: TextStyle( style: TextStyle(
color: Colors.red, color: Colors.red,
@@ -484,8 +473,7 @@ class _RegisterPageState extends State<RegisterPage> {
), ),
child: _isLoadingCities child: _isLoadingCities
? const Padding( ? const Padding(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(vertical: 16),
vertical: 16),
child: Center( child: Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
@@ -497,20 +485,16 @@ class _RegisterPageState extends State<RegisterPage> {
Icons.location_city_outlined, Icons.location_city_outlined,
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
), ),
hintText: _postalCodeController hintText: _postalCodeController.text.length < 3
.text.length <
3
? 'Entrez d\'abord au moins 3 chiffres du code postal' ? 'Entrez d\'abord au moins 3 chiffres du code postal'
: _cities.isEmpty : _cities.isEmpty
? 'Aucune commune trouvée pour ce code postal' ? 'Aucune commune trouvée pour ce code postal'
: 'Sélectionnez une commune', : 'Sélectionnez une commune',
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: borderRadius: BorderRadius.circular(12),
BorderRadius.circular(12),
borderSide: BorderSide.none, borderSide: BorderSide.none,
), ),
contentPadding: contentPadding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
vertical: 16, vertical: 16,
), ),
@@ -528,18 +512,13 @@ class _RegisterPageState extends State<RegisterPage> {
// Mettre à jour le code postal avec celui de la ville sélectionnée // Mettre à jour le code postal avec celui de la ville sélectionnée
if (newValue != null) { if (newValue != null) {
// Désactiver temporairement le listener pour éviter une boucle infinie // Désactiver temporairement le listener pour éviter une boucle infinie
_postalCodeController _postalCodeController.removeListener(_onPostalCodeChanged);
.removeListener(
_onPostalCodeChanged);
// Mettre à jour le code postal // Mettre à jour le code postal
_postalCodeController.text = _postalCodeController.text = newValue.postalCode;
newValue.postalCode;
// Réactiver le listener // Réactiver le listener
_postalCodeController _postalCodeController.addListener(_onPostalCodeChanged);
.addListener(
_onPostalCodeChanged);
} }
}); });
}, },
@@ -574,8 +553,7 @@ class _RegisterPageState extends State<RegisterPage> {
const SizedBox(height: 8), const SizedBox(height: 8),
CustomTextField( CustomTextField(
controller: _captchaController, controller: _captchaController,
label: label: 'Combien font $_captchaNum1 + $_captchaNum2 ?',
'Combien font $_captchaNum1 + $_captchaNum2 ?',
hintText: 'Entrez le résultat', hintText: 'Entrez le résultat',
prefixIcon: Icons.security, prefixIcon: Icons.security,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
@@ -612,43 +590,30 @@ class _RegisterPageState extends State<RegisterPage> {
// Bouton d'inscription // Bouton d'inscription
CustomButton( CustomButton(
onPressed: (_isLoading || onPressed: (_isLoading || (_isMobile && !_isConnected))
(_isMobile && !_isConnected))
? null ? null
: () async { : () async {
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
// Vérifier la connexion Internet avant de soumettre // Vérifier la connexion Internet avant de soumettre
// Utiliser l'instance globale de connectivityService définie dans app.dart // Utiliser l'instance globale de connectivityService définie dans app.dart
await connectivityService await connectivityService.checkConnectivity();
.checkConnectivity();
if (!connectivityService.isConnected) { if (!connectivityService.isConnected) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context).showSnackBar(
.showSnackBar(
SnackBar( SnackBar(
content: const Text( content: const Text('Aucune connexion Internet. L\'inscription nécessite une connexion active.'),
'Aucune connexion Internet. L\'inscription nécessite une connexion active.'), backgroundColor: theme.colorScheme.error,
backgroundColor: duration: const Duration(seconds: 3),
theme.colorScheme.error,
duration:
const Duration(seconds: 3),
action: SnackBarAction( action: SnackBarAction(
label: 'Réessayer', label: 'Réessayer',
onPressed: () async { onPressed: () async {
await connectivityService await connectivityService.checkConnectivity();
.checkConnectivity(); if (connectivityService.isConnected && mounted) {
if (connectivityService ScaffoldMessenger.of(context).showSnackBar(
.isConnected &&
mounted) {
ScaffoldMessenger.of(
context)
.showSnackBar(
SnackBar( SnackBar(
content: Text( content: Text('Connexion Internet ${connectivityService.connectionType} détectée.'),
'Connexion Internet ${connectivityService.connectionType} détectée.'), backgroundColor: Colors.green,
backgroundColor:
Colors.green,
), ),
); );
} }
@@ -660,15 +625,11 @@ class _RegisterPageState extends State<RegisterPage> {
return; return;
} }
// Vérifier que le captcha est correct // Vérifier que le captcha est correct
final int? captchaAnswer = int.tryParse( final int? captchaAnswer = int.tryParse(_captchaController.text);
_captchaController.text); if (captchaAnswer != _captchaNum1 + _captchaNum2) {
if (captchaAnswer != ScaffoldMessenger.of(context).showSnackBar(
_captchaNum1 + _captchaNum2) {
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar( const SnackBar(
content: Text( content: Text('La vérification de sécurité a échoué. Veuillez réessayer.'),
'La vérification de sécurité a échoué. Veuillez réessayer.'),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
@@ -679,16 +640,11 @@ class _RegisterPageState extends State<RegisterPage> {
final Map<String, dynamic> formData = { final Map<String, dynamic> formData = {
'email': _emailController.text.trim(), 'email': _emailController.text.trim(),
'name': _nameController.text.trim(), 'name': _nameController.text.trim(),
'amicale_name': _amicaleNameController 'amicale_name': _amicaleNameController.text.trim(),
.text 'postal_code': _postalCodeController.text,
.trim(), 'city_name': _selectedCity?.name ?? '',
'postal_code':
_postalCodeController.text,
'city_name':
_selectedCity?.name ?? '',
'captcha_answer': captchaAnswer, 'captcha_answer': captchaAnswer,
'captcha_expected': 'captcha_expected': _captchaNum1 + _captchaNum2,
_captchaNum1 + _captchaNum2,
'token': _hiddenToken, 'token': _hiddenToken,
}; };
@@ -700,14 +656,12 @@ class _RegisterPageState extends State<RegisterPage> {
try { try {
// Envoyer les données à l'API // Envoyer les données à l'API
final baseUrl = Uri.base.origin; final baseUrl = Uri.base.origin;
final apiUrl = final apiUrl = '$baseUrl/api/register';
'$baseUrl/api/register';
final response = await http.post( final response = await http.post(
Uri.parse(apiUrl), Uri.parse(apiUrl),
headers: { headers: {
'Content-Type': 'Content-Type': 'application/json',
'application/json',
}, },
body: json.encode(formData), body: json.encode(formData),
); );
@@ -718,34 +672,23 @@ class _RegisterPageState extends State<RegisterPage> {
}); });
// Traiter la réponse // Traiter la réponse
if (response.statusCode == 200 || if (response.statusCode == 200 || response.statusCode == 201) {
response.statusCode == 201) { final responseData = json.decode(response.body);
final responseData =
json.decode(response.body);
// Vérifier si la réponse indique un succès // Vérifier si la réponse indique un succès
final bool isSuccess = final bool isSuccess = responseData['success'] == true || responseData['status'] == 'success';
responseData['success'] ==
true ||
responseData['status'] ==
'success';
// Récupérer le message de la réponse // Récupérer le message de la réponse
final String message = responseData[ final String message = responseData['message'] ??
'message'] ?? (isSuccess ? 'Inscription réussie !' : 'Échec de l\'inscription. Veuillez réessayer.');
(isSuccess
? 'Inscription réussie !'
: 'Échec de l\'inscription. Veuillez réessayer.');
if (isSuccess) { if (isSuccess) {
if (mounted) { if (mounted) {
// Afficher une boîte de dialogue de succès // Afficher une boîte de dialogue de succès
showDialog( showDialog(
context: context, context: context,
barrierDismissible: barrierDismissible: false, // L'utilisateur doit cliquer sur OK
false, // L'utilisateur doit cliquer sur OK builder: (BuildContext context) {
builder:
(BuildContext context) {
return AlertDialog( return AlertDialog(
title: const Row( title: const Row(
children: [ children: [
@@ -754,84 +697,50 @@ class _RegisterPageState extends State<RegisterPage> {
color: Colors.green, color: Colors.green,
), ),
SizedBox(width: 10), SizedBox(width: 10),
Text( Text('Inscription réussie'),
'Inscription réussie'),
], ],
), ),
content: Column( content: Column(
mainAxisSize: mainAxisSize: MainAxisSize.min,
MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment
.start,
children: [ children: [
Text( Text(
'Votre demande d\'inscription a été enregistrée avec succès.', 'Votre demande d\'inscription a été enregistrée avec succès.',
style: theme style: theme.textTheme.bodyLarge,
.textTheme
.bodyLarge,
), ),
SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'Vous allez recevoir un email contenant :', 'Vous allez recevoir un email contenant :',
style: theme style: theme.textTheme.bodyMedium,
.textTheme
.bodyMedium,
), ),
SizedBox(height: 8), const SizedBox(height: 8),
Row( Row(
crossAxisAlignment: crossAxisAlignment: CrossAxisAlignment.start,
CrossAxisAlignment
.start,
children: [ children: [
Icon( Icon(Icons.arrow_right, size: 20, color: theme.colorScheme.primary),
Icons const SizedBox(width: 4),
.arrow_right, const Expanded(
size: 20, child: Text('Votre identifiant de connexion'),
color: theme
.colorScheme
.primary),
const SizedBox(
width: 4),
Expanded(
child: Text(
'Votre identifiant de connexion'),
), ),
], ],
), ),
SizedBox(height: 4), const SizedBox(height: 4),
Row( Row(
crossAxisAlignment: crossAxisAlignment: CrossAxisAlignment.start,
CrossAxisAlignment
.start,
children: [ children: [
Icon( Icon(Icons.arrow_right, size: 20, color: theme.colorScheme.primary),
Icons const SizedBox(width: 4),
.arrow_right, const Expanded(
size: 20, child: Text('Un lien pour définir votre mot de passe'),
color: theme
.colorScheme
.primary),
SizedBox(
width: 4),
Expanded(
child: Text(
'Un lien pour définir votre mot de passe'),
), ),
], ],
), ),
SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'Vérifiez votre boîte de réception et vos spams.', 'Vérifiez votre boîte de réception et vos spams.',
style: TextStyle( style: TextStyle(
fontStyle: fontStyle: FontStyle.italic,
FontStyle color: theme.colorScheme.onSurface.withOpacity(0.7),
.italic,
color: theme
.colorScheme
.onSurface
.withOpacity(
0.7),
), ),
), ),
], ],
@@ -839,25 +748,15 @@ class _RegisterPageState extends State<RegisterPage> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.of( Navigator.of(context).pop();
context)
.pop();
// Rediriger vers la page de connexion // Rediriger vers la page de connexion
context context.go('/login');
.go('/login');
}, },
child: Text('OK'), style: TextButton.styleFrom(
style: TextButton foregroundColor: theme.colorScheme.primary,
.styleFrom( textStyle: const TextStyle(fontWeight: FontWeight.bold),
foregroundColor:
theme
.colorScheme
.primary,
textStyle: TextStyle(
fontWeight:
FontWeight
.bold),
), ),
child: const Text('OK'),
), ),
], ],
); );
@@ -870,21 +769,16 @@ class _RegisterPageState extends State<RegisterPage> {
// Afficher un message d'erreur plus visible // Afficher un message d'erreur plus visible
showDialog( showDialog(
context: context, context: context,
builder: builder: (BuildContext context) {
(BuildContext context) {
return AlertDialog( return AlertDialog(
title: const Text( title: const Text('Erreur d\'inscription'),
'Erreur d\'inscription'),
content: Text(message), content: Text(message),
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.of( Navigator.of(context).pop();
context)
.pop();
}, },
child: child: const Text('OK'),
const Text('OK'),
), ),
], ],
); );
@@ -892,8 +786,7 @@ class _RegisterPageState extends State<RegisterPage> {
); );
// Afficher également un SnackBar // Afficher également un SnackBar
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context).showSnackBar(
.showSnackBar(
SnackBar( SnackBar(
content: Text(message), content: Text(message),
backgroundColor: Colors.red, backgroundColor: Colors.red,
@@ -904,11 +797,9 @@ class _RegisterPageState extends State<RegisterPage> {
} else { } else {
// Gérer les erreurs HTTP // Gérer les erreurs HTTP
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context).showSnackBar(
.showSnackBar(
SnackBar( SnackBar(
content: Text( content: Text('Erreur ${response.statusCode}: ${response.reasonPhrase ?? "Échec de l'inscription"}'),
'Erreur ${response.statusCode}: ${response.reasonPhrase ?? "Échec de l\'inscription"}'),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
@@ -922,11 +813,9 @@ class _RegisterPageState extends State<RegisterPage> {
// Gérer les exceptions // Gérer les exceptions
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context).showSnackBar(
.showSnackBar(
SnackBar( SnackBar(
content: Text( content: Text('Erreur: ${e.toString()}'),
'Erreur: ${e.toString()}'),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
@@ -934,9 +823,7 @@ class _RegisterPageState extends State<RegisterPage> {
} }
} }
}, },
text: (_isMobile && !_isConnected) text: (_isMobile && !_isConnected) ? 'Connexion Internet requise' : 'Enregistrer mon amicale',
? 'Connexion Internet requise'
: 'Enregistrer mon amicale',
isLoading: _isLoading, isLoading: _isLoading,
), ),
const SizedBox(height: 24), const SizedBox(height: 24),

View File

@@ -6,20 +6,23 @@ import 'package:geosector_app/core/repositories/user_repository.dart';
import 'package:geosector_app/core/services/api_service.dart'; import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/presentation/widgets/mapbox_map.dart'; import 'package:geosector_app/presentation/widgets/mapbox_map.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart';
import 'custom_text_field.dart'; import 'custom_text_field.dart';
class AmicaleForm extends StatefulWidget { class AmicaleForm extends StatefulWidget {
final AmicaleModel? amicale; final AmicaleModel? amicale;
final Function(AmicaleModel)? onSubmit; final Function(AmicaleModel)? onSubmit;
final bool readOnly; final bool readOnly;
final UserRepository userRepository; // Nouveau paramètre
final ApiService? apiService; // Nouveau paramètre optionnel
const AmicaleForm({ const AmicaleForm({
Key? key, super.key,
this.amicale, this.amicale,
this.onSubmit, this.onSubmit,
this.readOnly = false, this.readOnly = false,
}) : super(key: key); required this.userRepository, // Requis
this.apiService, // Optionnel
});
@override @override
State<AmicaleForm> createState() => _AmicaleFormState(); State<AmicaleForm> createState() => _AmicaleFormState();
@@ -59,8 +62,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
_nameController = TextEditingController(text: amicale?.name ?? ''); _nameController = TextEditingController(text: amicale?.name ?? '');
_adresse1Controller = TextEditingController(text: amicale?.adresse1 ?? ''); _adresse1Controller = TextEditingController(text: amicale?.adresse1 ?? '');
_adresse2Controller = TextEditingController(text: amicale?.adresse2 ?? ''); _adresse2Controller = TextEditingController(text: amicale?.adresse2 ?? '');
_codePostalController = _codePostalController = TextEditingController(text: amicale?.codePostal ?? '');
TextEditingController(text: amicale?.codePostal ?? '');
_villeController = TextEditingController(text: amicale?.ville ?? ''); _villeController = TextEditingController(text: amicale?.ville ?? '');
_phoneController = TextEditingController(text: amicale?.phone ?? ''); _phoneController = TextEditingController(text: amicale?.phone ?? '');
_mobileController = TextEditingController(text: amicale?.mobile ?? ''); _mobileController = TextEditingController(text: amicale?.mobile ?? '');
@@ -125,9 +127,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
}; };
// Ajouter les champs réservés aux administrateurs si l'utilisateur est admin // Ajouter les champs réservés aux administrateurs si l'utilisateur est admin
final userRepository = final userRole = widget.userRepository.getUserRole();
Provider.of<UserRepository>(context, listen: false);
final userRole = userRepository.getUserRole();
if (userRole > 2) { if (userRole > 2) {
data['gps_lat'] = amicale.gpsLat; data['gps_lat'] = amicale.gpsLat;
data['gps_lng'] = amicale.gpsLng; data['gps_lng'] = amicale.gpsLng;
@@ -136,24 +136,46 @@ class _AmicaleFormState extends State<AmicaleForm> {
data['chk_active'] = amicale.chkActive; data['chk_active'] = amicale.chkActive;
} }
// Appeler l'API
try {
// Obtenir l'instance du service API
final apiService = Provider.of<ApiService>(context, listen: false);
// Appeler la méthode post du service API
await apiService.post('/entite/update', data: data);
// Fermer l'indicateur de chargement // Fermer l'indicateur de chargement
Navigator.of(context).pop(); Navigator.of(context).pop();
// Appeler l'API si le service est disponible
if (widget.apiService != null) {
try {
await widget.apiService!.post('/entite/update', data: data);
// Afficher un message de succès // Afficher un message de succès
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text('Amicale mise à jour avec succès'), content: Text('Amicale mise à jour avec succès'),
backgroundColor: Colors.green, backgroundColor: Colors.green,
), ),
); );
}
} catch (error) {
// Afficher un message d'erreur
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la mise à jour de l\'amicale: $error'),
backgroundColor: Colors.red,
),
);
}
return; // Sortir de la fonction en cas d'erreur
}
} else {
// Pas d'API service, afficher un message d'information
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Modifications enregistrées localement'),
backgroundColor: Colors.blue,
),
);
}
}
// Appeler la fonction onSubmit si elle existe // Appeler la fonction onSubmit si elle existe
if (widget.onSubmit != null) { if (widget.onSubmit != null) {
@@ -161,25 +183,17 @@ class _AmicaleFormState extends State<AmicaleForm> {
} }
// Fermer le formulaire // Fermer le formulaire
if (mounted) {
Navigator.of(context).pop(); Navigator.of(context).pop();
} catch (error) {
// Fermer l'indicateur de chargement
Navigator.of(context).pop();
// Afficher un message d'erreur
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content:
Text('Erreur lors de la mise à jour de l\'amicale: $error'),
backgroundColor: Colors.red,
),
);
} }
} catch (e) { } catch (e) {
// Fermer l'indicateur de chargement // Fermer l'indicateur de chargement si encore ouvert
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop(); Navigator.of(context).pop();
}
// Afficher un message d'erreur // Afficher un message d'erreur
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Erreur: ${e.toString()}'), content: Text('Erreur: ${e.toString()}'),
@@ -188,6 +202,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
); );
} }
} }
}
void _submitForm() { void _submitForm() {
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
@@ -195,13 +210,13 @@ class _AmicaleFormState extends State<AmicaleForm> {
if (_phoneController.text.isEmpty && _mobileController.text.isEmpty) { if (_phoneController.text.isEmpty && _mobileController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: content: Text('Veuillez renseigner au moins un numéro de téléphone'),
Text('Veuillez renseigner au moins un numéro de téléphone'),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
return; return;
} }
final amicale = widget.amicale?.copyWith( final amicale = widget.amicale?.copyWith(
name: _nameController.text, name: _nameController.text,
adresse1: _adresse1Controller.text, adresse1: _adresse1Controller.text,
@@ -246,10 +261,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
// Appeler l'API pour mettre à jour l'amicale // Appeler l'API pour mettre à jour l'amicale
_updateAmicale(amicale); _updateAmicale(amicale);
// Appeler la fonction onSubmit si elle existe (pour la compatibilité avec le code existant) // Ne pas appeler widget.onSubmit ici car c'est fait dans _updateAmicale
if (widget.onSubmit != null) {
widget.onSubmit!(amicale);
}
} }
} }
@@ -293,8 +305,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
// TODO: Implémenter la sélection d'image // TODO: Implémenter la sélection d'image
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text( content: Text('Fonctionnalité de modification du logo à venir'),
'Fonctionnalité de modification du logo à venir'),
), ),
); );
}, },
@@ -447,7 +458,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
child: Text( child: Text(
label, label,
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onBackground, color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
@@ -481,7 +492,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
"Adresse", "Adresse",
style: theme.textTheme.titleMedium?.copyWith( style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: theme.colorScheme.onBackground, color: theme.colorScheme.onSurface,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -566,7 +577,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
"Région", "Région",
style: theme.textTheme.titleSmall?.copyWith( style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: theme.colorScheme.onBackground, color: theme.colorScheme.onSurface,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -580,7 +591,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
"Contact", "Contact",
style: theme.textTheme.titleMedium?.copyWith( style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: theme.colorScheme.onBackground, color: theme.colorScheme.onSurface,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -657,7 +668,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
"Informations avancées", "Informations avancées",
style: theme.textTheme.titleMedium?.copyWith( style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: theme.colorScheme.onBackground, color: theme.colorScheme.onSurface,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -671,8 +682,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
child: CustomTextField( child: CustomTextField(
controller: _gpsLatController, controller: _gpsLatController,
label: "GPS Latitude", label: "GPS Latitude",
keyboardType: keyboardType: const TextInputType.numberWithOptions(decimal: true),
const TextInputType.numberWithOptions(decimal: true),
readOnly: restrictedFieldsReadOnly, readOnly: restrictedFieldsReadOnly,
), ),
), ),
@@ -682,8 +692,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
child: CustomTextField( child: CustomTextField(
controller: _gpsLngController, controller: _gpsLngController,
label: "GPS Longitude", label: "GPS Longitude",
keyboardType: keyboardType: const TextInputType.numberWithOptions(decimal: true),
const TextInputType.numberWithOptions(decimal: true),
readOnly: restrictedFieldsReadOnly, readOnly: restrictedFieldsReadOnly,
), ),
), ),
@@ -749,7 +758,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
Text( Text(
"Accepte les règlements en CB", "Accepte les règlements en CB",
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onBackground, color: theme.colorScheme.onSurface,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
@@ -760,8 +769,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
controller: _stripeIdController, controller: _stripeIdController,
label: "ID Stripe Paiements CB", label: "ID Stripe Paiements CB",
readOnly: restrictedFieldsReadOnly, readOnly: restrictedFieldsReadOnly,
helperText: helperText: "Les règlements par CB sont taxés d'une commission de 1.4%",
"Les règlements par CB sont taxés d'une commission de 1.4%",
), ),
), ),
], ],
@@ -774,7 +782,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
"Options", "Options",
style: theme.textTheme.titleMedium?.copyWith( style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: theme.colorScheme.onBackground, color: theme.colorScheme.onSurface,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -849,8 +857,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF20335E), foregroundColor: const Color(0xFF20335E),
side: const BorderSide(color: Color(0xFF20335E)), side: const BorderSide(color: Color(0xFF20335E)),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
horizontal: 24, vertical: 16),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50), borderRadius: BorderRadius.circular(50),
), ),
@@ -871,8 +878,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF20335E), backgroundColor: const Color(0xFF20335E),
foregroundColor: Colors.white, foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
horizontal: 24, vertical: 16),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50), borderRadius: BorderRadius.circular(50),
), ),
@@ -895,42 +901,28 @@ class _AmicaleFormState extends State<AmicaleForm> {
// Vérifier si les informations avancées doivent être affichées // Vérifier si les informations avancées doivent être affichées
bool _shouldShowAdvancedInfo() { bool _shouldShowAdvancedInfo() {
final userRepository = Provider.of<UserRepository>(context, listen: false); final userRole = widget.userRepository.getUserRole();
final userRole = userRepository.getUserRole();
final bool canEditRestrictedFields = userRole > 2; final bool canEditRestrictedFields = userRole > 2;
return canEditRestrictedFields || return canEditRestrictedFields || _gpsLatController.text.isNotEmpty || _gpsLngController.text.isNotEmpty || _stripeIdController.text.isNotEmpty;
_gpsLatController.text.isNotEmpty ||
_gpsLngController.text.isNotEmpty ||
_stripeIdController.text.isNotEmpty;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final userRepository = Provider.of<UserRepository>(context, listen: false); final userRole = widget.userRepository.getUserRole();
final userRole = userRepository.getUserRole();
// Déterminer si l'utilisateur peut modifier les champs restreints // Déterminer si l'utilisateur peut modifier les champs restreints
final bool canEditRestrictedFields = userRole > 2; final bool canEditRestrictedFields = userRole > 2;
// Lecture seule pour les champs restreints si l'utilisateur n'a pas les droits // Lecture seule pour les champs restreints si l'utilisateur n'a pas les droits
final bool restrictedFieldsReadOnly = final bool restrictedFieldsReadOnly = widget.readOnly || !canEditRestrictedFields;
widget.readOnly || !canEditRestrictedFields;
// Calculer la largeur maximale du formulaire pour les écrans larges // Calculer la largeur maximale du formulaire pour les écrans larges
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
final maxFormWidth = screenWidth > 800 ? 800.0 : screenWidth; final maxFormWidth = screenWidth > 800 ? 800.0 : screenWidth;
return Scaffold( final formContent = Container(
appBar: AppBar(
title: Text(
widget.readOnly ? 'Détails de l\'amicale' : 'Modifier l\'amicale'),
backgroundColor: theme.appBarTheme.backgroundColor,
foregroundColor: theme.appBarTheme.foregroundColor,
),
body: Center(
child: Container(
width: maxFormWidth, width: maxFormWidth,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Form( child: Form(
@@ -957,8 +949,25 @@ class _AmicaleFormState extends State<AmicaleForm> {
), ),
), ),
), ),
);
// Vérifier si on est dans une Dialog en regardant le type du widget parent
final route = ModalRoute.of(context);
final isInDialog = route?.settings.name == null;
// Si on est dans une Dialog, ne pas utiliser Scaffold
if (isInDialog) {
return Center(child: formContent);
}
// Sinon, utiliser Scaffold pour les pages complètes
return Scaffold(
appBar: AppBar(
title: Text(widget.readOnly ? 'Détails de l\'amicale' : 'Modifier l\'amicale'),
backgroundColor: theme.appBarTheme.backgroundColor,
foregroundColor: theme.appBarTheme.foregroundColor,
), ),
), body: Center(child: formContent),
); );
} }
} }

View File

@@ -1,31 +1,33 @@
import 'package:flutter/material.dart'; 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/data/models/amicale_model.dart';
/// Widget pour afficher une ligne du tableau d'amicales /// Widget pour afficher une ligne du tableau d'amicales
/// Affiche les colonnes id, name, codePostal, libRegion et une colonne Actions /// Affiche les colonnes id, name, codePostal, libRegion et une colonne Actions (conditionnelle)
/// La colonne Actions contient un bouton Delete pour les utilisateurs avec rôle > 2 /// La colonne Actions contient des boutons Edit et Delete selon les permissions
/// La ligne entière est cliquable pour afficher les détails de l'amicale /// Pour un admin d'amicale (rôle 2), seule la ligne est cliquable sans colonne Actions
class AmicaleRowWidget extends StatelessWidget { class AmicaleRowWidget extends StatelessWidget {
final AmicaleModel amicale; final AmicaleModel amicale;
final Function(AmicaleModel)? onTap; final Function(AmicaleModel)? onTap;
final Function(AmicaleModel)? onEdit;
final Function(AmicaleModel)? onDelete; final Function(AmicaleModel)? onDelete;
final bool isHeader; final bool isHeader;
final bool isAlternate; final bool isAlternate;
final bool showActionsColumn;
const AmicaleRowWidget({ const AmicaleRowWidget({
Key? key, super.key,
required this.amicale, required this.amicale,
this.onTap, this.onTap,
this.onEdit,
this.onDelete, this.onDelete,
this.isHeader = false, this.isHeader = false,
this.isAlternate = false, this.isAlternate = false,
}) : super(key: key); this.showActionsColumn = true,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(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) // Définir les styles en fonction du type de ligne (en-tête ou données)
final textStyle = isHeader final textStyle = isHeader
@@ -36,11 +38,7 @@ class AmicaleRowWidget extends StatelessWidget {
: theme.textTheme.bodyMedium; : theme.textTheme.bodyMedium;
// Couleur de fond en fonction du type de ligne // Couleur de fond en fonction du type de ligne
final backgroundColor = isHeader final backgroundColor = isHeader ? theme.colorScheme.primary.withOpacity(0.1) : (isAlternate ? theme.colorScheme.surface : theme.colorScheme.surface);
? theme.colorScheme.primary.withOpacity(0.1)
: (isAlternate
? theme.colorScheme.surface
: theme.colorScheme.background);
return InkWell( return InkWell(
onTap: isHeader || onTap == null ? null : () => onTap!(amicale), onTap: isHeader || onTap == null ? null : () => onTap!(amicale),
@@ -55,7 +53,7 @@ class AmicaleRowWidget extends StatelessWidget {
), ),
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.symmetric(vertical: 12.0),
child: Row( child: Row(
children: [ children: [
// Colonne ID // Colonne ID
@@ -103,7 +101,7 @@ class AmicaleRowWidget extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text( child: Text(
isHeader ? 'Ville' : (amicale.ville ?? ''), isHeader ? 'Ville' : amicale.ville,
style: textStyle, style: textStyle,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -123,8 +121,8 @@ class AmicaleRowWidget extends StatelessWidget {
), ),
), ),
// Colonne Actions - seulement si l'utilisateur a le rôle > 2 et onDelete n'est pas null // Colonne Actions (conditionnelle)
if (isHeader || (userRole > 2 && onDelete != null)) if (showActionsColumn && (isHeader || onEdit != null || onDelete != null))
Expanded( Expanded(
flex: 2, flex: 2,
child: Padding( child: Padding(
@@ -138,7 +136,25 @@ class AmicaleRowWidget extends StatelessWidget {
: Row( : Row(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
// Bouton Edit
if (onEdit != null)
IconButton(
icon: Icon(
Icons.edit,
color: theme.colorScheme.primary,
size: 20,
),
tooltip: 'Modifier',
onPressed: () => onEdit!(amicale),
constraints: const BoxConstraints(
minWidth: 36,
minHeight: 36,
),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
),
// Bouton Delete // Bouton Delete
if (onDelete != null)
IconButton( IconButton(
icon: Icon( icon: Icon(
Icons.delete, Icons.delete,

View File

@@ -1,11 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:geosector_app/app.dart'; import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/core/data/models/amicale_model.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/core/repositories/user_repository.dart';
import 'package:geosector_app/core/repositories/amicale_repository.dart';
import 'package:geosector_app/presentation/widgets/amicale_row_widget.dart'; import 'package:geosector_app/presentation/widgets/amicale_row_widget.dart';
import 'package:geosector_app/presentation/widgets/amicale_form.dart'; import 'package:geosector_app/presentation/widgets/amicale_form.dart';
import 'package:provider/provider.dart';
/// Widget de tableau pour afficher une liste d'amicales /// Widget de tableau pour afficher une liste d'amicales
/// ///
@@ -19,19 +18,83 @@ import 'package:provider/provider.dart';
/// Lorsqu'on clique sur une ligne, une modale s'affiche avec le formulaire EntiteForm /// Lorsqu'on clique sur une ligne, une modale s'affiche avec le formulaire EntiteForm
class AmicaleTableWidget extends StatelessWidget { class AmicaleTableWidget extends StatelessWidget {
final List<AmicaleModel> amicales; final List<AmicaleModel> amicales;
final Function(AmicaleModel)? onEdit;
final Function(AmicaleModel)? onDelete; final Function(AmicaleModel)? onDelete;
final AmicaleRepository amicaleRepository;
final UserRepository userRepository; // Nouveau paramètre
final ApiService? apiService; // Nouveau paramètre optionnel
final bool isLoading; final bool isLoading;
final String? emptyMessage; final String? emptyMessage;
final bool readOnly; final bool readOnly;
final bool showActionsColumn;
const AmicaleTableWidget({ const AmicaleTableWidget({
Key? key, super.key,
required this.amicales, required this.amicales,
required this.amicaleRepository,
required this.userRepository, // Requis
this.onEdit,
this.onDelete, this.onDelete,
this.apiService, // Optionnel
this.isLoading = false, this.isLoading = false,
this.emptyMessage, this.emptyMessage,
this.readOnly = false, this.readOnly = false,
}) : super(key: key); this.showActionsColumn = true,
});
// Ajouter cette nouvelle méthode pour ouvrir directement le formulaire d'édition :
void _showAmicaleEditForm(BuildContext context, AmicaleModel amicale) {
showDialog(
context: context,
barrierDismissible: false,
builder: (dialogContext) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
width: MediaQuery.of(dialogContext).size.width * 0.9,
height: MediaQuery.of(dialogContext).size.height * 0.9,
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Header de la dialog
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Modifier 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 Divider(),
// Contenu du formulaire
Expanded(
child: AmicaleForm(
amicale: amicale,
readOnly: false,
userRepository: userRepository,
apiService: apiService,
onSubmit: (updatedAmicale) {
Navigator.of(dialogContext).pop();
// La mise à jour sera gérée par les ValueListenableBuilder
},
),
),
],
),
),
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -51,7 +114,9 @@ class AmicaleTableWidget extends StatelessWidget {
), ),
isHeader: true, isHeader: true,
onTap: null, onTap: null,
onEdit: null,
onDelete: null, onDelete: null,
showActionsColumn: showActionsColumn,
), ),
// Corps du tableau // Corps du tableau
@@ -90,8 +155,7 @@ class AmicaleTableWidget extends StatelessWidget {
child: Text( child: Text(
emptyMessage ?? 'Aucune amicale trouvée', emptyMessage ?? 'Aucune amicale trouvée',
style: Theme.of(context).textTheme.bodyLarge?.copyWith( style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
), ),
), ),
), ),
@@ -107,10 +171,18 @@ class AmicaleTableWidget extends StatelessWidget {
final amicale = amicales[index]; final amicale = amicales[index];
return AmicaleRowWidget( return AmicaleRowWidget(
amicale: amicale, amicale: amicale,
isAlternate: index % 2 == 1, // Alterner les couleurs isAlternate: index % 2 == 1,
onTap: (selectedAmicale) => onTap: (selectedAmicale) {
_showAmicaleDetails(context, selectedAmicale), // Si pas de colonne Actions, ouvrir directement le formulaire d'édition
if (!showActionsColumn) {
_showAmicaleEditForm(context, selectedAmicale);
} else {
_showAmicaleDetails(context, selectedAmicale);
}
},
onEdit: onEdit,
onDelete: onDelete, onDelete: onDelete,
showActionsColumn: showActionsColumn,
); );
}, },
); );
@@ -118,20 +190,9 @@ class AmicaleTableWidget extends StatelessWidget {
// Afficher une modale avec le formulaire EntiteForm // Afficher une modale avec le formulaire EntiteForm
void _showAmicaleDetails(BuildContext context, AmicaleModel amicale) { 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( showDialog(
context: context, context: context,
builder: (dialogContext) => MultiProvider( builder: (dialogContext) => Dialog(
providers: [
// Fournir les repositories nécessaires au formulaire
Provider<UserRepository>.value(value: userRepo),
Provider<RegionRepository>.value(value: regionRepo),
],
child: Dialog(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
@@ -148,13 +209,9 @@ class AmicaleTableWidget extends StatelessWidget {
children: [ children: [
Text( Text(
'Détails de l\'amicale', 'Détails de l\'amicale',
style: Theme.of(dialogContext) style: Theme.of(dialogContext).textTheme.headlineSmall?.copyWith(
.textTheme
.headlineSmall
?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: color: Theme.of(dialogContext).colorScheme.primary,
Theme.of(dialogContext).colorScheme.primary,
), ),
), ),
IconButton( IconButton(
@@ -164,13 +221,14 @@ class AmicaleTableWidget extends StatelessWidget {
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Formulaire EntiteForm en mode lecture seule // Formulaire AmicaleForm en mode lecture seule
AmicaleForm( AmicaleForm(
amicale: amicale, amicale: amicale,
readOnly: readOnly, readOnly: true,
userRepository: userRepository,
apiService: apiService,
onSubmit: (updatedAmicale) { onSubmit: (updatedAmicale) {
Navigator.of(dialogContext).pop(); Navigator.of(dialogContext).pop();
// Ici, vous pourriez ajouter une logique pour mettre à jour l'amicale
}, },
), ),
], ],
@@ -178,7 +236,6 @@ class AmicaleTableWidget extends StatelessWidget {
), ),
), ),
), ),
),
); );
} }
} }

View File

@@ -26,14 +26,14 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
final VoidCallback? onLogoutPressed; final VoidCallback? onLogoutPressed;
const DashboardAppBar({ const DashboardAppBar({
Key? key, super.key,
required this.title, required this.title,
this.pageTitle, this.pageTitle,
this.showNewPassageButton = true, this.showNewPassageButton = true,
this.onNewPassagePressed, this.onNewPassagePressed,
this.isAdmin = false, this.isAdmin = false,
this.onLogoutPressed, this.onLogoutPressed,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -82,10 +82,12 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
), ),
); );
actions.add(const SizedBox(width: 8));
// Ajouter la version de l'application // Ajouter la version de l'application
actions.add( actions.add(
Text( Text(
AppInfoService.fullVersion, "v${AppInfoService.version}",
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.white70, color: Colors.white70,
@@ -93,11 +95,12 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
), ),
); );
actions.add(const SizedBox(width: 8));
actions.add( actions.add(
TextButton.icon( TextButton.icon(
icon: const Icon(Icons.add_location_alt, color: Colors.white), icon: const Icon(Icons.add_location_alt, color: Colors.white),
label: const Text('Nouveau passage', label: const Text('Nouveau passage', style: TextStyle(color: Colors.white)),
style: TextStyle(color: Colors.white)),
onPressed: onNewPassagePressed, onPressed: onNewPassagePressed,
style: TextButton.styleFrom( style: TextButton.styleFrom(
backgroundColor: theme.colorScheme.secondary, backgroundColor: theme.colorScheme.secondary,
@@ -106,6 +109,8 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
), ),
); );
actions.add(const SizedBox(width: 8));
// Ajouter le bouton "Mon compte" // Ajouter le bouton "Mon compte"
actions.add( actions.add(
IconButton( IconButton(
@@ -128,6 +133,8 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
), ),
); );
actions.add(const SizedBox(width: 8));
// Ajouter le bouton de déconnexion // Ajouter le bouton de déconnexion
actions.add( actions.add(
IconButton( IconButton(
@@ -139,8 +146,7 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
context: context, context: context,
builder: (dialogContext) => AlertDialog( builder: (dialogContext) => AlertDialog(
title: const Text('Déconnexion'), title: const Text('Déconnexion'),
content: content: const Text('Voulez-vous vraiment vous déconnecter ?'),
const Text('Voulez-vous vraiment vous déconnecter ?'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(dialogContext).pop(), onPressed: () => Navigator.of(dialogContext).pop(),
@@ -157,8 +163,7 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
// Vérification supplémentaire et navigation forcée si nécessaire // Vérification supplémentaire et navigation forcée si nécessaire
if (success && context.mounted) { if (success && context.mounted) {
// Attendre un court instant pour que les changements d'état se propagent // Attendre un court instant pour que les changements d'état se propagent
await Future.delayed( await Future.delayed(const Duration(milliseconds: 100));
const Duration(milliseconds: 100));
// Navigation forcée vers la page d'accueil // Navigation forcée vers la page d'accueil
context.go('/'); context.go('/');

View File

@@ -3,32 +3,31 @@ import 'package:geosector_app/core/data/models/membre_model.dart';
class MembreRowWidget extends StatelessWidget { class MembreRowWidget extends StatelessWidget {
final MembreModel membre; final MembreModel membre;
final Function()? onEdit; final Function(MembreModel)? onEdit;
final Function()? onDelete; final Function(MembreModel)? onDelete;
final bool isAlternate;
const MembreRowWidget({ const MembreRowWidget({
Key? key, super.key,
required this.membre, required this.membre,
this.onEdit, this.onEdit,
this.onDelete, this.onDelete,
}) : super(key: key); this.isAlternate = false,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return Container( // Couleur de fond alternée
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), final backgroundColor = isAlternate ? theme.colorScheme.surface : theme.colorScheme.surface;
return InkWell(
onTap: () => _showMembreDetails(context),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: backgroundColor,
borderRadius: BorderRadius.circular(8.0),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
), ),
child: Row( child: Row(
children: [ children: [
@@ -61,11 +60,11 @@ class MembreRowWidget extends StatelessWidget {
), ),
), ),
// Secteur (sectName) // Email
Expanded( Expanded(
flex: 2, flex: 3,
child: Text( child: Text(
membre.sectName ?? '', membre.email,
style: theme.textTheme.bodyMedium, style: theme.textTheme.bodyMedium,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -80,36 +79,134 @@ class MembreRowWidget extends StatelessWidget {
), ),
), ),
// Statut (actif/inactif)
Expanded(
flex: 1,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: membre.chkActive == 1 ? Colors.green.withOpacity(0.1) : Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: membre.chkActive == 1 ? Colors.green.withOpacity(0.3) : Colors.red.withOpacity(0.3),
),
),
child: Text(
membre.chkActive == 1 ? 'Actif' : 'Inactif',
style: theme.textTheme.bodySmall?.copyWith(
color: membre.chkActive == 1 ? Colors.green[700] : Colors.red[700],
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
),
// Actions // Actions
if (onEdit != null || onDelete != null)
Expanded( Expanded(
flex: 2, flex: 2,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
// Bouton Edit // Bouton Edit
if (onEdit != null)
IconButton( IconButton(
icon: const Icon(Icons.edit, size: 20), icon: Icon(
Icons.edit,
size: 20,
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
onPressed: onEdit, ),
onPressed: () => onEdit!(membre),
tooltip: 'Modifier', tooltip: 'Modifier',
constraints: const BoxConstraints(), constraints: const BoxConstraints(
padding: const EdgeInsets.all(8), minWidth: 36,
minHeight: 36,
),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
), ),
// Espacement entre les boutons
if (onEdit != null && onDelete != null) const SizedBox(width: 8),
// Bouton Delete // Bouton Delete
if (onDelete != null)
IconButton( IconButton(
icon: const Icon(Icons.delete, size: 20), icon: Icon(
Icons.delete,
size: 20,
color: theme.colorScheme.error, color: theme.colorScheme.error,
onPressed: onDelete, ),
onPressed: () => onDelete!(membre),
tooltip: 'Supprimer', tooltip: 'Supprimer',
constraints: const BoxConstraints(), constraints: const BoxConstraints(
padding: const EdgeInsets.all(8), minWidth: 36,
minHeight: 36,
),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
), ),
], ],
), ),
), ),
], ],
), ),
),
);
}
// Afficher les détails du membre dans une boîte de dialogue
void _showMembreDetails(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('${membre.firstName} ${membre.name}'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailRow('ID', membre.id.toString()),
_buildDetailRow('Email', membre.email),
_buildDetailRow('Username', membre.username),
_buildDetailRow('Rôle', _getRoleName(membre.fkRole)),
_buildDetailRow('Titre', membre.fkTitre.toString()),
_buildDetailRow('Secteur', membre.sectName ?? 'Non défini'),
_buildDetailRow('Statut', membre.chkActive == 1 ? 'Actif' : 'Inactif'),
if (membre.dateNaissance != null)
_buildDetailRow('Date de naissance', '${membre.dateNaissance!.day}/${membre.dateNaissance!.month}/${membre.dateNaissance!.year}'),
if (membre.dateEmbauche != null)
_buildDetailRow('Date d\'embauche', '${membre.dateEmbauche!.day}/${membre.dateEmbauche!.month}/${membre.dateEmbauche!.year}'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Fermer'),
),
],
),
);
}
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 120,
child: Text(
'$label:',
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
Expanded(
child: Text(value),
),
],
),
); );
} }

View File

@@ -1,24 +1,31 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:geosector_app/core/data/models/membre_model.dart'; import 'package:geosector_app/core/data/models/membre_model.dart';
import 'package:geosector_app/core/repositories/membre_repository.dart';
import 'package:geosector_app/presentation/widgets/membre_row_widget.dart'; import 'package:geosector_app/presentation/widgets/membre_row_widget.dart';
class MembreTableWidget extends StatelessWidget { class MembreTableWidget extends StatelessWidget {
final List<MembreModel> membres; final List<MembreModel> membres;
final Function(MembreModel)? onEdit; final Function(MembreModel)? onEdit;
final Function(MembreModel)? onDelete; final Function(MembreModel)? onDelete;
final MembreRepository membreRepository;
final bool showHeader; final bool showHeader;
final double? height; final double? height;
final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? padding;
final bool isLoading;
final String? emptyMessage;
const MembreTableWidget({ const MembreTableWidget({
Key? key, super.key,
required this.membres, required this.membres,
required this.membreRepository,
this.onEdit, this.onEdit,
this.onDelete, this.onDelete,
this.showHeader = true, this.showHeader = true,
this.height, this.height,
this.padding, this.padding,
}) : super(key: key); this.isLoading = false,
this.emptyMessage,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -44,8 +51,7 @@ class MembreTableWidget extends StatelessWidget {
// En-tête du tableau // En-tête du tableau
if (showHeader) if (showHeader)
Padding( Padding(
padding: padding: const EdgeInsets.only(bottom: 16.0, left: 16.0, right: 16.0),
const EdgeInsets.only(bottom: 16.0, left: 16.0, right: 16.0),
child: Row( child: Row(
children: [ children: [
// ID // ID
@@ -55,6 +61,7 @@ class MembreTableWidget extends StatelessWidget {
'ID', 'ID',
style: theme.textTheme.titleSmall?.copyWith( style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
), ),
), ),
), ),
@@ -66,6 +73,7 @@ class MembreTableWidget extends StatelessWidget {
'Prénom', 'Prénom',
style: theme.textTheme.titleSmall?.copyWith( style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
), ),
), ),
), ),
@@ -77,17 +85,19 @@ class MembreTableWidget extends StatelessWidget {
'Nom', 'Nom',
style: theme.textTheme.titleSmall?.copyWith( style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
), ),
), ),
), ),
// Secteur (sectName) // Email
Expanded( Expanded(
flex: 2, flex: 3,
child: Text( child: Text(
'Secteur', 'Email',
style: theme.textTheme.titleSmall?.copyWith( style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
), ),
), ),
), ),
@@ -99,17 +109,32 @@ class MembreTableWidget extends StatelessWidget {
'Rôle', 'Rôle',
style: theme.textTheme.titleSmall?.copyWith( style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
), ),
), ),
), ),
// Actions // Statut
Expanded(
flex: 1,
child: Text(
'Statut',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
),
// Actions (si onEdit ou onDelete sont fournis)
if (onEdit != null || onDelete != null)
Expanded( Expanded(
flex: 2, flex: 2,
child: Text( child: Text(
'Actions', 'Actions',
style: theme.textTheme.titleSmall?.copyWith( style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
), ),
textAlign: TextAlign.end, textAlign: TextAlign.end,
), ),
@@ -118,32 +143,49 @@ class MembreTableWidget extends StatelessWidget {
), ),
), ),
// Liste des membres // Corps du tableau
Expanded( Expanded(
child: membres.isEmpty child: _buildTableContent(context),
? 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,
);
},
),
), ),
], ],
), ),
); );
} }
Widget _buildTableContent(BuildContext context) {
// Afficher un indicateur de chargement si isLoading est true
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
// Afficher un message si la liste est vide
if (membres.isEmpty) {
return Center(
child: Text(
emptyMessage ?? 'Aucun membre trouvé',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
),
);
}
// Afficher la liste des membres
return ListView.separated(
itemCount: membres.length,
separatorBuilder: (context, index) => Divider(
color: Theme.of(context).dividerColor.withOpacity(0.3),
height: 1,
),
itemBuilder: (context, index) {
final membre = membres[index];
return MembreRowWidget(
membre: membre,
onEdit: onEdit,
onDelete: onDelete,
isAlternate: index % 2 == 1,
);
},
);
}
} }

View File

@@ -1,7 +1,7 @@
name: geosector_app name: geosector_app
description: 'GEOSECTOR - Gestion de distribution des calendriers par secteurs géographiques pour les amicales de pompiers' description: 'GEOSECTOR - Gestion de distribution des calendriers par secteurs géographiques pour les amicales de pompiers'
publish_to: 'none' publish_to: 'none'
version: 0.3.2 version: 0.3.3
environment: environment:
sdk: '>=3.0.0 <4.0.0' sdk: '>=3.0.0 <4.0.0'

View File

@@ -1,79 +0,0 @@
# 🎯 Prompt : Widget Formulaire Passage Modal Cross-Platform
## Contexte
Développement d'un widget modal de formulaire de passage pour GEOSECTOR, compatible Svelte (web) et Flutter (mobile).
## Prompt à utiliser avec Cline/Claude
```
Je développe un widget modal de formulaire de passage pour GEOSECTOR qui doit fonctionner de manière identique sur :
- Web avec Svelte/SvelteKit
- Mobile avec Flutter
### Spécifications du widget
#### Interface utilisateur
- Modal centré avec overlay semi-transparent
- Formulaire avec 4 champs principaux :
1. **Date** : sélecteur de date natif
2. **Heure** : sélecteur d'heure (format 24h)
3. **Commentaire** : textarea avec compteur (max 500 caractères)
4. **Statut** : dropdown avec options ["En attente", "Validé", "Rejeté", "En cours"]
#### Fonctionnalités avancées
- Validation en temps réel avec messages d'erreur contextuels
- Sauvegarde automatique toutes les 30 secondes (draft local)
- Animations d'ouverture/fermeture fluides
- Gestion des états : loading, success, error
- Boutons d'action : Annuler, Sauvegarder (draft), Valider (final)
#### Contraintes techniques
- Design system cohérent entre Svelte et Flutter
- Accessibilité : labels, focus management, navigation clavier
- Responsive : adaptation automatique mobile/desktop
- Performance : lazy loading du composant
- Validation côté client + préparation validation serveur
### Structure souhaitée
#### Svelte (lib/components/PassageFormModal.svelte)
```
<script>
// Props et logique de validation
// Store pour gestion état
// Auto-save logic
</script>
<div class="modal-overlay">
<div class="modal-content">
<form>
<!-- Champs formulaire -->
</form>
</div>
</div>
```
#### Flutter (lib/widgets/passage_form_modal.dart)
```
class PassageFormModal extends StatefulWidget {
// Widget avec même logique qu'en Svelte
// Form validation
// Auto-save functionality
}
```
Peux-tu m'accompagner dans le développement de ce widget en respectant ces spécifications et en optimisant pour la réutilisabilité ?
```
## Bonnes pratiques à appliquer
- Utiliser des validateurs côté client robustes
- Implémenter un système de debouncing pour l'auto-save
- Gérer proprement les animations et transitions
- Prévoir la gestion d'erreur réseau
- Tester l'accessibilité sur les deux plateformes
## Tests suggérés
- Tests unitaires de validation
- Tests d'intégration du formulaire
- Tests d'accessibilité
- Tests de performance (auto-save)

View File

@@ -1,3 +0,0 @@
{
"recommendations": ["svelte.svelte-vscode"]
}