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 createState() => _CotisationTimelineWidgetState(); } class _CotisationTimelineWidgetState extends State with TickerProviderStateMixin { late final AnimationController _animationController; late final List> _itemAnimations; List _timelineEvents = []; @override void initState() { super.initState(); _generateTimelineEvents(); _animationController = AnimationController( duration: Duration(milliseconds: 300 * _timelineEvents.length), vsync: this, ); _itemAnimations = List.generate( _timelineEvents.length, (index) => Tween(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, }); }