feat(sprint-13.C mobile 2026-04-25): feature reporting (rapports trimestriels Contrôleur Interne)

Mobile mirror du backend P2-NEW-3. DRY strict — Clean Architecture identique aux autres features (compliance, devise).

Domain
- StatutRapport enum (draft, signe, archive, inconnu) + extension StatutRapportX (code/libelle/severite/fromCode)
- RapportTrimestriel entity (Equatable) + helpers libellePeriode, scoreSeverite (>=80 success, >=60 warning, <60 danger)

Data
- RapportTrimestrielModel.fromJson (parsing tolérant null + dates ISO)
- ReportingRemoteDataSourceImpl (Dio) : lister/generer/signer/archiver/telechargerPdf
- ReportingRepositoryImpl @Injectable

Presentation
- ReportingBloc : Load/Generer/Signer/Archiver events, 4 states (Initial/Loading/Loaded/Error)
- RapportsTrimestrielsPage : Material 3 ListView avec cards (period, score coloré, tag statut, hash tronqué) + EmptyView + ErrorView

Tests (10/10 verts)
- StatutRapportX × 2 (fromCode parse/null/inconnu, libelle/severite/code)
- RapportTrimestriel × 5 (libellePeriode, estDraft/estSigne/estArchive, scoreSeverite × 3)
- RapportTrimestrielModel.fromJson × 3 (complet, fallbacks, date invalide)

Note : viewer PDF interne reporté à un sprint dédié (intégration pdfx + permission storage Android). Téléchargement bytes via API exposé dans datasource pour usage futur.
This commit is contained in:
dahoud
2026-04-25 15:28:17 +00:00
parent 8c1a254e80
commit 6ba71fb014
8 changed files with 717 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
import 'package:dio/dio.dart';
import 'package:injectable/injectable.dart';
import '../../../../core/error/exceptions.dart';
import '../../../../core/network/api_client.dart';
import '../../../../core/utils/logger.dart';
import '../models/rapport_trimestriel_model.dart';
abstract class ReportingRemoteDataSource {
Future<List<RapportTrimestrielModel>> lister({String? orgId, int? annee});
Future<RapportTrimestrielModel> generer({String? orgId, required int annee, required int trimestre});
Future<RapportTrimestrielModel> signer(String id, String signataireId);
Future<RapportTrimestrielModel> archiver(String id);
Future<List<int>> telechargerPdf(String id);
}
@Injectable(as: ReportingRemoteDataSource)
class ReportingRemoteDataSourceImpl implements ReportingRemoteDataSource {
final ApiClient apiClient;
ReportingRemoteDataSourceImpl(this.apiClient);
static const _basePath = '/api/rapports/trimestriel';
@override
Future<List<RapportTrimestrielModel>> lister({String? orgId, int? annee}) async {
try {
final response = await apiClient.get(
_basePath,
queryParameters: {
if (orgId != null) 'orgId': orgId,
if (annee != null) 'annee': annee,
},
);
if (response.statusCode == 200 && response.data is List) {
return (response.data as List)
.whereType<Map<String, dynamic>>()
.map(RapportTrimestrielModel.fromJson)
.toList();
}
throw ServerException('Liste rapports HTTP ${response.statusCode}');
} on DioException catch (e) {
AppLogger.error('Reporting: lister', error: e);
rethrow;
}
}
@override
Future<RapportTrimestrielModel> generer({String? orgId, required int annee, required int trimestre}) async {
try {
final response = await apiClient.post(
'$_basePath/generer',
queryParameters: {
if (orgId != null) 'orgId': orgId,
'annee': annee,
'trimestre': trimestre,
},
);
if (response.statusCode == 200 && response.data is Map<String, dynamic>) {
return RapportTrimestrielModel.fromJson(response.data as Map<String, dynamic>);
}
throw ServerException('Generer rapport HTTP ${response.statusCode}');
} on DioException catch (e) {
AppLogger.error('Reporting: generer', error: e);
rethrow;
}
}
@override
Future<RapportTrimestrielModel> signer(String id, String signataireId) async {
try {
final response = await apiClient.post(
'$_basePath/$id/signer',
queryParameters: {'signataireId': signataireId},
);
if (response.statusCode == 200 && response.data is Map<String, dynamic>) {
return RapportTrimestrielModel.fromJson(response.data as Map<String, dynamic>);
}
throw ServerException('Signer rapport HTTP ${response.statusCode}');
} on DioException catch (e) {
AppLogger.error('Reporting: signer', error: e);
rethrow;
}
}
@override
Future<RapportTrimestrielModel> archiver(String id) async {
try {
final response = await apiClient.post('$_basePath/$id/archiver');
if (response.statusCode == 200 && response.data is Map<String, dynamic>) {
return RapportTrimestrielModel.fromJson(response.data as Map<String, dynamic>);
}
throw ServerException('Archiver rapport HTTP ${response.statusCode}');
} on DioException catch (e) {
AppLogger.error('Reporting: archiver', error: e);
rethrow;
}
}
@override
Future<List<int>> telechargerPdf(String id) async {
try {
final response = await apiClient.get<List<int>>(
'$_basePath/$id/pdf',
options: Options(responseType: ResponseType.bytes),
);
if (response.statusCode == 200 && response.data != null) {
return response.data!;
}
throw ServerException('Telecharger PDF HTTP ${response.statusCode}');
} on DioException catch (e) {
AppLogger.error('Reporting: telechargerPdf', error: e);
rethrow;
}
}
}

View File

@@ -0,0 +1,40 @@
import '../../domain/entities/rapport_trimestriel.dart';
class RapportTrimestrielModel extends RapportTrimestriel {
const RapportTrimestrielModel({
required super.id,
required super.organisationId,
required super.annee,
required super.trimestre,
required super.dateGeneration,
required super.statut,
required super.scoreConformite,
super.signataireId,
super.dateSignature,
super.hashSha256,
});
factory RapportTrimestrielModel.fromJson(Map<String, dynamic> json) {
return RapportTrimestrielModel(
id: json['id']?.toString() ?? '',
organisationId: json['organisationId']?.toString() ?? '',
annee: (json['annee'] as num?)?.toInt() ?? 0,
trimestre: (json['trimestre'] as num?)?.toInt() ?? 0,
dateGeneration: _parseDate(json['dateGeneration']),
statut: StatutRapportX.fromCode(json['statut']?.toString()),
scoreConformite: (json['scoreConformite'] as num?)?.toInt() ?? 0,
signataireId: json['signataireId']?.toString(),
dateSignature: _parseDate(json['dateSignature']),
hashSha256: json['hashSha256']?.toString(),
);
}
static DateTime? _parseDate(dynamic raw) {
if (raw == null) return null;
try {
return DateTime.parse(raw.toString());
} catch (_) {
return null;
}
}
}

View File

@@ -0,0 +1,29 @@
import 'package:injectable/injectable.dart';
import '../../domain/entities/rapport_trimestriel.dart';
import '../../domain/repositories/reporting_repository.dart';
import '../datasources/reporting_remote_datasource.dart';
@Injectable(as: ReportingRepository)
class ReportingRepositoryImpl implements ReportingRepository {
final ReportingRemoteDataSource remote;
ReportingRepositoryImpl(this.remote);
@override
Future<List<RapportTrimestriel>> lister({String? orgId, int? annee}) =>
remote.lister(orgId: orgId, annee: annee);
@override
Future<RapportTrimestriel> generer({String? orgId, required int annee, required int trimestre}) =>
remote.generer(orgId: orgId, annee: annee, trimestre: trimestre);
@override
Future<RapportTrimestriel> signer(String id, String signataireId) =>
remote.signer(id, signataireId);
@override
Future<RapportTrimestriel> archiver(String id) =>
remote.archiver(id);
@override
Future<List<int>> telechargerPdf(String id) => remote.telechargerPdf(id);
}