feat: WebSocket temps réel + Finance Workflow + corrections
- Task #6: WebSocket /ws/dashboard + Kafka events (5 topics) * Backend: KafkaEventProducer, KafkaEventConsumer * Mobile: WebSocketService (reconnection, heartbeat, typed events) * DashboardBloc: Auto-refresh depuis WebSocket events - Finance Workflow: approbations + budgets (backend + mobile) * Backend: entities, services, resources, migrations Flyway V6 * Mobile: features finance_workflow complète avec BLoC - Corrections DI: interfaces IRepository partout * IProfileRepository, IOrganizationRepository, IMembreRepository * GetIt configuré avec @injectable - Spec-Kit: constitution + templates mis à jour * .specify/memory/constitution.md enrichie * Templates agent, plan, spec, tasks, checklist - Nettoyage: fichiers temporaires supprimés Signed-off-by: lions dev Team
This commit is contained in:
@@ -3,17 +3,21 @@ library adhesions_bloc;
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../../core/utils/logger.dart';
|
||||
import '../data/models/adhesion_model.dart';
|
||||
import '../data/repositories/adhesion_repository.dart';
|
||||
|
||||
part 'adhesions_event.dart';
|
||||
part 'adhesions_state.dart';
|
||||
|
||||
@injectable
|
||||
class AdhesionsBloc extends Bloc<AdhesionsEvent, AdhesionsState> {
|
||||
final AdhesionRepository _repository;
|
||||
|
||||
AdhesionsBloc(this._repository) : super(const AdhesionsState()) {
|
||||
on<LoadAdhesions>(_onLoadAdhesions);
|
||||
on<LoadAdhesionsByMembre>(_onLoadAdhesionsByMembre);
|
||||
on<LoadAdhesionsEnAttente>(_onLoadAdhesionsEnAttente);
|
||||
on<LoadAdhesionsByStatut>(_onLoadAdhesionsByStatut);
|
||||
on<LoadAdhesionById>(_onLoadAdhesionById);
|
||||
@@ -34,6 +38,17 @@ class AdhesionsBloc extends Bloc<AdhesionsEvent, AdhesionsState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLoadAdhesionsByMembre(LoadAdhesionsByMembre event, Emitter<AdhesionsState> emit) async {
|
||||
emit(state.copyWith(status: AdhesionsStatus.loading, message: 'Chargement...'));
|
||||
try {
|
||||
final list = await _repository.getByMembre(event.membreId, page: event.page, size: event.size);
|
||||
emit(state.copyWith(status: AdhesionsStatus.loaded, adhesions: list));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(status: AdhesionsStatus.error, message: e.toString(), error: e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> _onLoadAdhesionsEnAttente(LoadAdhesionsEnAttente event, Emitter<AdhesionsState> emit) async {
|
||||
emit(state.copyWith(status: AdhesionsStatus.loading, message: 'Chargement...'));
|
||||
try {
|
||||
@@ -116,6 +131,13 @@ class AdhesionsBloc extends Bloc<AdhesionsEvent, AdhesionsState> {
|
||||
try {
|
||||
final stats = await _repository.getStats();
|
||||
emit(state.copyWith(stats: stats));
|
||||
} catch (_) {}
|
||||
} catch (e, st) {
|
||||
AppLogger.error('AdhesionsBloc: chargement stats échoué', error: e, stackTrace: st);
|
||||
emit(state.copyWith(
|
||||
status: AdhesionsStatus.error,
|
||||
message: e.toString(),
|
||||
error: e,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,15 @@ class LoadAdhesions extends AdhesionsEvent {
|
||||
List<Object?> get props => [page, size];
|
||||
}
|
||||
|
||||
class LoadAdhesionsByMembre extends AdhesionsEvent {
|
||||
final String membreId;
|
||||
final int page;
|
||||
final int size;
|
||||
const LoadAdhesionsByMembre(this.membreId, {this.page = 0, this.size = 20});
|
||||
@override
|
||||
List<Object?> get props => [membreId, page, size];
|
||||
}
|
||||
|
||||
class LoadAdhesionsEnAttente extends AdhesionsEvent {
|
||||
final int page;
|
||||
final int size;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
library adhesion_repository;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:unionflow_mobile_apps/core/network/api_client.dart';
|
||||
import '../models/adhesion_model.dart';
|
||||
|
||||
abstract class AdhesionRepository {
|
||||
@@ -24,30 +26,44 @@ abstract class AdhesionRepository {
|
||||
Future<Map<String, dynamic>?> getStats();
|
||||
}
|
||||
|
||||
@LazySingleton(as: AdhesionRepository)
|
||||
class AdhesionRepositoryImpl implements AdhesionRepository {
|
||||
final Dio _dio;
|
||||
final ApiClient _apiClient;
|
||||
static const String _base = '/api/adhesions';
|
||||
|
||||
AdhesionRepositoryImpl(this._dio);
|
||||
AdhesionRepositoryImpl(this._apiClient);
|
||||
|
||||
/// Parse une réponse API : liste directe ou objet paginé avec clé "content".
|
||||
List<AdhesionModel> _parseListResponse(dynamic data) {
|
||||
if (data is List) {
|
||||
return data
|
||||
.map((e) => AdhesionModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
if (data is Map && data.containsKey('content')) {
|
||||
final content = data['content'] as List<dynamic>? ?? [];
|
||||
return content
|
||||
.map((e) => AdhesionModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<AdhesionModel>> getAll({int page = 0, int size = 20}) async {
|
||||
final response = await _dio.get(
|
||||
final response = await _apiClient.get(
|
||||
_base,
|
||||
queryParameters: {'page': page, 'size': size},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = response.data as List<dynamic>;
|
||||
return data
|
||||
.map((e) => AdhesionModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
return _parseListResponse(response.data);
|
||||
}
|
||||
throw Exception('Erreur ${response.statusCode}');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AdhesionModel?> getById(String id) async {
|
||||
final response = await _dio.get('$_base/$id');
|
||||
final response = await _apiClient.get('$_base/$id');
|
||||
if (response.statusCode == 200) {
|
||||
return AdhesionModel.fromJson(response.data as Map<String, dynamic>);
|
||||
}
|
||||
@@ -59,7 +75,7 @@ class AdhesionRepositoryImpl implements AdhesionRepository {
|
||||
Future<AdhesionModel> create(AdhesionModel adhesion) async {
|
||||
final body = adhesion.toJson();
|
||||
// Backend attend membreId, organisationId, fraisAdhesion, codeDevise (optionnel)
|
||||
final response = await _dio.post(_base, data: body);
|
||||
final response = await _apiClient.post(_base, data: body);
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
return AdhesionModel.fromJson(response.data as Map<String, dynamic>);
|
||||
}
|
||||
@@ -68,7 +84,7 @@ class AdhesionRepositoryImpl implements AdhesionRepository {
|
||||
|
||||
@override
|
||||
Future<AdhesionModel> approuver(String id, {String? approuvePar}) async {
|
||||
final response = await _dio.post(
|
||||
final response = await _apiClient.post(
|
||||
'$_base/$id/approuver',
|
||||
queryParameters: approuvePar != null ? {'approuvePar': approuvePar} : null,
|
||||
);
|
||||
@@ -80,7 +96,7 @@ class AdhesionRepositoryImpl implements AdhesionRepository {
|
||||
|
||||
@override
|
||||
Future<AdhesionModel> rejeter(String id, String motifRejet) async {
|
||||
final response = await _dio.post(
|
||||
final response = await _apiClient.post(
|
||||
'$_base/$id/rejeter',
|
||||
queryParameters: {'motifRejet': motifRejet},
|
||||
);
|
||||
@@ -100,7 +116,7 @@ class AdhesionRepositoryImpl implements AdhesionRepository {
|
||||
final q = <String, dynamic>{'montantPaye': montantPaye};
|
||||
if (methodePaiement != null) q['methodePaiement'] = methodePaiement;
|
||||
if (referencePaiement != null) q['referencePaiement'] = referencePaiement;
|
||||
final response = await _dio.post('$_base/$id/paiement', queryParameters: q);
|
||||
final response = await _apiClient.post('$_base/$id/paiement', queryParameters: q);
|
||||
if (response.statusCode == 200) {
|
||||
return AdhesionModel.fromJson(response.data as Map<String, dynamic>);
|
||||
}
|
||||
@@ -109,67 +125,55 @@ class AdhesionRepositoryImpl implements AdhesionRepository {
|
||||
|
||||
@override
|
||||
Future<List<AdhesionModel>> getByMembre(String membreId, {int page = 0, int size = 20}) async {
|
||||
final response = await _dio.get(
|
||||
final response = await _apiClient.get(
|
||||
'$_base/membre/$membreId',
|
||||
queryParameters: {'page': page, 'size': size},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = response.data as List<dynamic>;
|
||||
return data
|
||||
.map((e) => AdhesionModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
return _parseListResponse(response.data);
|
||||
}
|
||||
throw Exception('Erreur ${response.statusCode}');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<AdhesionModel>> getByOrganisation(String organisationId, {int page = 0, int size = 20}) async {
|
||||
final response = await _dio.get(
|
||||
final response = await _apiClient.get(
|
||||
'$_base/organisation/$organisationId',
|
||||
queryParameters: {'page': page, 'size': size},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = response.data as List<dynamic>;
|
||||
return data
|
||||
.map((e) => AdhesionModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
return _parseListResponse(response.data);
|
||||
}
|
||||
throw Exception('Erreur ${response.statusCode}');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<AdhesionModel>> getByStatut(String statut, {int page = 0, int size = 20}) async {
|
||||
final response = await _dio.get(
|
||||
final response = await _apiClient.get(
|
||||
'$_base/statut/$statut',
|
||||
queryParameters: {'page': page, 'size': size},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = response.data as List<dynamic>;
|
||||
return data
|
||||
.map((e) => AdhesionModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
return _parseListResponse(response.data);
|
||||
}
|
||||
throw Exception('Erreur ${response.statusCode}');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<AdhesionModel>> getEnAttente({int page = 0, int size = 20}) async {
|
||||
final response = await _dio.get(
|
||||
final response = await _apiClient.get(
|
||||
'$_base/en-attente',
|
||||
queryParameters: {'page': page, 'size': size},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = response.data as List<dynamic>;
|
||||
return data
|
||||
.map((e) => AdhesionModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
return _parseListResponse(response.data);
|
||||
}
|
||||
throw Exception('Erreur ${response.statusCode}');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>?> getStats() async {
|
||||
final response = await _dio.get('$_base/stats');
|
||||
final response = await _apiClient.get('$_base/stats');
|
||||
if (response.statusCode == 200) {
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
/// Configuration de l'injection de dépendances pour le module Adhésions
|
||||
library adhesions_di;
|
||||
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../bloc/adhesions_bloc.dart';
|
||||
import '../data/repositories/adhesion_repository.dart';
|
||||
|
||||
void registerAdhesionsDependencies(GetIt getIt) {
|
||||
getIt.registerLazySingleton<AdhesionRepository>(
|
||||
() => AdhesionRepositoryImpl(getIt<Dio>()),
|
||||
);
|
||||
getIt.registerFactory<AdhesionsBloc>(
|
||||
() => AdhesionsBloc(getIt<AdhesionRepository>()),
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
/// Page détail d'une demande d'adhésion + actions (approuver, rejeter, paiement)
|
||||
library adhesion_detail_page;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../../../shared/widgets/core_card.dart';
|
||||
import '../../../../shared/widgets/info_badge.dart';
|
||||
import '../../../../shared/widgets/mini_avatar.dart';
|
||||
import '../../bloc/adhesions_bloc.dart';
|
||||
import '../../data/models/adhesion_model.dart';
|
||||
import '../widgets/paiement_adhesion_dialog.dart';
|
||||
import '../widgets/rejet_adhesion_dialog.dart';
|
||||
import '../../../authentication/presentation/bloc/auth_bloc.dart';
|
||||
|
||||
class AdhesionDetailPage extends StatefulWidget {
|
||||
final String adhesionId;
|
||||
@@ -30,8 +32,11 @@ class _AdhesionDetailPageState extends State<AdhesionDetailPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Détail adhésion'),
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: const UFAppBar(
|
||||
title: 'DÉTAIL ADHÉSION',
|
||||
backgroundColor: AppColors.surface,
|
||||
foregroundColor: AppColors.textPrimaryLight,
|
||||
),
|
||||
body: BlocConsumer<AdhesionsBloc, AdhesionsState>(
|
||||
listenWhen: (prev, curr) => prev.status != curr.status,
|
||||
@@ -73,9 +78,11 @@ class _AdhesionDetailPageState extends State<AdhesionDetailPage> {
|
||||
title: 'Référence',
|
||||
value: a.numeroReference ?? a.id ?? '—',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_InfoCard(title: 'Statut', value: a.statutLibelle),
|
||||
const SizedBox(height: 12),
|
||||
_InfoCard(
|
||||
title: 'Statut',
|
||||
value: a.statutLibelle,
|
||||
trail: _buildStatutBadge(a.statut),
|
||||
),
|
||||
_InfoCard(
|
||||
title: 'Organisation',
|
||||
value: a.nomOrganisation ?? a.organisationId ?? '—',
|
||||
@@ -109,8 +116,7 @@ class _AdhesionDetailPageState extends State<AdhesionDetailPage> {
|
||||
),
|
||||
if (a.motifRejet != null && a.motifRejet!.isNotEmpty)
|
||||
_InfoCard(title: 'Motif rejet', value: a.motifRejet!),
|
||||
const SizedBox(height: 24),
|
||||
_ActionsSection(adhesion: a, currencyFormat: _currencyFormat),
|
||||
_ActionsSection(adhesion: a, currencyFormat: _currencyFormat, isGestionnaire: _isGestionnaire()),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -118,93 +124,156 @@ class _AdhesionDetailPageState extends State<AdhesionDetailPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _isGestionnaire() {
|
||||
final state = context.read<AuthBloc>().state;
|
||||
if (state is AuthAuthenticated) {
|
||||
return state.effectiveRole.level >= 50;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoCard extends StatelessWidget {
|
||||
final String title;
|
||||
final String value;
|
||||
final Widget? trail;
|
||||
|
||||
const _InfoCard({required this.title, required this.value});
|
||||
const _InfoCard({required this.title, required this.value, this.trail});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey[700],
|
||||
return CoreCard(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title.toUpperCase(),
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 9,
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
value,
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(child: Text(value)),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (trail != null) trail!,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildStatutBadge(String? statut) {
|
||||
Color color;
|
||||
switch (statut) {
|
||||
case 'APPROUVEE':
|
||||
case 'PAYEE':
|
||||
color = AppColors.success;
|
||||
break;
|
||||
case 'REJETEE':
|
||||
case 'ANNULEE':
|
||||
color = AppColors.error;
|
||||
break;
|
||||
case 'EN_ATTENTE':
|
||||
color = AppColors.brandGreenLight;
|
||||
break;
|
||||
case 'EN_PAIEMENT':
|
||||
color = Colors.blue;
|
||||
break;
|
||||
default:
|
||||
color = AppColors.textSecondaryLight;
|
||||
}
|
||||
return InfoBadge(text: statut ?? 'INCONNU', backgroundColor: color);
|
||||
}
|
||||
|
||||
class _ActionsSection extends StatelessWidget {
|
||||
final AdhesionModel adhesion;
|
||||
final NumberFormat currencyFormat;
|
||||
final bool isGestionnaire;
|
||||
|
||||
const _ActionsSection({
|
||||
required this.adhesion,
|
||||
required this.currencyFormat,
|
||||
required this.isGestionnaire,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!isGestionnaire) return const SizedBox.shrink(); // Normal members cannot approve/pay an adhesion on someone else's behalf (or their own) currently in the UI design.
|
||||
|
||||
final bloc = context.read<AdhesionsBloc>();
|
||||
if (adhesion.statut == 'EN_ATTENTE') {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'Actions (admin)',
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
if (adhesion.id == null) return;
|
||||
bloc.add(ApprouverAdhesion(adhesion.id!));
|
||||
},
|
||||
icon: const Icon(Icons.check_circle),
|
||||
label: const Text('Approuver'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
'ACTIONS ADMINISTRATIVES',
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
if (adhesion.id == null) return;
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => BlocProvider.value(
|
||||
value: bloc,
|
||||
child: RejetAdhesionDialog(
|
||||
adhesionId: adhesion.id!,
|
||||
onRejected: () => Navigator.of(ctx).pop(),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
if (adhesion.id == null) return;
|
||||
bloc.add(ApprouverAdhesion(adhesion.id!));
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.success,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
),
|
||||
child: Text('APPROUVER', style: AppTypography.actionText.copyWith(fontSize: 11, color: Colors.white)),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.cancel),
|
||||
label: const Text('Rejeter'),
|
||||
style: OutlinedButton.styleFrom(foregroundColor: Colors.red),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
if (adhesion.id == null) return;
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => BlocProvider.value(
|
||||
value: bloc,
|
||||
child: RejetAdhesionDialog(
|
||||
adhesionId: adhesion.id!,
|
||||
onRejected: () => Navigator.of(ctx).pop(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.error,
|
||||
side: const BorderSide(color: AppColors.error),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
),
|
||||
child: Text('REJETER', style: AppTypography.actionText.copyWith(fontSize: 11)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -213,14 +282,18 @@ class _ActionsSection extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'Paiement',
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
'PAIEMENT',
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton.icon(
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
@@ -234,8 +307,14 @@ class _ActionsSection extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.payment),
|
||||
label: const Text('Enregistrer un paiement'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
),
|
||||
child: Text('ENREGISTRER UN PAIEMENT', style: AppTypography.actionText.copyWith(fontSize: 11, color: Colors.white)),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
/// Page liste des demandes d'adhésion
|
||||
library adhesions_page;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../../../shared/widgets/core_card.dart';
|
||||
import '../../../../shared/widgets/info_badge.dart';
|
||||
import '../../../../shared/widgets/mini_avatar.dart';
|
||||
import '../../bloc/adhesions_bloc.dart';
|
||||
import '../../data/models/adhesion_model.dart';
|
||||
import 'adhesion_detail_page.dart';
|
||||
import '../widgets/create_adhesion_dialog.dart';
|
||||
import '../../../authentication/presentation/bloc/auth_bloc.dart';
|
||||
|
||||
class AdhesionsPage extends StatefulWidget {
|
||||
const AdhesionsPage({super.key});
|
||||
@@ -25,7 +27,7 @@ class _AdhesionsPageState extends State<AdhesionsPage>
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 4, vsync: this);
|
||||
context.read<AdhesionsBloc>().add(const LoadAdhesions());
|
||||
_loadTab(0);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -35,19 +37,34 @@ class _AdhesionsPageState extends State<AdhesionsPage>
|
||||
}
|
||||
|
||||
void _loadTab(int index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
context.read<AdhesionsBloc>().add(const LoadAdhesions());
|
||||
break;
|
||||
case 1:
|
||||
context.read<AdhesionsBloc>().add(const LoadAdhesionsEnAttente());
|
||||
break;
|
||||
case 2:
|
||||
context.read<AdhesionsBloc>().add(const LoadAdhesionsByStatut('APPROUVEE'));
|
||||
break;
|
||||
case 3:
|
||||
context.read<AdhesionsBloc>().add(const LoadAdhesionsByStatut('PAYEE'));
|
||||
break;
|
||||
bool isGestionnaire = false;
|
||||
String? membreId;
|
||||
final authState = context.read<AuthBloc>().state;
|
||||
if (authState is AuthAuthenticated) {
|
||||
isGestionnaire = authState.effectiveRole.level >= 50;
|
||||
membreId = authState.user.id;
|
||||
}
|
||||
|
||||
if (isGestionnaire) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
context.read<AdhesionsBloc>().add(const LoadAdhesions());
|
||||
break;
|
||||
case 1:
|
||||
context.read<AdhesionsBloc>().add(const LoadAdhesionsEnAttente());
|
||||
break;
|
||||
case 2:
|
||||
context.read<AdhesionsBloc>().add(const LoadAdhesionsByStatut('APPROUVEE'));
|
||||
break;
|
||||
case 3:
|
||||
context.read<AdhesionsBloc>().add(const LoadAdhesionsByStatut('PAYEE'));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Normal member: always fetch their own records to ensure security
|
||||
if (membreId != null) {
|
||||
context.read<AdhesionsBloc>().add(LoadAdhesionsByMembre(membreId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,25 +87,34 @@ class _AdhesionsPageState extends State<AdhesionsPage>
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Demandes d\'adhésion'),
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
onTap: _loadTab,
|
||||
tabs: const [
|
||||
Tab(text: 'Toutes', icon: Icon(Icons.list)),
|
||||
Tab(text: 'En attente', icon: Icon(Icons.schedule)),
|
||||
Tab(text: 'Approuvées', icon: Icon(Icons.check_circle_outline)),
|
||||
Tab(text: 'Payées', icon: Icon(Icons.payment)),
|
||||
],
|
||||
),
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: UFAppBar(
|
||||
title: 'ADHÉSIONS',
|
||||
backgroundColor: AppColors.surface,
|
||||
foregroundColor: AppColors.textPrimaryLight,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
icon: const Icon(Icons.add, size: 20),
|
||||
onPressed: () => _showCreateDialog(),
|
||||
tooltip: 'Nouvelle demande',
|
||||
),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
onTap: _loadTab,
|
||||
isScrollable: true,
|
||||
labelColor: AppColors.primaryGreen,
|
||||
unselectedLabelColor: AppColors.textSecondaryLight,
|
||||
indicatorColor: AppColors.primaryGreen,
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold),
|
||||
tabs: const [
|
||||
Tab(child: Text('TOUTES')),
|
||||
Tab(child: Text('ATTENTE')),
|
||||
Tab(child: Text('APPROUVÉES')),
|
||||
Tab(child: Text('PAYÉES')),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
@@ -193,106 +219,96 @@ class _AdhesionCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
return CoreCard(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
onTap: onTap,
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
adhesion.numeroReference ?? adhesion.id ?? '—',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
_StatutChip(statut: adhesion.statut),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
adhesion.nomOrganisation ?? adhesion.organisationId ?? 'Organisation',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
if (adhesion.nomMembreComplet.isNotEmpty)
|
||||
Text(
|
||||
adhesion.nomMembreComplet,
|
||||
style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
adhesion.fraisAdhesion != null
|
||||
? currencyFormat.format(adhesion.fraisAdhesion)
|
||||
: '—',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
if (adhesion.dateDemande != null) ...[
|
||||
const Spacer(),
|
||||
const MiniAvatar(size: 24, fallbackText: '🏢'),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
DateFormat('dd/MM/yyyy').format(adhesion.dateDemande!),
|
||||
style: theme.textTheme.bodySmall,
|
||||
adhesion.nomOrganisation ?? adhesion.organisationId ?? 'Organisation',
|
||||
style: AppTypography.actionText.copyWith(fontSize: 12),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
adhesion.numeroReference ?? adhesion.id?.substring(0, 8) ?? '—',
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 9),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildStatutBadge(adhesion.statut),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('FRAIS D\'ADHÉSION', style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
adhesion.fraisAdhesion != null ? currencyFormat.format(adhesion.fraisAdhesion) : '—',
|
||||
style: AppTypography.headerSmall.copyWith(fontSize: 13, color: AppColors.primaryGreen),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (adhesion.dateDemande != null)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text('DATE', style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
DateFormat('dd/MM/yyyy').format(adhesion.dateDemande!),
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontSize: 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (adhesion.nomMembreComplet.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'MEMBRE : ${adhesion.nomMembreComplet.toUpperCase()}',
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 8, color: AppColors.textSecondaryLight),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatutChip extends StatelessWidget {
|
||||
final String? statut;
|
||||
|
||||
const _StatutChip({this.statut});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget _buildStatutBadge(String? statut) {
|
||||
Color color;
|
||||
switch (statut) {
|
||||
case 'EN_ATTENTE':
|
||||
color = Colors.orange;
|
||||
break;
|
||||
case 'APPROUVEE':
|
||||
case 'PAYEE':
|
||||
color = Colors.green;
|
||||
color = AppColors.success;
|
||||
break;
|
||||
case 'REJETEE':
|
||||
color = Colors.red;
|
||||
break;
|
||||
case 'ANNULEE':
|
||||
color = Colors.grey;
|
||||
color = AppColors.error;
|
||||
break;
|
||||
case 'EN_ATTENTE':
|
||||
color = AppColors.brandGreenLight;
|
||||
break;
|
||||
case 'EN_PAIEMENT':
|
||||
color = Colors.blue;
|
||||
break;
|
||||
default:
|
||||
color = Colors.grey;
|
||||
color = AppColors.textSecondaryLight;
|
||||
}
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
statut ?? '—',
|
||||
style: TextStyle(fontSize: 12, color: color, fontWeight: FontWeight.w500),
|
||||
),
|
||||
);
|
||||
return InfoBadge(text: statut ?? 'INCONNU', backgroundColor: color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,13 @@ library create_adhesion_dialog;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import '../../../../core/utils/logger.dart';
|
||||
import '../../bloc/adhesions_bloc.dart';
|
||||
import '../../data/models/adhesion_model.dart';
|
||||
import '../../../organizations/data/models/organization_model.dart';
|
||||
import '../../../organizations/data/repositories/organization_repository.dart';
|
||||
import '../../../members/data/services/membre_search_service.dart';
|
||||
import '../../../organizations/domain/repositories/organization_repository.dart';
|
||||
import '../../../members/data/models/membre_complete_model.dart';
|
||||
import '../../../profile/domain/repositories/profile_repository.dart';
|
||||
|
||||
class CreateAdhesionDialog extends StatefulWidget {
|
||||
final VoidCallback onCreated;
|
||||
@@ -22,16 +23,42 @@ class CreateAdhesionDialog extends StatefulWidget {
|
||||
|
||||
class _CreateAdhesionDialogState extends State<CreateAdhesionDialog> {
|
||||
final _fraisController = TextEditingController();
|
||||
String? _membreId;
|
||||
String? _organisationId;
|
||||
bool _loading = false;
|
||||
bool _isInitLoading = true;
|
||||
List<OrganizationModel> _organisations = [];
|
||||
List<MembreCompletModel> _membres = [];
|
||||
MembreCompletModel? _me;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadOrgs();
|
||||
_loadInitialData();
|
||||
}
|
||||
|
||||
Future<void> _loadInitialData() async {
|
||||
try {
|
||||
final user = await GetIt.instance<IProfileRepository>().getMe();
|
||||
final orgRepo = GetIt.instance<IOrganizationRepository>();
|
||||
final list = await orgRepo.getOrganizations(page: 0, size: 100);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_me = user;
|
||||
_organisations = list;
|
||||
_isInitLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e, st) {
|
||||
AppLogger.error('CreateAdhesionDialog: chargement profil/organisations échoué', error: e, stackTrace: st);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isInitLoading = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Impossible de charger le profil ou les organisations. Réessayez.')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -40,32 +67,14 @@ class _CreateAdhesionDialogState extends State<CreateAdhesionDialog> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadOrgs() async {
|
||||
try {
|
||||
final repo = GetIt.instance<OrganizationRepository>();
|
||||
final list = await repo.getOrganizations(page: 0, size: 100);
|
||||
if (mounted) setState(() => _organisations = list);
|
||||
} catch (_) {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _searchMembres(String query) async {
|
||||
if (query.length < 2) {
|
||||
setState(() => _membres = []);
|
||||
void _submit() {
|
||||
if (_me == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Profil non chargé, veuillez réessayer')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final service = GetIt.instance<MembreSearchService>();
|
||||
final result = await service.quickSearch(query: query, size: 20);
|
||||
if (mounted) setState(() => _membres = result.membres);
|
||||
} catch (_) {
|
||||
if (mounted) setState(() => _membres = []);
|
||||
}
|
||||
}
|
||||
|
||||
void _submit() {
|
||||
if (_membreId == null || _organisationId == null) {
|
||||
if (_organisationId == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Veuillez sélectionner un membre et une organisation')),
|
||||
);
|
||||
@@ -80,7 +89,7 @@ class _CreateAdhesionDialogState extends State<CreateAdhesionDialog> {
|
||||
}
|
||||
setState(() => _loading = true);
|
||||
final adhesion = AdhesionModel(
|
||||
membreId: _membreId,
|
||||
membreId: _me!.id,
|
||||
organisationId: _organisationId,
|
||||
fraisAdhesion: frais,
|
||||
codeDevise: 'XOF',
|
||||
@@ -102,32 +111,24 @@ class _CreateAdhesionDialogState extends State<CreateAdhesionDialog> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Rechercher un membre (nom, prénom)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: _searchMembres,
|
||||
enabled: !_loading,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
DropdownButtonFormField<String>(
|
||||
value: _membreId,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Membre',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: _membres
|
||||
.map((m) => DropdownMenuItem<String>(
|
||||
value: m.id,
|
||||
child: Text('${m.prenom} ${m.nom}'),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: _loading ? null : (v) => setState(() => _membreId = v),
|
||||
),
|
||||
if (_isInitLoading)
|
||||
const CircularProgressIndicator()
|
||||
else if (_me != null)
|
||||
TextFormField(
|
||||
initialValue: '${_me!.prenom} ${_me!.nom}',
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Membre',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.person),
|
||||
),
|
||||
enabled: false,
|
||||
)
|
||||
else
|
||||
const Text('Impossible de récupérer votre profil', style: TextStyle(color: Colors.red)),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<String>(
|
||||
value: _organisationId,
|
||||
isExpanded: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Organisation',
|
||||
border: OutlineInputBorder(),
|
||||
@@ -135,7 +136,7 @@ class _CreateAdhesionDialogState extends State<CreateAdhesionDialog> {
|
||||
items: _organisations
|
||||
.map((o) => DropdownMenuItem<String>(
|
||||
value: o.id,
|
||||
child: Text(o.nom),
|
||||
child: Text(o.nom, overflow: TextOverflow.ellipsis, maxLines: 1),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: _loading ? null : (v) => setState(() => _organisationId = v),
|
||||
|
||||
@@ -3,6 +3,7 @@ library paiement_adhesion_dialog;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../shared/constants/payment_method_assets.dart';
|
||||
import '../../bloc/adhesions_bloc.dart';
|
||||
|
||||
class PaiementAdhesionDialog extends StatefulWidget {
|
||||
@@ -40,6 +41,25 @@ class _PaiementAdhesionDialogState extends State<PaiementAdhesionDialog> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<DropdownMenuItem<String>> _buildPaymentMethodItems() {
|
||||
const codes = ['ESPECES', 'VIREMENT', 'WAVE_MONEY', 'ORANGE_MONEY', 'CHEQUE'];
|
||||
const labels = {'ESPECES': 'Espèces', 'VIREMENT': 'Virement', 'WAVE_MONEY': 'Wave Money', 'ORANGE_MONEY': 'Orange Money', 'CHEQUE': 'Chèque'};
|
||||
return codes.map((code) {
|
||||
final label = labels[code] ?? code;
|
||||
return DropdownMenuItem<String>(
|
||||
value: code,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PaymentMethodIcon(paymentMethodCode: code, width: 24, height: 24),
|
||||
const SizedBox(width: 12),
|
||||
Text(label),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void _submit() {
|
||||
final montant = double.tryParse(_montantController.text.replaceAll(',', '.'));
|
||||
if (montant == null || montant <= 0) {
|
||||
@@ -98,13 +118,7 @@ class _PaiementAdhesionDialogState extends State<PaiementAdhesionDialog> {
|
||||
labelText: 'Méthode de paiement',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'ESPECES', child: Text('Espèces')),
|
||||
DropdownMenuItem(value: 'VIREMENT', child: Text('Virement')),
|
||||
DropdownMenuItem(value: 'WAVE_MONEY', child: Text('Wave Money')),
|
||||
DropdownMenuItem(value: 'ORANGE_MONEY', child: Text('Orange Money')),
|
||||
DropdownMenuItem(value: 'CHEQUE', child: Text('Chèque')),
|
||||
],
|
||||
items: _buildPaymentMethodItems(),
|
||||
onChanged: _loading ? null : (v) => setState(() => _methode = v),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@@ -22,6 +22,7 @@ class RejetAdhesionDialog extends StatefulWidget {
|
||||
class _RejetAdhesionDialogState extends State<RejetAdhesionDialog> {
|
||||
final _controller = TextEditingController();
|
||||
bool _loading = false;
|
||||
bool _rejectSent = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -37,18 +38,36 @@ class _RejetAdhesionDialogState extends State<RejetAdhesionDialog> {
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(() => _loading = true);
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_rejectSent = true;
|
||||
});
|
||||
context.read<AdhesionsBloc>().add(RejeterAdhesion(widget.adhesionId, motif));
|
||||
widget.onRejected();
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
return BlocListener<AdhesionsBloc, AdhesionsState>(
|
||||
listenWhen: (_, state) => _rejectSent && (state.status == AdhesionsStatus.loaded || state.status == AdhesionsStatus.error),
|
||||
listener: (context, state) {
|
||||
if (!_rejectSent || !mounted) return;
|
||||
if (state.status == AdhesionsStatus.error) {
|
||||
setState(() {
|
||||
_loading = false;
|
||||
_rejectSent = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.message ?? 'Erreur lors du rejet')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (state.status == AdhesionsStatus.loaded) {
|
||||
setState(() => _rejectSent = false);
|
||||
widget.onRejected();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: AlertDialog(
|
||||
title: const Text('Rejeter la demande'),
|
||||
content: TextField(
|
||||
controller: _controller,
|
||||
@@ -77,6 +96,7 @@ class _RejetAdhesionDialogState extends State<RejetAdhesionDialog> {
|
||||
: const Text('Rejeter'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user