Initial commit: unionflow-mobile-apps
Application Flutter complète (sans build artifacts). Signed-off-by: lions dev Team
This commit is contained in:
335
lib/features/contributions/data/models/contribution_model.dart
Normal file
335
lib/features/contributions/data/models/contribution_model.dart
Normal file
@@ -0,0 +1,335 @@
|
||||
/// Modèle de données pour les contributions
|
||||
library contribution_model;
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'contribution_model.g.dart';
|
||||
|
||||
/// Statut d'une contribution
|
||||
enum ContributionStatus {
|
||||
@JsonValue('PAYEE')
|
||||
payee,
|
||||
@JsonValue('NON_PAYEE')
|
||||
nonPayee,
|
||||
@JsonValue('EN_ATTENTE')
|
||||
enAttente,
|
||||
@JsonValue('EN_RETARD')
|
||||
enRetard,
|
||||
@JsonValue('PARTIELLE')
|
||||
partielle,
|
||||
@JsonValue('ANNULEE')
|
||||
annulee,
|
||||
}
|
||||
|
||||
/// Type de contribution
|
||||
enum ContributionType {
|
||||
@JsonValue('ANNUELLE')
|
||||
annuelle,
|
||||
@JsonValue('MENSUELLE')
|
||||
mensuelle,
|
||||
@JsonValue('TRIMESTRIELLE')
|
||||
trimestrielle,
|
||||
@JsonValue('SEMESTRIELLE')
|
||||
semestrielle,
|
||||
@JsonValue('EXCEPTIONNELLE')
|
||||
exceptionnelle,
|
||||
}
|
||||
|
||||
/// Méthode de paiement
|
||||
enum PaymentMethod {
|
||||
@JsonValue('ESPECES')
|
||||
especes,
|
||||
@JsonValue('CHEQUE')
|
||||
cheque,
|
||||
@JsonValue('VIREMENT')
|
||||
virement,
|
||||
@JsonValue('CARTE_BANCAIRE')
|
||||
carteBancaire,
|
||||
@JsonValue('WAVE_MONEY')
|
||||
waveMoney,
|
||||
@JsonValue('ORANGE_MONEY')
|
||||
orangeMoney,
|
||||
@JsonValue('FREE_MONEY')
|
||||
freeMoney,
|
||||
@JsonValue('MOBILE_MONEY')
|
||||
mobileMoney,
|
||||
@JsonValue('AUTRE')
|
||||
autre,
|
||||
}
|
||||
|
||||
/// Extension pour obtenir le code API d'une méthode de paiement (ex: pour icônes assets).
|
||||
extension PaymentMethodCode on PaymentMethod {
|
||||
String get code {
|
||||
switch (this) {
|
||||
case PaymentMethod.especes: return 'ESPECES';
|
||||
case PaymentMethod.cheque: return 'CHEQUE';
|
||||
case PaymentMethod.virement: return 'VIREMENT';
|
||||
case PaymentMethod.carteBancaire: return 'CARTE_BANCAIRE';
|
||||
case PaymentMethod.waveMoney: return 'WAVE_MONEY';
|
||||
case PaymentMethod.orangeMoney: return 'ORANGE_MONEY';
|
||||
case PaymentMethod.freeMoney: return 'FREE_MONEY';
|
||||
case PaymentMethod.mobileMoney: return 'MOBILE_MONEY';
|
||||
case PaymentMethod.autre: return 'AUTRE';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Modèle complet d'une contribution
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class ContributionModel extends Equatable {
|
||||
/// Identifiant unique
|
||||
final String? id;
|
||||
|
||||
/// Membre concerné
|
||||
final String membreId;
|
||||
final String? membreNom;
|
||||
final String? membrePrenom;
|
||||
|
||||
/// Organisation
|
||||
final String? organisationId;
|
||||
final String? organisationNom;
|
||||
|
||||
/// Informations de la contribution
|
||||
final ContributionType type;
|
||||
final ContributionStatus statut;
|
||||
final double montant;
|
||||
final double? montantPaye;
|
||||
final String devise;
|
||||
|
||||
/// Dates
|
||||
final DateTime dateEcheance;
|
||||
final DateTime? datePaiement;
|
||||
final DateTime? dateRappel;
|
||||
|
||||
/// Paiement
|
||||
final PaymentMethod? methodePaiement;
|
||||
final String? numeroPaiement;
|
||||
final String? referencePaiement;
|
||||
|
||||
/// Période
|
||||
final int annee;
|
||||
final int? mois;
|
||||
final int? trimestre;
|
||||
final int? semestre;
|
||||
|
||||
/// Informations complémentaires
|
||||
final String? description;
|
||||
final String? notes;
|
||||
final String? recu;
|
||||
|
||||
/// Métadonnées
|
||||
final DateTime? dateCreation;
|
||||
final DateTime? dateModification;
|
||||
final String? creeParId;
|
||||
final String? modifieParId;
|
||||
|
||||
const ContributionModel({
|
||||
this.id,
|
||||
required this.membreId,
|
||||
this.membreNom,
|
||||
this.membrePrenom,
|
||||
this.organisationId,
|
||||
this.organisationNom,
|
||||
this.type = ContributionType.annuelle,
|
||||
this.statut = ContributionStatus.nonPayee,
|
||||
required this.montant,
|
||||
this.montantPaye,
|
||||
this.devise = 'XOF',
|
||||
required this.dateEcheance,
|
||||
this.datePaiement,
|
||||
this.dateRappel,
|
||||
this.methodePaiement,
|
||||
this.numeroPaiement,
|
||||
this.referencePaiement,
|
||||
required this.annee,
|
||||
this.mois,
|
||||
this.trimestre,
|
||||
this.semestre,
|
||||
this.description,
|
||||
this.notes,
|
||||
this.recu,
|
||||
this.dateCreation,
|
||||
this.dateModification,
|
||||
this.creeParId,
|
||||
this.modifieParId,
|
||||
});
|
||||
|
||||
/// Désérialisation depuis JSON
|
||||
factory ContributionModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$ContributionModelFromJson(json);
|
||||
|
||||
/// Sérialisation vers JSON
|
||||
Map<String, dynamic> toJson() => _$ContributionModelToJson(this);
|
||||
|
||||
/// Copie avec modifications
|
||||
ContributionModel copyWith({
|
||||
String? id,
|
||||
String? membreId,
|
||||
String? membreNom,
|
||||
String? membrePrenom,
|
||||
String? organisationId,
|
||||
String? organisationNom,
|
||||
ContributionType? type,
|
||||
ContributionStatus? statut,
|
||||
double? montant,
|
||||
double? montantPaye,
|
||||
String? devise,
|
||||
DateTime? dateEcheance,
|
||||
DateTime? datePaiement,
|
||||
DateTime? dateRappel,
|
||||
PaymentMethod? methodePaiement,
|
||||
String? numeroPaiement,
|
||||
String? referencePaiement,
|
||||
int? annee,
|
||||
int? mois,
|
||||
int? trimestre,
|
||||
int? semestre,
|
||||
String? description,
|
||||
String? notes,
|
||||
String? recu,
|
||||
DateTime? dateCreation,
|
||||
DateTime? dateModification,
|
||||
String? creeParId,
|
||||
String? modifieParId,
|
||||
}) {
|
||||
return ContributionModel(
|
||||
id: id ?? this.id,
|
||||
membreId: membreId ?? this.membreId,
|
||||
membreNom: membreNom ?? this.membreNom,
|
||||
membrePrenom: membrePrenom ?? this.membrePrenom,
|
||||
organisationId: organisationId ?? this.organisationId,
|
||||
organisationNom: organisationNom ?? this.organisationNom,
|
||||
type: type ?? this.type,
|
||||
statut: statut ?? this.statut,
|
||||
montant: montant ?? this.montant,
|
||||
montantPaye: montantPaye ?? this.montantPaye,
|
||||
devise: devise ?? this.devise,
|
||||
dateEcheance: dateEcheance ?? this.dateEcheance,
|
||||
datePaiement: datePaiement ?? this.datePaiement,
|
||||
dateRappel: dateRappel ?? this.dateRappel,
|
||||
methodePaiement: methodePaiement ?? this.methodePaiement,
|
||||
numeroPaiement: numeroPaiement ?? this.numeroPaiement,
|
||||
referencePaiement: referencePaiement ?? this.referencePaiement,
|
||||
annee: annee ?? this.annee,
|
||||
mois: mois ?? this.mois,
|
||||
trimestre: trimestre ?? this.trimestre,
|
||||
semestre: semestre ?? this.semestre,
|
||||
description: description ?? this.description,
|
||||
notes: notes ?? this.notes,
|
||||
recu: recu ?? this.recu,
|
||||
dateCreation: dateCreation ?? this.dateCreation,
|
||||
dateModification: dateModification ?? this.dateModification,
|
||||
creeParId: creeParId ?? this.creeParId,
|
||||
modifieParId: modifieParId ?? this.modifieParId,
|
||||
);
|
||||
}
|
||||
|
||||
/// Nom complet du membre
|
||||
String get membreNomComplet {
|
||||
if (membreNom != null && membrePrenom != null) {
|
||||
return '$membrePrenom $membreNom';
|
||||
}
|
||||
return membreNom ?? membrePrenom ?? 'Membre inconnu';
|
||||
}
|
||||
|
||||
/// Montant restant à payer
|
||||
double get montantRestant {
|
||||
if (montantPaye == null) return montant;
|
||||
return montant - montantPaye!;
|
||||
}
|
||||
|
||||
/// Pourcentage payé
|
||||
double get pourcentagePaye {
|
||||
if (montantPaye == null || montant == 0) return 0;
|
||||
return (montantPaye! / montant) * 100;
|
||||
}
|
||||
|
||||
/// Vérifie si la contribution est payée
|
||||
bool get estPayee => statut == ContributionStatus.payee;
|
||||
|
||||
/// Vérifie si la contribution est en retard
|
||||
bool get estEnRetard {
|
||||
if (estPayee) return false;
|
||||
return DateTime.now().isAfter(dateEcheance);
|
||||
}
|
||||
|
||||
/// Nombre de jours avant/après l'échéance
|
||||
int get joursAvantEcheance {
|
||||
return dateEcheance.difference(DateTime.now()).inDays;
|
||||
}
|
||||
|
||||
/// Libellé de la période
|
||||
String get libellePeriode {
|
||||
switch (type) {
|
||||
case ContributionType.annuelle:
|
||||
return 'Année $annee';
|
||||
case ContributionType.mensuelle:
|
||||
if (mois != null) {
|
||||
return '${_getNomMois(mois!)} $annee';
|
||||
}
|
||||
return 'Année $annee';
|
||||
case ContributionType.trimestrielle:
|
||||
if (trimestre != null) {
|
||||
return 'T$trimestre $annee';
|
||||
}
|
||||
return 'Année $annee';
|
||||
case ContributionType.semestrielle:
|
||||
if (semestre != null) {
|
||||
return 'S$semestre $annee';
|
||||
}
|
||||
return 'Année $annee';
|
||||
case ContributionType.exceptionnelle:
|
||||
return 'Exceptionnelle $annee';
|
||||
}
|
||||
}
|
||||
|
||||
/// Nom du mois
|
||||
String _getNomMois(int mois) {
|
||||
const moisFr = [
|
||||
'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
||||
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'
|
||||
];
|
||||
if (mois >= 1 && mois <= 12) {
|
||||
return moisFr[mois - 1];
|
||||
}
|
||||
return 'Mois $mois';
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
membreId,
|
||||
membreNom,
|
||||
membrePrenom,
|
||||
organisationId,
|
||||
organisationNom,
|
||||
type,
|
||||
statut,
|
||||
montant,
|
||||
montantPaye,
|
||||
devise,
|
||||
dateEcheance,
|
||||
datePaiement,
|
||||
dateRappel,
|
||||
methodePaiement,
|
||||
numeroPaiement,
|
||||
referencePaiement,
|
||||
annee,
|
||||
mois,
|
||||
trimestre,
|
||||
semestre,
|
||||
description,
|
||||
notes,
|
||||
recu,
|
||||
dateCreation,
|
||||
dateModification,
|
||||
creeParId,
|
||||
modifieParId,
|
||||
];
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'ContributionModel(id: $id, membre: $membreNomComplet, montant: $montant $devise, statut: $statut)';
|
||||
}
|
||||
|
||||
112
lib/features/contributions/data/models/contribution_model.g.dart
Normal file
112
lib/features/contributions/data/models/contribution_model.g.dart
Normal file
@@ -0,0 +1,112 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'contribution_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
ContributionModel _$ContributionModelFromJson(Map<String, dynamic> json) =>
|
||||
ContributionModel(
|
||||
id: json['id'] as String?,
|
||||
membreId: json['membreId'] as String,
|
||||
membreNom: json['membreNom'] as String?,
|
||||
membrePrenom: json['membrePrenom'] as String?,
|
||||
organisationId: json['organisationId'] as String?,
|
||||
organisationNom: json['organisationNom'] as String?,
|
||||
type: $enumDecodeNullable(_$ContributionTypeEnumMap, json['type']) ??
|
||||
ContributionType.annuelle,
|
||||
statut:
|
||||
$enumDecodeNullable(_$ContributionStatusEnumMap, json['statut']) ??
|
||||
ContributionStatus.nonPayee,
|
||||
montant: (json['montant'] as num).toDouble(),
|
||||
montantPaye: (json['montantPaye'] as num?)?.toDouble(),
|
||||
devise: json['devise'] as String? ?? 'XOF',
|
||||
dateEcheance: DateTime.parse(json['dateEcheance'] as String),
|
||||
datePaiement: json['datePaiement'] == null
|
||||
? null
|
||||
: DateTime.parse(json['datePaiement'] as String),
|
||||
dateRappel: json['dateRappel'] == null
|
||||
? null
|
||||
: DateTime.parse(json['dateRappel'] as String),
|
||||
methodePaiement:
|
||||
$enumDecodeNullable(_$PaymentMethodEnumMap, json['methodePaiement']),
|
||||
numeroPaiement: json['numeroPaiement'] as String?,
|
||||
referencePaiement: json['referencePaiement'] as String?,
|
||||
annee: (json['annee'] as num).toInt(),
|
||||
mois: (json['mois'] as num?)?.toInt(),
|
||||
trimestre: (json['trimestre'] as num?)?.toInt(),
|
||||
semestre: (json['semestre'] as num?)?.toInt(),
|
||||
description: json['description'] as String?,
|
||||
notes: json['notes'] as String?,
|
||||
recu: json['recu'] as String?,
|
||||
dateCreation: json['dateCreation'] == null
|
||||
? null
|
||||
: DateTime.parse(json['dateCreation'] as String),
|
||||
dateModification: json['dateModification'] == null
|
||||
? null
|
||||
: DateTime.parse(json['dateModification'] as String),
|
||||
creeParId: json['creeParId'] as String?,
|
||||
modifieParId: json['modifieParId'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ContributionModelToJson(ContributionModel instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'membreId': instance.membreId,
|
||||
'membreNom': instance.membreNom,
|
||||
'membrePrenom': instance.membrePrenom,
|
||||
'organisationId': instance.organisationId,
|
||||
'organisationNom': instance.organisationNom,
|
||||
'type': _$ContributionTypeEnumMap[instance.type]!,
|
||||
'statut': _$ContributionStatusEnumMap[instance.statut]!,
|
||||
'montant': instance.montant,
|
||||
'montantPaye': instance.montantPaye,
|
||||
'devise': instance.devise,
|
||||
'dateEcheance': instance.dateEcheance.toIso8601String(),
|
||||
'datePaiement': instance.datePaiement?.toIso8601String(),
|
||||
'dateRappel': instance.dateRappel?.toIso8601String(),
|
||||
'methodePaiement': _$PaymentMethodEnumMap[instance.methodePaiement],
|
||||
'numeroPaiement': instance.numeroPaiement,
|
||||
'referencePaiement': instance.referencePaiement,
|
||||
'annee': instance.annee,
|
||||
'mois': instance.mois,
|
||||
'trimestre': instance.trimestre,
|
||||
'semestre': instance.semestre,
|
||||
'description': instance.description,
|
||||
'notes': instance.notes,
|
||||
'recu': instance.recu,
|
||||
'dateCreation': instance.dateCreation?.toIso8601String(),
|
||||
'dateModification': instance.dateModification?.toIso8601String(),
|
||||
'creeParId': instance.creeParId,
|
||||
'modifieParId': instance.modifieParId,
|
||||
};
|
||||
|
||||
const _$ContributionTypeEnumMap = {
|
||||
ContributionType.annuelle: 'ANNUELLE',
|
||||
ContributionType.mensuelle: 'MENSUELLE',
|
||||
ContributionType.trimestrielle: 'TRIMESTRIELLE',
|
||||
ContributionType.semestrielle: 'SEMESTRIELLE',
|
||||
ContributionType.exceptionnelle: 'EXCEPTIONNELLE',
|
||||
};
|
||||
|
||||
const _$ContributionStatusEnumMap = {
|
||||
ContributionStatus.payee: 'PAYEE',
|
||||
ContributionStatus.nonPayee: 'NON_PAYEE',
|
||||
ContributionStatus.enAttente: 'EN_ATTENTE',
|
||||
ContributionStatus.enRetard: 'EN_RETARD',
|
||||
ContributionStatus.partielle: 'PARTIELLE',
|
||||
ContributionStatus.annulee: 'ANNULEE',
|
||||
};
|
||||
|
||||
const _$PaymentMethodEnumMap = {
|
||||
PaymentMethod.especes: 'ESPECES',
|
||||
PaymentMethod.cheque: 'CHEQUE',
|
||||
PaymentMethod.virement: 'VIREMENT',
|
||||
PaymentMethod.carteBancaire: 'CARTE_BANCAIRE',
|
||||
PaymentMethod.waveMoney: 'WAVE_MONEY',
|
||||
PaymentMethod.orangeMoney: 'ORANGE_MONEY',
|
||||
PaymentMethod.freeMoney: 'FREE_MONEY',
|
||||
PaymentMethod.mobileMoney: 'MOBILE_MONEY',
|
||||
PaymentMethod.autre: 'AUTRE',
|
||||
};
|
||||
@@ -0,0 +1,404 @@
|
||||
/// Implémentation du repository des cotisations via l'API backend
|
||||
library contribution_repository_impl;
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:unionflow_mobile_apps/core/network/api_client.dart';
|
||||
import 'package:unionflow_mobile_apps/core/utils/logger.dart';
|
||||
import '../../domain/repositories/contribution_repository.dart';
|
||||
import '../models/contribution_model.dart';
|
||||
|
||||
/// Implémentation du repository des cotisations - appels API réels vers /api/cotisations
|
||||
@LazySingleton(as: IContributionRepository)
|
||||
class ContributionRepositoryImpl implements IContributionRepository {
|
||||
final ApiClient _apiClient;
|
||||
static const String _baseUrl = '/api/cotisations';
|
||||
|
||||
ContributionRepositoryImpl(this._apiClient);
|
||||
|
||||
/// Toutes les cotisations du membre connecté (GET /api/cotisations/mes-cotisations).
|
||||
Future<ContributionPageResult> getMesCotisations({int page = 0, int size = 50}) async {
|
||||
final response = await _apiClient.get(
|
||||
'$_baseUrl/mes-cotisations',
|
||||
queryParameters: {'page': page, 'size': size},
|
||||
);
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception(
|
||||
'Erreur lors de la récupération des cotisations: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
final data = response.data;
|
||||
final List<dynamic> list = data is List ? data as List<dynamic> : <dynamic>[];
|
||||
final contributions = list.map((e) => _summaryToModel(e as Map<String, dynamic>)).toList();
|
||||
return ContributionPageResult(
|
||||
contributions: contributions,
|
||||
total: contributions.length,
|
||||
page: page,
|
||||
size: size,
|
||||
totalPages: list.isEmpty ? 0 : 1,
|
||||
);
|
||||
}
|
||||
|
||||
/// Récupère les cotisations en attente du membre connecté (endpoint dédié).
|
||||
Future<ContributionPageResult> getMesCotisationsEnAttente() async {
|
||||
final path = '$_baseUrl/mes-cotisations/en-attente';
|
||||
final response = await _apiClient.get(path);
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception(
|
||||
'Erreur lors de la récupération des cotisations: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
final data = response.data;
|
||||
final List<dynamic> list = data is List ? data : (data is Map ? (data['data'] ?? data['content'] ?? []) as List<dynamic>? ?? [] : []);
|
||||
final contributions = list
|
||||
.map((e) => _summaryToModel(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
return ContributionPageResult(
|
||||
contributions: contributions,
|
||||
total: contributions.length,
|
||||
page: 0,
|
||||
size: contributions.length,
|
||||
totalPages: contributions.isEmpty ? 0 : 1,
|
||||
);
|
||||
}
|
||||
|
||||
static ContributionModel _summaryToModel(Map<String, dynamic> json) {
|
||||
final id = json['id']?.toString();
|
||||
final statutStr = json['statut'] as String? ?? 'EN_ATTENTE';
|
||||
final statut = _mapStatut(statutStr);
|
||||
final montantDu = (json['montantDu'] as num?)?.toDouble() ?? 0.0;
|
||||
final montantPaye = (json['montantPaye'] as num?)?.toDouble();
|
||||
final dateEcheanceStr = json['dateEcheance'] as String?;
|
||||
final dateEcheance = dateEcheanceStr != null
|
||||
? DateTime.tryParse(dateEcheanceStr) ?? DateTime.now()
|
||||
: DateTime.now();
|
||||
final annee = (json['annee'] as num?)?.toInt() ?? dateEcheance.year;
|
||||
return ContributionModel(
|
||||
id: id,
|
||||
membreId: '', // membre implicite (endpoint "mes cotisations")
|
||||
membreNom: (json['nomMembre'] ?? json['nomCompletMembre']) as String?,
|
||||
type: ContributionType.annuelle,
|
||||
statut: statut,
|
||||
montant: montantDu,
|
||||
montantPaye: montantPaye,
|
||||
devise: 'XOF',
|
||||
dateEcheance: dateEcheance,
|
||||
annee: annee,
|
||||
);
|
||||
}
|
||||
|
||||
static ContributionStatus _mapStatut(String code) {
|
||||
switch (code.toUpperCase()) {
|
||||
case 'PAYEE':
|
||||
return ContributionStatus.payee;
|
||||
case 'EN_RETARD':
|
||||
return ContributionStatus.enRetard;
|
||||
case 'PARTIELLE':
|
||||
return ContributionStatus.partielle;
|
||||
case 'ANNULEE':
|
||||
return ContributionStatus.annulee;
|
||||
case 'EN_ATTENTE':
|
||||
case 'NON_PAYEE':
|
||||
default:
|
||||
return ContributionStatus.nonPayee;
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère la liste des cotisations avec pagination (toutes cotisations, nécessite droits admin)
|
||||
Future<ContributionPageResult> getCotisations({
|
||||
int page = 0,
|
||||
int size = 20,
|
||||
String? membreId,
|
||||
String? statut,
|
||||
String? type,
|
||||
int? annee,
|
||||
}) async {
|
||||
final queryParams = <String, dynamic>{
|
||||
'page': page,
|
||||
'size': size,
|
||||
};
|
||||
if (membreId != null) queryParams['membreId'] = membreId;
|
||||
if (statut != null) queryParams['statut'] = statut;
|
||||
if (type != null) queryParams['type'] = type;
|
||||
if (annee != null) queryParams['annee'] = annee;
|
||||
|
||||
final response = await _apiClient.get(
|
||||
_baseUrl,
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
if (data is List) {
|
||||
final contributions = data
|
||||
.map((json) => ContributionModel.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
return ContributionPageResult(
|
||||
contributions: contributions,
|
||||
total: contributions.length,
|
||||
page: page,
|
||||
size: size,
|
||||
totalPages: 1,
|
||||
);
|
||||
} else if (data is Map<String, dynamic>) {
|
||||
final List<dynamic> content = data['content'] ?? data['items'] ?? [];
|
||||
final contributions = content
|
||||
.map((json) => ContributionModel.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
return ContributionPageResult(
|
||||
contributions: contributions,
|
||||
total: data['totalElements'] ?? data['total'] ?? contributions.length,
|
||||
page: data['number'] ?? page,
|
||||
size: data['size'] ?? size,
|
||||
totalPages: data['totalPages'] ?? 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
throw Exception('Erreur lors de la récupération des cotisations: ${response.statusCode}');
|
||||
}
|
||||
|
||||
/// Récupère une cotisation par ID
|
||||
Future<ContributionModel> getCotisationById(String id) async {
|
||||
final response = await _apiClient.get('$_baseUrl/$id');
|
||||
if (response.statusCode == 200) {
|
||||
return ContributionModel.fromJson(response.data as Map<String, dynamic>);
|
||||
}
|
||||
throw Exception('Cotisation non trouvée');
|
||||
}
|
||||
|
||||
/// Crée une nouvelle cotisation (payload conforme au backend CreateCotisationRequest)
|
||||
Future<ContributionModel> createCotisation(ContributionModel contribution) async {
|
||||
final body = _toCreateCotisationRequest(contribution);
|
||||
final response = await _apiClient.post(_baseUrl, data: body);
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
final data = Map<String, dynamic>.from(response.data as Map<String, dynamic>);
|
||||
_normalizeCotisationResponse(data);
|
||||
return ContributionModel.fromJson(data);
|
||||
}
|
||||
final message = response.data is Map
|
||||
? (response.data as Map)['error'] ?? response.data.toString()
|
||||
: response.data?.toString() ?? 'Erreur ${response.statusCode}';
|
||||
throw Exception('Erreur lors de la création: $message');
|
||||
}
|
||||
|
||||
/// Construit le body attendu par POST /api/cotisations (CreateCotisationRequest)
|
||||
static Map<String, dynamic> _toCreateCotisationRequest(ContributionModel c) {
|
||||
if (c.organisationId == null || c.organisationId!.trim().isEmpty) {
|
||||
throw Exception('L\'organisation du membre est requise pour créer une cotisation.');
|
||||
}
|
||||
final typeStr = _contributionTypeToBackend(c.type);
|
||||
final dateStr = _formatLocalDate(c.dateEcheance);
|
||||
final desc = c.description?.trim();
|
||||
final libelle = desc != null && desc.isNotEmpty
|
||||
? (desc.length > 100 ? desc.substring(0, 100) : desc)
|
||||
: 'Cotisation $typeStr ${c.annee}';
|
||||
final description = desc != null && desc.isNotEmpty
|
||||
? (desc.length > 500 ? desc.substring(0, 500) : desc)
|
||||
: null;
|
||||
return {
|
||||
'membreId': c.membreId,
|
||||
'organisationId': c.organisationId!.trim(),
|
||||
'typeCotisation': typeStr,
|
||||
'libelle': libelle,
|
||||
if (description != null) 'description': description,
|
||||
'montantDu': c.montant,
|
||||
'codeDevise': c.devise.length == 3 ? c.devise : 'XOF',
|
||||
'dateEcheance': dateStr,
|
||||
'periode': '${_monthName(c.dateEcheance.month)} ${c.dateEcheance.year}',
|
||||
'annee': c.annee,
|
||||
'mois': c.mois ?? c.dateEcheance.month,
|
||||
'recurrente': false,
|
||||
if (c.notes != null && c.notes!.isNotEmpty) 'observations': c.notes,
|
||||
};
|
||||
}
|
||||
|
||||
static String _contributionTypeToBackend(ContributionType t) {
|
||||
switch (t) {
|
||||
case ContributionType.mensuelle:
|
||||
return 'MENSUELLE';
|
||||
case ContributionType.trimestrielle:
|
||||
return 'TRIMESTRIELLE';
|
||||
case ContributionType.semestrielle:
|
||||
return 'SEMESTRIELLE';
|
||||
case ContributionType.annuelle:
|
||||
return 'ANNUELLE';
|
||||
case ContributionType.exceptionnelle:
|
||||
return 'EXCEPTIONNELLE';
|
||||
}
|
||||
}
|
||||
|
||||
static String _formatLocalDate(DateTime d) =>
|
||||
'${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
|
||||
|
||||
static String _monthName(int month) {
|
||||
const names = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
|
||||
return month >= 1 && month <= 12 ? names[month - 1] : 'Mois $month';
|
||||
}
|
||||
|
||||
/// Adapte les clés de la réponse backend (CotisationResponse) vers le modèle mobile
|
||||
static void _normalizeCotisationResponse(Map<String, dynamic> data) {
|
||||
if (data.containsKey('nomMembre') && !data.containsKey('membreNom')) data['membreNom'] = data['nomMembre'];
|
||||
if (data.containsKey('nomOrganisation') && !data.containsKey('organisationNom')) data['organisationNom'] = data['nomOrganisation'];
|
||||
if (data.containsKey('codeDevise') && !data.containsKey('devise')) data['devise'] = data['codeDevise'];
|
||||
if (data.containsKey('montantDu') && !data.containsKey('montant')) data['montant'] = data['montantDu'];
|
||||
if (data['id'] != null && data['id'] is! String) data['id'] = data['id'].toString();
|
||||
if (data['membreId'] != null && data['membreId'] is! String) data['membreId'] = data['membreId'].toString();
|
||||
if (data['organisationId'] != null && data['organisationId'] is! String) data['organisationId'] = data['organisationId'].toString();
|
||||
}
|
||||
|
||||
/// Met à jour une cotisation
|
||||
Future<ContributionModel> updateCotisation(String id, ContributionModel contribution) async {
|
||||
final response = await _apiClient.put('$_baseUrl/$id', data: contribution.toJson());
|
||||
if (response.statusCode == 200) {
|
||||
return ContributionModel.fromJson(response.data as Map<String, dynamic>);
|
||||
}
|
||||
throw Exception('Erreur lors de la mise à jour: ${response.statusCode}');
|
||||
}
|
||||
|
||||
/// Supprime une cotisation
|
||||
Future<void> deleteCotisation(String id) async {
|
||||
final response = await _apiClient.delete('$_baseUrl/$id');
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
throw Exception('Erreur lors de la suppression: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Initie un paiement en ligne (Wave Checkout API).
|
||||
/// Retourne l'URL à ouvrir (wave_launch_url) pour que le membre confirme dans l'app Wave.
|
||||
/// Spec: https://docs.wave.com/checkout
|
||||
Future<WavePaiementInitResult> initierPaiementEnLigne({
|
||||
required String cotisationId,
|
||||
required String methodePaiement,
|
||||
required String numeroTelephone,
|
||||
}) async {
|
||||
final response = await _apiClient.post(
|
||||
'/api/paiements/initier-paiement-en-ligne',
|
||||
data: {
|
||||
'cotisationId': cotisationId,
|
||||
'methodePaiement': methodePaiement,
|
||||
'numeroTelephone': numeroTelephone.replaceAll(RegExp(r'\D'), ''),
|
||||
},
|
||||
);
|
||||
if (response.statusCode != 201 && response.statusCode != 200) {
|
||||
final msg = response.data is Map
|
||||
? (response.data['message'] ?? response.data['error'] ?? response.statusCode)
|
||||
: response.statusCode;
|
||||
throw Exception('Impossible d\'initier le paiement: $msg');
|
||||
}
|
||||
final data = response.data is Map<String, dynamic>
|
||||
? response.data as Map<String, dynamic>
|
||||
: Map<String, dynamic>.from(response.data as Map);
|
||||
return WavePaiementInitResult(
|
||||
redirectUrl: data['redirectUrl'] as String? ?? data['waveLaunchUrl'] as String? ?? '',
|
||||
waveLaunchUrl: data['waveLaunchUrl'] as String? ?? data['redirectUrl'] as String? ?? '',
|
||||
waveCheckoutSessionId: data['waveCheckoutSessionId'] as String?,
|
||||
clientReference: data['clientReference'] as String?,
|
||||
message: data['message'] as String? ?? 'Ouvrez Wave pour confirmer le paiement.',
|
||||
);
|
||||
}
|
||||
|
||||
/// Enregistre un paiement
|
||||
Future<ContributionModel> enregistrerPaiement(
|
||||
String cotisationId, {
|
||||
required double montant,
|
||||
required DateTime datePaiement,
|
||||
required String methodePaiement,
|
||||
String? numeroPaiement,
|
||||
String? referencePaiement,
|
||||
}) async {
|
||||
final response = await _apiClient.post(
|
||||
'$_baseUrl/$cotisationId/paiement',
|
||||
data: {
|
||||
'montant': montant,
|
||||
'datePaiement': datePaiement.toIso8601String(),
|
||||
'methodePaiement': methodePaiement,
|
||||
if (numeroPaiement != null) 'numeroPaiement': numeroPaiement,
|
||||
if (referencePaiement != null) 'referencePaiement': referencePaiement,
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
return ContributionModel.fromJson(response.data as Map<String, dynamic>);
|
||||
}
|
||||
throw Exception('Erreur lors de l\'enregistrement du paiement: ${response.statusCode}');
|
||||
}
|
||||
|
||||
/// Synthèse personnelle du membre connecté (GET /api/cotisations/mes-cotisations/synthese)
|
||||
Future<Map<String, dynamic>?> getMesCotisationsSynthese() async {
|
||||
try {
|
||||
final response = await _apiClient.get('$_baseUrl/mes-cotisations/synthese');
|
||||
if (response.statusCode == 200 && response.data != null) {
|
||||
final data = response.data is Map<String, dynamic>
|
||||
? response.data as Map<String, dynamic>
|
||||
: Map<String, dynamic>.from(response.data as Map);
|
||||
data['isMesSynthese'] = true;
|
||||
return data;
|
||||
}
|
||||
return null;
|
||||
} catch (e, st) {
|
||||
AppLogger.error('ContributionRepository: getMesCotisationsSynthese échoué', error: e, stackTrace: st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère les statistiques des cotisations (globales ou mes selon usage)
|
||||
Future<Map<String, dynamic>> getStatistiques() async {
|
||||
final response = await _apiClient.get('$_baseUrl/statistiques');
|
||||
if (response.statusCode == 200) {
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
throw Exception('Erreur lors de la récupération des statistiques');
|
||||
}
|
||||
|
||||
/// Envoie un rappel de paiement
|
||||
Future<void> envoyerRappel(String cotisationId) async {
|
||||
final response = await _apiClient.post('$_baseUrl/$cotisationId/rappel');
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Erreur lors de l\'envoi du rappel');
|
||||
}
|
||||
}
|
||||
|
||||
/// Génère les cotisations annuelles
|
||||
Future<int> genererCotisationsAnnuelles(int annee) async {
|
||||
final response = await _apiClient.post(
|
||||
'$_baseUrl/generer',
|
||||
data: {'annee': annee},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
return response.data['nombreGenere'] ?? 0;
|
||||
}
|
||||
throw Exception('Erreur lors de la génération');
|
||||
}
|
||||
}
|
||||
|
||||
/// Résultat de l'initiation d'un paiement Wave (redirection vers l'app Wave).
|
||||
class WavePaiementInitResult {
|
||||
final String redirectUrl;
|
||||
final String waveLaunchUrl;
|
||||
final String? waveCheckoutSessionId;
|
||||
final String? clientReference;
|
||||
final String message;
|
||||
|
||||
const WavePaiementInitResult({
|
||||
required this.redirectUrl,
|
||||
required this.waveLaunchUrl,
|
||||
this.waveCheckoutSessionId,
|
||||
this.clientReference,
|
||||
required this.message,
|
||||
});
|
||||
}
|
||||
|
||||
/// Résultat paginé de cotisations
|
||||
class ContributionPageResult {
|
||||
final List<ContributionModel> contributions;
|
||||
final int total;
|
||||
final int page;
|
||||
final int size;
|
||||
final int totalPages;
|
||||
|
||||
const ContributionPageResult({
|
||||
required this.contributions,
|
||||
required this.total,
|
||||
required this.page,
|
||||
required this.size,
|
||||
required this.totalPages,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user