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

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

File diff suppressed because one or more lines are too long

View File

@@ -24,13 +24,14 @@ class MembreModelAdapter extends TypeAdapter<MembreModel> {
name: fields[8] as String,
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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -552,7 +552,6 @@ file:///Users/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/classes/b
file:///Users/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/classes/lonlat.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/classes/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

View File

@@ -552,7 +552,6 @@ file:///Users/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/classes/b
file:///Users/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/classes/lonlat.dart
file:///Users/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/classes/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

View File

@@ -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

View File

@@ -1,156 +0,0 @@
{
"window.zoomLevel": 1, // Permet de zoomer, pratique si vous faites une présentation
// Apparence
// -- Editeur
"workbench.startupEditor": "none", // On ne veut pas une page d'accueil chargée
"editor.minimap.enabled": true, // On veut voir la minimap
"editor.minimap.showSlider": "always", // On veut voir la minimap
"editor.minimap.size": "fill", // On veut voir la minimap
"editor.minimap.scale": 1,
"editor.tokenColorCustomizations": {
"textMateRules": [
{
"scope": [
"storage.type.function",
"storage.type.class"
],
"settings": {
"fontStyle": "bold",
"foreground": "#4B9CD3"
}
}
]
},
"editor.minimap.renderCharacters": true,
"editor.minimap.maxColumn": 120,
"breadcrumbs.enabled": true,
// -- Tabs
"workbench.editor.wrapTabs": true, // On veut voir les tabs
"workbench.editor.tabSizing": "shrink", // On veut voir les tabs
"workbench.editor.pinnedTabSizing": "compact",
"workbench.editor.enablePreview": false, // Un clic sur un fichier l'ouvre
// -- Sidebar
"workbench.tree.indent": 15, // Indente plus pour plus de clarté dans la sidebar
"workbench.tree.renderIndentGuides": "always",
// -- Code
"editor.occurrencesHighlight": "singleFile", // On veut voir les occurences d'une variable
"editor.renderWhitespace": "trailing", // On ne veut pas laisser d'espace en fin de ligne
"editor.renderControlCharacters": true, // On veut voir les caractères de contrôle
// Thème
"editor.fontFamily": "'JetBrains Mono', 'Fira Code', 'Operator Mono Lig', monospace",
"editor.fontLigatures": false,
"editor.fontSize": 13,
"editor.lineHeight": 22,
"editor.guides.bracketPairs": "active",
// Ergonomie
"editor.wordWrap": "off",
"editor.rulers": [300],
"editor.wordWrapColumn": 300,
"editor.suggest.insertMode": "replace", // L'autocomplétion remplace le mot en cours
"editor.acceptSuggestionOnCommitCharacter": false, // Evite que l'autocomplétion soit accepté lors d'un . par exemple
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.linkedEditing": true, // Quand on change un élément HTML, change la balise fermante
"editor.tabSize": 2,
"editor.unicodeHighlight.nonBasicASCII": false,
"[php]": {
"editor.defaultFormatter": "bmewburn.vscode-intelephense-client",
"editor.formatOnSave": true,
"editor.formatOnPaste": true
},
"intelephense.format.braces": "k&r",
"intelephense.format.enable": true,
"php.validate.executablePath": "/opt/homebrew/opt/php@8.3/bin/php",
"php.executablePath": "/opt/homebrew/opt/php@8.3/bin/php",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.formatOnPaste": true
},
"prettier.printWidth": 360,
"prettier.semi": true,
"prettier.singleQuote": true,
"prettier.tabWidth": 2,
"prettier.trailingComma": "es5",
"explorer.autoReveal": false,
"explorer.confirmDragAndDrop": false,
"emmet.triggerExpansionOnTab": true,
"emmet.includeLanguages": {
"javascript": "javascript",
"php": "php",
"svelte": "html",
"dart": "dart"
},
"problems.decorations.enabled": true,
"explorer.decorations.colors": true,
"explorer.decorations.badges": true,
"php.validate.enable": true,
"php.suggest.basic": false,
"dart.analysisExcludedFolders": [],
"dart.enableSdkFormatter": true,
// Fichiers
"files.defaultLanguage": "markdown",
"files.autoSaveWorkspaceFilesOnly": true,
"files.exclude": {
"**/.idea": true
},
// Languages
"javascript.preferences.importModuleSpecifierEnding": "js",
"typescript.preferences.importModuleSpecifierEnding": "js",
// Extensions
"tailwindCSS.experimental.configFile": "web/tailwind.config.js",
"editor.quickSuggestions": {
"strings": true
},
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode",
"editor.formatOnSave": true
},
"prettier.documentSelectors": [
"**/*.svelte"
],
"svelte.plugin.svelte.diagnostics.enable": false,
"js/ts.implicitProjectConfig.checkJs": false,
"svelte.enable-ts-plugin": false,
"cline.autoApproveLimit": 100,
"cline.autoApproveRequests": true,
"cline.enableMemoryBank": true,
"cline.includeSnippetsFromMemory": true,
"cline.contextLength": 10000,
"cline.autoFormat": true,
"cline.primaryDocumentationFile": ".cline",
"cline.gitIntegration": true,
"cline.projectStructure": {
"api": "php",
"app": "flutter",
"web": "svelte"
},
"cline.referenceFiles": {
"database": "docs/db-resalice.dump",
"apiEndpoints": "docs/api_endpoints.md",
"architecture": "docs/architecture.md"
},
"cline.databaseSchema": "docs/db-resalice.dump",
"peacock.color": "#42b883",
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#65c89b",
"activityBar.background": "#65c89b",
"activityBar.foreground": "#15202b",
"activityBar.inactiveForeground": "#15202b99",
"activityBarBadge.background": "#945bc4",
"activityBarBadge.foreground": "#e7e7e7",
"commandCenter.border": "#15202b99",
"sash.hoverBorder": "#65c89b",
"statusBar.background": "#42b883",
"statusBar.foreground": "#15202b",
"statusBarItem.hoverBackground": "#359268",
"statusBarItem.remoteBackground": "#42b883",
"statusBarItem.remoteForeground": "#15202b",
"titleBar.activeBackground": "#42b883",
"titleBar.activeForeground": "#15202b",
"titleBar.inactiveBackground": "#42b88399",
"titleBar.inactiveForeground": "#15202b99"
}
}

1537
app/PLAN2-APP.md Normal file

File diff suppressed because it is too large Load Diff

2853
app/README2-APP.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -37,6 +37,9 @@ class MembreModel extends HiveObject {
@HiveField(10)
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,
);
}
}

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -13,16 +13,16 @@ class ApiService {
late final String _baseUrl;
late final String _appIdentifier;
String? _sessionId;
// Détermine l'environnement actuel (DEV, REC, PROD) en fonction de l'URL
String _determineEnvironment() {
if (!kIsWeb) {
// En mode non-web, utiliser l'environnement de développement par défaut
return 'DEV';
}
final currentUrl = html.window.location.href.toLowerCase();
if (currentUrl.contains('dapp.geosector.fr')) {
return 'DEV';
} else if (currentUrl.contains('rapp.geosector.fr')) {
@@ -31,11 +31,11 @@ class ApiService {
return 'PROD';
}
}
// Configure l'URL de base API et l'identifiant d'application selon l'environnement
void _configureEnvironment() {
final env = _determineEnvironment();
switch (env) {
case 'DEV':
_baseUrl = AppKeys.baseApiUrlDev;
@@ -49,23 +49,23 @@ class ApiService {
_baseUrl = AppKeys.baseApiUrlProd;
_appIdentifier = AppKeys.appIdentifierProd;
}
debugPrint('GEOSECTOR 🔗 Environnement: $env, API: $_baseUrl');
}
ApiService() {
// Configurer l'environnement
_configureEnvironment();
// Configurer Dio
_dio.options.baseUrl = _baseUrl;
_dio.options.connectTimeout = AppKeys.connectionTimeout;
_dio.options.receiveTimeout = AppKeys.receiveTimeout;
// Ajouter les en-têtes par défaut avec l'identifiant d'application adapté à l'environnement
final headers = Map<String, String>.from(AppKeys.defaultHeaders);
headers['X-App-Identifier'] = _appIdentifier;
_dio.options.headers.addAll(headers);
// Ajouter des intercepteurs pour l'authentification par session
@@ -89,17 +89,17 @@ class ApiService {
void setSessionId(String? sessionId) {
_sessionId = sessionId;
}
// Obtenir l'environnement actuel (utile pour le débogage)
String getCurrentEnvironment() {
return _determineEnvironment();
}
// Obtenir l'URL API actuelle (utile pour le débogage)
String getCurrentApiUrl() {
return _baseUrl;
}
// Obtenir l'identifiant d'application actuel (utile pour le débogage)
String getCurrentAppIdentifier() {
return _appIdentifier;
@@ -119,7 +119,7 @@ class ApiService {
rethrow;
}
}
// Méthode GET générique
Future<Response> get(String path, {Map<String, dynamic>? queryParameters}) async {
try {
@@ -128,7 +128,7 @@ class ApiService {
rethrow;
}
}
// Méthode PUT générique
Future<Response> put(String path, {dynamic data}) async {
try {
@@ -137,7 +137,7 @@ class ApiService {
rethrow;
}
}
// Méthode DELETE générique
Future<Response> delete(String path) async {
try {
@@ -159,7 +159,7 @@ class ApiService {
// Vérifier la structure de la réponse
final data = response.data as Map<String, dynamic>;
final status = data['status'] as String?;
// Afficher le message en cas d'erreur
if (status != 'success') {
final message = data['message'] as String?;
@@ -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;

View File

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

View File

@@ -0,0 +1,370 @@
import 'package:flutter/material.dart';
import 'dart:math' as math;
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/core/data/models/amicale_model.dart';
import 'package:geosector_app/core/data/models/membre_model.dart';
import 'package:geosector_app/core/data/models/user_model.dart';
import 'package:geosector_app/core/repositories/user_repository.dart';
import 'package:geosector_app/core/repositories/amicale_repository.dart';
import 'package:geosector_app/core/repositories/membre_repository.dart';
import 'package:geosector_app/presentation/widgets/amicale_table_widget.dart';
import 'package:geosector_app/presentation/widgets/membre_table_widget.dart';
/// Class pour dessiner les petits points blancs sur le fond
class DotsPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white.withOpacity(0.5)
..style = PaintingStyle.fill;
final random = math.Random(42); // Seed fixe pour consistance
final numberOfDots = (size.width * size.height) ~/ 1500;
for (int i = 0; i < numberOfDots; i++) {
final x = random.nextDouble() * size.width;
final y = random.nextDouble() * size.height;
final radius = 1.0 + random.nextDouble() * 2.0;
canvas.drawCircle(Offset(x, y), radius, paint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
/// Page d'administration de l'amicale et des membres
/// Cette page est intégrée dans le tableau de bord administrateur
class AdminAmicalePage extends StatefulWidget {
final UserRepository userRepository;
final AmicaleRepository amicaleRepository;
final MembreRepository membreRepository;
const AdminAmicalePage({
super.key,
required this.userRepository,
required this.amicaleRepository,
required this.membreRepository,
});
@override
State<AdminAmicalePage> createState() => _AdminAmicalePageState();
}
class _AdminAmicalePageState extends State<AdminAmicalePage> {
UserModel? _currentUser;
String? _errorMessage;
@override
void initState() {
super.initState();
_loadCurrentUser();
}
void _loadCurrentUser() {
final currentUser = widget.userRepository.getCurrentUser();
if (currentUser == null) {
setState(() {
_errorMessage = 'Utilisateur non connecté';
});
return;
}
if (currentUser.fkEntite == null) {
setState(() {
_errorMessage = 'Utilisateur non associé à une amicale';
});
return;
}
setState(() {
_currentUser = currentUser;
_errorMessage = null;
});
}
void _handleEditAmicale(AmicaleModel amicale) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Modifier l\'amicale'),
content: Text('Voulez-vous modifier l\'amicale ${amicale.name} ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
// TODO: Naviguer vers la page de modification
// Navigator.of(context).push(
// MaterialPageRoute(
// builder: (context) => EditAmicalePage(
// amicale: amicale,
// amicaleRepository: widget.amicaleRepository,
// ),
// ),
// );
},
child: const Text('Modifier'),
),
],
),
);
}
void _handleEditMembre(MembreModel membre) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Modifier le membre'),
content: Text('Voulez-vous modifier le membre ${membre.firstName} ${membre.name} ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
// TODO: Naviguer vers la page de modification
// Navigator.of(context).push(
// MaterialPageRoute(
// builder: (context) => EditMembrePage(
// membre: membre,
// membreRepository: widget.membreRepository,
// ),
// ),
// );
},
child: const Text('Modifier'),
),
],
),
);
}
void _handleAddMembre() {
if (_currentUser?.fkEntite == null) return;
// TODO: Naviguer vers la page d'ajout de membre
// Navigator.of(context).push(
// MaterialPageRoute(
// builder: (context) => AddMembrePage(
// amicaleId: _currentUser!.fkEntite!,
// membreRepository: widget.membreRepository,
// ),
// ),
// );
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Stack(
children: [
// Fond dégradé avec petits points blancs
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.white, Colors.red.shade300],
),
),
child: CustomPaint(
painter: DotsPainter(),
child: const SizedBox(width: double.infinity, height: double.infinity),
),
),
// Contenu principal
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Titre de la page
Text(
'Mon amicale et ses membres',
style: theme.textTheme.headlineMedium?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 24),
// Message d'erreur si présent
if (_errorMessage != null)
Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.withOpacity(0.3)),
),
child: Row(
children: [
const Icon(Icons.error_outline, color: Colors.red),
const SizedBox(width: 12),
Expanded(
child: Text(
_errorMessage!,
style: const TextStyle(color: Colors.red),
),
),
],
),
),
// Contenu principal avec ValueListenableBuilder
if (_currentUser != null && _currentUser!.fkEntite != null)
Expanded(
child: ValueListenableBuilder<Box<AmicaleModel>>(
valueListenable: widget.amicaleRepository.getAmicalesBox().listenable(),
builder: (context, amicalesBox, child) {
final amicale = amicalesBox.get(_currentUser!.fkEntite!);
if (amicale == null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.business_outlined,
size: 64,
color: theme.colorScheme.primary.withOpacity(0.7),
),
const SizedBox(height: 16),
Text(
'Amicale non trouvée',
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'L\'amicale associée à votre compte n\'existe plus.',
textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge,
),
],
),
);
}
return ValueListenableBuilder<Box<MembreModel>>(
valueListenable: widget.membreRepository.getMembresBox().listenable(),
builder: (context, membresBox, child) {
// Filtrer les membres par amicale
// Note: Il faudra ajouter le champ fkEntite au modèle MembreModel
final membres = membresBox.values.where((membre) => membre.fkEntite == _currentUser!.fkEntite).toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section Amicale
Text(
'Informations de l\'amicale',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
// Tableau Amicale
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: AmicaleTableWidget(
amicales: [amicale],
onEdit: null,
onDelete: null,
amicaleRepository: widget.amicaleRepository,
userRepository: widget.userRepository,
apiService: null, // Ou passez l'ApiService si vous l'avez disponible
showActionsColumn: false,
),
),
const SizedBox(height: 32),
// Section Membres
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Membres de l\'amicale (${membres.length})',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
ElevatedButton.icon(
onPressed: _handleAddMembre,
icon: const Icon(Icons.add),
label: const Text('Ajouter un membre'),
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
foregroundColor: Colors.white,
),
),
],
),
const SizedBox(height: 16),
// Tableau Membres
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: MembreTableWidget(
membres: membres,
onEdit: _handleEditMembre,
onDelete: null, // Géré par l'admin principal
membreRepository: widget.membreRepository,
),
),
),
],
);
},
);
},
),
),
// Message si pas d'utilisateur connecté
if (_currentUser == null)
const Expanded(
child: Center(
child: CircularProgressIndicator(),
),
),
],
),
),
],
);
}
}

View File

@@ -11,7 +11,7 @@ import 'admin_statistics_page.dart';
import 'admin_history_page.dart';
import 'admin_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,
});
}

View File

@@ -1,361 +0,0 @@
import 'package:flutter/material.dart';
import 'dart:math' as math;
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
import 'package:geosector_app/core/data/models/amicale_model.dart';
import 'package:geosector_app/core/data/models/membre_model.dart';
import 'package:geosector_app/presentation/widgets/amicale_table_widget.dart';
import 'package:geosector_app/presentation/widgets/membre_table_widget.dart';
/// Class pour dessiner les petits points blancs sur le fond
class DotsPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white.withOpacity(0.5)
..style = PaintingStyle.fill;
final random = math.Random(42); // Seed fixe pour consistance
final numberOfDots = (size.width * size.height) ~/ 1500;
for (int i = 0; i < numberOfDots; i++) {
final x = random.nextDouble() * size.width;
final y = random.nextDouble() * size.height;
final radius = 1.0 + random.nextDouble() * 2.0;
canvas.drawCircle(Offset(x, y), radius, paint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
/// Page d'administration de l'amicale et des membres
/// Cette page est intégrée dans le tableau de bord administrateur
class AdminEntitePage extends StatefulWidget {
const AdminEntitePage({Key? key}) : super(key: key);
@override
State<AdminEntitePage> createState() => _AdminEntitePageState();
}
class _AdminEntitePageState extends State<AdminEntitePage> {
bool _isLoading = true;
AmicaleModel? _amicale;
List<MembreModel> _membres = [];
String? _errorMessage;
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
// Récupérer l'utilisateur connecté en utilisant l'instance globale
final currentUser = userRepository.getCurrentUser();
if (currentUser == null) {
setState(() {
_errorMessage = 'Utilisateur non connecté';
_isLoading = false;
});
return;
}
// Vérifier si fkEntite est null
if (currentUser.fkEntite == null) {
setState(() {
_errorMessage = 'Utilisateur non associé à une amicale';
_isLoading = false;
});
return;
}
// Récupérer l'amicale de l'utilisateur en utilisant l'instance globale
final amicale = amicaleRepository.getAmicaleById(currentUser.fkEntite!);
if (amicale == null) {
setState(() {
_errorMessage = 'Amicale non trouvée';
_isLoading = false;
});
return;
}
// Récupérer tous les membres
// Note: Dans un cas réel, nous devrions filtrer les membres par amicale,
// mais le modèle MembreModel n'a pas de champ fkEntite pour le moment
final membres = membreRepository.getAllMembres();
setState(() {
_amicale = amicale;
_membres = membres;
_isLoading = false;
});
} catch (e) {
setState(() {
_errorMessage = 'Erreur lors du chargement des données: $e';
_isLoading = false;
});
}
}
void _handleEditAmicale(AmicaleModel amicale) {
// Afficher une boîte de dialogue de confirmation
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Modifier l\'amicale'),
content: Text('Voulez-vous modifier l\'amicale ${amicale.name} ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
// Naviguer vers la page de modification
// Navigator.of(context).push(
// MaterialPageRoute(
// builder: (context) => EditAmicalePage(amicale: amicale),
// ),
// );
},
child: const Text('Modifier'),
),
],
),
);
}
void _handleEditMembre(MembreModel membre) {
// Afficher une boîte de dialogue de confirmation
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Modifier le membre'),
content: Text(
'Voulez-vous modifier le membre ${membre.firstName} ${membre.name} ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
// Naviguer vers la page de modification
// Navigator.of(context).push(
// MaterialPageRoute(
// builder: (context) => EditMembrePage(membre: membre),
// ),
// );
},
child: const Text('Modifier'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Stack(
children: [
// Fond dégradé avec petits points blancs
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.white, Colors.red.shade300],
),
),
child: CustomPaint(
painter: DotsPainter(),
child: Container(width: double.infinity, height: double.infinity),
),
),
// Contenu principal
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Titre de la page
Text(
'Mon amicale et ses membres',
style: theme.textTheme.headlineMedium?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 24),
// Message d'erreur si présent
if (_errorMessage != null)
Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.withOpacity(0.3)),
),
child: Row(
children: [
const Icon(Icons.error_outline, color: Colors.red),
const SizedBox(width: 12),
Expanded(
child: Text(
_errorMessage!,
style: const TextStyle(color: Colors.red),
),
),
],
),
),
// Contenu principal
if (_isLoading)
const Expanded(
child: Center(
child: CircularProgressIndicator(),
),
)
else if (_amicale == null)
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.business_outlined,
size: 64,
color: theme.colorScheme.primary.withOpacity(0.7),
),
const SizedBox(height: 16),
Text(
'Aucune amicale associée',
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'Vous n\'êtes pas associé à une amicale.',
textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge,
),
],
),
),
)
else
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section Amicale
Text(
'Informations de l\'amicale',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
// Tableau Amicale
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: AmicaleTableWidget(
amicales: [_amicale!],
// Pas de bouton de suppression pour sa propre amicale
onDelete: null,
),
),
const SizedBox(height: 32),
// Section Membres
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Membres de l\'amicale',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
ElevatedButton.icon(
onPressed: () {
// Naviguer vers la page d'ajout de membre
// Navigator.of(context).push(
// MaterialPageRoute(
// builder: (context) => AddMembrePage(amicaleId: _amicale!.id),
// ),
// );
},
icon: const Icon(Icons.add),
label: const Text('Ajouter un membre'),
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
foregroundColor: Colors.white,
),
),
],
),
const SizedBox(height: 16),
// Tableau Membres
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: MembreTableWidget(
membres: _membres,
onEdit: _handleEditMembre,
// Pas de bouton de suppression pour les membres de sa propre amicale
// sauf si l'utilisateur a un rôle élevé
onDelete: null,
),
),
),
],
),
),
],
),
),
],
);
}
}

View File

@@ -78,9 +78,7 @@ class _LoginPageState extends State<LoginPage> {
// Fallback sur la version du AppInfoService si elle existe
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'),
),
],
);

View File

@@ -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),

View File

@@ -6,20 +6,23 @@ import 'package:geosector_app/core/repositories/user_repository.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/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),
);
}
}

View File

@@ -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,
),
],
),
),

View File

@@ -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();
},
),
],
),
),
),

View File

@@ -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('/');

View File

@@ -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),
),
],
),

View File

@@ -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,
);
},
);
}
}

View File

@@ -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'