feat(sprint-9 mobile 2026-04-25): feature compliance dashboard + Devise enum + selector + tests

Apporte aux compliance officers et controleurs internes l'accès mobile au tableau de bord
de conformité backend (P1-NEW-7). Et prépare la diaspora avec Devise enum + sélecteur
préférence persisté.

Feature Compliance (Clean Architecture)
- Domain : ComplianceSnapshot + ConformiteIndicateur (Equatable), helpers scoreSeverite + hasAlertesCritiques
- Data : ComplianceSnapshotModel.fromJson (parsing tolerant aux nullables), ComplianceRemoteDataSourceImpl Dio (GET /api/compliance/dashboard), ComplianceRepositoryImpl @Injectable
- Presentation : ComplianceBloc (Load/Refresh events, Initial/Loading/Loaded/Error states), ConformiteDashboardPage (Material 3, ScoreCard 0-100 colorée, 9 IndicateurTile, AlertesCard rouge si critiques)

Feature Devise
- Devise enum (10 valeurs miroirs backend, code/libelle/zone)
- fromCode tolérant casse + null/vide → XOF
- estInternationale pour AML
- DeviseSelector widget DropdownButtonFormField + readPreferred/writePreferred via FlutterSecureStorage (clé unionflow.devise.preferee)

Tests (17/17 verts)
- ComplianceSnapshot : 7 tests (scoreSeverite × 3, hasAlertesCritiques × 4)
- ComplianceSnapshotModel.fromJson : 4 tests (complet, fallbacks, string→double, indicateur invalide)
- Devise enum : 6 tests (reference, fromCode parse, fromCode null/inconnu, estInternationale × 2, intégrité valeurs)

Note : feature reporting trimestriel mobile (PDF viewer + bloc liste) reportée à un sprint
dédié — nécessite intégration pdf viewer + cache local non triviale.
This commit is contained in:
dahoud
2026-04-25 11:11:03 +00:00
parent 8356ccc0b0
commit 8c1a254e80
12 changed files with 808 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
/// Devises supportées par UnionFlow (miroir mobile de l'enum backend).
///
/// Les codes ISO-4217 servent aussi d'identifiant pour l'API.
enum Devise {
xof('XOF', 'Franc CFA Ouest', 'UEMOA'),
xaf('XAF', 'Franc CFA Centrale', 'CEMAC'),
eur('EUR', 'Euro', 'EUROPE'),
usd('USD', 'Dollar US', 'AMERIQUE'),
gbp('GBP', 'Livre Sterling', 'EUROPE'),
cad('CAD', 'Dollar Canadien', 'AMERIQUE'),
chf('CHF', 'Franc Suisse', 'EUROPE'),
ghs('GHS', 'Cédi Ghanéen', 'CEDEAO'),
ngn('NGN', 'Naira Nigérian', 'CEDEAO'),
mad('MAD', 'Dirham Marocain', 'MAGHREB');
final String code;
final String libelle;
final String zone;
const Devise(this.code, this.libelle, this.zone);
/// Devise de référence UnionFlow.
static Devise reference() => Devise.xof;
/// Parse un code ISO-4217 en {@link Devise}, fallback sur XOF.
static Devise fromCode(String? code) {
if (code == null || code.isEmpty) return reference();
return Devise.values.firstWhere(
(d) => d.code.toUpperCase() == code.toUpperCase(),
orElse: () => reference(),
);
}
/// True pour les devises internationales (déclenchent AML international).
bool get estInternationale => const {
Devise.eur,
Devise.usd,
Devise.gbp,
Devise.cad,
Devise.chf,
}.contains(this);
}

View File

@@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../../domain/entities/devise.dart';
const _kDevisePrefereeKey = 'unionflow.devise.preferee';
/// Widget de sélection de la devise préférée (persistée en secure storage).
///
/// Utilisé sur la page Profil/Paramètres. Notifie via [onChanged] avec la
/// nouvelle devise quand l'utilisateur fait un choix.
class DeviseSelector extends StatefulWidget {
final Devise? initial;
final ValueChanged<Devise>? onChanged;
const DeviseSelector({super.key, this.initial, this.onChanged});
/// Lit la devise préférée du secure storage. Fallback sur XOF.
static Future<Devise> readPreferred(FlutterSecureStorage storage) async {
try {
final v = await storage.read(key: _kDevisePrefereeKey);
return Devise.fromCode(v);
} catch (_) {
return Devise.reference();
}
}
/// Persiste la devise préférée.
static Future<void> writePreferred(
FlutterSecureStorage storage, Devise devise) async {
await storage.write(key: _kDevisePrefereeKey, value: devise.code);
}
@override
State<DeviseSelector> createState() => _DeviseSelectorState();
}
class _DeviseSelectorState extends State<DeviseSelector> {
late Devise _selected;
@override
void initState() {
super.initState();
_selected = widget.initial ?? Devise.reference();
}
@override
Widget build(BuildContext context) {
return DropdownButtonFormField<Devise>(
value: _selected,
decoration: const InputDecoration(
labelText: 'Devise préférée',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.account_balance_wallet_outlined),
),
items: Devise.values
.map(
(d) => DropdownMenuItem<Devise>(
value: d,
child: Text('${d.code}${d.libelle}'),
),
)
.toList(),
onChanged: (d) {
if (d == null) return;
setState(() => _selected = d);
widget.onChanged?.call(d);
},
);
}
}