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:
dahoud
2026-03-15 02:12:17 +00:00
parent bbc409de9d
commit e8ad874015
635 changed files with 58160 additions and 20674 deletions

View File

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