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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user