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:
90
.vscode/settings.json
vendored
90
.vscode/settings.json
vendored
@@ -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"
|
||||
}
|
||||
153
api/.vscode/settings.json
vendored
153
api/.vscode/settings.json
vendored
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -155,7 +155,8 @@ class LoginController {
|
||||
'email' => $email,
|
||||
'name' => $decryptedName,
|
||||
'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
|
||||
];
|
||||
Session::login($sessionData);
|
||||
@@ -406,7 +407,7 @@ class LoginController {
|
||||
// 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'])) {
|
||||
$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,
|
||||
date_naissance, date_embauche, chk_active
|
||||
FROM users
|
||||
@@ -422,6 +423,7 @@ class LoginController {
|
||||
$membreItem = [
|
||||
'id' => $membre['id'],
|
||||
'fk_role' => $membre['fk_role'],
|
||||
'fk_entite' => $membre['fk_entite'],
|
||||
'fk_titre' => $membre['fk_titre'],
|
||||
'first_name' => $membre['first_name'] ?? '',
|
||||
'sect_name' => $membre['sect_name'] ?? '',
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -24,13 +24,14 @@ class MembreModelAdapter extends TypeAdapter<MembreModel> {
|
||||
name: fields[8] as String,
|
||||
username: fields[9] as String,
|
||||
email: fields[10] as String,
|
||||
fkEntite: fields[11] as int,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, MembreModel obj) {
|
||||
writer
|
||||
..writeByte(11)
|
||||
..writeByte(12)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
@@ -52,7 +53,9 @@ class MembreModelAdapter extends TypeAdapter<MembreModel> {
|
||||
..writeByte(9)
|
||||
..write(obj.username)
|
||||
..writeByte(10)
|
||||
..write(obj.email);
|
||||
..write(obj.email)
|
||||
..writeByte(11)
|
||||
..write(obj.fkEntite);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -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.
|
||||
@@ -1 +0,0 @@
|
||||
{"version":2,"entries":[{"package":"geosector_app","rootUri":"../","packageUri":"lib/"}]}
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -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/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/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/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
|
||||
@@ -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/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/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/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
|
||||
@@ -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/theme/app_theme.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_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_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_map_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
@@ -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/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/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/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
|
||||
@@ -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/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/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/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
|
||||
@@ -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/theme/app_theme.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_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_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_map_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
@@ -5,7 +5,7 @@
|
||||
"packages": [
|
||||
{
|
||||
"name": "geosector_app",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"dependencies": [
|
||||
"connectivity_plus",
|
||||
"cupertino_icons",
|
||||
|
||||
File diff suppressed because one or more lines are too long
156
app/.vscode/settings.json
vendored
156
app/.vscode/settings.json
vendored
@@ -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
1537
app/PLAN2-APP.md
Normal file
File diff suppressed because it is too large
Load Diff
2853
app/README2-APP.md
Normal file
2853
app/README2-APP.md
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"guid":"dc4b70c03e8043e50e38f2068887b1d4","name":"Pods","path":"/Users/pierre/dev/geosector/app/ios/Pods/Pods.xcodeproj/project.xcworkspace","projects":["PROJECT@v11_mod=9c247933552af22255bf791d596f2dce_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1"]}
|
||||
{"guid":"dc4b70c03e8043e50e38f2068887b1d4","name":"Pods","path":"/Users/pierre/dev/geosector/app/ios/Pods/Pods.xcodeproj/project.xcworkspace","projects":["PROJECT@v11_mod=0987d131841f12618fa22c05d7871702_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1"]}
|
||||
@@ -39,6 +39,6 @@ _flutter.buildConfig = {"engineRevision":"1425e5e9ec5eeb4f225c401d8db69b860e0fde
|
||||
|
||||
_flutter.loader.load({
|
||||
serviceWorkerSettings: {
|
||||
serviceWorkerVersion: "4125207360"
|
||||
serviceWorkerVersion: "2020305023"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,13 +3,13 @@ const MANIFEST = 'flutter-app-manifest';
|
||||
const TEMP = 'flutter-temp-cache';
|
||||
const CACHE_NAME = 'flutter-app-cache';
|
||||
|
||||
const RESOURCES = {"flutter_bootstrap.js": "d60817d374508a2c4a7c0224cbbb6b37",
|
||||
"version.json": "f56c9eb4f24ffe0ef4ae51981126f500",
|
||||
const RESOURCES = {"flutter_bootstrap.js": "aac8cb24376ac6215b72a88087969ea7",
|
||||
"version.json": "727f6f584c125faac83c6d2a4c96fb3d",
|
||||
"index.html": "2aab03d10fea3b608e3eddc0fc0077e5",
|
||||
"/": "2aab03d10fea3b608e3eddc0fc0077e5",
|
||||
"favicon-64.png": "259540a3217e969237530444ca0eaed3",
|
||||
"favicon-16.png": "106142fb24eba190e475dbe6513cc9ff",
|
||||
"main.dart.js": "e8b59f013355f8e56dfe50453c6b7f95",
|
||||
"main.dart.js": "00dc118c0abed215458c35d4ba5a89b1",
|
||||
"flutter.js": "83d881c1dbb6d6bcd6b42e274605b69c",
|
||||
"favicon.png": "21510778ead066ac826ad69302400773",
|
||||
"icons/Icon-192.png": "f36879dd176101fac324b68793e4683c",
|
||||
|
||||
126257
app/build/web/main.dart.js
126257
app/build/web/main.dart.js
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"app_name":"geosector_app","version":"0.3.2","package_name":"geosector_app"}
|
||||
{"app_name":"geosector_app","version":"0.3.3","package_name":"geosector_app"}
|
||||
@@ -37,6 +37,9 @@ class MembreModel extends HiveObject {
|
||||
@HiveField(10)
|
||||
final String email;
|
||||
|
||||
@HiveField(11)
|
||||
final int fkEntite;
|
||||
|
||||
MembreModel({
|
||||
required this.id,
|
||||
required this.fkRole,
|
||||
@@ -49,6 +52,7 @@ class MembreModel extends HiveObject {
|
||||
required this.name,
|
||||
required this.username,
|
||||
required this.email,
|
||||
required this.fkEntite,
|
||||
});
|
||||
|
||||
// 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
|
||||
final dynamic rawTitre = json['fk_titre'];
|
||||
final int fkTitre =
|
||||
rawTitre is String ? int.parse(rawTitre) : rawTitre as int;
|
||||
final int fkTitre = rawTitre is String ? int.parse(rawTitre) : rawTitre as int;
|
||||
|
||||
// Convertir le chkActive en int, qu'il soit déjà int ou string
|
||||
final dynamic rawActive = json['chk_active'];
|
||||
final int chkActive =
|
||||
rawActive is String ? int.parse(rawActive) : rawActive as int;
|
||||
final int chkActive = 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(
|
||||
id: id,
|
||||
@@ -77,16 +83,13 @@ class MembreModel extends HiveObject {
|
||||
fkTitre: fkTitre,
|
||||
firstName: json['first_name'] ?? '',
|
||||
sectName: json['sect_name'],
|
||||
dateNaissance: json['date_naissance'] != null
|
||||
? DateTime.parse(json['date_naissance'])
|
||||
: null,
|
||||
dateEmbauche: json['date_embauche'] != null
|
||||
? DateTime.parse(json['date_embauche'])
|
||||
: null,
|
||||
dateNaissance: json['date_naissance'] != null ? DateTime.parse(json['date_naissance']) : null,
|
||||
dateEmbauche: json['date_embauche'] != null ? DateTime.parse(json['date_embauche']) : null,
|
||||
chkActive: chkActive,
|
||||
name: json['name'] ?? '',
|
||||
username: json['username'] ?? '',
|
||||
email: json['email'] ?? '',
|
||||
fkEntite: fkEntite,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,6 +107,7 @@ class MembreModel extends HiveObject {
|
||||
'name': name,
|
||||
'username': username,
|
||||
'email': email,
|
||||
'fk_entite': fkEntite,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -119,9 +123,10 @@ class MembreModel extends HiveObject {
|
||||
String? name,
|
||||
String? username,
|
||||
String? email,
|
||||
int? fkEntite,
|
||||
}) {
|
||||
return MembreModel(
|
||||
id: this.id,
|
||||
id: id,
|
||||
fkRole: fkRole ?? this.fkRole,
|
||||
fkTitre: fkTitre ?? this.fkTitre,
|
||||
firstName: firstName ?? this.firstName,
|
||||
@@ -132,6 +137,7 @@ class MembreModel extends HiveObject {
|
||||
name: name ?? this.name,
|
||||
username: username ?? this.username,
|
||||
email: email ?? this.email,
|
||||
fkEntite: fkEntite ?? this.fkEntite,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,13 +28,14 @@ class MembreModelAdapter extends TypeAdapter<MembreModel> {
|
||||
name: fields[8] as String,
|
||||
username: fields[9] as String,
|
||||
email: fields[10] as String,
|
||||
fkEntite: fields[11] as int,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, MembreModel obj) {
|
||||
writer
|
||||
..writeByte(11)
|
||||
..writeByte(12)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
@@ -56,7 +57,9 @@ class MembreModelAdapter extends TypeAdapter<MembreModel> {
|
||||
..writeByte(9)
|
||||
..write(obj.username)
|
||||
..writeByte(10)
|
||||
..write(obj.email);
|
||||
..write(obj.email)
|
||||
..writeByte(11)
|
||||
..write(obj.fkEntite);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -7,8 +7,7 @@ import 'package:geosector_app/core/data/models/amicale_model.dart';
|
||||
|
||||
class AmicaleRepository extends ChangeNotifier {
|
||||
// Utilisation de getters lazy pour n'accéder à la boîte que lorsque nécessaire
|
||||
Box<AmicaleModel> get _amicaleBox =>
|
||||
Hive.box<AmicaleModel>(AppKeys.amicaleBoxName);
|
||||
Box<AmicaleModel> get _amicaleBox => Hive.box<AmicaleModel>(AppKeys.amicaleBoxName);
|
||||
|
||||
final ApiService _apiService;
|
||||
bool _isLoading = false;
|
||||
@@ -18,6 +17,19 @@ class AmicaleRepository extends ChangeNotifier {
|
||||
// Getters
|
||||
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
|
||||
Future<void> _ensureBoxIsOpen() async {
|
||||
try {
|
||||
@@ -59,8 +71,7 @@ class AmicaleRepository extends ChangeNotifier {
|
||||
_ensureBoxIsOpen();
|
||||
return _amicaleBox.get(fkEntite);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'Erreur lors de la récupération de l\'amicale de l\'utilisateur: $e');
|
||||
debugPrint('Erreur lors de la récupération de l\'amicale de l\'utilisateur: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -146,8 +157,7 @@ class AmicaleRepository extends ChangeNotifier {
|
||||
final amicale = AmicaleModel.fromJson(amicaleMap);
|
||||
await _amicaleBox.put(amicale.id, amicale);
|
||||
count++;
|
||||
debugPrint(
|
||||
'Amicale unique traitée: ${amicale.name} (ID: ${amicale.id})');
|
||||
debugPrint('Amicale unique traitée: ${amicale.name} (ID: ${amicale.id})');
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du traitement de l\'amicale unique: $e');
|
||||
debugPrint('Exception détaillée: $e');
|
||||
@@ -177,8 +187,7 @@ class AmicaleRepository extends ChangeNotifier {
|
||||
await processAmicalesData(amicalesData);
|
||||
return getAllAmicales();
|
||||
} else {
|
||||
debugPrint(
|
||||
'Erreur lors de la récupération des amicales: ${response.statusCode}');
|
||||
debugPrint('Erreur lors de la récupération des amicales: ${response.statusCode}');
|
||||
return [];
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -204,8 +213,7 @@ class AmicaleRepository extends ChangeNotifier {
|
||||
await saveAmicale(amicale);
|
||||
return amicale;
|
||||
} else {
|
||||
debugPrint(
|
||||
'Erreur lors de la récupération de l\'amicale: ${response.statusCode}');
|
||||
debugPrint('Erreur lors de la récupération de l\'amicale: ${response.statusCode}');
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -234,8 +242,7 @@ class AmicaleRepository extends ChangeNotifier {
|
||||
await saveAmicale(updatedAmicale);
|
||||
return updatedAmicale;
|
||||
} else {
|
||||
debugPrint(
|
||||
'Erreur lors de la mise à jour de l\'amicale: ${response.statusCode}');
|
||||
debugPrint('Erreur lors de la mise à jour de l\'amicale: ${response.statusCode}');
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -254,23 +261,17 @@ class AmicaleRepository extends ChangeNotifier {
|
||||
}
|
||||
|
||||
final lowercaseQuery = query.toLowerCase();
|
||||
return _amicaleBox.values
|
||||
.where((amicale) => amicale.name.toLowerCase().contains(lowercaseQuery))
|
||||
.toList();
|
||||
return _amicaleBox.values.where((amicale) => amicale.name.toLowerCase().contains(lowercaseQuery)).toList();
|
||||
}
|
||||
|
||||
// Filtrer les amicales par type
|
||||
List<AmicaleModel> getAmicalesByType(int type) {
|
||||
return _amicaleBox.values
|
||||
.where((amicale) => amicale.fkType == type)
|
||||
.toList();
|
||||
return _amicaleBox.values.where((amicale) => amicale.fkType == type).toList();
|
||||
}
|
||||
|
||||
// Filtrer les amicales par région
|
||||
List<AmicaleModel> getAmicalesByRegion(int regionId) {
|
||||
return _amicaleBox.values
|
||||
.where((amicale) => amicale.fkRegion == regionId)
|
||||
.toList();
|
||||
return _amicaleBox.values.where((amicale) => amicale.fkRegion == regionId).toList();
|
||||
}
|
||||
|
||||
// Filtrer les amicales actives
|
||||
@@ -280,16 +281,12 @@ class AmicaleRepository extends ChangeNotifier {
|
||||
|
||||
// Filtrer les amicales par code postal
|
||||
List<AmicaleModel> getAmicalesByPostalCode(String postalCode) {
|
||||
return _amicaleBox.values
|
||||
.where((amicale) => amicale.codePostal == postalCode)
|
||||
.toList();
|
||||
return _amicaleBox.values.where((amicale) => amicale.codePostal == postalCode).toList();
|
||||
}
|
||||
|
||||
// Filtrer les amicales par ville
|
||||
List<AmicaleModel> getAmicalesByCity(String city) {
|
||||
final lowercaseCity = city.toLowerCase();
|
||||
return _amicaleBox.values
|
||||
.where((amicale) => amicale.ville.toLowerCase().contains(lowercaseCity))
|
||||
.toList();
|
||||
return _amicaleBox.values.where((amicale) => amicale.ville.toLowerCase().contains(lowercaseCity)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@ import 'package:geosector_app/core/data/models/membre_model.dart';
|
||||
|
||||
class MembreRepository extends ChangeNotifier {
|
||||
// Utilisation de getters lazy pour n'accéder à la boîte que lorsque nécessaire
|
||||
Box<MembreModel> get _membreBox =>
|
||||
Hive.box<MembreModel>(AppKeys.membresBoxName);
|
||||
Box<MembreModel> get _membreBox => Hive.box<MembreModel>(AppKeys.membresBoxName);
|
||||
|
||||
final ApiService _apiService;
|
||||
bool _isLoading = false;
|
||||
@@ -20,6 +19,19 @@ class MembreRepository extends ChangeNotifier {
|
||||
bool get isLoading => _isLoading;
|
||||
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
|
||||
Future<void> _ensureBoxIsOpen() async {
|
||||
try {
|
||||
@@ -29,10 +41,37 @@ class MembreRepository extends ChangeNotifier {
|
||||
debugPrint('Boîte ${AppKeys.membresBoxName} ouverte avec succès');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'Erreur lors de l\'ouverture de la boîte ${AppKeys.membresBoxName}: $e');
|
||||
throw Exception(
|
||||
'Impossible d\'ouvrir la boîte ${AppKeys.membresBoxName}: $e');
|
||||
debugPrint('Erreur lors de l\'ouverture de 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 {
|
||||
final hasConnection = await _apiService.hasInternetConnection();
|
||||
if (!hasConnection) {
|
||||
debugPrint(
|
||||
'Pas de connexion Internet, utilisation des données locales');
|
||||
debugPrint('Pas de connexion Internet, utilisation des données locales');
|
||||
return getAllMembres();
|
||||
}
|
||||
|
||||
@@ -107,8 +145,7 @@ class MembreRepository extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
return membres;
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'Erreur lors de la récupération des membres depuis l\'API: $e');
|
||||
debugPrint('Erreur lors de la récupération des membres depuis l\'API: $e');
|
||||
return getAllMembres();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
@@ -129,8 +166,7 @@ class MembreRepository extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// Endpoint à adapter selon votre API
|
||||
final response =
|
||||
await _apiService.post('/membres', data: membre.toJson());
|
||||
final response = await _apiService.post('/membres', data: membre.toJson());
|
||||
final membreData = response.data['membre'];
|
||||
|
||||
final newMembre = MembreModel.fromJson(membreData);
|
||||
@@ -154,14 +190,12 @@ class MembreRepository extends ChangeNotifier {
|
||||
try {
|
||||
final hasConnection = await _apiService.hasInternetConnection();
|
||||
if (!hasConnection) {
|
||||
debugPrint(
|
||||
'Pas de connexion Internet, impossible de mettre à jour le membre');
|
||||
debugPrint('Pas de connexion Internet, impossible de mettre à jour le membre');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Endpoint à adapter selon votre API
|
||||
final response =
|
||||
await _apiService.put('/membres/${membre.id}', data: membre.toJson());
|
||||
final response = await _apiService.put('/membres/${membre.id}', data: membre.toJson());
|
||||
final membreData = response.data['membre'];
|
||||
|
||||
final updatedMembre = MembreModel.fromJson(membreData);
|
||||
@@ -185,8 +219,7 @@ class MembreRepository extends ChangeNotifier {
|
||||
try {
|
||||
final hasConnection = await _apiService.hasInternetConnection();
|
||||
if (!hasConnection) {
|
||||
debugPrint(
|
||||
'Pas de connexion Internet, impossible de supprimer le membre');
|
||||
debugPrint('Pas de connexion Internet, impossible de supprimer le membre');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -203,9 +203,7 @@ class ApiService {
|
||||
retryIf: (e) => e is SocketException || e is TimeoutException,
|
||||
);
|
||||
|
||||
return (response.data as List)
|
||||
.map((json) => UserModel.fromJson(json))
|
||||
.toList();
|
||||
return (response.data as List).map((json) => UserModel.fromJson(json)).toList();
|
||||
} catch (e) {
|
||||
// Gérer les erreurs
|
||||
rethrow;
|
||||
|
||||
@@ -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
|
||||
370
app/lib/presentation/admin/admin_amicale_page.dart
Normal file
370
app/lib/presentation/admin/admin_amicale_page.dart
Normal 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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import 'admin_statistics_page.dart';
|
||||
import 'admin_history_page.dart';
|
||||
import 'admin_communication_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 DotsPainter extends CustomPainter {
|
||||
@@ -37,14 +37,13 @@ class DotsPainter extends CustomPainter {
|
||||
}
|
||||
|
||||
class AdminDashboardPage extends StatefulWidget {
|
||||
const AdminDashboardPage({Key? key}) : super(key: key);
|
||||
const AdminDashboardPage({super.key});
|
||||
|
||||
@override
|
||||
State<AdminDashboardPage> createState() => _AdminDashboardPageState();
|
||||
}
|
||||
|
||||
class _AdminDashboardPageState extends State<AdminDashboardPage>
|
||||
with WidgetsBindingObserver {
|
||||
class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBindingObserver {
|
||||
int _selectedIndex = 0;
|
||||
|
||||
// Liste des pages à afficher
|
||||
@@ -59,31 +58,31 @@ class _AdminDashboardPageState extends State<AdminDashboardPage>
|
||||
label: 'Tableau de bord',
|
||||
icon: Icons.dashboard_outlined,
|
||||
selectedIcon: Icons.dashboard,
|
||||
page: AdminDashboardHomePage(),
|
||||
pageType: _PageType.dashboardHome,
|
||||
),
|
||||
const _NavigationItem(
|
||||
label: 'Statistiques',
|
||||
icon: Icons.bar_chart_outlined,
|
||||
selectedIcon: Icons.bar_chart,
|
||||
page: AdminStatisticsPage(),
|
||||
pageType: _PageType.statistics,
|
||||
),
|
||||
const _NavigationItem(
|
||||
label: 'Historique',
|
||||
icon: Icons.history_outlined,
|
||||
selectedIcon: Icons.history,
|
||||
page: AdminHistoryPage(),
|
||||
pageType: _PageType.history,
|
||||
),
|
||||
const _NavigationItem(
|
||||
label: 'Messages',
|
||||
icon: Icons.chat_outlined,
|
||||
selectedIcon: Icons.chat,
|
||||
page: AdminCommunicationPage(),
|
||||
pageType: _PageType.communication,
|
||||
),
|
||||
const _NavigationItem(
|
||||
label: 'Carte',
|
||||
icon: Icons.map_outlined,
|
||||
selectedIcon: Icons.map,
|
||||
page: AdminMapPage(),
|
||||
pageType: _PageType.map,
|
||||
),
|
||||
];
|
||||
|
||||
@@ -93,18 +92,42 @@ class _AdminDashboardPageState extends State<AdminDashboardPage>
|
||||
label: 'Amicale & membres',
|
||||
icon: Icons.business_outlined,
|
||||
selectedIcon: Icons.business,
|
||||
page: AdminEntitePage(),
|
||||
pageType: _PageType.amicale,
|
||||
requiredRole: 2,
|
||||
),
|
||||
const _NavigationItem(
|
||||
label: 'Opérations',
|
||||
icon: Icons.calendar_today_outlined,
|
||||
selectedIcon: Icons.calendar_today,
|
||||
page: Scaffold(body: Center(child: Text('Page Opérations'))),
|
||||
pageType: _PageType.operations,
|
||||
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
|
||||
List<NavigationDestination> _buildNavigationDestinations() {
|
||||
final destinations = <NavigationDestination>[];
|
||||
@@ -145,13 +168,15 @@ class _AdminDashboardPageState extends State<AdminDashboardPage>
|
||||
final currentUser = userRepository.getCurrentUser();
|
||||
|
||||
// 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
|
||||
if (currentUser?.role == 2) {
|
||||
for (final item in _adminNavigationItems) {
|
||||
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é');
|
||||
final currentUser = userRepository.getCurrentUser();
|
||||
if (currentUser == null) {
|
||||
debugPrint(
|
||||
'ERREUR: Aucun utilisateur connecté dans AdminDashboardPage');
|
||||
debugPrint('ERREUR: Aucun utilisateur connecté dans AdminDashboardPage');
|
||||
} else {
|
||||
debugPrint(
|
||||
'Utilisateur connecté: ${currentUser.username} (${currentUser.id})');
|
||||
debugPrint('Utilisateur connecté: ${currentUser.username} (${currentUser.id})');
|
||||
}
|
||||
userRepository.addListener(_handleUserRepositoryChanges);
|
||||
|
||||
@@ -276,7 +299,7 @@ class _AdminDashboardPageState extends State<AdminDashboardPage>
|
||||
),
|
||||
child: CustomPaint(
|
||||
painter: DotsPainter(),
|
||||
child: Container(width: double.infinity, height: double.infinity),
|
||||
child: const SizedBox(width: double.infinity, height: double.infinity),
|
||||
),
|
||||
),
|
||||
// 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
|
||||
class _NavigationItem {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final IconData selectedIcon;
|
||||
final Widget page;
|
||||
final _PageType pageType;
|
||||
final int? requiredRole; // null si accessible à tous les rôles
|
||||
|
||||
const _NavigationItem({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.selectedIcon,
|
||||
required this.page,
|
||||
required this.pageType,
|
||||
this.requiredRole,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -78,9 +78,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
// Fallback sur la version du AppInfoService si elle existe
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_appVersion = AppInfoService.fullVersion
|
||||
.split(' ')
|
||||
.last; // Extraire juste le numéro
|
||||
_appVersion = AppInfoService.fullVersion.split(' ').last; // Extraire juste le numéro
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -103,8 +101,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
// Vérification du type de connexion
|
||||
if (widget.loginType == null) {
|
||||
// Si aucun type n'est spécifié, naviguer vers la splash page
|
||||
print(
|
||||
'LoginPage: Aucun type de connexion spécifié, navigation vers splash page');
|
||||
print('LoginPage: Aucun type de connexion spécifié, navigation vers splash page');
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
GoRouter.of(context).go('/');
|
||||
});
|
||||
@@ -157,13 +154,10 @@ class _LoginPageState extends State<LoginPage> {
|
||||
'''
|
||||
]);
|
||||
|
||||
if (result != null &&
|
||||
result is String &&
|
||||
result.toLowerCase() == 'user') {
|
||||
if (result != null && result is String && result.toLowerCase() == 'user') {
|
||||
setState(() {
|
||||
_loginType = 'user';
|
||||
print(
|
||||
'LoginPage: Type détecté depuis sessionStorage: $_loginType');
|
||||
print('LoginPage: Type détecté depuis sessionStorage: $_loginType');
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -217,7 +211,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
if (lastUser.role is String) {
|
||||
roleValue = int.tryParse(lastUser.role as String) ?? 0;
|
||||
} else {
|
||||
roleValue = lastUser.role as int;
|
||||
roleValue = lastUser.role;
|
||||
}
|
||||
|
||||
// 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)');
|
||||
} else if (_loginType == 'admin' && roleValue > 1) {
|
||||
roleMatches = true;
|
||||
debugPrint(
|
||||
'Rôle administrateur (${roleValue}) correspond au type de login (admin)');
|
||||
debugPrint('Rôle administrateur ($roleValue) correspond au type de login (admin)');
|
||||
}
|
||||
|
||||
// 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) {
|
||||
_usernameController.text = lastUser.email;
|
||||
_usernameFocusNode.unfocus();
|
||||
debugPrint(
|
||||
'Champ username pré-rempli avec email: ${lastUser.email}');
|
||||
debugPrint('Champ username pré-rempli avec email: ${lastUser.email}');
|
||||
}
|
||||
} else {
|
||||
debugPrint(
|
||||
'Le rôle (${roleValue}) ne correspond pas au type de login ($_loginType), champ username non pré-rempli');
|
||||
debugPrint('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(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: _loginType == 'user'
|
||||
? [Colors.white, Colors.green.shade300]
|
||||
: [Colors.white, Colors.red.shade300],
|
||||
colors: _loginType == 'user' ? [Colors.white, Colors.green.shade300] : [Colors.white, Colors.red.shade300],
|
||||
),
|
||||
),
|
||||
child: CustomPaint(
|
||||
painter: DotsPainter(),
|
||||
child: Container(width: double.infinity, height: double.infinity),
|
||||
child: const SizedBox(width: double.infinity, height: double.infinity),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
@@ -345,11 +334,8 @@ class _LoginPageState extends State<LoginPage> {
|
||||
constraints: const BoxConstraints(maxWidth: 500),
|
||||
child: Card(
|
||||
elevation: 8,
|
||||
shadowColor: _loginType == 'user'
|
||||
? Colors.green.withOpacity(0.5)
|
||||
: Colors.red.withOpacity(0.5),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16.0)),
|
||||
shadowColor: _loginType == 'user' ? Colors.green.withOpacity(0.5) : Colors.red.withOpacity(0.5),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
@@ -378,9 +364,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.error.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color:
|
||||
theme.colorScheme.error.withOpacity(0.3)),
|
||||
border: Border.all(color: theme.colorScheme.error.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -391,8 +375,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_locationErrorMessage ??
|
||||
'L\'accès à la localisation est nécessaire pour utiliser cette application.',
|
||||
_locationErrorMessage ?? 'L\'accès à la localisation est nécessaire pour utiliser cette application.',
|
||||
style: theme.textTheme.bodyLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -416,14 +399,10 @@ class _LoginPageState extends State<LoginPage> {
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInstructionStep(theme, 1,
|
||||
'Ouvrez les paramètres de votre appareil'),
|
||||
_buildInstructionStep(theme, 2,
|
||||
'Accédez aux paramètres de confidentialité ou de localisation'),
|
||||
_buildInstructionStep(theme, 3,
|
||||
'Recherchez GEOSECTOR dans la liste des applications'),
|
||||
_buildInstructionStep(theme, 4,
|
||||
'Activez l\'accès à la localisation pour cette application'),
|
||||
_buildInstructionStep(theme, 1, 'Ouvrez les paramètres de votre appareil'),
|
||||
_buildInstructionStep(theme, 2, 'Accédez aux paramètres de confidentialité ou de localisation'),
|
||||
_buildInstructionStep(theme, 3, 'Recherchez GEOSECTOR dans la liste des applications'),
|
||||
_buildInstructionStep(theme, 4, 'Activez l\'accès à la localisation pour cette application'),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Boutons d'action
|
||||
@@ -469,8 +448,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
}
|
||||
|
||||
/// Construit une étape d'instruction pour activer la localisation
|
||||
Widget _buildInstructionStep(
|
||||
ThemeData theme, int stepNumber, String instruction) {
|
||||
Widget _buildInstructionStep(ThemeData theme, int stepNumber, String instruction) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Row(
|
||||
@@ -530,14 +508,12 @@ class _LoginPageState extends State<LoginPage> {
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: _loginType == 'user'
|
||||
? [Colors.white, Colors.green.shade300]
|
||||
: [Colors.white, Colors.red.shade300],
|
||||
colors: _loginType == 'user' ? [Colors.white, Colors.green.shade300] : [Colors.white, Colors.red.shade300],
|
||||
),
|
||||
),
|
||||
child: CustomPaint(
|
||||
painter: DotsPainter(),
|
||||
child: Container(width: double.infinity, height: double.infinity),
|
||||
child: const SizedBox(width: double.infinity, height: double.infinity),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
@@ -548,11 +524,8 @@ class _LoginPageState extends State<LoginPage> {
|
||||
constraints: const BoxConstraints(maxWidth: 500),
|
||||
child: Card(
|
||||
elevation: 8,
|
||||
shadowColor: _loginType == 'user'
|
||||
? Colors.green.withOpacity(0.5)
|
||||
: Colors.red.withOpacity(0.5),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16.0)),
|
||||
shadowColor: _loginType == 'user' ? Colors.green.withOpacity(0.5) : Colors.red.withOpacity(0.5),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
@@ -566,39 +539,26 @@ class _LoginPageState extends State<LoginPage> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
_loginType == 'user'
|
||||
? 'Connexion Utilisateur'
|
||||
: 'Connexion Administrateur',
|
||||
_loginType == 'user' ? 'Connexion Utilisateur' : 'Connexion Administrateur',
|
||||
style: theme.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _loginType == 'user'
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
color: _loginType == 'user' ? Colors.green : Colors.red,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
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
|
||||
if (kDebugMode)
|
||||
Text(
|
||||
'Type de connexion: $_loginType',
|
||||
style:
|
||||
TextStyle(fontSize: 10, color: Colors.grey),
|
||||
style: const TextStyle(fontSize: 10, color: Colors.grey),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Bienvenue sur GEOSECTOR',
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: theme.colorScheme.onBackground
|
||||
.withOpacity(0.7),
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -616,23 +576,17 @@ class _LoginPageState extends State<LoginPage> {
|
||||
color: theme.colorScheme.error.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color:
|
||||
theme.colorScheme.error.withOpacity(0.3),
|
||||
color: theme.colorScheme.error.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(Icons.signal_wifi_off,
|
||||
color: theme.colorScheme.error, size: 32),
|
||||
Icon(Icons.signal_wifi_off, color: theme.colorScheme.error, size: 32),
|
||||
const SizedBox(height: 8),
|
||||
Text('Connexion Internet requise',
|
||||
style: theme.textTheme.titleMedium
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.error)),
|
||||
style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold, color: theme.colorScheme.error)),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Veuillez vous connecter à Internet (WiFi ou données mobiles) pour pouvoir vous connecter.'),
|
||||
const Text('Veuillez vous connecter à Internet (WiFi ou données mobiles) pour pouvoir vous connecter.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -669,9 +623,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
obscureText: _obscurePassword,
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscurePassword
|
||||
? Icons.visibility_outlined
|
||||
: Icons.visibility_off_outlined,
|
||||
_obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
@@ -686,21 +638,17 @@ class _LoginPageState extends State<LoginPage> {
|
||||
return null;
|
||||
},
|
||||
onFieldSubmitted: (_) async {
|
||||
if (!userRepository.isLoading &&
|
||||
_formKey.currentState!.validate()) {
|
||||
if (!userRepository.isLoading && _formKey.currentState!.validate()) {
|
||||
// Vérifier que le type de connexion est spécifié
|
||||
if (_loginType.isEmpty) {
|
||||
print(
|
||||
'Login: Type non spécifié, redirection vers la page de démarrage');
|
||||
print('Login: Type non spécifié, redirection vers la page de démarrage');
|
||||
context.go('/');
|
||||
return;
|
||||
}
|
||||
|
||||
print(
|
||||
'Login: Tentative avec type: $_loginType');
|
||||
print('Login: Tentative avec type: $_loginType');
|
||||
|
||||
final success =
|
||||
await userRepository.login(
|
||||
final success = await userRepository.login(
|
||||
_usernameController.text.trim(),
|
||||
_passwordController.text,
|
||||
type: _loginType,
|
||||
@@ -708,16 +656,12 @@ class _LoginPageState extends State<LoginPage> {
|
||||
|
||||
if (success && mounted) {
|
||||
// Récupérer directement le rôle de l'utilisateur
|
||||
final user =
|
||||
userRepository.getCurrentUser();
|
||||
final user = userRepository.getCurrentUser();
|
||||
if (user == null) {
|
||||
debugPrint(
|
||||
'ERREUR: Utilisateur non trouvé après connexion réussie');
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
debugPrint('ERREUR: Utilisateur non trouvé après connexion réussie');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Erreur de connexion. Veuillez réessayer.'),
|
||||
content: Text('Erreur de connexion. Veuillez réessayer.'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
@@ -727,32 +671,25 @@ class _LoginPageState extends State<LoginPage> {
|
||||
// Convertir le rôle en int si nécessaire
|
||||
int roleValue;
|
||||
if (user.role is String) {
|
||||
roleValue = int.tryParse(
|
||||
user.role as String) ??
|
||||
1;
|
||||
roleValue = int.tryParse(user.role as String) ?? 1;
|
||||
} else {
|
||||
roleValue = user.role as int;
|
||||
roleValue = user.role;
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'Role de l\'utilisateur: $roleValue');
|
||||
debugPrint('Role de l\'utilisateur: $roleValue');
|
||||
|
||||
// Redirection simple basée sur le rôle
|
||||
if (roleValue > 1) {
|
||||
debugPrint(
|
||||
'Redirection vers /admin (rôle > 1)');
|
||||
debugPrint('Redirection vers /admin (rôle > 1)');
|
||||
context.go('/admin');
|
||||
} else {
|
||||
debugPrint(
|
||||
'Redirection vers /user (rôle = 1)');
|
||||
debugPrint('Redirection vers /user (rôle = 1)');
|
||||
context.go('/user');
|
||||
}
|
||||
} else if (mounted) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Échec de la connexion. Vérifiez vos identifiants.'),
|
||||
content: Text('Échec de la connexion. Vérifiez vos identifiants.'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
@@ -781,23 +718,19 @@ class _LoginPageState extends State<LoginPage> {
|
||||
|
||||
// Bouton de connexion
|
||||
CustomButton(
|
||||
onPressed: (userRepository.isLoading ||
|
||||
!_isConnected)
|
||||
onPressed: (userRepository.isLoading || !_isConnected)
|
||||
? null
|
||||
: () async {
|
||||
if (_formKey.currentState!
|
||||
.validate()) {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
// Vérifier à nouveau les permissions de géolocalisation avant de se connecter (sauf en version web)
|
||||
if (!kIsWeb) {
|
||||
await _checkLocationPermission();
|
||||
|
||||
// Si l'utilisateur n'a toujours pas accordé les permissions, ne pas continuer
|
||||
if (!_hasLocationPermission) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'L\'accès à la localisation est nécessaire pour utiliser cette application.'),
|
||||
content: Text('L\'accès à la localisation est nécessaire pour utiliser cette application.'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
@@ -806,36 +739,23 @@ class _LoginPageState extends State<LoginPage> {
|
||||
}
|
||||
|
||||
// Vérifier la connexion Internet
|
||||
await connectivityService
|
||||
.checkConnectivity();
|
||||
await connectivityService.checkConnectivity();
|
||||
|
||||
if (!connectivityService
|
||||
.isConnected) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
if (!connectivityService.isConnected) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text(
|
||||
'Aucune connexion Internet. La connexion n\'est pas possible hors ligne.'),
|
||||
backgroundColor:
|
||||
theme.colorScheme.error,
|
||||
duration: const Duration(
|
||||
seconds: 3),
|
||||
content: const Text('Aucune connexion Internet. La connexion n\'est pas possible hors ligne.'),
|
||||
backgroundColor: theme.colorScheme.error,
|
||||
duration: const Duration(seconds: 3),
|
||||
action: SnackBarAction(
|
||||
label: 'Réessayer',
|
||||
onPressed: () async {
|
||||
await connectivityService
|
||||
.checkConnectivity();
|
||||
if (connectivityService
|
||||
.isConnected &&
|
||||
mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context)
|
||||
.showSnackBar(
|
||||
await connectivityService.checkConnectivity();
|
||||
if (connectivityService.isConnected && mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Connexion Internet ${connectivityService.connectionType} détectée.'),
|
||||
backgroundColor:
|
||||
Colors.green,
|
||||
content: Text('Connexion Internet ${connectivityService.connectionType} détectée.'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -848,18 +768,15 @@ class _LoginPageState extends State<LoginPage> {
|
||||
|
||||
// Vérifier que le type de connexion est spécifié
|
||||
if (_loginType.isEmpty) {
|
||||
print(
|
||||
'Login: Type non spécifié, redirection vers la page de démarrage');
|
||||
print('Login: Type non spécifié, redirection vers la page de démarrage');
|
||||
context.go('/');
|
||||
return;
|
||||
}
|
||||
|
||||
print(
|
||||
'Login: Tentative avec type: $_loginType');
|
||||
print('Login: Tentative avec type: $_loginType');
|
||||
|
||||
// Utiliser directement userRepository avec l'overlay de chargement
|
||||
final success = await userRepository
|
||||
.loginWithUI(
|
||||
final success = await userRepository.loginWithUI(
|
||||
context,
|
||||
_usernameController.text.trim(),
|
||||
_passwordController.text,
|
||||
@@ -867,20 +784,15 @@ class _LoginPageState extends State<LoginPage> {
|
||||
);
|
||||
|
||||
if (success && mounted) {
|
||||
debugPrint(
|
||||
'Connexion réussie, tentative de redirection...');
|
||||
debugPrint('Connexion réussie, tentative de redirection...');
|
||||
|
||||
// Récupérer directement le rôle de l'utilisateur
|
||||
final user = userRepository
|
||||
.getCurrentUser();
|
||||
final user = userRepository.getCurrentUser();
|
||||
if (user == null) {
|
||||
debugPrint(
|
||||
'ERREUR: Utilisateur non trouvé après connexion réussie');
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
debugPrint('ERREUR: Utilisateur non trouvé après connexion réussie');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Erreur de connexion. Veuillez réessayer.'),
|
||||
content: Text('Erreur de connexion. Veuillez réessayer.'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
@@ -890,41 +802,32 @@ class _LoginPageState extends State<LoginPage> {
|
||||
// Convertir le rôle en int si nécessaire
|
||||
int roleValue;
|
||||
if (user.role is String) {
|
||||
roleValue = int.tryParse(
|
||||
user.role as String) ??
|
||||
1;
|
||||
roleValue = int.tryParse(user.role as String) ?? 1;
|
||||
} else {
|
||||
roleValue = user.role as int;
|
||||
roleValue = user.role;
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'Role de l\'utilisateur: $roleValue');
|
||||
debugPrint('Role de l\'utilisateur: $roleValue');
|
||||
|
||||
// Redirection simple basée sur le rôle
|
||||
if (roleValue > 1) {
|
||||
debugPrint(
|
||||
'Redirection vers /admin (rôle > 1)');
|
||||
debugPrint('Redirection vers /admin (rôle > 1)');
|
||||
context.go('/admin');
|
||||
} else {
|
||||
debugPrint(
|
||||
'Redirection vers /user (rôle = 1)');
|
||||
debugPrint('Redirection vers /user (rôle = 1)');
|
||||
context.go('/user');
|
||||
}
|
||||
} else if (mounted) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Échec de la connexion. Vérifiez vos identifiants.'),
|
||||
content: Text('Échec de la connexion. Vérifiez vos identifiants.'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
text: _isConnected
|
||||
? 'Se connecter'
|
||||
: 'Connexion Internet requise',
|
||||
text: _isConnected ? 'Se connecter' : 'Connexion Internet requise',
|
||||
isLoading: userRepository.isLoading,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
@@ -1047,8 +950,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer votre email';
|
||||
}
|
||||
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
|
||||
.hasMatch(value)) {
|
||||
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
|
||||
return 'Veuillez entrer un email valide';
|
||||
}
|
||||
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
|
||||
if (response.statusCode == 404) {
|
||||
// Essayer avec une URL alternative
|
||||
final alternativeUrl =
|
||||
'$baseUrl/api/index.php/lostpassword';
|
||||
print(
|
||||
'Tentative avec URL alternative: $alternativeUrl');
|
||||
final alternativeUrl = '$baseUrl/api/index.php/lostpassword';
|
||||
print('Tentative avec URL alternative: $alternativeUrl');
|
||||
|
||||
final alternativeResponse = await http.post(
|
||||
Uri.parse(alternativeUrl),
|
||||
@@ -1118,10 +1018,8 @@ class _LoginPageState extends State<LoginPage> {
|
||||
}),
|
||||
);
|
||||
|
||||
print(
|
||||
'Réponse alternative reçue: ${alternativeResponse.statusCode}');
|
||||
print(
|
||||
'Corps de la réponse alternative: ${alternativeResponse.body}');
|
||||
print('Réponse alternative reçue: ${alternativeResponse.statusCode}');
|
||||
print('Corps de la réponse alternative: ${alternativeResponse.body}');
|
||||
|
||||
// Si la réponse alternative est un succès, utiliser cette réponse
|
||||
if (alternativeResponse.statusCode == 200) {
|
||||
@@ -1129,14 +1027,12 @@ class _LoginPageState extends State<LoginPage> {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print(
|
||||
'Erreur lors de l\'envoi de la requête: $e');
|
||||
print('Erreur lors de l\'envoi de la requête: $e');
|
||||
throw Exception('Erreur de connexion: $e');
|
||||
}
|
||||
|
||||
// Traiter la réponse
|
||||
if (response != null &&
|
||||
response.statusCode == 200) {
|
||||
if (response.statusCode == 200) {
|
||||
// Modifier le contenu de la boîte de dialogue pour afficher le message de succès
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
@@ -1148,7 +1044,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
// 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()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
@@ -1180,16 +1076,13 @@ class _LoginPageState extends State<LoginPage> {
|
||||
|
||||
// Afficher un message d'erreur
|
||||
final responseData = json.decode(response.body);
|
||||
throw Exception(responseData['message'] ??
|
||||
'Erreur lors de la récupération du mot de passe');
|
||||
throw Exception(responseData['message'] ?? 'Erreur lors de la récupération du mot de passe');
|
||||
}
|
||||
} catch (e) {
|
||||
// Afficher un message d'erreur
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(e
|
||||
.toString()
|
||||
.contains('Exception:')
|
||||
content: Text(e.toString().contains('Exception:')
|
||||
? e.toString().split('Exception: ')[1]
|
||||
: 'Erreur lors de la récupération du mot de passe'),
|
||||
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(
|
||||
backgroundColor: Colors.blue,
|
||||
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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -73,10 +73,8 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
final String _hiddenToken = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
|
||||
// Valeurs pour le captcha simple
|
||||
final int _captchaNum1 =
|
||||
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 _captchaNum1 = 2 + (DateTime.now().second % 5); // Nombre entre 2 et 6
|
||||
final int _captchaNum2 = 3 + (DateTime.now().minute % 4); // Nombre entre 3 et 6
|
||||
|
||||
// État de la connexion Internet et de la plateforme
|
||||
bool _isConnected = false;
|
||||
@@ -102,9 +100,7 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
// Fallback sur la version du AppInfoService si elle existe
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_appVersion = AppInfoService.fullVersion
|
||||
.split(' ')
|
||||
.last; // Extraire juste le numéro
|
||||
_appVersion = AppInfoService.fullVersion.split(' ').last; // Extraire juste le numéro
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -168,8 +164,7 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
|
||||
try {
|
||||
// Utiliser l'API interne de geosector pour récupérer les villes par code postal
|
||||
final baseUrl = Uri
|
||||
.base.origin; // Récupère l'URL de base (ex: https://app.geosector.fr)
|
||||
final baseUrl = Uri.base.origin; // Récupère l'URL de base (ex: https://app.geosector.fr)
|
||||
final apiUrl = '$baseUrl/api/villes?code_postal=$postalCode';
|
||||
|
||||
final response = await http.get(
|
||||
@@ -251,7 +246,7 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
),
|
||||
child: CustomPaint(
|
||||
painter: DotsPainter(),
|
||||
child: Container(width: double.infinity, height: double.infinity),
|
||||
child: const SizedBox(width: double.infinity, height: double.infinity),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
@@ -282,8 +277,7 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
Text(
|
||||
'Enregistrez votre amicale sur GeoSector',
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color:
|
||||
theme.colorScheme.onBackground.withOpacity(0.7),
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -295,8 +289,7 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
if (mounted && _isConnected != isConnected) {
|
||||
setState(() {
|
||||
_isConnected = isConnected;
|
||||
_connectionType =
|
||||
connectivityService.connectionType;
|
||||
_connectionType = connectivityService.connectionType;
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -343,8 +336,7 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
if (_isConnected && mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Connexion Internet $_connectionType détectée.'),
|
||||
content: Text('Connexion Internet $_connectionType détectée.'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
@@ -396,8 +388,7 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer votre email';
|
||||
}
|
||||
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
|
||||
.hasMatch(value)) {
|
||||
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
|
||||
return 'Veuillez entrer un email valide';
|
||||
}
|
||||
return null;
|
||||
@@ -424,8 +415,7 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
CustomTextField(
|
||||
controller: _postalCodeController,
|
||||
label: 'Code postal de l\'amicale',
|
||||
hintText:
|
||||
'Entrez le code postal de votre amicale',
|
||||
hintText: 'Entrez le code postal de votre amicale',
|
||||
prefixIcon: Icons.location_on_outlined,
|
||||
keyboardType: TextInputType.number,
|
||||
isRequired: true,
|
||||
@@ -453,13 +443,12 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
children: [
|
||||
Text(
|
||||
'Commune de l\'amicale',
|
||||
style:
|
||||
theme.textTheme.titleSmall?.copyWith(
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: theme.colorScheme.onBackground,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
const Text(
|
||||
' •',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
@@ -484,8 +473,7 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
),
|
||||
child: _isLoadingCities
|
||||
? const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 16),
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
@@ -497,20 +485,16 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
Icons.location_city_outlined,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
hintText: _postalCodeController
|
||||
.text.length <
|
||||
3
|
||||
hintText: _postalCodeController.text.length < 3
|
||||
? 'Entrez d\'abord au moins 3 chiffres du code postal'
|
||||
: _cities.isEmpty
|
||||
? 'Aucune commune trouvée pour ce code postal'
|
||||
: 'Sélectionnez une commune',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 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
|
||||
if (newValue != null) {
|
||||
// Désactiver temporairement le listener pour éviter une boucle infinie
|
||||
_postalCodeController
|
||||
.removeListener(
|
||||
_onPostalCodeChanged);
|
||||
_postalCodeController.removeListener(_onPostalCodeChanged);
|
||||
|
||||
// Mettre à jour le code postal
|
||||
_postalCodeController.text =
|
||||
newValue.postalCode;
|
||||
_postalCodeController.text = newValue.postalCode;
|
||||
|
||||
// Réactiver le listener
|
||||
_postalCodeController
|
||||
.addListener(
|
||||
_onPostalCodeChanged);
|
||||
_postalCodeController.addListener(_onPostalCodeChanged);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -574,8 +553,7 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
const SizedBox(height: 8),
|
||||
CustomTextField(
|
||||
controller: _captchaController,
|
||||
label:
|
||||
'Combien font $_captchaNum1 + $_captchaNum2 ?',
|
||||
label: 'Combien font $_captchaNum1 + $_captchaNum2 ?',
|
||||
hintText: 'Entrez le résultat',
|
||||
prefixIcon: Icons.security,
|
||||
keyboardType: TextInputType.number,
|
||||
@@ -612,43 +590,30 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
|
||||
// Bouton d'inscription
|
||||
CustomButton(
|
||||
onPressed: (_isLoading ||
|
||||
(_isMobile && !_isConnected))
|
||||
onPressed: (_isLoading || (_isMobile && !_isConnected))
|
||||
? null
|
||||
: () async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
// Vérifier la connexion Internet avant de soumettre
|
||||
// Utiliser l'instance globale de connectivityService définie dans app.dart
|
||||
await connectivityService
|
||||
.checkConnectivity();
|
||||
await connectivityService.checkConnectivity();
|
||||
|
||||
if (!connectivityService.isConnected) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text(
|
||||
'Aucune connexion Internet. L\'inscription nécessite une connexion active.'),
|
||||
backgroundColor:
|
||||
theme.colorScheme.error,
|
||||
duration:
|
||||
const Duration(seconds: 3),
|
||||
content: const Text('Aucune connexion Internet. L\'inscription nécessite une connexion active.'),
|
||||
backgroundColor: theme.colorScheme.error,
|
||||
duration: const Duration(seconds: 3),
|
||||
action: SnackBarAction(
|
||||
label: 'Réessayer',
|
||||
onPressed: () async {
|
||||
await connectivityService
|
||||
.checkConnectivity();
|
||||
if (connectivityService
|
||||
.isConnected &&
|
||||
mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context)
|
||||
.showSnackBar(
|
||||
await connectivityService.checkConnectivity();
|
||||
if (connectivityService.isConnected && mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Connexion Internet ${connectivityService.connectionType} détectée.'),
|
||||
backgroundColor:
|
||||
Colors.green,
|
||||
content: Text('Connexion Internet ${connectivityService.connectionType} détectée.'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -660,15 +625,11 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
return;
|
||||
}
|
||||
// Vérifier que le captcha est correct
|
||||
final int? captchaAnswer = int.tryParse(
|
||||
_captchaController.text);
|
||||
if (captchaAnswer !=
|
||||
_captchaNum1 + _captchaNum2) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
final int? captchaAnswer = int.tryParse(_captchaController.text);
|
||||
if (captchaAnswer != _captchaNum1 + _captchaNum2) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'La vérification de sécurité a échoué. Veuillez réessayer.'),
|
||||
content: Text('La vérification de sécurité a échoué. Veuillez réessayer.'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
@@ -679,16 +640,11 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
final Map<String, dynamic> formData = {
|
||||
'email': _emailController.text.trim(),
|
||||
'name': _nameController.text.trim(),
|
||||
'amicale_name': _amicaleNameController
|
||||
.text
|
||||
.trim(),
|
||||
'postal_code':
|
||||
_postalCodeController.text,
|
||||
'city_name':
|
||||
_selectedCity?.name ?? '',
|
||||
'amicale_name': _amicaleNameController.text.trim(),
|
||||
'postal_code': _postalCodeController.text,
|
||||
'city_name': _selectedCity?.name ?? '',
|
||||
'captcha_answer': captchaAnswer,
|
||||
'captcha_expected':
|
||||
_captchaNum1 + _captchaNum2,
|
||||
'captcha_expected': _captchaNum1 + _captchaNum2,
|
||||
'token': _hiddenToken,
|
||||
};
|
||||
|
||||
@@ -700,14 +656,12 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
try {
|
||||
// Envoyer les données à l'API
|
||||
final baseUrl = Uri.base.origin;
|
||||
final apiUrl =
|
||||
'$baseUrl/api/register';
|
||||
final apiUrl = '$baseUrl/api/register';
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse(apiUrl),
|
||||
headers: {
|
||||
'Content-Type':
|
||||
'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: json.encode(formData),
|
||||
);
|
||||
@@ -718,34 +672,23 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
});
|
||||
|
||||
// Traiter la réponse
|
||||
if (response.statusCode == 200 ||
|
||||
response.statusCode == 201) {
|
||||
final responseData =
|
||||
json.decode(response.body);
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
final responseData = json.decode(response.body);
|
||||
|
||||
// Vérifier si la réponse indique un succès
|
||||
final bool isSuccess =
|
||||
responseData['success'] ==
|
||||
true ||
|
||||
responseData['status'] ==
|
||||
'success';
|
||||
final bool isSuccess = responseData['success'] == true || responseData['status'] == 'success';
|
||||
|
||||
// Récupérer le message de la réponse
|
||||
final String message = responseData[
|
||||
'message'] ??
|
||||
(isSuccess
|
||||
? 'Inscription réussie !'
|
||||
: 'Échec de l\'inscription. Veuillez réessayer.');
|
||||
final String message = responseData['message'] ??
|
||||
(isSuccess ? 'Inscription réussie !' : 'Échec de l\'inscription. Veuillez réessayer.');
|
||||
|
||||
if (isSuccess) {
|
||||
if (mounted) {
|
||||
// Afficher une boîte de dialogue de succès
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible:
|
||||
false, // L'utilisateur doit cliquer sur OK
|
||||
builder:
|
||||
(BuildContext context) {
|
||||
barrierDismissible: false, // L'utilisateur doit cliquer sur OK
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Row(
|
||||
children: [
|
||||
@@ -754,84 +697,50 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
color: Colors.green,
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Text(
|
||||
'Inscription réussie'),
|
||||
Text('Inscription réussie'),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize:
|
||||
MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment
|
||||
.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Votre demande d\'inscription a été enregistrée avec succès.',
|
||||
style: theme
|
||||
.textTheme
|
||||
.bodyLarge,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Vous allez recevoir un email contenant :',
|
||||
style: theme
|
||||
.textTheme
|
||||
.bodyMedium,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment
|
||||
.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons
|
||||
.arrow_right,
|
||||
size: 20,
|
||||
color: theme
|
||||
.colorScheme
|
||||
.primary),
|
||||
const SizedBox(
|
||||
width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Votre identifiant de connexion'),
|
||||
Icon(Icons.arrow_right, size: 20, color: theme.colorScheme.primary),
|
||||
const SizedBox(width: 4),
|
||||
const Expanded(
|
||||
child: Text('Votre identifiant de connexion'),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment
|
||||
.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons
|
||||
.arrow_right,
|
||||
size: 20,
|
||||
color: theme
|
||||
.colorScheme
|
||||
.primary),
|
||||
SizedBox(
|
||||
width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Un lien pour définir votre mot de passe'),
|
||||
Icon(Icons.arrow_right, size: 20, color: theme.colorScheme.primary),
|
||||
const SizedBox(width: 4),
|
||||
const Expanded(
|
||||
child: Text('Un lien pour définir votre mot de passe'),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Vérifiez votre boîte de réception et vos spams.',
|
||||
style: TextStyle(
|
||||
fontStyle:
|
||||
FontStyle
|
||||
.italic,
|
||||
color: theme
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(
|
||||
0.7),
|
||||
fontStyle: FontStyle.italic,
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -839,25 +748,15 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(
|
||||
context)
|
||||
.pop();
|
||||
Navigator.of(context).pop();
|
||||
// Rediriger vers la page de connexion
|
||||
context
|
||||
.go('/login');
|
||||
context.go('/login');
|
||||
},
|
||||
child: Text('OK'),
|
||||
style: TextButton
|
||||
.styleFrom(
|
||||
foregroundColor:
|
||||
theme
|
||||
.colorScheme
|
||||
.primary,
|
||||
textStyle: TextStyle(
|
||||
fontWeight:
|
||||
FontWeight
|
||||
.bold),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: theme.colorScheme.primary,
|
||||
textStyle: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -870,21 +769,16 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
// Afficher un message d'erreur plus visible
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(BuildContext context) {
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text(
|
||||
'Erreur d\'inscription'),
|
||||
title: const Text('Erreur d\'inscription'),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(
|
||||
context)
|
||||
.pop();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child:
|
||||
const Text('OK'),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -892,8 +786,7 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
);
|
||||
|
||||
// Afficher également un SnackBar
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.red,
|
||||
@@ -904,11 +797,9 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
} else {
|
||||
// Gérer les erreurs HTTP
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Erreur ${response.statusCode}: ${response.reasonPhrase ?? "Échec de l\'inscription"}'),
|
||||
content: Text('Erreur ${response.statusCode}: ${response.reasonPhrase ?? "Échec de l'inscription"}'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
@@ -922,11 +813,9 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
|
||||
// Gérer les exceptions
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Erreur: ${e.toString()}'),
|
||||
content: Text('Erreur: ${e.toString()}'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
@@ -934,9 +823,7 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
}
|
||||
}
|
||||
},
|
||||
text: (_isMobile && !_isConnected)
|
||||
? 'Connexion Internet requise'
|
||||
: 'Enregistrer mon amicale',
|
||||
text: (_isMobile && !_isConnected) ? 'Connexion Internet requise' : 'Enregistrer mon amicale',
|
||||
isLoading: _isLoading,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
@@ -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/presentation/widgets/mapbox_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'custom_text_field.dart';
|
||||
|
||||
class AmicaleForm extends StatefulWidget {
|
||||
final AmicaleModel? amicale;
|
||||
final Function(AmicaleModel)? onSubmit;
|
||||
final bool readOnly;
|
||||
final UserRepository userRepository; // Nouveau paramètre
|
||||
final ApiService? apiService; // Nouveau paramètre optionnel
|
||||
|
||||
const AmicaleForm({
|
||||
Key? key,
|
||||
super.key,
|
||||
this.amicale,
|
||||
this.onSubmit,
|
||||
this.readOnly = false,
|
||||
}) : super(key: key);
|
||||
required this.userRepository, // Requis
|
||||
this.apiService, // Optionnel
|
||||
});
|
||||
|
||||
@override
|
||||
State<AmicaleForm> createState() => _AmicaleFormState();
|
||||
@@ -59,8 +62,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
|
||||
_nameController = TextEditingController(text: amicale?.name ?? '');
|
||||
_adresse1Controller = TextEditingController(text: amicale?.adresse1 ?? '');
|
||||
_adresse2Controller = TextEditingController(text: amicale?.adresse2 ?? '');
|
||||
_codePostalController =
|
||||
TextEditingController(text: amicale?.codePostal ?? '');
|
||||
_codePostalController = TextEditingController(text: amicale?.codePostal ?? '');
|
||||
_villeController = TextEditingController(text: amicale?.ville ?? '');
|
||||
_phoneController = TextEditingController(text: amicale?.phone ?? '');
|
||||
_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
|
||||
final userRepository =
|
||||
Provider.of<UserRepository>(context, listen: false);
|
||||
final userRole = userRepository.getUserRole();
|
||||
final userRole = widget.userRepository.getUserRole();
|
||||
if (userRole > 2) {
|
||||
data['gps_lat'] = amicale.gpsLat;
|
||||
data['gps_lng'] = amicale.gpsLng;
|
||||
@@ -136,56 +136,71 @@ class _AmicaleFormState extends State<AmicaleForm> {
|
||||
data['chk_active'] = amicale.chkActive;
|
||||
}
|
||||
|
||||
// Appeler l'API
|
||||
try {
|
||||
// Obtenir l'instance du service API
|
||||
final apiService = Provider.of<ApiService>(context, listen: false);
|
||||
// Fermer l'indicateur de chargement
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// Appeler la méthode post du service API
|
||||
await apiService.post('/entite/update', data: data);
|
||||
// Appeler l'API si le service est disponible
|
||||
if (widget.apiService != null) {
|
||||
try {
|
||||
await widget.apiService!.post('/entite/update', data: data);
|
||||
|
||||
// Fermer l'indicateur de chargement
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// Afficher un message de succès
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Amicale mise à jour avec succès'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
|
||||
// Appeler la fonction onSubmit si elle existe
|
||||
if (widget.onSubmit != null) {
|
||||
widget.onSubmit!(amicale);
|
||||
// Afficher un message de succès
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Amicale mise à jour avec succès'),
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fermer le formulaire
|
||||
Navigator.of(context).pop();
|
||||
} catch (error) {
|
||||
// Fermer l'indicateur de chargement
|
||||
Navigator.of(context).pop();
|
||||
// Appeler la fonction onSubmit si elle existe
|
||||
if (widget.onSubmit != null) {
|
||||
widget.onSubmit!(amicale);
|
||||
}
|
||||
|
||||
// Afficher un message d'erreur
|
||||
// Fermer le formulaire
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
} catch (e) {
|
||||
// Fermer l'indicateur de chargement si encore ouvert
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
// Afficher un message d'erreur
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content:
|
||||
Text('Erreur lors de la mise à jour de l\'amicale: $error'),
|
||||
content: Text('Erreur: ${e.toString()}'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// Fermer l'indicateur de chargement
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// Afficher un message d'erreur
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Erreur: ${e.toString()}'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,13 +210,13 @@ class _AmicaleFormState extends State<AmicaleForm> {
|
||||
if (_phoneController.text.isEmpty && _mobileController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content:
|
||||
Text('Veuillez renseigner au moins un numéro de téléphone'),
|
||||
content: Text('Veuillez renseigner au moins un numéro de téléphone'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final amicale = widget.amicale?.copyWith(
|
||||
name: _nameController.text,
|
||||
adresse1: _adresse1Controller.text,
|
||||
@@ -246,10 +261,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
|
||||
// Appeler l'API pour mettre à jour l'amicale
|
||||
_updateAmicale(amicale);
|
||||
|
||||
// Appeler la fonction onSubmit si elle existe (pour la compatibilité avec le code existant)
|
||||
if (widget.onSubmit != null) {
|
||||
widget.onSubmit!(amicale);
|
||||
}
|
||||
// Ne pas appeler widget.onSubmit ici car c'est fait dans _updateAmicale
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,8 +305,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
|
||||
// TODO: Implémenter la sélection d'image
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Fonctionnalité de modification du logo à venir'),
|
||||
content: Text('Fonctionnalité de modification du logo à venir'),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -447,7 +458,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
@@ -481,7 +492,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
|
||||
"Adresse",
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.onBackground,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -566,7 +577,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
|
||||
"Région",
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: theme.colorScheme.onBackground,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -580,7 +591,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
|
||||
"Contact",
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.onBackground,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -657,7 +668,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
|
||||
"Informations avancées",
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.onBackground,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -671,8 +682,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
|
||||
child: CustomTextField(
|
||||
controller: _gpsLatController,
|
||||
label: "GPS Latitude",
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(decimal: true),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
readOnly: restrictedFieldsReadOnly,
|
||||
),
|
||||
),
|
||||
@@ -682,8 +692,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
|
||||
child: CustomTextField(
|
||||
controller: _gpsLngController,
|
||||
label: "GPS Longitude",
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(decimal: true),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
readOnly: restrictedFieldsReadOnly,
|
||||
),
|
||||
),
|
||||
@@ -749,7 +758,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
|
||||
Text(
|
||||
"Accepte les règlements en CB",
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onBackground,
|
||||
color: theme.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
@@ -760,8 +769,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
|
||||
controller: _stripeIdController,
|
||||
label: "ID Stripe Paiements CB",
|
||||
readOnly: restrictedFieldsReadOnly,
|
||||
helperText:
|
||||
"Les règlements par CB sont taxés d'une commission de 1.4%",
|
||||
helperText: "Les règlements par CB sont taxés d'une commission de 1.4%",
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -774,7 +782,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
|
||||
"Options",
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.onBackground,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -849,8 +857,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF20335E),
|
||||
side: const BorderSide(color: Color(0xFF20335E)),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24, vertical: 16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
@@ -871,8 +878,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF20335E),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24, vertical: 16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
@@ -895,70 +901,73 @@ class _AmicaleFormState extends State<AmicaleForm> {
|
||||
|
||||
// Vérifier si les informations avancées doivent être affichées
|
||||
bool _shouldShowAdvancedInfo() {
|
||||
final userRepository = Provider.of<UserRepository>(context, listen: false);
|
||||
final userRole = userRepository.getUserRole();
|
||||
final userRole = widget.userRepository.getUserRole();
|
||||
final bool canEditRestrictedFields = userRole > 2;
|
||||
|
||||
return canEditRestrictedFields ||
|
||||
_gpsLatController.text.isNotEmpty ||
|
||||
_gpsLngController.text.isNotEmpty ||
|
||||
_stripeIdController.text.isNotEmpty;
|
||||
return canEditRestrictedFields || _gpsLatController.text.isNotEmpty || _gpsLngController.text.isNotEmpty || _stripeIdController.text.isNotEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final userRepository = Provider.of<UserRepository>(context, listen: false);
|
||||
final userRole = userRepository.getUserRole();
|
||||
final userRole = widget.userRepository.getUserRole();
|
||||
|
||||
// Déterminer si l'utilisateur peut modifier les champs restreints
|
||||
final bool canEditRestrictedFields = userRole > 2;
|
||||
|
||||
// Lecture seule pour les champs restreints si l'utilisateur n'a pas les droits
|
||||
final bool restrictedFieldsReadOnly =
|
||||
widget.readOnly || !canEditRestrictedFields;
|
||||
final bool restrictedFieldsReadOnly = widget.readOnly || !canEditRestrictedFields;
|
||||
|
||||
// Calculer la largeur maximale du formulaire pour les écrans larges
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final maxFormWidth = screenWidth > 800 ? 800.0 : screenWidth;
|
||||
|
||||
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: Container(
|
||||
width: maxFormWidth,
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
final formContent = Container(
|
||||
width: maxFormWidth,
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header avec logo et minimap
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// Header avec logo et minimap
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// Section Logo
|
||||
_buildLogoSection(),
|
||||
// Section MiniMap
|
||||
_buildMiniMap(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Formulaire principal
|
||||
_buildMainForm(theme, restrictedFieldsReadOnly),
|
||||
// Section Logo
|
||||
_buildLogoSection(),
|
||||
// Section MiniMap
|
||||
_buildMiniMap(),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Formulaire principal
|
||||
_buildMainForm(theme, restrictedFieldsReadOnly),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geosector_app/app.dart';
|
||||
import 'package:geosector_app/core/data/models/amicale_model.dart';
|
||||
|
||||
/// Widget pour afficher une ligne du tableau d'amicales
|
||||
/// Affiche les colonnes id, name, codePostal, libRegion et une colonne Actions
|
||||
/// La colonne Actions contient un bouton Delete pour les utilisateurs avec rôle > 2
|
||||
/// La ligne entière est cliquable pour afficher les détails de l'amicale
|
||||
/// Affiche les colonnes id, name, codePostal, libRegion et une colonne Actions (conditionnelle)
|
||||
/// La colonne Actions contient des boutons Edit et Delete selon les permissions
|
||||
/// Pour un admin d'amicale (rôle 2), seule la ligne est cliquable sans colonne Actions
|
||||
class AmicaleRowWidget extends StatelessWidget {
|
||||
final AmicaleModel amicale;
|
||||
final Function(AmicaleModel)? onTap;
|
||||
final Function(AmicaleModel)? onEdit;
|
||||
final Function(AmicaleModel)? onDelete;
|
||||
final bool isHeader;
|
||||
final bool isAlternate;
|
||||
final bool showActionsColumn;
|
||||
|
||||
const AmicaleRowWidget({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.amicale,
|
||||
this.onTap,
|
||||
this.onEdit,
|
||||
this.onDelete,
|
||||
this.isHeader = false,
|
||||
this.isAlternate = false,
|
||||
}) : super(key: key);
|
||||
this.showActionsColumn = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final userRole = userRepository.getUserRole();
|
||||
|
||||
// Définir les styles en fonction du type de ligne (en-tête ou données)
|
||||
final textStyle = isHeader
|
||||
@@ -36,11 +38,7 @@ class AmicaleRowWidget extends StatelessWidget {
|
||||
: theme.textTheme.bodyMedium;
|
||||
|
||||
// Couleur de fond en fonction du type de ligne
|
||||
final backgroundColor = isHeader
|
||||
? theme.colorScheme.primary.withOpacity(0.1)
|
||||
: (isAlternate
|
||||
? theme.colorScheme.surface
|
||||
: theme.colorScheme.background);
|
||||
final backgroundColor = isHeader ? theme.colorScheme.primary.withOpacity(0.1) : (isAlternate ? theme.colorScheme.surface : theme.colorScheme.surface);
|
||||
|
||||
return InkWell(
|
||||
onTap: isHeader || onTap == null ? null : () => onTap!(amicale),
|
||||
@@ -55,7 +53,7 @@ class AmicaleRowWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: Row(
|
||||
children: [
|
||||
// Colonne ID
|
||||
@@ -103,7 +101,7 @@ class AmicaleRowWidget extends StatelessWidget {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text(
|
||||
isHeader ? 'Ville' : (amicale.ville ?? ''),
|
||||
isHeader ? 'Ville' : amicale.ville,
|
||||
style: textStyle,
|
||||
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
|
||||
if (isHeader || (userRole > 2 && onDelete != null))
|
||||
// Colonne Actions (conditionnelle)
|
||||
if (showActionsColumn && (isHeader || onEdit != null || onDelete != null))
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
@@ -138,22 +136,40 @@ class AmicaleRowWidget extends StatelessWidget {
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
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
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.delete,
|
||||
color: theme.colorScheme.error,
|
||||
size: 20,
|
||||
if (onDelete != null)
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.delete,
|
||||
color: theme.colorScheme.error,
|
||||
size: 20,
|
||||
),
|
||||
tooltip: 'Supprimer',
|
||||
onPressed: () => onDelete!(amicale),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 36,
|
||||
minHeight: 36,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
tooltip: 'Supprimer',
|
||||
onPressed: () => onDelete!(amicale),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 36,
|
||||
minHeight: 36,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
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/repositories/region_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_form.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
/// 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
|
||||
class AmicaleTableWidget extends StatelessWidget {
|
||||
final List<AmicaleModel> amicales;
|
||||
final Function(AmicaleModel)? onEdit;
|
||||
final Function(AmicaleModel)? onDelete;
|
||||
final AmicaleRepository amicaleRepository;
|
||||
final UserRepository userRepository; // Nouveau paramètre
|
||||
final ApiService? apiService; // Nouveau paramètre optionnel
|
||||
final bool isLoading;
|
||||
final String? emptyMessage;
|
||||
final bool readOnly;
|
||||
final bool showActionsColumn;
|
||||
|
||||
const AmicaleTableWidget({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.amicales,
|
||||
required this.amicaleRepository,
|
||||
required this.userRepository, // Requis
|
||||
this.onEdit,
|
||||
this.onDelete,
|
||||
this.apiService, // Optionnel
|
||||
this.isLoading = false,
|
||||
this.emptyMessage,
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
@@ -51,7 +114,9 @@ class AmicaleTableWidget extends StatelessWidget {
|
||||
),
|
||||
isHeader: true,
|
||||
onTap: null,
|
||||
onEdit: null,
|
||||
onDelete: null,
|
||||
showActionsColumn: showActionsColumn,
|
||||
),
|
||||
|
||||
// Corps du tableau
|
||||
@@ -90,8 +155,7 @@ class AmicaleTableWidget extends StatelessWidget {
|
||||
child: Text(
|
||||
emptyMessage ?? 'Aucune amicale trouvée',
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -107,10 +171,18 @@ class AmicaleTableWidget extends StatelessWidget {
|
||||
final amicale = amicales[index];
|
||||
return AmicaleRowWidget(
|
||||
amicale: amicale,
|
||||
isAlternate: index % 2 == 1, // Alterner les couleurs
|
||||
onTap: (selectedAmicale) =>
|
||||
_showAmicaleDetails(context, selectedAmicale),
|
||||
isAlternate: index % 2 == 1,
|
||||
onTap: (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,
|
||||
showActionsColumn: showActionsColumn,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -118,63 +190,48 @@ class AmicaleTableWidget extends StatelessWidget {
|
||||
|
||||
// Afficher une modale avec le formulaire EntiteForm
|
||||
void _showAmicaleDetails(BuildContext context, AmicaleModel amicale) {
|
||||
// Utiliser l'instance globale de userRepository définie dans app.dart
|
||||
final userRepo = userRepository;
|
||||
// Créer une instance de RegionRepository
|
||||
final regionRepo = RegionRepository();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => MultiProvider(
|
||||
providers: [
|
||||
// Fournir les repositories nécessaires au formulaire
|
||||
Provider<UserRepository>.value(value: userRepo),
|
||||
Provider<RegionRepository>.value(value: regionRepo),
|
||||
],
|
||||
child: Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Container(
|
||||
width: MediaQuery.of(dialogContext).size.width * 0.6,
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Détails de l\'amicale',
|
||||
style: Theme.of(dialogContext)
|
||||
.textTheme
|
||||
.headlineSmall
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color:
|
||||
Theme.of(dialogContext).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Formulaire EntiteForm en mode lecture seule
|
||||
AmicaleForm(
|
||||
amicale: amicale,
|
||||
readOnly: readOnly,
|
||||
onSubmit: (updatedAmicale) {
|
||||
Navigator.of(dialogContext).pop();
|
||||
// Ici, vous pourriez ajouter une logique pour mettre à jour l'amicale
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
builder: (dialogContext) => Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Container(
|
||||
width: MediaQuery.of(dialogContext).size.width * 0.6,
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Détails de l\'amicale',
|
||||
style: Theme.of(dialogContext).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(dialogContext).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Formulaire AmicaleForm en mode lecture seule
|
||||
AmicaleForm(
|
||||
amicale: amicale,
|
||||
readOnly: true,
|
||||
userRepository: userRepository,
|
||||
apiService: apiService,
|
||||
onSubmit: (updatedAmicale) {
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -26,14 +26,14 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final VoidCallback? onLogoutPressed;
|
||||
|
||||
const DashboardAppBar({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.title,
|
||||
this.pageTitle,
|
||||
this.showNewPassageButton = true,
|
||||
this.onNewPassagePressed,
|
||||
this.isAdmin = false,
|
||||
this.onLogoutPressed,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
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
|
||||
actions.add(
|
||||
Text(
|
||||
AppInfoService.fullVersion,
|
||||
"v${AppInfoService.version}",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white70,
|
||||
@@ -93,11 +95,12 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
),
|
||||
);
|
||||
|
||||
actions.add(const SizedBox(width: 8));
|
||||
|
||||
actions.add(
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.add_location_alt, color: Colors.white),
|
||||
label: const Text('Nouveau passage',
|
||||
style: TextStyle(color: Colors.white)),
|
||||
label: const Text('Nouveau passage', style: TextStyle(color: Colors.white)),
|
||||
onPressed: onNewPassagePressed,
|
||||
style: TextButton.styleFrom(
|
||||
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"
|
||||
actions.add(
|
||||
IconButton(
|
||||
@@ -128,6 +133,8 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
),
|
||||
);
|
||||
|
||||
actions.add(const SizedBox(width: 8));
|
||||
|
||||
// Ajouter le bouton de déconnexion
|
||||
actions.add(
|
||||
IconButton(
|
||||
@@ -139,8 +146,7 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Déconnexion'),
|
||||
content:
|
||||
const Text('Voulez-vous vraiment vous déconnecter ?'),
|
||||
content: const Text('Voulez-vous vraiment vous déconnecter ?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
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
|
||||
if (success && context.mounted) {
|
||||
// Attendre un court instant pour que les changements d'état se propagent
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: 100));
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
// Navigation forcée vers la page d'accueil
|
||||
context.go('/');
|
||||
|
||||
@@ -3,110 +3,207 @@ import 'package:geosector_app/core/data/models/membre_model.dart';
|
||||
|
||||
class MembreRowWidget extends StatelessWidget {
|
||||
final MembreModel membre;
|
||||
final Function()? onEdit;
|
||||
final Function()? onDelete;
|
||||
final Function(MembreModel)? onEdit;
|
||||
final Function(MembreModel)? onDelete;
|
||||
final bool isAlternate;
|
||||
|
||||
const MembreRowWidget({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.membre,
|
||||
this.onEdit,
|
||||
this.onDelete,
|
||||
}) : super(key: key);
|
||||
this.isAlternate = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
// Couleur de fond alternée
|
||||
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(
|
||||
color: backgroundColor,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// ID
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
membre.id.toString(),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
|
||||
// Prénom (firstName)
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
membre.firstName,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
// Nom (name)
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
membre.name,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
// Email
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text(
|
||||
membre.email,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
// Rôle (fkRole)
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
_getRoleName(membre.fkRole),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
|
||||
// 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
|
||||
if (onEdit != null || onDelete != null)
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
// Bouton Edit
|
||||
if (onEdit != null)
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.edit,
|
||||
size: 20,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
onPressed: () => onEdit!(membre),
|
||||
tooltip: 'Modifier',
|
||||
constraints: const BoxConstraints(
|
||||
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
|
||||
if (onDelete != null)
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.delete,
|
||||
size: 20,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
onPressed: () => onDelete!(membre),
|
||||
tooltip: 'Supprimer',
|
||||
constraints: const BoxConstraints(
|
||||
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: [
|
||||
// ID
|
||||
Expanded(
|
||||
flex: 1,
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Text(
|
||||
membre.id.toString(),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
'$label:',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
|
||||
// Prénom (firstName)
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
membre.firstName,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
// Nom (name)
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
membre.name,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
// Secteur (sectName)
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
membre.sectName ?? '',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
// Rôle (fkRole)
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
_getRoleName(membre.fkRole),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
|
||||
// Actions
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
// Bouton Edit
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit, size: 20),
|
||||
color: theme.colorScheme.primary,
|
||||
onPressed: onEdit,
|
||||
tooltip: 'Modifier',
|
||||
constraints: const BoxConstraints(),
|
||||
padding: const EdgeInsets.all(8),
|
||||
),
|
||||
|
||||
// Bouton Delete
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, size: 20),
|
||||
color: theme.colorScheme.error,
|
||||
onPressed: onDelete,
|
||||
tooltip: 'Supprimer',
|
||||
constraints: const BoxConstraints(),
|
||||
padding: const EdgeInsets.all(8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Text(value),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
import 'package:flutter/material.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';
|
||||
|
||||
class MembreTableWidget extends StatelessWidget {
|
||||
final List<MembreModel> membres;
|
||||
final Function(MembreModel)? onEdit;
|
||||
final Function(MembreModel)? onDelete;
|
||||
final MembreRepository membreRepository;
|
||||
final bool showHeader;
|
||||
final double? height;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final bool isLoading;
|
||||
final String? emptyMessage;
|
||||
|
||||
const MembreTableWidget({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.membres,
|
||||
required this.membreRepository,
|
||||
this.onEdit,
|
||||
this.onDelete,
|
||||
this.showHeader = true,
|
||||
this.height,
|
||||
this.padding,
|
||||
}) : super(key: key);
|
||||
this.isLoading = false,
|
||||
this.emptyMessage,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -44,8 +51,7 @@ class MembreTableWidget extends StatelessWidget {
|
||||
// En-tête du tableau
|
||||
if (showHeader)
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(bottom: 16.0, left: 16.0, right: 16.0),
|
||||
padding: const EdgeInsets.only(bottom: 16.0, left: 16.0, right: 16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
// ID
|
||||
@@ -55,6 +61,7 @@ class MembreTableWidget extends StatelessWidget {
|
||||
'ID',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -66,6 +73,7 @@ class MembreTableWidget extends StatelessWidget {
|
||||
'Prénom',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -77,17 +85,19 @@ class MembreTableWidget extends StatelessWidget {
|
||||
'Nom',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Secteur (sectName)
|
||||
// Email
|
||||
Expanded(
|
||||
flex: 2,
|
||||
flex: 3,
|
||||
child: Text(
|
||||
'Secteur',
|
||||
'Email',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -99,51 +109,83 @@ class MembreTableWidget extends StatelessWidget {
|
||||
'Rôle',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Actions
|
||||
// Statut
|
||||
Expanded(
|
||||
flex: 2,
|
||||
flex: 1,
|
||||
child: Text(
|
||||
'Actions',
|
||||
'Statut',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
|
||||
// Actions (si onEdit ou onDelete sont fournis)
|
||||
if (onEdit != null || onDelete != null)
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
'Actions',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Liste des membres
|
||||
// Corps du tableau
|
||||
Expanded(
|
||||
child: membres.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
'Aucun membre disponible',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
itemCount: membres.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(height: 8.0),
|
||||
itemBuilder: (context, index) {
|
||||
final membre = membres[index];
|
||||
return MembreRowWidget(
|
||||
membre: membre,
|
||||
onEdit: onEdit != null ? () => onEdit!(membre) : null,
|
||||
onDelete:
|
||||
onDelete != null ? () => onDelete!(membre) : null,
|
||||
);
|
||||
},
|
||||
),
|
||||
child: _buildTableContent(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: geosector_app
|
||||
description: 'GEOSECTOR - Gestion de distribution des calendriers par secteurs géographiques pour les amicales de pompiers'
|
||||
publish_to: 'none'
|
||||
version: 0.3.2
|
||||
version: 0.3.3
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
||||
@@ -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)
|
||||
3
web/.vscode/extensions.json
vendored
3
web/.vscode/extensions.json
vendored
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"recommendations": ["svelte.svelte-vscode"]
|
||||
}
|
||||
Reference in New Issue
Block a user