Appli Flutter se connecte bien à l'API.
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
|
||||
> Application mobile moderne pour la gestion d'associations en Côte d'Ivoire avec intégration Wave Money
|
||||
|
||||
[](https://reactnative.dev/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://flutter.dev/)
|
||||
[](https://dart.dev/)
|
||||
[](https://wave.com/)
|
||||
[](https://www.gouv.ci/)
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
### 🎨 **Interface Ultra Moderne**
|
||||
- **Design System** cohérent inspiré des couleurs ivoiriennes
|
||||
- **Animations fluides** avec Reanimated 3
|
||||
- **Animations fluides** avec Flutter Animations
|
||||
- **Mode sombre** automatique
|
||||
- **Responsive design** pour tous les écrans
|
||||
- **Accessibilité** complète (WCAG 2.1)
|
||||
@@ -38,15 +38,37 @@
|
||||
- **Synchronisation temps réel** avec le backend
|
||||
- **Cache intelligent** pour performance optimale
|
||||
|
||||
## 🚀 Installation
|
||||
## 🚀 Installation et Configuration
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
### **Prérequis**
|
||||
- Flutter SDK 3.5.3+
|
||||
- Dart SDK 3.5.3+
|
||||
- Android Studio / VS Code
|
||||
- Émulateur Android ou appareil physique
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
### **Installation**
|
||||
```bash
|
||||
# Cloner le projet
|
||||
git clone <repository-url>
|
||||
cd unionflow-mobile-apps
|
||||
|
||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||
# Installer les dépendances
|
||||
flutter pub get
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
# Générer les fichiers de code (DI)
|
||||
flutter packages pub run build_runner build
|
||||
|
||||
# Lancer l'application
|
||||
flutter run
|
||||
```
|
||||
|
||||
### **Configuration API**
|
||||
Modifier l'URL de base dans `lib/core/network/dio_client.dart` :
|
||||
```dart
|
||||
baseUrl: 'http://your-api-url:8081', // Remplacer par votre URL API
|
||||
```
|
||||
|
||||
### **Scripts utiles**
|
||||
- `flutter test` - Exécuter les tests
|
||||
- `flutter analyze` - Analyser le code
|
||||
- `flutter build apk` - Construire l'APK Android
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:injectable/injectable.dart';
|
||||
import 'package:jwt_decoder/jwt_decoder.dart';
|
||||
import '../models/auth_state.dart';
|
||||
import '../models/login_request.dart';
|
||||
import '../models/login_response.dart';
|
||||
|
||||
import '../models/user_info.dart';
|
||||
import '../storage/secure_token_storage.dart';
|
||||
import 'auth_api_service.dart';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../models/auth_state.dart';
|
||||
import '../models/login_request.dart';
|
||||
import '../models/user_info.dart';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../models/auth_state.dart';
|
||||
import '../models/login_request.dart';
|
||||
import '../models/user_info.dart';
|
||||
|
||||
@@ -83,7 +83,8 @@ class SecureTokenStorage {
|
||||
/// Récupère la date d'expiration du refresh token
|
||||
Future<DateTime?> getRefreshTokenExpirationDate() async {
|
||||
try {
|
||||
final expiresAtString = await _storage.read(key: _refreshExpiresAtKey);
|
||||
final prefs = await _prefs;
|
||||
final expiresAtString = prefs.getString(_refreshExpiresAtKey);
|
||||
if (expiresAtString == null) return null;
|
||||
|
||||
return DateTime.parse(expiresAtString);
|
||||
@@ -133,9 +134,10 @@ class SecureTokenStorage {
|
||||
/// Met à jour le token d'accès
|
||||
Future<void> updateAccessToken(String accessToken, DateTime expiresAt) async {
|
||||
try {
|
||||
final prefs = await _prefs;
|
||||
await Future.wait([
|
||||
_storage.write(key: _accessTokenKey, value: accessToken),
|
||||
_storage.write(key: _expiresAtKey, value: expiresAt.toIso8601String()),
|
||||
prefs.setString(_accessTokenKey, accessToken),
|
||||
prefs.setString(_expiresAtKey, expiresAt.toIso8601String()),
|
||||
]);
|
||||
} catch (e) {
|
||||
throw StorageException('Erreur lors de la mise à jour du token d\'accès: $e');
|
||||
@@ -145,8 +147,9 @@ class SecureTokenStorage {
|
||||
/// Vérifie si les données d'authentification existent
|
||||
Future<bool> hasAuthData() async {
|
||||
try {
|
||||
final accessToken = await _storage.read(key: _accessTokenKey);
|
||||
final refreshToken = await _storage.read(key: _refreshTokenKey);
|
||||
final prefs = await _prefs;
|
||||
final accessToken = prefs.getString(_accessTokenKey);
|
||||
final refreshToken = prefs.getString(_refreshTokenKey);
|
||||
return accessToken != null && refreshToken != null;
|
||||
} catch (e) {
|
||||
return false;
|
||||
@@ -184,12 +187,13 @@ class SecureTokenStorage {
|
||||
/// Efface toutes les données d'authentification
|
||||
Future<void> clearAuthData() async {
|
||||
try {
|
||||
final prefs = await _prefs;
|
||||
await Future.wait([
|
||||
_storage.delete(key: _accessTokenKey),
|
||||
_storage.delete(key: _refreshTokenKey),
|
||||
_storage.delete(key: _userInfoKey),
|
||||
_storage.delete(key: _expiresAtKey),
|
||||
_storage.delete(key: _refreshExpiresAtKey),
|
||||
prefs.remove(_accessTokenKey),
|
||||
prefs.remove(_refreshTokenKey),
|
||||
prefs.remove(_userInfoKey),
|
||||
prefs.remove(_expiresAtKey),
|
||||
prefs.remove(_refreshExpiresAtKey),
|
||||
]);
|
||||
} catch (e) {
|
||||
throw StorageException('Erreur lors de l\'effacement des données d\'authentification: $e');
|
||||
@@ -199,7 +203,8 @@ class SecureTokenStorage {
|
||||
/// Active/désactive l'authentification biométrique
|
||||
Future<void> setBiometricEnabled(bool enabled) async {
|
||||
try {
|
||||
await _storage.write(key: _biometricEnabledKey, value: enabled.toString());
|
||||
final prefs = await _prefs;
|
||||
await prefs.setBool(_biometricEnabledKey, enabled);
|
||||
} catch (e) {
|
||||
throw StorageException('Erreur lors de la configuration biométrique: $e');
|
||||
}
|
||||
@@ -208,8 +213,8 @@ class SecureTokenStorage {
|
||||
/// Vérifie si l'authentification biométrique est activée
|
||||
Future<bool> isBiometricEnabled() async {
|
||||
try {
|
||||
final enabled = await _storage.read(key: _biometricEnabledKey);
|
||||
return enabled == 'true';
|
||||
final prefs = await _prefs;
|
||||
return prefs.getBool(_biometricEnabledKey) ?? false;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
@@ -218,7 +223,8 @@ class SecureTokenStorage {
|
||||
/// Efface toutes les données stockées
|
||||
Future<void> clearAll() async {
|
||||
try {
|
||||
await _storage.deleteAll();
|
||||
final prefs = await _prefs;
|
||||
await prefs.clear();
|
||||
} catch (e) {
|
||||
throw StorageException('Erreur lors de l\'effacement de toutes les données: $e');
|
||||
}
|
||||
@@ -227,8 +233,8 @@ class SecureTokenStorage {
|
||||
/// Vérifie si le stockage sécurisé est disponible
|
||||
Future<bool> isAvailable() async {
|
||||
try {
|
||||
await _storage.containsKey(key: 'test');
|
||||
return true;
|
||||
final prefs = await _prefs;
|
||||
return prefs.containsKey('test');
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class AppConstants {
|
||||
// API Configuration
|
||||
static const String baseUrl = 'http://localhost:8099'; // Backend UnionFlow
|
||||
static const String apiVersion = '/api/v1';
|
||||
static const String baseUrl = 'http://192.168.1.13:8080'; // Backend UnionFlow
|
||||
static const String apiVersion = '/api';
|
||||
|
||||
// Timeout
|
||||
static const Duration connectTimeout = Duration(seconds: 30);
|
||||
|
||||
@@ -4,42 +4,60 @@
|
||||
// InjectableConfigGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: unnecessary_lambdas
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
// ignore_for_file: type=lint
|
||||
// coverage:ignore-file
|
||||
|
||||
import 'package:get_it/get_it.dart' as _i1;
|
||||
import 'package:injectable/injectable.dart' as _i2;
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:get_it/get_it.dart' as _i174;
|
||||
import 'package:injectable/injectable.dart' as _i526;
|
||||
import 'package:unionflow_mobile_apps/core/auth/bloc/auth_bloc.dart' as _i635;
|
||||
import 'package:unionflow_mobile_apps/core/auth/services/auth_api_service.dart'
|
||||
as _i705;
|
||||
import 'package:unionflow_mobile_apps/core/auth/services/auth_service.dart'
|
||||
as _i423;
|
||||
import 'package:unionflow_mobile_apps/core/auth/storage/secure_token_storage.dart'
|
||||
as _i394;
|
||||
import 'package:unionflow_mobile_apps/core/network/auth_interceptor.dart'
|
||||
as _i772;
|
||||
import 'package:unionflow_mobile_apps/core/network/dio_client.dart' as _i978;
|
||||
import 'package:unionflow_mobile_apps/core/services/api_service.dart' as _i238;
|
||||
import 'package:unionflow_mobile_apps/features/members/data/repositories/membre_repository_impl.dart'
|
||||
as _i108;
|
||||
import 'package:unionflow_mobile_apps/features/members/domain/repositories/membre_repository.dart'
|
||||
as _i930;
|
||||
import 'package:unionflow_mobile_apps/features/members/presentation/bloc/membres_bloc.dart'
|
||||
as _i41;
|
||||
|
||||
import '../auth/bloc/auth_bloc.dart' as _i7;
|
||||
import '../auth/services/auth_api_service.dart' as _i4;
|
||||
import '../auth/services/auth_service.dart' as _i6;
|
||||
import '../auth/storage/secure_token_storage.dart' as _i3;
|
||||
import '../network/auth_interceptor.dart' as _i5;
|
||||
import '../network/dio_client.dart' as _i8;
|
||||
|
||||
extension GetItInjectableX on _i1.GetIt {
|
||||
extension GetItInjectableX on _i174.GetIt {
|
||||
// initializes the registration of main-scope dependencies inside of GetIt
|
||||
Future<_i1.GetIt> init({
|
||||
_i174.GetIt init({
|
||||
String? environment,
|
||||
_i2.EnvironmentFilter? environmentFilter,
|
||||
}) async {
|
||||
final gh = _i2.GetItHelper(
|
||||
_i526.EnvironmentFilter? environmentFilter,
|
||||
}) {
|
||||
final gh = _i526.GetItHelper(
|
||||
this,
|
||||
environment,
|
||||
environmentFilter,
|
||||
);
|
||||
gh.singleton<_i3.SecureTokenStorage>(() => _i3.SecureTokenStorage());
|
||||
gh.singleton<_i8.DioClient>(() => _i8.DioClient());
|
||||
gh.singleton<_i5.AuthInterceptor>(() => _i5.AuthInterceptor(gh<_i3.SecureTokenStorage>()));
|
||||
gh.singleton<_i4.AuthApiService>(() => _i4.AuthApiService(gh<_i8.DioClient>()));
|
||||
gh.singleton<_i6.AuthService>(() => _i6.AuthService(
|
||||
gh<_i3.SecureTokenStorage>(),
|
||||
gh<_i4.AuthApiService>(),
|
||||
gh<_i5.AuthInterceptor>(),
|
||||
gh<_i8.DioClient>(),
|
||||
gh.singleton<_i394.SecureTokenStorage>(() => _i394.SecureTokenStorage());
|
||||
gh.singleton<_i978.DioClient>(() => _i978.DioClient());
|
||||
gh.singleton<_i705.AuthApiService>(
|
||||
() => _i705.AuthApiService(gh<_i978.DioClient>()));
|
||||
gh.singleton<_i238.ApiService>(
|
||||
() => _i238.ApiService(gh<_i978.DioClient>()));
|
||||
gh.singleton<_i772.AuthInterceptor>(
|
||||
() => _i772.AuthInterceptor(gh<_i394.SecureTokenStorage>()));
|
||||
gh.lazySingleton<_i930.MembreRepository>(
|
||||
() => _i108.MembreRepositoryImpl(gh<_i238.ApiService>()));
|
||||
gh.factory<_i41.MembresBloc>(
|
||||
() => _i41.MembresBloc(gh<_i930.MembreRepository>()));
|
||||
gh.singleton<_i423.AuthService>(() => _i423.AuthService(
|
||||
gh<_i394.SecureTokenStorage>(),
|
||||
gh<_i705.AuthApiService>(),
|
||||
gh<_i772.AuthInterceptor>(),
|
||||
gh<_i978.DioClient>(),
|
||||
));
|
||||
gh.singleton<_i7.AuthBloc>(() => _i7.AuthBloc(gh<_i6.AuthService>()));
|
||||
gh.singleton<_i635.AuthBloc>(() => _i635.AuthBloc(gh<_i423.AuthService>()));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ final GetIt getIt = GetIt.instance;
|
||||
/// Configure l'injection de dépendances
|
||||
@InjectableInit()
|
||||
Future<void> configureDependencies() async {
|
||||
await getIt.init();
|
||||
getIt.init();
|
||||
}
|
||||
|
||||
/// Réinitialise les dépendances (utile pour les tests)
|
||||
|
||||
186
unionflow-mobile-apps/lib/core/models/membre_model.dart
Normal file
186
unionflow-mobile-apps/lib/core/models/membre_model.dart
Normal file
@@ -0,0 +1,186 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'membre_model.g.dart';
|
||||
|
||||
/// Modèle de données pour un membre UnionFlow
|
||||
/// Aligné avec MembreDTO du serveur API
|
||||
@JsonSerializable()
|
||||
class MembreModel extends Equatable {
|
||||
/// ID unique du membre
|
||||
final String? id;
|
||||
|
||||
/// Numéro unique du membre (format: UF-YYYY-XXXXXXXX)
|
||||
@JsonKey(name: 'numeroMembre')
|
||||
final String numeroMembre;
|
||||
|
||||
/// Nom de famille du membre
|
||||
final String nom;
|
||||
|
||||
/// Prénom du membre
|
||||
final String prenom;
|
||||
|
||||
/// Adresse email
|
||||
final String email;
|
||||
|
||||
/// Numéro de téléphone
|
||||
final String telephone;
|
||||
|
||||
/// Date de naissance
|
||||
@JsonKey(name: 'dateNaissance')
|
||||
final DateTime? dateNaissance;
|
||||
|
||||
/// Adresse complète
|
||||
final String? adresse;
|
||||
|
||||
/// Ville
|
||||
final String? ville;
|
||||
|
||||
/// Code postal
|
||||
@JsonKey(name: 'codePostal')
|
||||
final String? codePostal;
|
||||
|
||||
/// Pays
|
||||
final String? pays;
|
||||
|
||||
/// Profession
|
||||
final String? profession;
|
||||
|
||||
/// Statut du membre (ACTIF, INACTIF, SUSPENDU)
|
||||
final String statut;
|
||||
|
||||
/// Date d'adhésion
|
||||
@JsonKey(name: 'dateAdhesion')
|
||||
final DateTime dateAdhesion;
|
||||
|
||||
/// Date de création
|
||||
@JsonKey(name: 'dateCreation')
|
||||
final DateTime dateCreation;
|
||||
|
||||
/// Date de dernière modification
|
||||
@JsonKey(name: 'dateModification')
|
||||
final DateTime? dateModification;
|
||||
|
||||
/// Indique si le membre est actif
|
||||
final bool actif;
|
||||
|
||||
/// Version pour optimistic locking
|
||||
final int version;
|
||||
|
||||
const MembreModel({
|
||||
this.id,
|
||||
required this.numeroMembre,
|
||||
required this.nom,
|
||||
required this.prenom,
|
||||
required this.email,
|
||||
required this.telephone,
|
||||
this.dateNaissance,
|
||||
this.adresse,
|
||||
this.ville,
|
||||
this.codePostal,
|
||||
this.pays,
|
||||
this.profession,
|
||||
required this.statut,
|
||||
required this.dateAdhesion,
|
||||
required this.dateCreation,
|
||||
this.dateModification,
|
||||
required this.actif,
|
||||
required this.version,
|
||||
});
|
||||
|
||||
/// Constructeur depuis JSON
|
||||
factory MembreModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$MembreModelFromJson(json);
|
||||
|
||||
/// Conversion vers JSON
|
||||
Map<String, dynamic> toJson() => _$MembreModelToJson(this);
|
||||
|
||||
/// Nom complet du membre
|
||||
String get nomComplet => '$prenom $nom';
|
||||
|
||||
/// Initiales du membre
|
||||
String get initiales {
|
||||
final prenomInitial = prenom.isNotEmpty ? prenom[0].toUpperCase() : '';
|
||||
final nomInitial = nom.isNotEmpty ? nom[0].toUpperCase() : '';
|
||||
return '$prenomInitial$nomInitial';
|
||||
}
|
||||
|
||||
/// Adresse complète formatée
|
||||
String get adresseComplete {
|
||||
final parts = <String>[];
|
||||
if (adresse?.isNotEmpty == true) parts.add(adresse!);
|
||||
if (ville?.isNotEmpty == true) parts.add(ville!);
|
||||
if (codePostal?.isNotEmpty == true) parts.add(codePostal!);
|
||||
if (pays?.isNotEmpty == true) parts.add(pays!);
|
||||
return parts.join(', ');
|
||||
}
|
||||
|
||||
/// Copie avec modifications
|
||||
MembreModel copyWith({
|
||||
String? id,
|
||||
String? numeroMembre,
|
||||
String? nom,
|
||||
String? prenom,
|
||||
String? email,
|
||||
String? telephone,
|
||||
DateTime? dateNaissance,
|
||||
String? adresse,
|
||||
String? ville,
|
||||
String? codePostal,
|
||||
String? pays,
|
||||
String? profession,
|
||||
String? statut,
|
||||
DateTime? dateAdhesion,
|
||||
DateTime? dateCreation,
|
||||
DateTime? dateModification,
|
||||
bool? actif,
|
||||
int? version,
|
||||
}) {
|
||||
return MembreModel(
|
||||
id: id ?? this.id,
|
||||
numeroMembre: numeroMembre ?? this.numeroMembre,
|
||||
nom: nom ?? this.nom,
|
||||
prenom: prenom ?? this.prenom,
|
||||
email: email ?? this.email,
|
||||
telephone: telephone ?? this.telephone,
|
||||
dateNaissance: dateNaissance ?? this.dateNaissance,
|
||||
adresse: adresse ?? this.adresse,
|
||||
ville: ville ?? this.ville,
|
||||
codePostal: codePostal ?? this.codePostal,
|
||||
pays: pays ?? this.pays,
|
||||
profession: profession ?? this.profession,
|
||||
statut: statut ?? this.statut,
|
||||
dateAdhesion: dateAdhesion ?? this.dateAdhesion,
|
||||
dateCreation: dateCreation ?? this.dateCreation,
|
||||
dateModification: dateModification ?? this.dateModification,
|
||||
actif: actif ?? this.actif,
|
||||
version: version ?? this.version,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
numeroMembre,
|
||||
nom,
|
||||
prenom,
|
||||
email,
|
||||
telephone,
|
||||
dateNaissance,
|
||||
adresse,
|
||||
ville,
|
||||
codePostal,
|
||||
pays,
|
||||
profession,
|
||||
statut,
|
||||
dateAdhesion,
|
||||
dateCreation,
|
||||
dateModification,
|
||||
actif,
|
||||
version,
|
||||
];
|
||||
|
||||
@override
|
||||
String toString() => 'MembreModel(id: $id, numeroMembre: $numeroMembre, '
|
||||
'nomComplet: $nomComplet, email: $email, statut: $statut)';
|
||||
}
|
||||
54
unionflow-mobile-apps/lib/core/models/membre_model.g.dart
Normal file
54
unionflow-mobile-apps/lib/core/models/membre_model.g.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'membre_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
MembreModel _$MembreModelFromJson(Map<String, dynamic> json) => MembreModel(
|
||||
id: json['id'] as String?,
|
||||
numeroMembre: json['numeroMembre'] as String,
|
||||
nom: json['nom'] as String,
|
||||
prenom: json['prenom'] as String,
|
||||
email: json['email'] as String,
|
||||
telephone: json['telephone'] as String,
|
||||
dateNaissance: json['dateNaissance'] == null
|
||||
? null
|
||||
: DateTime.parse(json['dateNaissance'] as String),
|
||||
adresse: json['adresse'] as String?,
|
||||
ville: json['ville'] as String?,
|
||||
codePostal: json['codePostal'] as String?,
|
||||
pays: json['pays'] as String?,
|
||||
profession: json['profession'] as String?,
|
||||
statut: json['statut'] as String,
|
||||
dateAdhesion: DateTime.parse(json['dateAdhesion'] as String),
|
||||
dateCreation: DateTime.parse(json['dateCreation'] as String),
|
||||
dateModification: json['dateModification'] == null
|
||||
? null
|
||||
: DateTime.parse(json['dateModification'] as String),
|
||||
actif: json['actif'] as bool,
|
||||
version: (json['version'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MembreModelToJson(MembreModel instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'numeroMembre': instance.numeroMembre,
|
||||
'nom': instance.nom,
|
||||
'prenom': instance.prenom,
|
||||
'email': instance.email,
|
||||
'telephone': instance.telephone,
|
||||
'dateNaissance': instance.dateNaissance?.toIso8601String(),
|
||||
'adresse': instance.adresse,
|
||||
'ville': instance.ville,
|
||||
'codePostal': instance.codePostal,
|
||||
'pays': instance.pays,
|
||||
'profession': instance.profession,
|
||||
'statut': instance.statut,
|
||||
'dateAdhesion': instance.dateAdhesion.toIso8601String(),
|
||||
'dateCreation': instance.dateCreation.toIso8601String(),
|
||||
'dateModification': instance.dateModification?.toIso8601String(),
|
||||
'actif': instance.actif,
|
||||
'version': instance.version,
|
||||
};
|
||||
@@ -0,0 +1,206 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'wave_checkout_session_model.g.dart';
|
||||
|
||||
/// Modèle pour les sessions de paiement Wave Money
|
||||
/// Aligné avec WaveCheckoutSessionDTO du serveur API
|
||||
@JsonSerializable()
|
||||
class WaveCheckoutSessionModel extends Equatable {
|
||||
/// ID unique de la session
|
||||
final String? id;
|
||||
|
||||
/// ID de la session Wave (retourné par l'API Wave)
|
||||
@JsonKey(name: 'waveSessionId')
|
||||
final String waveSessionId;
|
||||
|
||||
/// URL de la session de paiement Wave
|
||||
@JsonKey(name: 'waveUrl')
|
||||
final String? waveUrl;
|
||||
|
||||
/// Montant du paiement
|
||||
final double montant;
|
||||
|
||||
/// Devise (XOF pour la Côte d'Ivoire)
|
||||
final String devise;
|
||||
|
||||
/// URL de succès (redirection après paiement réussi)
|
||||
@JsonKey(name: 'successUrl')
|
||||
final String successUrl;
|
||||
|
||||
/// URL d'erreur (redirection après échec)
|
||||
@JsonKey(name: 'errorUrl')
|
||||
final String errorUrl;
|
||||
|
||||
/// Statut de la session
|
||||
final String statut;
|
||||
|
||||
/// ID de l'organisation qui effectue le paiement
|
||||
@JsonKey(name: 'organisationId')
|
||||
final String? organisationId;
|
||||
|
||||
/// Nom de l'organisation
|
||||
@JsonKey(name: 'nomOrganisation')
|
||||
final String? nomOrganisation;
|
||||
|
||||
/// ID du membre qui effectue le paiement
|
||||
@JsonKey(name: 'membreId')
|
||||
final String? membreId;
|
||||
|
||||
/// Nom du membre
|
||||
@JsonKey(name: 'nomMembre')
|
||||
final String? nomMembre;
|
||||
|
||||
/// Type de paiement (COTISATION, ADHESION, AIDE, EVENEMENT)
|
||||
@JsonKey(name: 'typePaiement')
|
||||
final String? typePaiement;
|
||||
|
||||
/// Description du paiement
|
||||
final String? description;
|
||||
|
||||
/// Référence externe
|
||||
@JsonKey(name: 'referenceExterne')
|
||||
final String? referenceExterne;
|
||||
|
||||
/// Date de création
|
||||
@JsonKey(name: 'dateCreation')
|
||||
final DateTime dateCreation;
|
||||
|
||||
/// Date d'expiration
|
||||
@JsonKey(name: 'dateExpiration')
|
||||
final DateTime? dateExpiration;
|
||||
|
||||
/// Date de dernière modification
|
||||
@JsonKey(name: 'dateModification')
|
||||
final DateTime? dateModification;
|
||||
|
||||
/// Indique si la session est active
|
||||
final bool actif;
|
||||
|
||||
/// Version pour optimistic locking
|
||||
final int version;
|
||||
|
||||
const WaveCheckoutSessionModel({
|
||||
this.id,
|
||||
required this.waveSessionId,
|
||||
this.waveUrl,
|
||||
required this.montant,
|
||||
required this.devise,
|
||||
required this.successUrl,
|
||||
required this.errorUrl,
|
||||
required this.statut,
|
||||
this.organisationId,
|
||||
this.nomOrganisation,
|
||||
this.membreId,
|
||||
this.nomMembre,
|
||||
this.typePaiement,
|
||||
this.description,
|
||||
this.referenceExterne,
|
||||
required this.dateCreation,
|
||||
this.dateExpiration,
|
||||
this.dateModification,
|
||||
required this.actif,
|
||||
required this.version,
|
||||
});
|
||||
|
||||
/// Constructeur depuis JSON
|
||||
factory WaveCheckoutSessionModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$WaveCheckoutSessionModelFromJson(json);
|
||||
|
||||
/// Conversion vers JSON
|
||||
Map<String, dynamic> toJson() => _$WaveCheckoutSessionModelToJson(this);
|
||||
|
||||
/// Montant formaté avec devise
|
||||
String get montantFormate => '${montant.toStringAsFixed(0)} $devise';
|
||||
|
||||
/// Indique si la session est expirée
|
||||
bool get estExpiree {
|
||||
if (dateExpiration == null) return false;
|
||||
return DateTime.now().isAfter(dateExpiration!);
|
||||
}
|
||||
|
||||
/// Indique si la session est en attente
|
||||
bool get estEnAttente => statut == 'PENDING' || statut == 'EN_ATTENTE';
|
||||
|
||||
/// Indique si la session est réussie
|
||||
bool get estReussie => statut == 'SUCCESS' || statut == 'REUSSIE';
|
||||
|
||||
/// Indique si la session a échoué
|
||||
bool get aEchoue => statut == 'FAILED' || statut == 'ECHEC';
|
||||
|
||||
/// Copie avec modifications
|
||||
WaveCheckoutSessionModel copyWith({
|
||||
String? id,
|
||||
String? waveSessionId,
|
||||
String? waveUrl,
|
||||
double? montant,
|
||||
String? devise,
|
||||
String? successUrl,
|
||||
String? errorUrl,
|
||||
String? statut,
|
||||
String? organisationId,
|
||||
String? nomOrganisation,
|
||||
String? membreId,
|
||||
String? nomMembre,
|
||||
String? typePaiement,
|
||||
String? description,
|
||||
String? referenceExterne,
|
||||
DateTime? dateCreation,
|
||||
DateTime? dateExpiration,
|
||||
DateTime? dateModification,
|
||||
bool? actif,
|
||||
int? version,
|
||||
}) {
|
||||
return WaveCheckoutSessionModel(
|
||||
id: id ?? this.id,
|
||||
waveSessionId: waveSessionId ?? this.waveSessionId,
|
||||
waveUrl: waveUrl ?? this.waveUrl,
|
||||
montant: montant ?? this.montant,
|
||||
devise: devise ?? this.devise,
|
||||
successUrl: successUrl ?? this.successUrl,
|
||||
errorUrl: errorUrl ?? this.errorUrl,
|
||||
statut: statut ?? this.statut,
|
||||
organisationId: organisationId ?? this.organisationId,
|
||||
nomOrganisation: nomOrganisation ?? this.nomOrganisation,
|
||||
membreId: membreId ?? this.membreId,
|
||||
nomMembre: nomMembre ?? this.nomMembre,
|
||||
typePaiement: typePaiement ?? this.typePaiement,
|
||||
description: description ?? this.description,
|
||||
referenceExterne: referenceExterne ?? this.referenceExterne,
|
||||
dateCreation: dateCreation ?? this.dateCreation,
|
||||
dateExpiration: dateExpiration ?? this.dateExpiration,
|
||||
dateModification: dateModification ?? this.dateModification,
|
||||
actif: actif ?? this.actif,
|
||||
version: version ?? this.version,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
waveSessionId,
|
||||
waveUrl,
|
||||
montant,
|
||||
devise,
|
||||
successUrl,
|
||||
errorUrl,
|
||||
statut,
|
||||
organisationId,
|
||||
nomOrganisation,
|
||||
membreId,
|
||||
nomMembre,
|
||||
typePaiement,
|
||||
description,
|
||||
referenceExterne,
|
||||
dateCreation,
|
||||
dateExpiration,
|
||||
dateModification,
|
||||
actif,
|
||||
version,
|
||||
];
|
||||
|
||||
@override
|
||||
String toString() => 'WaveCheckoutSessionModel(id: $id, '
|
||||
'waveSessionId: $waveSessionId, montant: $montantFormate, '
|
||||
'statut: $statut, typePaiement: $typePaiement)';
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'wave_checkout_session_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
WaveCheckoutSessionModel _$WaveCheckoutSessionModelFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
WaveCheckoutSessionModel(
|
||||
id: json['id'] as String?,
|
||||
waveSessionId: json['waveSessionId'] as String,
|
||||
waveUrl: json['waveUrl'] as String?,
|
||||
montant: (json['montant'] as num).toDouble(),
|
||||
devise: json['devise'] as String,
|
||||
successUrl: json['successUrl'] as String,
|
||||
errorUrl: json['errorUrl'] as String,
|
||||
statut: json['statut'] as String,
|
||||
organisationId: json['organisationId'] as String?,
|
||||
nomOrganisation: json['nomOrganisation'] as String?,
|
||||
membreId: json['membreId'] as String?,
|
||||
nomMembre: json['nomMembre'] as String?,
|
||||
typePaiement: json['typePaiement'] as String?,
|
||||
description: json['description'] as String?,
|
||||
referenceExterne: json['referenceExterne'] as String?,
|
||||
dateCreation: DateTime.parse(json['dateCreation'] as String),
|
||||
dateExpiration: json['dateExpiration'] == null
|
||||
? null
|
||||
: DateTime.parse(json['dateExpiration'] as String),
|
||||
dateModification: json['dateModification'] == null
|
||||
? null
|
||||
: DateTime.parse(json['dateModification'] as String),
|
||||
actif: json['actif'] as bool,
|
||||
version: (json['version'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$WaveCheckoutSessionModelToJson(
|
||||
WaveCheckoutSessionModel instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'waveSessionId': instance.waveSessionId,
|
||||
'waveUrl': instance.waveUrl,
|
||||
'montant': instance.montant,
|
||||
'devise': instance.devise,
|
||||
'successUrl': instance.successUrl,
|
||||
'errorUrl': instance.errorUrl,
|
||||
'statut': instance.statut,
|
||||
'organisationId': instance.organisationId,
|
||||
'nomOrganisation': instance.nomOrganisation,
|
||||
'membreId': instance.membreId,
|
||||
'nomMembre': instance.nomMembre,
|
||||
'typePaiement': instance.typePaiement,
|
||||
'description': instance.description,
|
||||
'referenceExterne': instance.referenceExterne,
|
||||
'dateCreation': instance.dateCreation.toIso8601String(),
|
||||
'dateExpiration': instance.dateExpiration?.toIso8601String(),
|
||||
'dateModification': instance.dateModification?.toIso8601String(),
|
||||
'actif': instance.actif,
|
||||
'version': instance.version,
|
||||
};
|
||||
@@ -19,7 +19,7 @@ class DioClient {
|
||||
void _configureOptions() {
|
||||
_dio.options = BaseOptions(
|
||||
// URL de base de l'API
|
||||
baseUrl: 'http://localhost:8081', // Adresse de votre API Quarkus
|
||||
baseUrl: 'http://192.168.1.13:8080', // Adresse de votre API Quarkus
|
||||
|
||||
// Timeouts
|
||||
connectTimeout: const Duration(seconds: 30),
|
||||
|
||||
214
unionflow-mobile-apps/lib/core/services/api_service.dart
Normal file
214
unionflow-mobile-apps/lib/core/services/api_service.dart
Normal file
@@ -0,0 +1,214 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../models/membre_model.dart';
|
||||
import '../models/wave_checkout_session_model.dart';
|
||||
import '../network/dio_client.dart';
|
||||
|
||||
/// Service API principal pour communiquer avec le serveur UnionFlow
|
||||
@singleton
|
||||
class ApiService {
|
||||
final DioClient _dioClient;
|
||||
|
||||
ApiService(this._dioClient);
|
||||
|
||||
Dio get _dio => _dioClient.dio;
|
||||
|
||||
// ========================================
|
||||
// MEMBRES
|
||||
// ========================================
|
||||
|
||||
/// Récupère la liste de tous les membres actifs
|
||||
Future<List<MembreModel>> getMembres() async {
|
||||
try {
|
||||
final response = await _dio.get('/api/membres');
|
||||
|
||||
if (response.data is List) {
|
||||
return (response.data as List)
|
||||
.map((json) => MembreModel.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
throw Exception('Format de réponse invalide pour la liste des membres');
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e, 'Erreur lors de la récupération des membres');
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère un membre par son ID
|
||||
Future<MembreModel> getMembreById(String id) async {
|
||||
try {
|
||||
final response = await _dio.get('/api/membres/$id');
|
||||
return MembreModel.fromJson(response.data as Map<String, dynamic>);
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e, 'Erreur lors de la récupération du membre');
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée un nouveau membre
|
||||
Future<MembreModel> createMembre(MembreModel membre) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
'/api/membres',
|
||||
data: membre.toJson(),
|
||||
);
|
||||
return MembreModel.fromJson(response.data as Map<String, dynamic>);
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e, 'Erreur lors de la création du membre');
|
||||
}
|
||||
}
|
||||
|
||||
/// Met à jour un membre existant
|
||||
Future<MembreModel> updateMembre(String id, MembreModel membre) async {
|
||||
try {
|
||||
final response = await _dio.put(
|
||||
'/api/membres/$id',
|
||||
data: membre.toJson(),
|
||||
);
|
||||
return MembreModel.fromJson(response.data as Map<String, dynamic>);
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e, 'Erreur lors de la mise à jour du membre');
|
||||
}
|
||||
}
|
||||
|
||||
/// Désactive un membre
|
||||
Future<void> deleteMembre(String id) async {
|
||||
try {
|
||||
await _dio.delete('/api/membres/$id');
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e, 'Erreur lors de la suppression du membre');
|
||||
}
|
||||
}
|
||||
|
||||
/// Recherche des membres par nom ou prénom
|
||||
Future<List<MembreModel>> searchMembres(String query) async {
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
'/api/membres/recherche',
|
||||
queryParameters: {'q': query},
|
||||
);
|
||||
|
||||
if (response.data is List) {
|
||||
return (response.data as List)
|
||||
.map((json) => MembreModel.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
throw Exception('Format de réponse invalide pour la recherche');
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e, 'Erreur lors de la recherche de membres');
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère les statistiques des membres
|
||||
Future<Map<String, dynamic>> getMembresStats() async {
|
||||
try {
|
||||
final response = await _dio.get('/api/membres/stats');
|
||||
return response.data as Map<String, dynamic>;
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e, 'Erreur lors de la récupération des statistiques');
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// PAIEMENTS WAVE
|
||||
// ========================================
|
||||
|
||||
/// Crée une session de paiement Wave
|
||||
Future<WaveCheckoutSessionModel> createWaveSession({
|
||||
required double montant,
|
||||
required String devise,
|
||||
required String successUrl,
|
||||
required String errorUrl,
|
||||
String? organisationId,
|
||||
String? membreId,
|
||||
String? typePaiement,
|
||||
String? description,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
'/api/paiements/wave/sessions',
|
||||
data: {
|
||||
'montant': montant,
|
||||
'devise': devise,
|
||||
'successUrl': successUrl,
|
||||
'errorUrl': errorUrl,
|
||||
if (organisationId != null) 'organisationId': organisationId,
|
||||
if (membreId != null) 'membreId': membreId,
|
||||
if (typePaiement != null) 'typePaiement': typePaiement,
|
||||
if (description != null) 'description': description,
|
||||
},
|
||||
);
|
||||
return WaveCheckoutSessionModel.fromJson(response.data as Map<String, dynamic>);
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e, 'Erreur lors de la création de la session Wave');
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère une session de paiement Wave par son ID
|
||||
Future<WaveCheckoutSessionModel> getWaveSession(String sessionId) async {
|
||||
try {
|
||||
final response = await _dio.get('/api/paiements/wave/sessions/$sessionId');
|
||||
return WaveCheckoutSessionModel.fromJson(response.data as Map<String, dynamic>);
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e, 'Erreur lors de la récupération de la session Wave');
|
||||
}
|
||||
}
|
||||
|
||||
/// Vérifie le statut d'une session de paiement Wave
|
||||
Future<String> checkWaveSessionStatus(String sessionId) async {
|
||||
try {
|
||||
final response = await _dio.get('/api/paiements/wave/sessions/$sessionId/status');
|
||||
return response.data['statut'] as String;
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioException(e, 'Erreur lors de la vérification du statut Wave');
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// GESTION DES ERREURS
|
||||
// ========================================
|
||||
|
||||
/// Gère les exceptions Dio et les convertit en messages d'erreur appropriés
|
||||
Exception _handleDioException(DioException e, String defaultMessage) {
|
||||
switch (e.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
case DioExceptionType.sendTimeout:
|
||||
case DioExceptionType.receiveTimeout:
|
||||
return Exception('Délai d\'attente dépassé. Vérifiez votre connexion internet.');
|
||||
|
||||
case DioExceptionType.badResponse:
|
||||
final statusCode = e.response?.statusCode;
|
||||
final responseData = e.response?.data;
|
||||
|
||||
if (statusCode == 400) {
|
||||
if (responseData is Map && responseData.containsKey('message')) {
|
||||
return Exception(responseData['message']);
|
||||
}
|
||||
return Exception('Données invalides');
|
||||
} else if (statusCode == 401) {
|
||||
return Exception('Non autorisé. Veuillez vous reconnecter.');
|
||||
} else if (statusCode == 403) {
|
||||
return Exception('Accès interdit');
|
||||
} else if (statusCode == 404) {
|
||||
return Exception('Ressource non trouvée');
|
||||
} else if (statusCode == 500) {
|
||||
return Exception('Erreur serveur. Veuillez réessayer plus tard.');
|
||||
}
|
||||
|
||||
return Exception('$defaultMessage (Code: $statusCode)');
|
||||
|
||||
case DioExceptionType.cancel:
|
||||
return Exception('Requête annulée');
|
||||
|
||||
case DioExceptionType.connectionError:
|
||||
return Exception('Erreur de connexion. Vérifiez votre connexion internet.');
|
||||
|
||||
case DioExceptionType.badCertificate:
|
||||
return Exception('Certificat SSL invalide');
|
||||
|
||||
case DioExceptionType.unknown:
|
||||
default:
|
||||
return Exception(defaultMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import '../../../../core/auth/bloc/auth_event.dart';
|
||||
import '../../../../core/auth/models/auth_state.dart';
|
||||
import '../../../../core/auth/models/login_request.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../shared/widgets/buttons/buttons.dart';
|
||||
import '../widgets/login_form.dart';
|
||||
import '../widgets/login_header.dart';
|
||||
import '../widgets/login_footer.dart';
|
||||
@@ -75,16 +74,17 @@ class _LoginPageState extends State<LoginPage>
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _shakeController,
|
||||
curve: Curves.elasticInOut,
|
||||
curve: Curves.elasticIn,
|
||||
));
|
||||
}
|
||||
|
||||
void _startEntryAnimation() {
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
if (mounted) {
|
||||
_animationController.forward();
|
||||
}
|
||||
});
|
||||
|
||||
void _startShakeAnimation() {
|
||||
_shakeController.reset();
|
||||
_shakeController.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -100,39 +100,46 @@ class _LoginPageState extends State<LoginPage>
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.backgroundLight,
|
||||
body: BlocListener<AuthBloc, AuthState>(
|
||||
listener: _handleAuthStateChange,
|
||||
child: SafeArea(
|
||||
child: AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, _slideAnimation.value),
|
||||
body: BlocConsumer<AuthBloc, AuthState>(
|
||||
listener: (context, state) {
|
||||
setState(() {
|
||||
_isLoading = state.status == AuthStatus.checking;
|
||||
});
|
||||
|
||||
if (state.status == AuthStatus.error) {
|
||||
_startShakeAnimation();
|
||||
_showErrorSnackBar(state.errorMessage ?? 'Erreur de connexion');
|
||||
} else if (state.status == AuthStatus.authenticated) {
|
||||
_showSuccessSnackBar('Connexion réussie !');
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return SafeArea(
|
||||
child: _buildLoginContent(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoginContent() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
return AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(0, _slideAnimation.value),
|
||||
child: Opacity(
|
||||
opacity: _fadeAnimation.value,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 60),
|
||||
|
||||
// Header avec logo et titre
|
||||
LoginHeader(
|
||||
onAnimationComplete: () {},
|
||||
),
|
||||
const LoginHeader(),
|
||||
|
||||
const SizedBox(height: 60),
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// Formulaire de connexion
|
||||
AnimatedBuilder(
|
||||
@@ -142,9 +149,23 @@ class _LoginPageState extends State<LoginPage>
|
||||
offset: Offset(
|
||||
_shakeAnimation.value * 10 *
|
||||
(1 - _shakeAnimation.value) *
|
||||
(1 - _shakeAnimation.value),
|
||||
((_shakeAnimation.value * 10).round() % 2 == 0 ? 1 : -1),
|
||||
0,
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: LoginForm(
|
||||
formKey: _formKey,
|
||||
emailController: _emailController,
|
||||
@@ -168,6 +189,8 @@ class _LoginPageState extends State<LoginPage>
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
@@ -178,92 +201,86 @@ class _LoginPageState extends State<LoginPage>
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _handleAuthStateChange(BuildContext context, AuthState state) {
|
||||
setState(() {
|
||||
_isLoading = state.isLoading;
|
||||
});
|
||||
|
||||
if (state.status == AuthStatus.authenticated) {
|
||||
// Connexion réussie - navigation gérée par l'app principal
|
||||
_showSuccessMessage();
|
||||
HapticFeedback.heavyImpact();
|
||||
|
||||
} else if (state.status == AuthStatus.error) {
|
||||
// Erreur de connexion
|
||||
_handleLoginError(state.errorMessage ?? 'Erreur inconnue');
|
||||
|
||||
} else if (state.status == AuthStatus.unauthenticated && state.errorMessage != null) {
|
||||
// Échec de connexion
|
||||
_handleLoginError(state.errorMessage!);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleLogin() {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
_triggerShakeAnimation();
|
||||
HapticFeedback.mediumImpact();
|
||||
_startShakeAnimation();
|
||||
return;
|
||||
}
|
||||
|
||||
final email = _emailController.text.trim();
|
||||
final password = _passwordController.text;
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
if (email.isEmpty || password.isEmpty) {
|
||||
_showErrorMessage('Veuillez remplir tous les champs');
|
||||
_triggerShakeAnimation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Déclencher la connexion
|
||||
final loginRequest = LoginRequest(
|
||||
email: email,
|
||||
password: password,
|
||||
email: _emailController.text.trim(),
|
||||
password: _passwordController.text,
|
||||
rememberMe: _rememberMe,
|
||||
);
|
||||
|
||||
context.read<AuthBloc>().add(AuthLoginRequested(loginRequest));
|
||||
|
||||
// Feedback haptique
|
||||
HapticFeedback.lightImpact();
|
||||
}
|
||||
|
||||
void _handleLoginError(String errorMessage) {
|
||||
_showErrorMessage(errorMessage);
|
||||
_triggerShakeAnimation();
|
||||
HapticFeedback.mediumImpact();
|
||||
|
||||
// Effacer l'erreur après affichage
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
if (mounted) {
|
||||
context.read<AuthBloc>().add(const AuthErrorCleared());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _triggerShakeAnimation() {
|
||||
_shakeController.reset();
|
||||
_shakeController.forward();
|
||||
}
|
||||
|
||||
void _showSuccessMessage() {
|
||||
void _showErrorSnackBar(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
'Connexion réussie !',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
margin: const EdgeInsets.all(16),
|
||||
duration: const Duration(seconds: 4),
|
||||
action: SnackBarAction(
|
||||
label: 'Fermer',
|
||||
textColor: Colors.white,
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showSuccessSnackBar(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check_circle_outline,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -278,44 +295,4 @@ class _LoginPageState extends State<LoginPage>
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showErrorMessage(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
margin: const EdgeInsets.all(16),
|
||||
duration: const Duration(seconds: 4),
|
||||
action: SnackBarAction(
|
||||
label: 'OK',
|
||||
textColor: Colors.white,
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../widgets/kpi_card.dart';
|
||||
|
||||
import '../widgets/clickable_kpi_card.dart';
|
||||
import '../widgets/chart_card.dart';
|
||||
import '../widgets/activity_feed.dart';
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../../../core/models/membre_model.dart';
|
||||
import '../../../../core/services/api_service.dart';
|
||||
import '../../domain/repositories/membre_repository.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
|
||||
/// Implémentation du repository des membres
|
||||
@LazySingleton(as: MembreRepository)
|
||||
class MembreRepositoryImpl implements MembreRepository {
|
||||
final ApiService _apiService;
|
||||
|
||||
MembreRepositoryImpl(this._apiService);
|
||||
|
||||
@override
|
||||
Future<List<MembreModel>> getMembres() async {
|
||||
try {
|
||||
return await _apiService.getMembres();
|
||||
} catch (e) {
|
||||
throw ServerFailure(message: e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MembreModel> getMembreById(String id) async {
|
||||
try {
|
||||
return await _apiService.getMembreById(id);
|
||||
} catch (e) {
|
||||
throw ServerFailure(message: e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MembreModel> createMembre(MembreModel membre) async {
|
||||
try {
|
||||
return await _apiService.createMembre(membre);
|
||||
} catch (e) {
|
||||
throw ServerFailure(message: e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MembreModel> updateMembre(String id, MembreModel membre) async {
|
||||
try {
|
||||
return await _apiService.updateMembre(id, membre);
|
||||
} catch (e) {
|
||||
throw ServerFailure(message: e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteMembre(String id) async {
|
||||
try {
|
||||
await _apiService.deleteMembre(id);
|
||||
} catch (e) {
|
||||
throw ServerFailure(message: e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MembreModel>> searchMembres(String query) async {
|
||||
try {
|
||||
return await _apiService.searchMembres(query);
|
||||
} catch (e) {
|
||||
throw ServerFailure(message: e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> getMembresStats() async {
|
||||
try {
|
||||
return await _apiService.getMembresStats();
|
||||
} catch (e) {
|
||||
throw ServerFailure(message: e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import '../../../../core/models/membre_model.dart';
|
||||
|
||||
/// Interface du repository des membres
|
||||
/// Définit les opérations disponibles pour la gestion des membres
|
||||
abstract class MembreRepository {
|
||||
/// Récupère la liste de tous les membres actifs
|
||||
Future<List<MembreModel>> getMembres();
|
||||
|
||||
/// Récupère un membre par son ID
|
||||
Future<MembreModel> getMembreById(String id);
|
||||
|
||||
/// Crée un nouveau membre
|
||||
Future<MembreModel> createMembre(MembreModel membre);
|
||||
|
||||
/// Met à jour un membre existant
|
||||
Future<MembreModel> updateMembre(String id, MembreModel membre);
|
||||
|
||||
/// Désactive un membre
|
||||
Future<void> deleteMembre(String id);
|
||||
|
||||
/// Recherche des membres par nom ou prénom
|
||||
Future<List<MembreModel>> searchMembres(String query);
|
||||
|
||||
/// Récupère les statistiques des membres
|
||||
Future<Map<String, dynamic>> getMembresStats();
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../domain/repositories/membre_repository.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../../../../core/models/membre_model.dart';
|
||||
import 'membres_event.dart';
|
||||
import 'membres_state.dart';
|
||||
|
||||
/// BLoC pour la gestion des membres
|
||||
@injectable
|
||||
class MembresBloc extends Bloc<MembresEvent, MembresState> {
|
||||
final MembreRepository _membreRepository;
|
||||
|
||||
MembresBloc(this._membreRepository) : super(const MembresInitial()) {
|
||||
// Enregistrement des handlers d'événements
|
||||
on<LoadMembres>(_onLoadMembres);
|
||||
on<RefreshMembres>(_onRefreshMembres);
|
||||
on<SearchMembres>(_onSearchMembres);
|
||||
on<LoadMembreById>(_onLoadMembreById);
|
||||
on<CreateMembre>(_onCreateMembre);
|
||||
on<UpdateMembre>(_onUpdateMembre);
|
||||
on<DeleteMembre>(_onDeleteMembre);
|
||||
on<LoadMembresStats>(_onLoadMembresStats);
|
||||
on<ClearMembresError>(_onClearMembresError);
|
||||
on<ResetMembresState>(_onResetMembresState);
|
||||
}
|
||||
|
||||
/// Handler pour charger la liste des membres
|
||||
Future<void> _onLoadMembres(
|
||||
LoadMembres event,
|
||||
Emitter<MembresState> emit,
|
||||
) async {
|
||||
emit(const MembresLoading());
|
||||
|
||||
try {
|
||||
final membres = await _membreRepository.getMembres();
|
||||
emit(MembresLoaded(membres: membres));
|
||||
} catch (e) {
|
||||
final failure = _mapExceptionToFailure(e);
|
||||
emit(MembresError(failure: failure));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler pour rafraîchir la liste des membres
|
||||
Future<void> _onRefreshMembres(
|
||||
RefreshMembres event,
|
||||
Emitter<MembresState> emit,
|
||||
) async {
|
||||
// Conserver les données actuelles pendant le refresh
|
||||
final currentState = state;
|
||||
List<MembreModel> currentMembres = [];
|
||||
|
||||
if (currentState is MembresLoaded) {
|
||||
currentMembres = currentState.membres;
|
||||
emit(MembresRefreshing(currentMembres));
|
||||
} else {
|
||||
emit(const MembresLoading());
|
||||
}
|
||||
|
||||
try {
|
||||
final membres = await _membreRepository.getMembres();
|
||||
emit(MembresLoaded(membres: membres));
|
||||
} catch (e) {
|
||||
final failure = _mapExceptionToFailure(e);
|
||||
|
||||
// Si on avait des données, les conserver avec l'erreur
|
||||
if (currentMembres.isNotEmpty) {
|
||||
emit(MembresErrorWithData(
|
||||
failure: failure,
|
||||
membres: currentMembres,
|
||||
));
|
||||
} else {
|
||||
emit(MembresError(failure: failure));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler pour rechercher des membres
|
||||
Future<void> _onSearchMembres(
|
||||
SearchMembres event,
|
||||
Emitter<MembresState> emit,
|
||||
) async {
|
||||
if (event.query.trim().isEmpty) {
|
||||
// Si la recherche est vide, recharger tous les membres
|
||||
add(const LoadMembres());
|
||||
return;
|
||||
}
|
||||
|
||||
emit(const MembresLoading());
|
||||
|
||||
try {
|
||||
final membres = await _membreRepository.searchMembres(event.query);
|
||||
emit(MembresLoaded(
|
||||
membres: membres,
|
||||
isSearchResult: true,
|
||||
searchQuery: event.query,
|
||||
));
|
||||
} catch (e) {
|
||||
final failure = _mapExceptionToFailure(e);
|
||||
emit(MembresError(failure: failure));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler pour charger un membre par ID
|
||||
Future<void> _onLoadMembreById(
|
||||
LoadMembreById event,
|
||||
Emitter<MembresState> emit,
|
||||
) async {
|
||||
emit(const MembresLoading());
|
||||
|
||||
try {
|
||||
final membre = await _membreRepository.getMembreById(event.id);
|
||||
emit(MembreDetailLoaded(membre));
|
||||
} catch (e) {
|
||||
final failure = _mapExceptionToFailure(e);
|
||||
emit(MembresError(failure: failure));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler pour créer un membre
|
||||
Future<void> _onCreateMembre(
|
||||
CreateMembre event,
|
||||
Emitter<MembresState> emit,
|
||||
) async {
|
||||
emit(const MembresLoading());
|
||||
|
||||
try {
|
||||
final nouveauMembre = await _membreRepository.createMembre(event.membre);
|
||||
emit(MembreCreated(nouveauMembre));
|
||||
|
||||
// Recharger la liste après création
|
||||
add(const LoadMembres());
|
||||
} catch (e) {
|
||||
final failure = _mapExceptionToFailure(e);
|
||||
emit(MembresError(failure: failure));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler pour mettre à jour un membre
|
||||
Future<void> _onUpdateMembre(
|
||||
UpdateMembre event,
|
||||
Emitter<MembresState> emit,
|
||||
) async {
|
||||
emit(const MembresLoading());
|
||||
|
||||
try {
|
||||
final membreMisAJour = await _membreRepository.updateMembre(
|
||||
event.id,
|
||||
event.membre,
|
||||
);
|
||||
emit(MembreUpdated(membreMisAJour));
|
||||
|
||||
// Recharger la liste après mise à jour
|
||||
add(const LoadMembres());
|
||||
} catch (e) {
|
||||
final failure = _mapExceptionToFailure(e);
|
||||
emit(MembresError(failure: failure));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler pour supprimer un membre
|
||||
Future<void> _onDeleteMembre(
|
||||
DeleteMembre event,
|
||||
Emitter<MembresState> emit,
|
||||
) async {
|
||||
emit(const MembresLoading());
|
||||
|
||||
try {
|
||||
await _membreRepository.deleteMembre(event.id);
|
||||
emit(MembreDeleted(event.id));
|
||||
|
||||
// Recharger la liste après suppression
|
||||
add(const LoadMembres());
|
||||
} catch (e) {
|
||||
final failure = _mapExceptionToFailure(e);
|
||||
emit(MembresError(failure: failure));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler pour charger les statistiques
|
||||
Future<void> _onLoadMembresStats(
|
||||
LoadMembresStats event,
|
||||
Emitter<MembresState> emit,
|
||||
) async {
|
||||
emit(const MembresLoading());
|
||||
|
||||
try {
|
||||
final stats = await _membreRepository.getMembresStats();
|
||||
emit(MembresStatsLoaded(stats));
|
||||
} catch (e) {
|
||||
final failure = _mapExceptionToFailure(e);
|
||||
emit(MembresError(failure: failure));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler pour effacer les erreurs
|
||||
void _onClearMembresError(
|
||||
ClearMembresError event,
|
||||
Emitter<MembresState> emit,
|
||||
) {
|
||||
final currentState = state;
|
||||
|
||||
if (currentState is MembresError && currentState.previousState != null) {
|
||||
emit(currentState.previousState!);
|
||||
} else if (currentState is MembresErrorWithData) {
|
||||
emit(MembresLoaded(
|
||||
membres: currentState.membres,
|
||||
isSearchResult: currentState.isSearchResult,
|
||||
searchQuery: currentState.searchQuery,
|
||||
));
|
||||
} else {
|
||||
emit(const MembresInitial());
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler pour réinitialiser l'état
|
||||
void _onResetMembresState(
|
||||
ResetMembresState event,
|
||||
Emitter<MembresState> emit,
|
||||
) {
|
||||
emit(const MembresInitial());
|
||||
}
|
||||
|
||||
/// Convertit une exception en Failure approprié
|
||||
Failure _mapExceptionToFailure(dynamic exception) {
|
||||
if (exception is Failure) {
|
||||
return exception;
|
||||
}
|
||||
|
||||
final message = exception.toString();
|
||||
|
||||
if (message.contains('connexion') || message.contains('network')) {
|
||||
return NetworkFailure(message: message);
|
||||
} else if (message.contains('401') || message.contains('unauthorized')) {
|
||||
return const AuthFailure(message: 'Session expirée. Veuillez vous reconnecter.');
|
||||
} else if (message.contains('400') || message.contains('validation')) {
|
||||
return ValidationFailure(message: message);
|
||||
} else if (message.contains('500') || message.contains('server')) {
|
||||
return ServerFailure(message: message);
|
||||
}
|
||||
|
||||
return ServerFailure(message: message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../../../core/models/membre_model.dart';
|
||||
|
||||
/// Événements pour le BLoC des membres
|
||||
abstract class MembresEvent extends Equatable {
|
||||
const MembresEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// Événement pour charger la liste des membres
|
||||
class LoadMembres extends MembresEvent {
|
||||
const LoadMembres();
|
||||
}
|
||||
|
||||
/// Événement pour rafraîchir la liste des membres
|
||||
class RefreshMembres extends MembresEvent {
|
||||
const RefreshMembres();
|
||||
}
|
||||
|
||||
/// Événement pour rechercher des membres
|
||||
class SearchMembres extends MembresEvent {
|
||||
const SearchMembres(this.query);
|
||||
|
||||
final String query;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [query];
|
||||
}
|
||||
|
||||
/// Événement pour charger un membre spécifique
|
||||
class LoadMembreById extends MembresEvent {
|
||||
const LoadMembreById(this.id);
|
||||
|
||||
final String id;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id];
|
||||
}
|
||||
|
||||
/// Événement pour créer un nouveau membre
|
||||
class CreateMembre extends MembresEvent {
|
||||
const CreateMembre(this.membre);
|
||||
|
||||
final MembreModel membre;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membre];
|
||||
}
|
||||
|
||||
/// Événement pour mettre à jour un membre
|
||||
class UpdateMembre extends MembresEvent {
|
||||
const UpdateMembre(this.id, this.membre);
|
||||
|
||||
final String id;
|
||||
final MembreModel membre;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, membre];
|
||||
}
|
||||
|
||||
/// Événement pour supprimer un membre
|
||||
class DeleteMembre extends MembresEvent {
|
||||
const DeleteMembre(this.id);
|
||||
|
||||
final String id;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id];
|
||||
}
|
||||
|
||||
/// Événement pour charger les statistiques des membres
|
||||
class LoadMembresStats extends MembresEvent {
|
||||
const LoadMembresStats();
|
||||
}
|
||||
|
||||
/// Événement pour effacer les erreurs
|
||||
class ClearMembresError extends MembresEvent {
|
||||
const ClearMembresError();
|
||||
}
|
||||
|
||||
/// Événement pour réinitialiser l'état
|
||||
class ResetMembresState extends MembresEvent {
|
||||
const ResetMembresState();
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../../../core/models/membre_model.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
|
||||
/// États pour le BLoC des membres
|
||||
abstract class MembresState extends Equatable {
|
||||
const MembresState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// État initial
|
||||
class MembresInitial extends MembresState {
|
||||
const MembresInitial();
|
||||
}
|
||||
|
||||
/// État de chargement
|
||||
class MembresLoading extends MembresState {
|
||||
const MembresLoading();
|
||||
}
|
||||
|
||||
/// État de chargement avec données existantes (pour le refresh)
|
||||
class MembresRefreshing extends MembresState {
|
||||
const MembresRefreshing(this.currentMembres);
|
||||
|
||||
final List<MembreModel> currentMembres;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [currentMembres];
|
||||
}
|
||||
|
||||
/// État de succès avec liste des membres
|
||||
class MembresLoaded extends MembresState {
|
||||
const MembresLoaded({
|
||||
required this.membres,
|
||||
this.isSearchResult = false,
|
||||
this.searchQuery,
|
||||
});
|
||||
|
||||
final List<MembreModel> membres;
|
||||
final bool isSearchResult;
|
||||
final String? searchQuery;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membres, isSearchResult, searchQuery];
|
||||
|
||||
/// Copie avec modifications
|
||||
MembresLoaded copyWith({
|
||||
List<MembreModel>? membres,
|
||||
bool? isSearchResult,
|
||||
String? searchQuery,
|
||||
}) {
|
||||
return MembresLoaded(
|
||||
membres: membres ?? this.membres,
|
||||
isSearchResult: isSearchResult ?? this.isSearchResult,
|
||||
searchQuery: searchQuery ?? this.searchQuery,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// État de succès pour un membre spécifique
|
||||
class MembreDetailLoaded extends MembresState {
|
||||
const MembreDetailLoaded(this.membre);
|
||||
|
||||
final MembreModel membre;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membre];
|
||||
}
|
||||
|
||||
/// État de succès pour les statistiques
|
||||
class MembresStatsLoaded extends MembresState {
|
||||
const MembresStatsLoaded(this.stats);
|
||||
|
||||
final Map<String, dynamic> stats;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [stats];
|
||||
}
|
||||
|
||||
/// État de succès pour la création d'un membre
|
||||
class MembreCreated extends MembresState {
|
||||
const MembreCreated(this.membre);
|
||||
|
||||
final MembreModel membre;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membre];
|
||||
}
|
||||
|
||||
/// État de succès pour la mise à jour d'un membre
|
||||
class MembreUpdated extends MembresState {
|
||||
const MembreUpdated(this.membre);
|
||||
|
||||
final MembreModel membre;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membre];
|
||||
}
|
||||
|
||||
/// État de succès pour la suppression d'un membre
|
||||
class MembreDeleted extends MembresState {
|
||||
const MembreDeleted(this.membreId);
|
||||
|
||||
final String membreId;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membreId];
|
||||
}
|
||||
|
||||
/// État d'erreur
|
||||
class MembresError extends MembresState {
|
||||
const MembresError({
|
||||
required this.failure,
|
||||
this.previousState,
|
||||
});
|
||||
|
||||
final Failure failure;
|
||||
final MembresState? previousState;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [failure, previousState];
|
||||
|
||||
/// Message d'erreur formaté
|
||||
String get message => failure.message;
|
||||
|
||||
/// Code d'erreur
|
||||
String? get code => failure.code;
|
||||
|
||||
/// Indique si c'est une erreur réseau
|
||||
bool get isNetworkError => failure is NetworkFailure;
|
||||
|
||||
/// Indique si c'est une erreur serveur
|
||||
bool get isServerError => failure is ServerFailure;
|
||||
|
||||
/// Indique si c'est une erreur d'authentification
|
||||
bool get isAuthError => failure is AuthFailure;
|
||||
|
||||
/// Indique si c'est une erreur de validation
|
||||
bool get isValidationError => failure is ValidationFailure;
|
||||
}
|
||||
|
||||
/// État d'erreur avec données existantes (pour les erreurs non critiques)
|
||||
class MembresErrorWithData extends MembresState {
|
||||
const MembresErrorWithData({
|
||||
required this.failure,
|
||||
required this.membres,
|
||||
this.isSearchResult = false,
|
||||
this.searchQuery,
|
||||
});
|
||||
|
||||
final Failure failure;
|
||||
final List<MembreModel> membres;
|
||||
final bool isSearchResult;
|
||||
final String? searchQuery;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [failure, membres, isSearchResult, searchQuery];
|
||||
|
||||
/// Message d'erreur formaté
|
||||
String get message => failure.message;
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../shared/widgets/coming_soon_page.dart';
|
||||
import '../bloc/membres_bloc.dart';
|
||||
import '../bloc/membres_event.dart';
|
||||
import '../bloc/membres_state.dart';
|
||||
import '../widgets/membre_card.dart';
|
||||
import '../widgets/membres_search_bar.dart';
|
||||
|
||||
|
||||
/// Page de liste des membres avec fonctionnalités avancées
|
||||
class MembresListPage extends StatefulWidget {
|
||||
const MembresListPage({super.key});
|
||||
|
||||
@override
|
||||
State<MembresListPage> createState() => _MembresListPageState();
|
||||
}
|
||||
|
||||
class _MembresListPageState extends State<MembresListPage> {
|
||||
final RefreshController _refreshController = RefreshController();
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
late MembresBloc _membresBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_membresBloc = getIt<MembresBloc>();
|
||||
_membresBloc.add(const LoadMembres());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_refreshController.dispose();
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _membresBloc,
|
||||
child: Scaffold(
|
||||
backgroundColor: AppTheme.backgroundLight,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Membres',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add_circle_outline),
|
||||
onPressed: () => _showAddMemberDialog(),
|
||||
tooltip: 'Ajouter un membre',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.analytics_outlined),
|
||||
onPressed: () => _showStatsDialog(),
|
||||
tooltip: 'Statistiques',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Barre de recherche
|
||||
Container(
|
||||
color: AppTheme.primaryColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: MembresSearchBar(
|
||||
controller: _searchController,
|
||||
onSearch: (query) {
|
||||
_membresBloc.add(SearchMembres(query));
|
||||
},
|
||||
onClear: () {
|
||||
_searchController.clear();
|
||||
_membresBloc.add(const LoadMembres());
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Liste des membres
|
||||
Expanded(
|
||||
child: BlocConsumer<MembresBloc, MembresState>(
|
||||
listener: (context, state) {
|
||||
if (state is MembresError) {
|
||||
_showErrorSnackBar(state.message);
|
||||
} else if (state is MembresErrorWithData) {
|
||||
_showErrorSnackBar(state.message);
|
||||
}
|
||||
|
||||
// Arrêter le refresh
|
||||
if (state is! MembresRefreshing && state is! MembresLoading) {
|
||||
_refreshController.refreshCompleted();
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is MembresLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(AppTheme.primaryColor),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is MembresError) {
|
||||
return _buildErrorWidget(state);
|
||||
}
|
||||
|
||||
if (state is MembresLoaded || state is MembresErrorWithData) {
|
||||
final membres = state is MembresLoaded
|
||||
? state.membres
|
||||
: (state as MembresErrorWithData).membres;
|
||||
|
||||
final isSearchResult = state is MembresLoaded
|
||||
? state.isSearchResult
|
||||
: (state as MembresErrorWithData).isSearchResult;
|
||||
|
||||
return SmartRefresher(
|
||||
controller: _refreshController,
|
||||
onRefresh: () => _membresBloc.add(const RefreshMembres()),
|
||||
header: const WaterDropHeader(
|
||||
waterDropColor: AppTheme.primaryColor,
|
||||
),
|
||||
child: membres.isEmpty
|
||||
? _buildEmptyWidget(isSearchResult)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: membres.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: MembreCard(
|
||||
membre: membres[index],
|
||||
onTap: () => _showMemberDetails(membres[index]),
|
||||
onEdit: () => _showEditMemberDialog(membres[index]),
|
||||
onDelete: () => _showDeleteConfirmation(membres[index]),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const Center(
|
||||
child: Text(
|
||||
'Aucune donnée disponible',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Widget d'erreur avec bouton de retry
|
||||
Widget _buildErrorWidget(MembresError state) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
state.isNetworkError ? Icons.wifi_off : Icons.error_outline,
|
||||
size: 64,
|
||||
color: AppTheme.errorColor,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
state.isNetworkError
|
||||
? 'Problème de connexion'
|
||||
: 'Une erreur est survenue',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
state.message,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _membresBloc.add(const LoadMembres()),
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Réessayer'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Widget vide (aucun membre trouvé)
|
||||
Widget _buildEmptyWidget(bool isSearchResult) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
isSearchResult ? Icons.search_off : Icons.people_outline,
|
||||
size: 64,
|
||||
color: AppTheme.textHint,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
isSearchResult
|
||||
? 'Aucun membre trouvé'
|
||||
: 'Aucun membre enregistré',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
isSearchResult
|
||||
? 'Essayez avec d\'autres termes de recherche'
|
||||
: 'Commencez par ajouter votre premier membre',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
if (!isSearchResult) ...[
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _showAddMemberDialog,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Ajouter un membre'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Affiche une snackbar d'erreur
|
||||
void _showErrorSnackBar(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
action: SnackBarAction(
|
||||
label: 'Fermer',
|
||||
textColor: Colors.white,
|
||||
onPressed: () => ScaffoldMessenger.of(context).hideCurrentSnackBar(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Affiche les détails d'un membre
|
||||
void _showMemberDetails(membre) {
|
||||
// TODO: Implémenter la page de détails
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => const ComingSoonPage(
|
||||
title: 'Détails du membre',
|
||||
description: 'La page de détails du membre sera bientôt disponible.',
|
||||
icon: Icons.person,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Affiche le dialog d'ajout de membre
|
||||
void _showAddMemberDialog() {
|
||||
// TODO: Implémenter le formulaire d'ajout
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => const ComingSoonPage(
|
||||
title: 'Ajouter un membre',
|
||||
description: 'Le formulaire d\'ajout de membre sera bientôt disponible.',
|
||||
icon: Icons.person_add,
|
||||
color: AppTheme.successColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Affiche le dialog d'édition de membre
|
||||
void _showEditMemberDialog(membre) {
|
||||
// TODO: Implémenter le formulaire d'édition
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => const ComingSoonPage(
|
||||
title: 'Modifier le membre',
|
||||
description: 'Le formulaire de modification sera bientôt disponible.',
|
||||
icon: Icons.edit,
|
||||
color: AppTheme.warningColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Affiche la confirmation de suppression
|
||||
void _showDeleteConfirmation(membre) {
|
||||
// TODO: Implémenter la confirmation de suppression
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => const ComingSoonPage(
|
||||
title: 'Supprimer le membre',
|
||||
description: 'La confirmation de suppression sera bientôt disponible.',
|
||||
icon: Icons.delete,
|
||||
color: AppTheme.errorColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Affiche les statistiques
|
||||
void _showStatsDialog() {
|
||||
// TODO: Implémenter les statistiques
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => const ComingSoonPage(
|
||||
title: 'Statistiques',
|
||||
description: 'Les statistiques des membres seront bientôt disponibles.',
|
||||
icon: Icons.analytics,
|
||||
color: AppTheme.infoColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/models/membre_model.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
|
||||
/// Card pour afficher un membre dans la liste
|
||||
class MembreCard extends StatelessWidget {
|
||||
const MembreCard({
|
||||
super.key,
|
||||
required this.membre,
|
||||
this.onTap,
|
||||
this.onEdit,
|
||||
this.onDelete,
|
||||
});
|
||||
|
||||
final MembreModel membre;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onEdit;
|
||||
final VoidCallback? onDelete;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header avec avatar et actions
|
||||
Row(
|
||||
children: [
|
||||
// Avatar
|
||||
CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
child: Text(
|
||||
membre.initiales,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Informations principales
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
membre.nomComplet,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
membre.numeroMembre,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Badge de statut
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor(membre.statut).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: _getStatusColor(membre.statut),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
_getStatusLabel(membre.statut),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _getStatusColor(membre.statut),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Menu d'actions
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(
|
||||
Icons.more_vert,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
onSelected: (value) {
|
||||
switch (value) {
|
||||
case 'edit':
|
||||
onEdit?.call();
|
||||
break;
|
||||
case 'delete':
|
||||
onDelete?.call();
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.edit, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Text('Modifier'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.delete, size: 16, color: AppTheme.errorColor),
|
||||
SizedBox(width: 8),
|
||||
Text('Supprimer', style: TextStyle(color: AppTheme.errorColor)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Informations de contact
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildInfoItem(
|
||||
icon: Icons.email_outlined,
|
||||
text: membre.email,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildInfoItem(
|
||||
icon: Icons.phone_outlined,
|
||||
text: membre.telephone,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Adresse si disponible
|
||||
if (membre.adresseComplete.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
_buildInfoItem(
|
||||
icon: Icons.location_on_outlined,
|
||||
text: membre.adresseComplete,
|
||||
),
|
||||
],
|
||||
|
||||
// Profession si disponible
|
||||
if (membre.profession?.isNotEmpty == true) ...[
|
||||
const SizedBox(height: 8),
|
||||
_buildInfoItem(
|
||||
icon: Icons.work_outline,
|
||||
text: membre.profession!,
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Footer avec date d'adhésion
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.calendar_today_outlined,
|
||||
size: 14,
|
||||
color: AppTheme.textHint,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Membre depuis ${_formatDate(membre.dateAdhesion)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textHint,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Widget pour afficher une information avec icône
|
||||
Widget _buildInfoItem({
|
||||
required IconData icon,
|
||||
required String text,
|
||||
}) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 14,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Retourne la couleur associée au statut
|
||||
Color _getStatusColor(String statut) {
|
||||
switch (statut.toUpperCase()) {
|
||||
case 'ACTIF':
|
||||
return AppTheme.successColor;
|
||||
case 'INACTIF':
|
||||
return AppTheme.warningColor;
|
||||
case 'SUSPENDU':
|
||||
return AppTheme.errorColor;
|
||||
default:
|
||||
return AppTheme.textSecondary;
|
||||
}
|
||||
}
|
||||
|
||||
/// Retourne le label du statut
|
||||
String _getStatusLabel(String statut) {
|
||||
switch (statut.toUpperCase()) {
|
||||
case 'ACTIF':
|
||||
return 'ACTIF';
|
||||
case 'INACTIF':
|
||||
return 'INACTIF';
|
||||
case 'SUSPENDU':
|
||||
return 'SUSPENDU';
|
||||
default:
|
||||
return statut.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
/// Formate une date pour l'affichage
|
||||
String _formatDate(DateTime date) {
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(date);
|
||||
|
||||
if (difference.inDays < 30) {
|
||||
return '${difference.inDays} jours';
|
||||
} else if (difference.inDays < 365) {
|
||||
final months = (difference.inDays / 30).floor();
|
||||
return '$months mois';
|
||||
} else {
|
||||
final years = (difference.inDays / 365).floor();
|
||||
return '$years an${years > 1 ? 's' : ''}';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
|
||||
/// Barre de recherche pour les membres
|
||||
class MembresSearchBar extends StatefulWidget {
|
||||
const MembresSearchBar({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.onSearch,
|
||||
required this.onClear,
|
||||
this.hintText = 'Rechercher un membre...',
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final ValueChanged<String> onSearch;
|
||||
final VoidCallback onClear;
|
||||
final String hintText;
|
||||
|
||||
@override
|
||||
State<MembresSearchBar> createState() => _MembresSearchBarState();
|
||||
}
|
||||
|
||||
class _MembresSearchBarState extends State<MembresSearchBar> {
|
||||
bool _isSearching = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.controller.addListener(_onTextChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(_onTextChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTextChanged() {
|
||||
final hasText = widget.controller.text.isNotEmpty;
|
||||
if (_isSearching != hasText) {
|
||||
setState(() {
|
||||
_isSearching = hasText;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onSubmitted(String value) {
|
||||
if (value.trim().isNotEmpty) {
|
||||
widget.onSearch(value.trim());
|
||||
} else {
|
||||
widget.onClear();
|
||||
}
|
||||
}
|
||||
|
||||
void _onClearPressed() {
|
||||
widget.controller.clear();
|
||||
widget.onClear();
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: widget.controller,
|
||||
onSubmitted: _onSubmitted,
|
||||
textInputAction: TextInputAction.search,
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText,
|
||||
hintStyle: const TextStyle(
|
||||
color: AppTheme.textHint,
|
||||
fontSize: 16,
|
||||
),
|
||||
prefixIcon: const Icon(
|
||||
Icons.search,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
suffixIcon: _isSearching
|
||||
? IconButton(
|
||||
icon: const Icon(
|
||||
Icons.clear,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
onPressed: _onClearPressed,
|
||||
tooltip: 'Effacer la recherche',
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(
|
||||
color: AppTheme.primaryColor,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
|
||||
/// Card pour afficher les statistiques des membres
|
||||
class MembresStatsCard extends StatelessWidget {
|
||||
const MembresStatsCard({
|
||||
super.key,
|
||||
required this.stats,
|
||||
});
|
||||
|
||||
final Map<String, dynamic> stats;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final nombreMembresActifs = stats['nombreMembresActifs'] as int? ?? 0;
|
||||
final nombreMembresInactifs = stats['nombreMembresInactifs'] as int? ?? 0;
|
||||
final nombreMembresSuspendus = stats['nombreMembresSuspendus'] as int? ?? 0;
|
||||
final total = nombreMembresActifs + nombreMembresInactifs + nombreMembresSuspendus;
|
||||
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.analytics,
|
||||
color: AppTheme.primaryColor,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Statistiques des membres',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Statistiques principales
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatItem(
|
||||
title: 'Total',
|
||||
value: total.toString(),
|
||||
color: AppTheme.primaryColor,
|
||||
icon: Icons.people,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildStatItem(
|
||||
title: 'Actifs',
|
||||
value: nombreMembresActifs.toString(),
|
||||
color: AppTheme.successColor,
|
||||
icon: Icons.check_circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatItem(
|
||||
title: 'Inactifs',
|
||||
value: nombreMembresInactifs.toString(),
|
||||
color: AppTheme.warningColor,
|
||||
icon: Icons.pause_circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildStatItem(
|
||||
title: 'Suspendus',
|
||||
value: nombreMembresSuspendus.toString(),
|
||||
color: AppTheme.errorColor,
|
||||
icon: Icons.block,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (total > 0) ...[
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Graphique en secteurs
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: PieChart(
|
||||
PieChartData(
|
||||
sections: [
|
||||
if (nombreMembresActifs > 0)
|
||||
PieChartSectionData(
|
||||
value: nombreMembresActifs.toDouble(),
|
||||
title: '${(nombreMembresActifs / total * 100).round()}%',
|
||||
color: AppTheme.successColor,
|
||||
radius: 60,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
if (nombreMembresInactifs > 0)
|
||||
PieChartSectionData(
|
||||
value: nombreMembresInactifs.toDouble(),
|
||||
title: '${(nombreMembresInactifs / total * 100).round()}%',
|
||||
color: AppTheme.warningColor,
|
||||
radius: 60,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
if (nombreMembresSuspendus > 0)
|
||||
PieChartSectionData(
|
||||
value: nombreMembresSuspendus.toDouble(),
|
||||
title: '${(nombreMembresSuspendus / total * 100).round()}%',
|
||||
color: AppTheme.errorColor,
|
||||
radius: 60,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
centerSpaceRadius: 40,
|
||||
sectionsSpace: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Légende
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
if (nombreMembresActifs > 0)
|
||||
_buildLegendItem('Actifs', AppTheme.successColor),
|
||||
if (nombreMembresInactifs > 0)
|
||||
_buildLegendItem('Inactifs', AppTheme.warningColor),
|
||||
if (nombreMembresSuspendus > 0)
|
||||
_buildLegendItem('Suspendus', AppTheme.errorColor),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Widget pour une statistique individuelle
|
||||
Widget _buildStatItem({
|
||||
required String title,
|
||||
required String value,
|
||||
required Color color,
|
||||
required IconData icon,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: color.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Widget pour un élément de légende
|
||||
Widget _buildLegendItem(String label, Color color) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../shared/widgets/coming_soon_page.dart';
|
||||
import '../../../../shared/widgets/buttons/buttons.dart';
|
||||
import '../../../dashboard/presentation/pages/enhanced_dashboard.dart';
|
||||
import '../../../members/presentation/pages/members_list_page.dart';
|
||||
import '../../../members/presentation/pages/membres_list_page.dart';
|
||||
import '../widgets/custom_bottom_nav_bar.dart';
|
||||
|
||||
class MainNavigation extends StatefulWidget {
|
||||
@@ -17,14 +17,12 @@ class MainNavigation extends StatefulWidget {
|
||||
class _MainNavigationState extends State<MainNavigation>
|
||||
with TickerProviderStateMixin {
|
||||
int _currentIndex = 0;
|
||||
late PageController _pageController;
|
||||
late AnimationController _fabAnimationController;
|
||||
late Animation<double> _fabAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pageController = PageController();
|
||||
|
||||
_fabAnimationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
@@ -44,7 +42,6 @@ class _MainNavigationState extends State<MainNavigation>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
_fabAnimationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -85,9 +82,8 @@ class _MainNavigationState extends State<MainNavigation>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: PageView(
|
||||
controller: _pageController,
|
||||
onPageChanged: _onPageChanged,
|
||||
body: IndexedStack(
|
||||
index: _currentIndex,
|
||||
children: [
|
||||
EnhancedDashboard(
|
||||
onNavigateToTab: _onTabTapped,
|
||||
@@ -151,7 +147,10 @@ class _MainNavigationState extends State<MainNavigation>
|
||||
}
|
||||
}
|
||||
|
||||
void _onPageChanged(int index) {
|
||||
|
||||
|
||||
void _onTabTapped(int index) {
|
||||
if (_currentIndex != index) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
@@ -166,19 +165,6 @@ class _MainNavigationState extends State<MainNavigation>
|
||||
// Vibration légère
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
|
||||
void _onTabTapped(int index) {
|
||||
if (_currentIndex != index) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
|
||||
_pageController.animateToPage(
|
||||
index,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onFabPressed() {
|
||||
@@ -219,11 +205,11 @@ class _MainNavigationState extends State<MainNavigation>
|
||||
}
|
||||
|
||||
Widget _buildMembresPage() {
|
||||
return MembersListPage();
|
||||
return const MembresListPage();
|
||||
}
|
||||
|
||||
Widget _buildCotisationsPage() {
|
||||
return ComingSoonPage(
|
||||
return const ComingSoonPage(
|
||||
title: 'Module Cotisations',
|
||||
description: 'Suivi et gestion des cotisations avec paiements automatiques',
|
||||
icon: Icons.payment_rounded,
|
||||
@@ -240,7 +226,7 @@ class _MainNavigationState extends State<MainNavigation>
|
||||
}
|
||||
|
||||
Widget _buildEventsPage() {
|
||||
return ComingSoonPage(
|
||||
return const ComingSoonPage(
|
||||
title: 'Module Événements',
|
||||
description: 'Organisation et gestion d\'événements avec calendrier intégré',
|
||||
icon: Icons.event_rounded,
|
||||
@@ -257,21 +243,42 @@ class _MainNavigationState extends State<MainNavigation>
|
||||
}
|
||||
|
||||
Widget _buildMorePage() {
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.backgroundLight,
|
||||
appBar: AppBar(
|
||||
title: const Text('Plus'),
|
||||
backgroundColor: AppTheme.infoColor,
|
||||
elevation: 0,
|
||||
automaticallyImplyLeading: false,
|
||||
actions: [
|
||||
return Container(
|
||||
color: AppTheme.backgroundLight,
|
||||
child: Column(
|
||||
children: [
|
||||
// Header personnalisé au lieu d'AppBar
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(16, 50, 16, 16),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppTheme.infoColor,
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(20),
|
||||
bottomRight: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Plus',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
icon: const Icon(Icons.settings, color: Colors.white),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
),
|
||||
// Contenu scrollable
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_buildMoreSection(
|
||||
@@ -305,6 +312,9 @@ class _MainNavigationState extends State<MainNavigation>
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,16 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'core/auth/bloc/temp_auth_bloc.dart';
|
||||
import 'core/auth/bloc/auth_event.dart';
|
||||
import 'core/auth/services/temp_auth_service.dart';
|
||||
import 'core/di/injection.dart';
|
||||
import 'shared/theme/app_theme.dart';
|
||||
import 'app_temp.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Configuration de l'injection de dépendances
|
||||
await configureDependencies();
|
||||
|
||||
// Configuration du système
|
||||
await _configureApp();
|
||||
|
||||
|
||||
@@ -4,13 +4,18 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'core/auth/bloc/temp_auth_bloc.dart';
|
||||
import 'core/auth/bloc/auth_event.dart';
|
||||
import 'core/auth/services/ultra_simple_auth_service.dart';
|
||||
import 'core/auth/services/temp_auth_service.dart';
|
||||
import 'core/di/injection.dart';
|
||||
|
||||
import 'shared/theme/app_theme.dart';
|
||||
import 'app_ultra_simple.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Configuration de l'injection de dépendances
|
||||
await configureDependencies();
|
||||
|
||||
// Configuration du système
|
||||
await _configureApp();
|
||||
|
||||
@@ -39,7 +44,7 @@ Future<void> _configureApp() async {
|
||||
|
||||
/// Classe BLoC ultra-simple qui utilise UltraSimpleAuthService
|
||||
class UltraSimpleAuthBloc extends TempAuthBloc {
|
||||
UltraSimpleAuthBloc(UltraSimpleAuthService authService) : super(authService);
|
||||
UltraSimpleAuthBloc(TempAuthService authService) : super(authService);
|
||||
}
|
||||
|
||||
/// Application principale ultra-simple
|
||||
@@ -50,7 +55,7 @@ class UnionFlowUltraSimpleApp extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<UltraSimpleAuthBloc>(
|
||||
create: (context) {
|
||||
final authService = UltraSimpleAuthService();
|
||||
final authService = TempAuthService();
|
||||
final authBloc = UltraSimpleAuthBloc(authService);
|
||||
authBloc.add(const AuthInitializeRequested());
|
||||
return authBloc;
|
||||
|
||||
@@ -118,6 +118,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.11.1"
|
||||
cached_network_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cached_network_image
|
||||
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.1"
|
||||
cached_network_image_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_platform_interface
|
||||
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.1"
|
||||
cached_network_image_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_web
|
||||
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -267,6 +291,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.6"
|
||||
flutter_cache_manager:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_cache_manager
|
||||
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.1"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -275,6 +307,54 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_secure_storage
|
||||
sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.2.4"
|
||||
flutter_secure_storage_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_linux
|
||||
sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.3"
|
||||
flutter_secure_storage_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_macos
|
||||
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
flutter_secure_storage_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_platform_interface
|
||||
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
flutter_secure_storage_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_web
|
||||
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
flutter_secure_storage_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_windows
|
||||
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -317,6 +397,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -374,13 +462,21 @@ packages:
|
||||
source: hosted
|
||||
version: "0.6.7"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
json_serializable:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: json_serializable
|
||||
sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.9.0"
|
||||
jwt_decoder:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -469,6 +565,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
mockito:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: mockito
|
||||
sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.4"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -477,6 +581,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
octo_image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: octo_image
|
||||
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -485,6 +597,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
package_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.3.1"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -493,6 +621,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.15"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -573,6 +725,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
pull_to_refresh:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pull_to_refresh
|
||||
sha256: bbadd5a931837b57739cf08736bea63167e284e71fb23b218c8c9a6e042aad12
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
recase:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -581,6 +741,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
rxdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rxdart
|
||||
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.28.0"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -653,6 +821,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
shimmer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shimmer
|
||||
sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@@ -666,6 +842,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
source_helper:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_helper
|
||||
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.5"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -674,6 +858,54 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sprintf
|
||||
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
sqflite:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
sqflite_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_android
|
||||
sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.4+6"
|
||||
sqflite_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_darwin
|
||||
sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1+1"
|
||||
sqflite_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_platform_interface
|
||||
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -706,6 +938,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.0+3"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -738,6 +978,78 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.1"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.14"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.3"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.3"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
uuid:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: uuid
|
||||
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.1"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -786,6 +1098,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.10.1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -20,7 +20,7 @@ dependencies:
|
||||
intl: ^0.19.0
|
||||
|
||||
# Authentication (versions compatibles)
|
||||
# flutter_secure_storage: ^9.2.2 # Temporairement désactivé pour Android
|
||||
flutter_secure_storage: ^9.2.2
|
||||
jwt_decoder: ^2.0.1
|
||||
crypto: ^3.0.5
|
||||
shared_preferences: ^2.3.2
|
||||
@@ -32,12 +32,27 @@ dependencies:
|
||||
get_it: ^7.7.0
|
||||
injectable: ^2.4.4
|
||||
|
||||
# JSON serialization
|
||||
json_annotation: ^4.9.0
|
||||
|
||||
# UI Components
|
||||
cached_network_image: ^3.4.1
|
||||
shimmer: ^3.0.0
|
||||
pull_to_refresh: ^2.0.0
|
||||
|
||||
# Utils
|
||||
uuid: ^4.5.1
|
||||
url_launcher: ^6.3.1
|
||||
package_info_plus: ^8.0.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^4.0.0
|
||||
injectable_generator: ^2.6.2
|
||||
build_runner: ^2.4.13
|
||||
json_serializable: ^6.8.0
|
||||
mockito: ^5.4.4
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
@@ -432,6 +432,31 @@ class FormuleAbonnementDTOBasicTest {
|
||||
formule.setPersonnalisationInterface(false);
|
||||
// Score = (10 + 15) * 100 / 100 = 25
|
||||
assertThat(formule.getScoreFonctionnalites()).isEqualTo(25);
|
||||
|
||||
// Test spécifique pour couvrir toutes les branches de l'expression ternaire
|
||||
// Cas où score = 0 mais total > 0 (toutes fonctionnalités false)
|
||||
formule.setSupportTechnique(false);
|
||||
formule.setSauvegardeAutomatique(false);
|
||||
formule.setFonctionnalitesAvancees(false);
|
||||
formule.setApiAccess(false);
|
||||
formule.setRapportsPersonnalises(false);
|
||||
formule.setIntegrationsTierces(false);
|
||||
formule.setMultiLangues(false);
|
||||
formule.setPersonnalisationInterface(false);
|
||||
// total = 100, score = 0, donc (0 * 100) / 100 = 0
|
||||
assertThat(formule.getScoreFonctionnalites()).isEqualTo(0);
|
||||
|
||||
// Test avec un seul élément activé pour vérifier la division
|
||||
formule.setSupportTechnique(true); // score = 10, total = 100
|
||||
formule.setSauvegardeAutomatique(false);
|
||||
formule.setFonctionnalitesAvancees(false);
|
||||
formule.setApiAccess(false);
|
||||
formule.setRapportsPersonnalises(false);
|
||||
formule.setIntegrationsTierces(false);
|
||||
formule.setMultiLangues(false);
|
||||
formule.setPersonnalisationInterface(false);
|
||||
// Score = (10 * 100) / 100 = 10
|
||||
assertThat(formule.getScoreFonctionnalites()).isEqualTo(10);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
43
unionflow-server-impl-quarkus/docker-compose.dev.yml
Normal file
43
unionflow-server-impl-quarkus/docker-compose.dev.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres-dev:
|
||||
image: postgres:15-alpine
|
||||
container_name: unionflow-postgres-dev
|
||||
environment:
|
||||
POSTGRES_DB: unionflow_dev
|
||||
POSTGRES_USER: unionflow_dev
|
||||
POSTGRES_PASSWORD: dev123
|
||||
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_dev_data:/var/lib/postgresql/data
|
||||
- ./src/main/resources/db/init:/docker-entrypoint-initdb.d
|
||||
networks:
|
||||
- unionflow-dev
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U unionflow_dev -d unionflow_dev"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
adminer:
|
||||
image: adminer:4.8.1
|
||||
container_name: unionflow-adminer
|
||||
ports:
|
||||
- "8081:8080"
|
||||
networks:
|
||||
- unionflow-dev
|
||||
depends_on:
|
||||
- postgres-dev
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_dev_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
unionflow-dev:
|
||||
driver: bridge
|
||||
@@ -80,15 +80,18 @@ quarkus:
|
||||
"%dev":
|
||||
quarkus:
|
||||
datasource:
|
||||
username: unionflow_dev
|
||||
password: dev123
|
||||
db-kind: h2
|
||||
username: sa
|
||||
password: ""
|
||||
jdbc:
|
||||
url: jdbc:postgresql://localhost:5432/unionflow_dev
|
||||
url: jdbc:h2:mem:unionflow_dev;DB_CLOSE_DELAY=-1;MODE=PostgreSQL
|
||||
hibernate-orm:
|
||||
database:
|
||||
generation: drop-and-create
|
||||
log:
|
||||
sql: true
|
||||
flyway:
|
||||
migrate-at-start: false
|
||||
log:
|
||||
category:
|
||||
"dev.lions.unionflow": DEBUG
|
||||
|
||||
Reference in New Issue
Block a user