import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../core/di/injection.dart'; import '../../domain/entities/compliance_snapshot.dart'; import '../bloc/compliance_bloc.dart'; /// Page tableau de bord de conformité — affiche le snapshot agrégé pour /// l'organisation active du membre connecté. /// /// Cible : Compliance Officer, Contrôleur Interne, Président, Trésorier, Admin Org. class ConformiteDashboardPage extends StatelessWidget { const ConformiteDashboardPage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => getIt()..add(const LoadComplianceSnapshot()), child: Scaffold( appBar: AppBar( title: const Text('Conformité'), actions: [ Builder( builder: (ctx) => IconButton( icon: const Icon(Icons.refresh), onPressed: () => ctx.read().add(const RefreshComplianceSnapshot()), ), ), ], ), body: BlocBuilder( builder: (context, state) { if (state is ComplianceLoading || state is ComplianceInitial) { return const Center(child: CircularProgressIndicator()); } if (state is ComplianceError) { return _Error(message: state.message); } if (state is ComplianceLoaded) { return _Content(snapshot: state.snapshot); } return const SizedBox.shrink(); }, ), ), ); } } class _Content extends StatelessWidget { final ComplianceSnapshot snapshot; const _Content({required this.snapshot}); @override Widget build(BuildContext context) { return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _ScoreCard(snapshot: snapshot), const SizedBox(height: 16), _IndicateurTile( label: 'Compliance Officer', value: snapshot.complianceOfficerDesigne ? 'Désigné' : 'ABSENT', severite: snapshot.complianceOfficerDesigne ? 'success' : 'danger', ), _IndicateurTile( label: 'AG annuelle', value: snapshot.agAnnuelle.statut, sous: snapshot.agAnnuelle.message, severite: _statutSeverite(snapshot.agAnnuelle.statut), ), _IndicateurTile( label: 'Rapport AIRMS', value: snapshot.rapportAirms.statut, severite: _statutSeverite(snapshot.rapportAirms.statut), ), _IndicateurTile( label: 'Dirigeants enrôlés CMU', value: snapshot.dirigeantsAvecCmu.toString(), severite: snapshot.dirigeantsAvecCmu >= 3 ? 'success' : 'warning', ), _IndicateurTile( label: 'Taux KYC à jour', value: '${snapshot.tauxKycAJourPct.toStringAsFixed(1)} %', ), _IndicateurTile( label: 'Taux formation LBC/FT', value: '${snapshot.tauxFormationLbcFtPct.toStringAsFixed(1)} %', ), _IndicateurTile( label: 'Couverture UBO', value: '${snapshot.couvertureUboPct.toStringAsFixed(1)} %', ), _IndicateurTile( label: 'Commissaire aux comptes', value: snapshot.commissaireAuxComptes.statut, sous: snapshot.commissaireAuxComptes.message, ), _IndicateurTile( label: 'FOMUS-CI', value: snapshot.fomusCi.statut, sous: snapshot.fomusCi.message, ), if (snapshot.hasAlertesCritiques) ...[ const SizedBox(height: 16), _AlertesCard(snapshot: snapshot), ], ], ), ); } String _statutSeverite(String s) { if (s == 'OK') return 'success'; if (s == 'RETARD') return 'danger'; return 'warning'; } } class _ScoreCard extends StatelessWidget { final ComplianceSnapshot snapshot; const _ScoreCard({required this.snapshot}); @override Widget build(BuildContext context) { final color = _color(snapshot.scoreSeverite); return Card( elevation: 2, child: Padding( padding: const EdgeInsets.all(20), child: Column( children: [ Text(snapshot.organisationNom, style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center), const SizedBox(height: 4), Text('Référentiel : ${snapshot.referentielComptable}', style: Theme.of(context).textTheme.bodySmall), const SizedBox(height: 16), Text('Score conformité', style: Theme.of(context).textTheme.bodyLarge), const SizedBox(height: 8), Text( '${snapshot.scoreGlobal} / 100', style: TextStyle( fontSize: 48, fontWeight: FontWeight.bold, color: color, ), ), ], ), ), ); } Color _color(String s) => switch (s) { 'success' => Colors.green, 'warning' => Colors.orange, 'danger' => Colors.red, _ => Colors.grey, }; } class _IndicateurTile extends StatelessWidget { final String label; final String value; final String? sous; final String? severite; const _IndicateurTile({ required this.label, required this.value, this.sous, this.severite, }); @override Widget build(BuildContext context) { final color = severite != null ? _color(severite!) : Colors.grey; return Card( child: ListTile( title: Text(label), subtitle: sous != null ? Text(sous!, maxLines: 2, overflow: TextOverflow.ellipsis) : null, trailing: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: color.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(20), ), child: Text( value, style: TextStyle(color: color, fontWeight: FontWeight.bold), ), ), ), ); } Color _color(String s) => switch (s) { 'success' => Colors.green, 'warning' => Colors.orange, 'danger' => Colors.red, _ => Colors.grey, }; } class _AlertesCard extends StatelessWidget { final ComplianceSnapshot snapshot; const _AlertesCard({required this.snapshot}); @override Widget build(BuildContext context) { final alertes = []; if (!snapshot.complianceOfficerDesigne) { alertes.add('Compliance Officer non désigné (Instr. BCEAO 001-03-2025)'); } if (snapshot.agAnnuelle.statut == 'RETARD') { alertes.add('AG annuelle en retard (échéance 30/06)'); } if (snapshot.scoreGlobal < 60) { alertes.add('Score conformité < 60 % — risque inspection'); } return Card( color: Colors.red.shade50, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ const Icon(Icons.warning, color: Colors.red), const SizedBox(width: 8), Text('Alertes critiques', style: Theme.of(context).textTheme.titleMedium), ]), const SizedBox(height: 8), for (final a in alertes) Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: Text('• $a', style: const TextStyle(color: Colors.red)), ), ], ), ), ); } } class _Error extends StatelessWidget { final String message; const _Error({required this.message}); @override Widget build(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, color: Colors.red, size: 48), const SizedBox(height: 16), Text( 'Impossible de charger le tableau de bord', style: Theme.of(context).textTheme.titleMedium, textAlign: TextAlign.center, ), const SizedBox(height: 8), Text(message, style: Theme.of(context).textTheme.bodySmall, textAlign: TextAlign.center), ], ), ), ); } }