Authentification stable - WIP
This commit is contained in:
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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')));
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user