Authentification stable - WIP

This commit is contained in:
DahoudG
2025-09-19 12:35:46 +00:00
parent 63fe107f98
commit 098894bdc1
383 changed files with 13072 additions and 93334 deletions

View File

@@ -1,244 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../core/models/cotisation_model.dart';
import '../../../../core/animations/loading_animations.dart';
import 'cotisation_card.dart';
/// Widget animé pour afficher une liste de cotisations avec animations d'apparition
class AnimatedCotisationList extends StatefulWidget {
final List<CotisationModel> cotisations;
final Function(CotisationModel)? onCotisationTap;
final bool isLoading;
final VoidCallback? onRefresh;
final ScrollController? scrollController;
const AnimatedCotisationList({
super.key,
required this.cotisations,
this.onCotisationTap,
this.isLoading = false,
this.onRefresh,
this.scrollController,
});
@override
State<AnimatedCotisationList> createState() => _AnimatedCotisationListState();
}
class _AnimatedCotisationListState extends State<AnimatedCotisationList>
with TickerProviderStateMixin {
late AnimationController _listController;
List<AnimationController> _itemControllers = [];
List<Animation<double>> _itemAnimations = [];
List<Animation<Offset>> _slideAnimations = [];
@override
void initState() {
super.initState();
_initializeAnimations();
}
@override
void didUpdateWidget(AnimatedCotisationList oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.cotisations.length != oldWidget.cotisations.length) {
_updateAnimations();
}
}
@override
void dispose() {
_listController.dispose();
for (final controller in _itemControllers) {
controller.dispose();
}
super.dispose();
}
void _initializeAnimations() {
_listController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_updateAnimations();
_listController.forward();
}
void _updateAnimations() {
// Dispose des anciens controllers s'ils existent
if (_itemControllers.isNotEmpty) {
for (final controller in _itemControllers) {
controller.dispose();
}
}
// Créer de nouveaux controllers pour chaque élément
_itemControllers = List.generate(
widget.cotisations.length,
(index) => AnimationController(
duration: Duration(milliseconds: 400 + (index * 80)),
vsync: this,
),
);
// Animations de fade et scale
_itemAnimations = _itemControllers.map((controller) {
return Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: controller,
curve: Curves.easeOutCubic,
),
);
}).toList();
// Animations de slide depuis la gauche
_slideAnimations = _itemControllers.map((controller) {
return Tween<Offset>(
begin: const Offset(-0.3, 0),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: controller,
curve: Curves.easeOutCubic,
),
);
}).toList();
// Démarrer les animations avec un délai progressif
for (int i = 0; i < _itemControllers.length; i++) {
Future.delayed(Duration(milliseconds: i * 120), () {
if (mounted) {
_itemControllers[i].forward();
}
});
}
}
@override
Widget build(BuildContext context) {
if (widget.isLoading && widget.cotisations.isEmpty) {
return _buildLoadingState();
}
if (widget.cotisations.isEmpty) {
return _buildEmptyState();
}
return RefreshIndicator(
onRefresh: () async {
widget.onRefresh?.call();
await Future.delayed(const Duration(milliseconds: 500));
},
child: ListView.builder(
controller: widget.scrollController,
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16),
itemCount: widget.cotisations.length + (widget.isLoading ? 1 : 0),
itemBuilder: (context, index) {
if (index >= widget.cotisations.length) {
return _buildLoadingIndicator();
}
return _buildAnimatedItem(index);
},
),
);
}
Widget _buildAnimatedItem(int index) {
final cotisation = widget.cotisations[index];
if (index >= _itemAnimations.length) {
// Fallback pour les nouveaux éléments
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: CotisationCard(
cotisation: cotisation,
onTap: () => widget.onCotisationTap?.call(cotisation),
),
);
}
return AnimatedBuilder(
animation: _itemAnimations[index],
builder: (context, child) {
return SlideTransition(
position: _slideAnimations[index],
child: FadeTransition(
opacity: _itemAnimations[index],
child: Transform.scale(
scale: 0.9 + (0.1 * _itemAnimations[index].value),
child: Padding(
padding: const EdgeInsets.only(bottom: 16),
child: CotisationCard(
cotisation: cotisation,
onTap: () => widget.onCotisationTap?.call(cotisation),
),
),
),
),
);
},
);
}
Widget _buildLoadingState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
LoadingAnimations.pulse(),
const SizedBox(height: 24),
const Text(
'Chargement des cotisations...',
style: TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
],
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.payment_outlined,
size: 80,
color: Colors.grey[400],
),
const SizedBox(height: 24),
Text(
'Aucune cotisation trouvée',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Text(
'Les cotisations apparaîtront ici',
style: TextStyle(
fontSize: 14,
color: Colors.grey[500],
),
),
],
),
);
}
Widget _buildLoadingIndicator() {
return Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: LoadingAnimations.spinner(),
),
);
}
}

View File

@@ -1,323 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import '../../../../core/models/cotisation_model.dart';
import '../../../../shared/theme/app_theme.dart';
/// Widget card pour afficher une cotisation
class CotisationCard extends StatelessWidget {
final CotisationModel cotisation;
final VoidCallback? onTap;
final VoidCallback? onPay;
final VoidCallback? onEdit;
final VoidCallback? onDelete;
const CotisationCard({
super.key,
required this.cotisation,
this.onTap,
this.onPay,
this.onEdit,
this.onDelete,
});
@override
Widget build(BuildContext context) {
final currencyFormat = NumberFormat.currency(
locale: 'fr_FR',
symbol: 'FCFA',
decimalDigits: 0,
);
final dateFormat = DateFormat('dd/MM/yyyy', 'fr_FR');
return Card(
elevation: 2,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: _getStatusColor().withOpacity(0.3),
width: 1,
),
),
child: InkWell(
onTap: () {
HapticFeedback.lightImpact();
onTap?.call();
},
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header avec statut et actions
Row(
children: [
// Statut badge
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getStatusColor().withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
cotisation.libelleStatut,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: _getStatusColor(),
),
),
),
const Spacer(),
// Actions
if (cotisation.statut == 'EN_ATTENTE' || cotisation.statut == 'EN_RETARD')
IconButton(
onPressed: () {
HapticFeedback.lightImpact();
onPay?.call();
},
icon: const Icon(Icons.payment, size: 20),
color: AppTheme.successColor,
tooltip: 'Payer',
),
if (onEdit != null)
IconButton(
onPressed: onEdit,
icon: const Icon(Icons.edit, size: 20),
color: AppTheme.primaryColor,
tooltip: 'Modifier',
),
if (onDelete != null)
IconButton(
onPressed: onDelete,
icon: const Icon(Icons.delete, size: 20),
color: AppTheme.errorColor,
tooltip: 'Supprimer',
),
],
),
const SizedBox(height: 12),
// Informations principales
Row(
children: [
// Icône du type
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Center(
child: Text(
cotisation.iconeTypeCotisation,
style: const TextStyle(fontSize: 20),
),
),
),
const SizedBox(width: 12),
// Détails
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
cotisation.libelleTypeCotisation,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
if (cotisation.nomMembre != null) ...[
const SizedBox(height: 2),
Text(
cotisation.nomMembre!,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
],
if (cotisation.periode != null) ...[
const SizedBox(height: 2),
Text(
cotisation.periode!,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textHint,
),
),
],
],
),
),
// Montant
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
currencyFormat.format(cotisation.montantDu),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
if (cotisation.montantPaye > 0) ...[
const SizedBox(height: 2),
Text(
'Payé: ${currencyFormat.format(cotisation.montantPaye)}',
style: const TextStyle(
fontSize: 12,
color: AppTheme.successColor,
),
),
],
],
),
],
),
const SizedBox(height: 12),
// Barre de progression du paiement
if (cotisation.montantPaye > 0 && !cotisation.isEntierementPayee) ...[
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Progression',
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
Text(
'${cotisation.pourcentagePaiement.toStringAsFixed(0)}%',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppTheme.textSecondary,
),
),
],
),
const SizedBox(height: 4),
LinearProgressIndicator(
value: cotisation.pourcentagePaiement / 100,
backgroundColor: AppTheme.borderColor,
valueColor: AlwaysStoppedAnimation<Color>(
cotisation.pourcentagePaiement >= 100
? AppTheme.successColor
: AppTheme.primaryColor,
),
),
],
),
const SizedBox(height: 12),
],
// Informations d'échéance
Row(
children: [
Icon(
Icons.schedule,
size: 16,
color: cotisation.isEnRetard
? AppTheme.errorColor
: cotisation.echeanceProche
? AppTheme.warningColor
: AppTheme.textHint,
),
const SizedBox(width: 4),
Text(
'Échéance: ${dateFormat.format(cotisation.dateEcheance)}',
style: TextStyle(
fontSize: 12,
color: cotisation.isEnRetard
? AppTheme.errorColor
: cotisation.echeanceProche
? AppTheme.warningColor
: AppTheme.textSecondary,
),
),
if (cotisation.messageUrgence.isNotEmpty) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: cotisation.isEnRetard
? AppTheme.errorColor.withOpacity(0.1)
: AppTheme.warningColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
cotisation.messageUrgence,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: cotisation.isEnRetard
? AppTheme.errorColor
: AppTheme.warningColor,
),
),
),
],
],
),
// Référence
const SizedBox(height: 8),
Row(
children: [
const Icon(
Icons.tag,
size: 16,
color: AppTheme.textHint,
),
const SizedBox(width: 4),
Text(
'Réf: ${cotisation.numeroReference}',
style: const TextStyle(
fontSize: 12,
color: AppTheme.textHint,
),
),
],
),
],
),
),
),
);
}
Color _getStatusColor() {
switch (cotisation.statut) {
case 'PAYEE':
return AppTheme.successColor;
case 'EN_ATTENTE':
return AppTheme.warningColor;
case 'EN_RETARD':
return AppTheme.errorColor;
case 'PARTIELLEMENT_PAYEE':
return AppTheme.infoColor;
case 'ANNULEE':
return AppTheme.textHint;
default:
return AppTheme.textSecondary;
}
}
}

View File

@@ -1,417 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../core/models/cotisation_model.dart';
import '../../../../shared/theme/app_theme.dart';
/// Widget d'affichage de la timeline d'une cotisation
class CotisationTimelineWidget extends StatefulWidget {
final CotisationModel cotisation;
const CotisationTimelineWidget({
super.key,
required this.cotisation,
});
@override
State<CotisationTimelineWidget> createState() => _CotisationTimelineWidgetState();
}
class _CotisationTimelineWidgetState extends State<CotisationTimelineWidget>
with TickerProviderStateMixin {
late final AnimationController _animationController;
late final List<Animation<double>> _itemAnimations;
List<TimelineEvent> _timelineEvents = [];
@override
void initState() {
super.initState();
_generateTimelineEvents();
_animationController = AnimationController(
duration: Duration(milliseconds: 300 * _timelineEvents.length),
vsync: this,
);
_itemAnimations = List.generate(
_timelineEvents.length,
(index) => Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _animationController,
curve: Interval(
index / _timelineEvents.length,
(index + 1) / _timelineEvents.length,
curve: Curves.easeOutCubic,
),
),
),
);
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _generateTimelineEvents() {
_timelineEvents = [
TimelineEvent(
title: 'Cotisation créée',
description: 'Cotisation ${widget.cotisation.typeCotisation} créée pour ${widget.cotisation.nomMembre}',
date: widget.cotisation.dateCreation,
icon: Icons.add_circle,
color: AppTheme.primaryColor,
isCompleted: true,
),
];
// Ajouter l'événement d'échéance
final now = DateTime.now();
final isOverdue = widget.cotisation.dateEcheance.isBefore(now);
_timelineEvents.add(
TimelineEvent(
title: isOverdue ? 'Échéance dépassée' : 'Échéance prévue',
description: 'Date limite de paiement: ${_formatDate(widget.cotisation.dateEcheance)}',
date: widget.cotisation.dateEcheance,
icon: isOverdue ? Icons.warning : Icons.schedule,
color: isOverdue ? AppTheme.errorColor : AppTheme.warningColor,
isCompleted: isOverdue,
isWarning: isOverdue,
),
);
// Ajouter les événements de paiement (simulés)
if (widget.cotisation.montantPaye > 0) {
_timelineEvents.add(
TimelineEvent(
title: 'Paiement partiel reçu',
description: 'Montant: ${widget.cotisation.montantPaye.toStringAsFixed(0)} XOF',
date: widget.cotisation.dateCreation.add(const Duration(days: 5)), // Simulé
icon: Icons.payment,
color: AppTheme.successColor,
isCompleted: true,
),
);
}
if (widget.cotisation.isEntierementPayee) {
_timelineEvents.add(
TimelineEvent(
title: 'Paiement complet',
description: 'Cotisation entièrement payée',
date: widget.cotisation.dateCreation.add(const Duration(days: 10)), // Simulé
icon: Icons.check_circle,
color: AppTheme.successColor,
isCompleted: true,
isSuccess: true,
),
);
} else {
// Ajouter les événements futurs
if (!isOverdue) {
_timelineEvents.add(
TimelineEvent(
title: 'Rappel automatique',
description: 'Rappel envoyé 3 jours avant l\'échéance',
date: widget.cotisation.dateEcheance.subtract(const Duration(days: 3)),
icon: Icons.notifications,
color: AppTheme.infoColor,
isCompleted: false,
isFuture: true,
),
);
}
_timelineEvents.add(
TimelineEvent(
title: 'Paiement en attente',
description: 'En attente du paiement complet',
date: DateTime.now(),
icon: Icons.hourglass_empty,
color: AppTheme.textSecondary,
isCompleted: false,
isFuture: true,
),
);
}
// Trier par date
_timelineEvents.sort((a, b) => a.date.compareTo(b.date));
}
@override
Widget build(BuildContext context) {
if (_timelineEvents.isEmpty) {
return const Center(
child: Text(
'Aucun historique disponible',
style: TextStyle(
fontSize: 16,
color: AppTheme.textSecondary,
),
),
);
}
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Historique de la cotisation',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 20),
Expanded(
child: ListView.builder(
itemCount: _timelineEvents.length,
itemBuilder: (context, index) {
return AnimatedBuilder(
animation: _itemAnimations[index],
builder: (context, child) {
return Transform.translate(
offset: Offset(
0,
50 * (1 - _itemAnimations[index].value),
),
child: Opacity(
opacity: _itemAnimations[index].value,
child: _buildTimelineItem(
_timelineEvents[index],
index,
index == _timelineEvents.length - 1,
),
),
);
},
);
},
),
),
],
),
);
}
Widget _buildTimelineItem(TimelineEvent event, int index, bool isLast) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Timeline indicator
Column(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: event.isCompleted
? event.color
: event.color.withOpacity(0.2),
border: Border.all(
color: event.color,
width: event.isCompleted ? 0 : 2,
),
),
child: Icon(
event.icon,
size: 20,
color: event.isCompleted
? Colors.white
: event.color,
),
),
if (!isLast)
Container(
width: 2,
height: 60,
color: event.isCompleted
? event.color.withOpacity(0.3)
: AppTheme.borderLight,
),
],
),
const SizedBox(width: 16),
// Event content
Expanded(
child: Container(
margin: const EdgeInsets.only(bottom: 20),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: _getEventBackgroundColor(event),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: event.color.withOpacity(0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
event.title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: event.isCompleted
? AppTheme.textPrimary
: AppTheme.textSecondary,
),
),
),
if (event.isSuccess)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: AppTheme.successColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'Terminé',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppTheme.successColor,
),
),
),
if (event.isWarning)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: AppTheme.errorColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'En retard',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppTheme.errorColor,
),
),
),
],
),
const SizedBox(height: 8),
Text(
event.description,
style: TextStyle(
fontSize: 14,
color: event.isCompleted
? AppTheme.textSecondary
: AppTheme.textHint,
),
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.access_time,
size: 16,
color: AppTheme.textHint,
),
const SizedBox(width: 4),
Text(
_formatDateTime(event.date),
style: const TextStyle(
fontSize: 12,
color: AppTheme.textHint,
),
),
if (event.isFuture) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: AppTheme.infoColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'À venir',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: AppTheme.infoColor,
),
),
),
],
],
),
],
),
),
),
],
);
}
Color _getEventBackgroundColor(TimelineEvent event) {
if (event.isSuccess) {
return AppTheme.successColor.withOpacity(0.05);
}
if (event.isWarning) {
return AppTheme.errorColor.withOpacity(0.05);
}
if (event.isFuture) {
return AppTheme.infoColor.withOpacity(0.05);
}
return Colors.white;
}
String _formatDate(DateTime date) {
return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
}
String _formatDateTime(DateTime date) {
return '${_formatDate(date)} à ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
}
}
/// Modèle pour les événements de la timeline
class TimelineEvent {
final String title;
final String description;
final DateTime date;
final IconData icon;
final Color color;
final bool isCompleted;
final bool isSuccess;
final bool isWarning;
final bool isFuture;
TimelineEvent({
required this.title,
required this.description,
required this.date,
required this.icon,
required this.color,
this.isCompleted = false,
this.isSuccess = false,
this.isWarning = false,
this.isFuture = false,
});
}

View File

@@ -1,283 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../../../shared/theme/app_theme.dart';
/// Widget pour afficher les statistiques des cotisations
class CotisationsStatsCard extends StatelessWidget {
final Map<String, dynamic> statistics;
const CotisationsStatsCard({
super.key,
required this.statistics,
});
@override
Widget build(BuildContext context) {
final currencyFormat = NumberFormat.currency(
locale: 'fr_FR',
symbol: 'FCFA',
decimalDigits: 0,
);
final totalCotisations = statistics['totalCotisations'] as int? ?? 0;
final cotisationsPayees = statistics['cotisationsPayees'] as int? ?? 0;
final cotisationsEnRetard = statistics['cotisationsEnRetard'] as int? ?? 0;
final tauxPaiement = statistics['tauxPaiement'] as double? ?? 0.0;
return Card(
elevation: 2,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Titre
Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: AppTheme.accentColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
),
child: const Icon(
Icons.analytics,
size: 18,
color: AppTheme.accentColor,
),
),
const SizedBox(width: 12),
const Text(
'Statistiques des cotisations',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
],
),
const SizedBox(height: 16),
// Grille des statistiques
Row(
children: [
// Total des cotisations
Expanded(
child: _buildStatItem(
icon: Icons.receipt_long,
label: 'Total',
value: totalCotisations.toString(),
color: AppTheme.primaryColor,
),
),
const SizedBox(width: 12),
// Cotisations payées
Expanded(
child: _buildStatItem(
icon: Icons.check_circle,
label: 'Payées',
value: cotisationsPayees.toString(),
color: AppTheme.successColor,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
// Cotisations en retard
Expanded(
child: _buildStatItem(
icon: Icons.warning,
label: 'En retard',
value: cotisationsEnRetard.toString(),
color: AppTheme.errorColor,
),
),
const SizedBox(width: 12),
// Taux de paiement
Expanded(
child: _buildStatItem(
icon: Icons.trending_up,
label: 'Taux paiement',
value: '${tauxPaiement.toStringAsFixed(1)}%',
color: tauxPaiement >= 80
? AppTheme.successColor
: tauxPaiement >= 60
? AppTheme.warningColor
: AppTheme.errorColor,
),
),
],
),
const SizedBox(height: 16),
// Barre de progression globale
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Progression globale',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.textSecondary,
),
),
Text(
'${tauxPaiement.toStringAsFixed(1)}%',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
],
),
const SizedBox(height: 8),
LinearProgressIndicator(
value: tauxPaiement / 100,
backgroundColor: AppTheme.borderColor,
valueColor: AlwaysStoppedAnimation<Color>(
tauxPaiement >= 80
? AppTheme.successColor
: tauxPaiement >= 60
? AppTheme.warningColor
: AppTheme.errorColor,
),
),
],
),
// Montants si disponibles
if (statistics.containsKey('montantTotal') ||
statistics.containsKey('montantPaye')) ...[
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
Row(
children: [
if (statistics.containsKey('montantTotal')) ...[
Expanded(
child: _buildMoneyStatItem(
label: 'Montant total',
value: currencyFormat.format(
(statistics['montantTotal'] as num?)?.toDouble() ?? 0.0
),
color: AppTheme.textPrimary,
),
),
],
if (statistics.containsKey('montantTotal') &&
statistics.containsKey('montantPaye'))
const SizedBox(width: 12),
if (statistics.containsKey('montantPaye')) ...[
Expanded(
child: _buildMoneyStatItem(
label: 'Montant payé',
value: currencyFormat.format(
(statistics['montantPaye'] as num?)?.toDouble() ?? 0.0
),
color: AppTheme.successColor,
),
),
],
],
),
],
],
),
),
);
}
Widget _buildStatItem({
required IconData icon,
required String label,
required String value,
required Color color,
}) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Icon(
icon,
size: 24,
color: color,
),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildMoneyStatItem({
required String label,
required String value,
required Color color,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: color,
),
),
],
);
}
}

View File

@@ -1,457 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../core/models/cotisation_model.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/widgets/buttons/buttons.dart';
import '../../../../shared/widgets/buttons/primary_button.dart';
import 'payment_method_selector.dart';
/// Widget de formulaire de paiement
class PaymentFormWidget extends StatefulWidget {
final CotisationModel cotisation;
final Function(Map<String, dynamic>) onPaymentInitiated;
const PaymentFormWidget({
super.key,
required this.cotisation,
required this.onPaymentInitiated,
});
@override
State<PaymentFormWidget> createState() => _PaymentFormWidgetState();
}
class _PaymentFormWidgetState extends State<PaymentFormWidget>
with TickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _phoneController = TextEditingController();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _amountController = TextEditingController();
late final AnimationController _animationController;
late final Animation<Offset> _slideAnimation;
String? _selectedPaymentMethod;
bool _isProcessing = false;
bool _acceptTerms = false;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeOutCubic,
));
// Initialiser le montant avec le montant restant à payer
final remainingAmount = widget.cotisation.montantDu - widget.cotisation.montantPaye;
_amountController.text = remainingAmount.toStringAsFixed(0);
_animationController.forward();
}
@override
void dispose() {
_phoneController.dispose();
_nameController.dispose();
_emailController.dispose();
_amountController.dispose();
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SlideTransition(
position: _slideAnimation,
child: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Sélection de la méthode de paiement
PaymentMethodSelector(
selectedMethod: _selectedPaymentMethod,
montant: double.tryParse(_amountController.text) ?? 0,
onMethodSelected: (method) {
setState(() {
_selectedPaymentMethod = method;
});
},
),
if (_selectedPaymentMethod != null) ...[
const SizedBox(height: 24),
_buildPaymentForm(),
],
],
),
),
),
);
}
Widget _buildPaymentForm() {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Informations de paiement',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 16),
// Montant à payer
_buildAmountField(),
const SizedBox(height: 16),
// Numéro de téléphone (pour Mobile Money)
if (_isMobileMoneyMethod()) ...[
_buildPhoneField(),
const SizedBox(height: 16),
],
// Nom du payeur
_buildNameField(),
const SizedBox(height: 16),
// Email (optionnel)
_buildEmailField(),
const SizedBox(height: 20),
// Conditions d'utilisation
_buildTermsCheckbox(),
const SizedBox(height: 24),
// Bouton de paiement
_buildPaymentButton(),
],
),
);
}
Widget _buildAmountField() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Montant à payer',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 8),
TextFormField(
controller: _amountController,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(8),
],
decoration: InputDecoration(
hintText: 'Entrez le montant',
suffixText: 'XOF',
prefixIcon: const Icon(Icons.attach_money),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppTheme.primaryColor, width: 2),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer un montant';
}
final amount = double.tryParse(value);
if (amount == null || amount <= 0) {
return 'Montant invalide';
}
final remaining = widget.cotisation.montantDu - widget.cotisation.montantPaye;
if (amount > remaining) {
return 'Montant supérieur au solde restant (${remaining.toStringAsFixed(0)} XOF)';
}
return null;
},
onChanged: (value) {
setState(() {}); // Recalculer les frais
},
),
],
);
}
Widget _buildPhoneField() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Numéro ${_getPaymentMethodName()}',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 8),
TextFormField(
controller: _phoneController,
keyboardType: TextInputType.phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(10),
],
decoration: InputDecoration(
hintText: 'Ex: 0123456789',
prefixIcon: const Icon(Icons.phone),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppTheme.primaryColor, width: 2),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre numéro de téléphone';
}
if (value.length < 8) {
return 'Numéro de téléphone invalide';
}
if (!_validatePhoneForMethod(value)) {
return 'Ce numéro n\'est pas compatible avec ${_getPaymentMethodName()}';
}
return null;
},
),
],
);
}
Widget _buildNameField() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Nom du payeur',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 8),
TextFormField(
controller: _nameController,
textCapitalization: TextCapitalization.words,
decoration: InputDecoration(
hintText: 'Entrez votre nom complet',
prefixIcon: const Icon(Icons.person),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppTheme.primaryColor, width: 2),
),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Veuillez entrer votre nom';
}
if (value.trim().length < 2) {
return 'Nom trop court';
}
return null;
},
),
],
);
}
Widget _buildEmailField() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Email (optionnel)',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 8),
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'exemple@email.com',
prefixIcon: const Icon(Icons.email),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppTheme.primaryColor, width: 2),
),
),
validator: (value) {
if (value != null && value.isNotEmpty) {
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return 'Email invalide';
}
}
return null;
},
),
],
);
}
Widget _buildTermsCheckbox() {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Checkbox(
value: _acceptTerms,
onChanged: (value) {
setState(() {
_acceptTerms = value ?? false;
});
},
activeColor: AppTheme.primaryColor,
),
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
_acceptTerms = !_acceptTerms;
});
},
child: const Text(
'J\'accepte les conditions d\'utilisation et la politique de confidentialité',
style: TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
),
),
],
);
}
Widget _buildPaymentButton() {
return SizedBox(
width: double.infinity,
child: PrimaryButton(
text: _isProcessing
? 'Traitement en cours...'
: 'Confirmer le paiement',
icon: _isProcessing ? null : Icons.payment,
onPressed: _canProceedPayment() ? _processPayment : null,
isLoading: _isProcessing,
),
);
}
bool _canProceedPayment() {
return _selectedPaymentMethod != null &&
_acceptTerms &&
!_isProcessing &&
_amountController.text.isNotEmpty;
}
bool _isMobileMoneyMethod() {
return _selectedPaymentMethod == 'ORANGE_MONEY' ||
_selectedPaymentMethod == 'WAVE' ||
_selectedPaymentMethod == 'MOOV_MONEY';
}
String _getPaymentMethodName() {
switch (_selectedPaymentMethod) {
case 'ORANGE_MONEY':
return 'Orange Money';
case 'WAVE':
return 'Wave';
case 'MOOV_MONEY':
return 'Moov Money';
case 'CARTE_BANCAIRE':
return 'Carte bancaire';
default:
return 'Paiement';
}
}
bool _validatePhoneForMethod(String phone) {
final cleanNumber = phone.replaceAll(RegExp(r'[^\d]'), '');
switch (_selectedPaymentMethod) {
case 'ORANGE_MONEY':
// Orange: 07, 08, 09
return RegExp(r'^(225)?(0[789])\d{8}$').hasMatch(cleanNumber);
case 'WAVE':
// Wave accepte tous les numéros ivoiriens
return RegExp(r'^(225)?(0[1-9])\d{8}$').hasMatch(cleanNumber);
case 'MOOV_MONEY':
// Moov: 01, 02, 03
return RegExp(r'^(225)?(0[123])\d{8}$').hasMatch(cleanNumber);
default:
return cleanNumber.length >= 8;
}
}
void _processPayment() {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_isProcessing = true;
});
// Préparer les données de paiement
final paymentData = {
'montant': double.parse(_amountController.text),
'methodePaiement': _selectedPaymentMethod!,
'numeroTelephone': _phoneController.text,
'nomPayeur': _nameController.text.trim(),
'emailPayeur': _emailController.text.trim().isEmpty
? null
: _emailController.text.trim(),
};
// Déclencher le paiement
widget.onPaymentInitiated(paymentData);
// Simuler un délai de traitement
Future.delayed(const Duration(seconds: 2), () {
if (mounted) {
setState(() {
_isProcessing = false;
});
}
});
}
}

View File

@@ -1,443 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../core/services/payment_service.dart';
import '../../../../shared/theme/app_theme.dart';
/// Widget de sélection des méthodes de paiement
class PaymentMethodSelector extends StatefulWidget {
final String? selectedMethod;
final Function(String) onMethodSelected;
final double montant;
const PaymentMethodSelector({
super.key,
this.selectedMethod,
required this.onMethodSelected,
required this.montant,
});
@override
State<PaymentMethodSelector> createState() => _PaymentMethodSelectorState();
}
class _PaymentMethodSelectorState extends State<PaymentMethodSelector>
with TickerProviderStateMixin {
late final AnimationController _animationController;
late final Animation<double> _scaleAnimation;
List<PaymentMethod> _paymentMethods = [];
String? _selectedMethod;
@override
void initState() {
super.initState();
_selectedMethod = widget.selectedMethod;
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.elasticOut),
);
_loadPaymentMethods();
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _loadPaymentMethods() {
// En production, ceci viendrait du PaymentService
_paymentMethods = [
PaymentMethod(
id: 'ORANGE_MONEY',
nom: 'Orange Money',
icone: '📱',
couleur: '#FF6600',
description: 'Paiement via Orange Money',
fraisMinimum: 0,
fraisMaximum: 1000,
montantMinimum: 100,
montantMaximum: 1000000,
),
PaymentMethod(
id: 'WAVE',
nom: 'Wave',
icone: '🌊',
couleur: '#00D4FF',
description: 'Paiement via Wave',
fraisMinimum: 0,
fraisMaximum: 500,
montantMinimum: 100,
montantMaximum: 2000000,
),
PaymentMethod(
id: 'MOOV_MONEY',
nom: 'Moov Money',
icone: '💙',
couleur: '#0066CC',
description: 'Paiement via Moov Money',
fraisMinimum: 0,
fraisMaximum: 800,
montantMinimum: 100,
montantMaximum: 1500000,
),
PaymentMethod(
id: 'CARTE_BANCAIRE',
nom: 'Carte bancaire',
icone: '💳',
couleur: '#4CAF50',
description: 'Paiement par carte bancaire',
fraisMinimum: 100,
fraisMaximum: 2000,
montantMinimum: 500,
montantMaximum: 5000000,
),
];
// Filtrer les méthodes disponibles selon le montant
_paymentMethods = _paymentMethods.where((method) {
return widget.montant >= method.montantMinimum &&
widget.montant <= method.montantMaximum;
}).toList();
setState(() {});
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: _scaleAnimation,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Choisissez votre méthode de paiement',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 16),
if (_paymentMethods.isEmpty)
_buildNoMethodsAvailable()
else
_buildMethodsList(),
if (_selectedMethod != null) ...[
const SizedBox(height: 20),
_buildSelectedMethodInfo(),
],
],
),
);
}
Widget _buildNoMethodsAvailable() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppTheme.warningColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.warningColor.withOpacity(0.3),
),
),
child: Column(
children: [
Icon(
Icons.warning_amber,
size: 48,
color: AppTheme.warningColor,
),
const SizedBox(height: 12),
const Text(
'Aucune méthode de paiement disponible',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
Text(
'Le montant de ${widget.montant.toStringAsFixed(0)} XOF ne correspond aux limites d\'aucune méthode de paiement.',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
],
),
);
}
Widget _buildMethodsList() {
return Column(
children: _paymentMethods.map((method) {
final isSelected = _selectedMethod == method.id;
final fees = _calculateFees(method);
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(bottom: 12),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _selectMethod(method),
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isSelected
? _getMethodColor(method.couleur).withOpacity(0.1)
: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected
? _getMethodColor(method.couleur)
: AppTheme.borderLight,
width: isSelected ? 2 : 1,
),
boxShadow: isSelected ? [
BoxShadow(
color: _getMethodColor(method.couleur).withOpacity(0.2),
blurRadius: 8,
offset: const Offset(0, 2),
),
] : null,
),
child: Row(
children: [
// Icône de la méthode
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: _getMethodColor(method.couleur).withOpacity(0.1),
borderRadius: BorderRadius.circular(25),
),
child: Center(
child: Text(
method.icone,
style: const TextStyle(fontSize: 24),
),
),
),
const SizedBox(width: 16),
// Informations de la méthode
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
method.nom,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: isSelected
? _getMethodColor(method.couleur)
: AppTheme.textPrimary,
),
),
const SizedBox(height: 4),
Text(
method.description,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
if (fees > 0) ...[
const SizedBox(height: 4),
Text(
'Frais: ${fees.toStringAsFixed(0)} XOF',
style: TextStyle(
fontSize: 12,
color: AppTheme.warningColor,
fontWeight: FontWeight.w500,
),
),
],
],
),
),
// Indicateur de sélection
AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSelected
? _getMethodColor(method.couleur)
: Colors.transparent,
border: Border.all(
color: isSelected
? _getMethodColor(method.couleur)
: AppTheme.borderLight,
width: 2,
),
),
child: isSelected
? const Icon(
Icons.check,
size: 16,
color: Colors.white,
)
: null,
),
],
),
),
),
),
);
}).toList(),
);
}
Widget _buildSelectedMethodInfo() {
final method = _paymentMethods.firstWhere((m) => m.id == _selectedMethod);
final fees = _calculateFees(method);
final total = widget.montant + fees;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: _getMethodColor(method.couleur).withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _getMethodColor(method.couleur).withOpacity(0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
method.icone,
style: const TextStyle(fontSize: 20),
),
const SizedBox(width: 8),
Text(
'Récapitulatif - ${method.nom}',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: _getMethodColor(method.couleur),
),
),
],
),
const SizedBox(height: 12),
_buildSummaryRow('Montant', '${widget.montant.toStringAsFixed(0)} XOF'),
if (fees > 0)
_buildSummaryRow('Frais', '${fees.toStringAsFixed(0)} XOF'),
const Divider(),
_buildSummaryRow(
'Total à payer',
'${total.toStringAsFixed(0)} XOF',
isTotal: true,
),
],
),
);
}
Widget _buildSummaryRow(String label, String value, {bool isTotal = false}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontSize: isTotal ? 16 : 14,
fontWeight: isTotal ? FontWeight.bold : FontWeight.normal,
color: AppTheme.textSecondary,
),
),
Text(
value,
style: TextStyle(
fontSize: isTotal ? 16 : 14,
fontWeight: FontWeight.bold,
color: isTotal ? AppTheme.textPrimary : AppTheme.textSecondary,
),
),
],
),
);
}
void _selectMethod(PaymentMethod method) {
setState(() {
_selectedMethod = method.id;
});
widget.onMethodSelected(method.id);
// Animation de feedback
_animationController.reset();
_animationController.forward();
}
double _calculateFees(PaymentMethod method) {
// Simulation du calcul des frais
switch (method.id) {
case 'ORANGE_MONEY':
return _calculateOrangeMoneyFees(widget.montant);
case 'WAVE':
return _calculateWaveFees(widget.montant);
case 'MOOV_MONEY':
return _calculateMoovMoneyFees(widget.montant);
case 'CARTE_BANCAIRE':
return _calculateCardFees(widget.montant);
default:
return 0.0;
}
}
double _calculateOrangeMoneyFees(double montant) {
if (montant <= 1000) return 0;
if (montant <= 5000) return 25;
if (montant <= 10000) return 50;
if (montant <= 25000) return 100;
if (montant <= 50000) return 200;
return montant * 0.005; // 0.5%
}
double _calculateWaveFees(double montant) {
if (montant <= 2000) return 0;
if (montant <= 10000) return 25;
if (montant <= 50000) return 100;
return montant * 0.003; // 0.3%
}
double _calculateMoovMoneyFees(double montant) {
if (montant <= 1000) return 0;
if (montant <= 5000) return 30;
if (montant <= 15000) return 75;
if (montant <= 50000) return 150;
return montant * 0.004; // 0.4%
}
double _calculateCardFees(double montant) {
return 100 + (montant * 0.025); // 100 XOF + 2.5%
}
Color _getMethodColor(String colorHex) {
return Color(int.parse(colorHex.replaceFirst('#', '0xFF')));
}
}

View File

@@ -1,363 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../core/models/cotisation_model.dart';
import '../../../../core/services/wave_payment_service.dart';
import '../../../../core/di/injection.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/widgets/buttons/primary_button.dart';
import '../pages/wave_payment_page.dart';
/// Widget d'intégration Wave Money pour les cotisations
/// Affiche les options de paiement Wave avec calcul des frais
class WavePaymentWidget extends StatefulWidget {
final CotisationModel cotisation;
final VoidCallback? onPaymentInitiated;
final bool showFullInterface;
const WavePaymentWidget({
super.key,
required this.cotisation,
this.onPaymentInitiated,
this.showFullInterface = false,
});
@override
State<WavePaymentWidget> createState() => _WavePaymentWidgetState();
}
class _WavePaymentWidgetState extends State<WavePaymentWidget>
with SingleTickerProviderStateMixin {
late WavePaymentService _wavePaymentService;
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_wavePaymentService = getIt<WavePaymentService>();
_animationController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.elasticOut),
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
);
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _fadeAnimation,
child: ScaleTransition(
scale: _scaleAnimation,
child: widget.showFullInterface
? _buildFullInterface()
: _buildCompactInterface(),
),
);
}
Widget _buildFullInterface() {
final remainingAmount = widget.cotisation.montantDu - widget.cotisation.montantPaye;
final fees = _wavePaymentService.calculateWaveFees(remainingAmount);
final total = remainingAmount + fees;
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF00D4FF), Color(0xFF0099CC)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: const Color(0xFF00D4FF).withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header Wave
Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: const Icon(
Icons.waves,
size: 28,
color: Color(0xFF00D4FF),
),
),
const SizedBox(width: 16),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Wave Money',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
'Paiement mobile instantané',
style: TextStyle(
fontSize: 12,
color: Colors.white70,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'🇨🇮 CI',
style: TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
],
),
const SizedBox(height: 20),
// Détails du paiement
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
_buildPaymentRow('Montant', '${remainingAmount.toStringAsFixed(0)} XOF'),
_buildPaymentRow('Frais Wave', '${fees.toStringAsFixed(0)} XOF'),
const Divider(color: Colors.white30, height: 20),
_buildPaymentRow(
'Total',
'${total.toStringAsFixed(0)} XOF',
isTotal: true,
),
],
),
),
const SizedBox(height: 20),
// Avantages Wave
_buildAdvantages(),
const SizedBox(height: 20),
// Bouton de paiement
SizedBox(
width: double.infinity,
child: PrimaryButton(
text: 'Payer avec Wave',
icon: Icons.payment,
onPressed: _navigateToWavePayment,
backgroundColor: Colors.white,
textColor: const Color(0xFF00D4FF),
),
),
],
),
);
}
Widget _buildCompactInterface() {
final remainingAmount = widget.cotisation.montantDu - widget.cotisation.montantPaye;
final fees = _wavePaymentService.calculateWaveFees(remainingAmount);
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF00D4FF).withOpacity(0.3)),
boxShadow: [
BoxShadow(
color: const Color(0xFF00D4FF).withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: const Color(0xFF00D4FF).withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: const Icon(
Icons.waves,
size: 24,
color: Color(0xFF00D4FF),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Wave Money',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
Text(
'Frais: ${fees.toStringAsFixed(0)} XOF • Instantané',
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
),
PrimaryButton(
text: 'Payer',
onPressed: _navigateToWavePayment,
backgroundColor: const Color(0xFF00D4FF),
isCompact: true,
),
],
),
);
}
Widget _buildPaymentRow(String label, String value, {bool isTotal = false}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontSize: isTotal ? 16 : 14,
fontWeight: isTotal ? FontWeight.bold : FontWeight.normal,
color: Colors.white70,
),
),
Text(
value,
style: TextStyle(
fontSize: isTotal ? 16 : 14,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
);
}
Widget _buildAdvantages() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Pourquoi choisir Wave ?',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 8),
_buildAdvantageItem('', 'Paiement instantané'),
_buildAdvantageItem('🔒', 'Sécurisé et fiable'),
_buildAdvantageItem('💰', 'Frais les plus bas'),
_buildAdvantageItem('📱', 'Simple et rapide'),
],
);
}
Widget _buildAdvantageItem(String icon, String text) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
Text(
icon,
style: const TextStyle(fontSize: 12),
),
const SizedBox(width: 8),
Text(
text,
style: const TextStyle(
fontSize: 12,
color: Colors.white70,
),
),
],
),
);
}
void _navigateToWavePayment() {
// Feedback haptique
HapticFeedback.lightImpact();
// Callback si fourni
widget.onPaymentInitiated?.call();
// Navigation vers la page de paiement Wave
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => WavePaymentPage(cotisation: widget.cotisation),
),
);
}
}