Files
unionflow-server-api/unionflow-mobile-apps/lib/features/analytics/presentation/widgets/kpi_card_widget.dart
2025-09-17 17:54:06 +00:00

358 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import '../../../../shared/widgets/common/unified_card.dart';
import '../../../../shared/theme/design_system.dart';
import '../../../../core/utils/formatters.dart';
import '../../domain/entities/analytics_data.dart';
/// Widget de carte KPI utilisant le design system unifié
class KPICardWidget extends StatelessWidget {
const KPICardWidget({
super.key,
required this.analyticsData,
this.onTap,
this.showTrend = true,
this.showDetails = false,
this.compact = false,
});
final AnalyticsData analyticsData;
final VoidCallback? onTap;
final bool showTrend;
final bool showDetails;
final bool compact;
@override
Widget build(BuildContext context) {
return UnifiedCard(
variant: UnifiedCardVariant.elevated,
onTap: onTap,
child: Padding(
padding: EdgeInsets.all(
compact ? DesignSystem.spacing12 : DesignSystem.spacing16,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// En-tête avec icône et titre
Row(
children: [
Container(
padding: const EdgeInsets.all(DesignSystem.spacing8),
decoration: BoxDecoration(
color: _getCouleurMetrique().withOpacity(0.1),
borderRadius: BorderRadius.circular(DesignSystem.radius8),
),
child: Icon(
_getIconeMetrique(),
color: _getCouleurMetrique(),
size: compact ? 20 : 24,
),
),
const SizedBox(width: DesignSystem.spacing12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
analyticsData.libelleAffichage,
style: compact
? DesignSystem.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
)
: DesignSystem.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (!compact && analyticsData.description != null)
Padding(
padding: const EdgeInsets.only(
top: DesignSystem.spacing4,
),
child: Text(
analyticsData.description!,
style: DesignSystem.textTheme.bodySmall?.copyWith(
color: DesignSystem.textSecondaryColor,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
// Indicateur de fiabilité
if (showDetails)
_buildIndicateurFiabilite(),
],
),
SizedBox(height: compact ? DesignSystem.spacing8 : DesignSystem.spacing16),
// Valeur principale
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Text(
analyticsData.valeurFormatee,
style: compact
? DesignSystem.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: _getCouleurMetrique(),
)
: DesignSystem.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: _getCouleurMetrique(),
),
),
),
// Évolution
if (showTrend && analyticsData.pourcentageEvolution != null)
_buildEvolution(),
],
),
// Détails supplémentaires
if (showDetails) ...[
const SizedBox(height: DesignSystem.spacing12),
_buildDetails(),
],
// Période et dernière mise à jour
if (!compact) ...[
const SizedBox(height: DesignSystem.spacing12),
_buildInfosPeriode(),
],
],
),
),
);
}
/// Widget d'évolution avec icône et pourcentage
Widget _buildEvolution() {
final evolution = analyticsData.pourcentageEvolution!;
final isPositive = evolution > 0;
final isNegative = evolution < 0;
Color couleur;
IconData icone;
if (isPositive) {
couleur = DesignSystem.successColor;
icone = Icons.trending_up;
} else if (isNegative) {
couleur = DesignSystem.errorColor;
icone = Icons.trending_down;
} else {
couleur = DesignSystem.warningColor;
icone = Icons.trending_flat;
}
return Container(
padding: const EdgeInsets.symmetric(
horizontal: DesignSystem.spacing8,
vertical: DesignSystem.spacing4,
),
decoration: BoxDecoration(
color: couleur.withOpacity(0.1),
borderRadius: BorderRadius.circular(DesignSystem.radius12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icone,
size: 16,
color: couleur,
),
const SizedBox(width: DesignSystem.spacing4),
Text(
analyticsData.evolutionFormatee,
style: DesignSystem.textTheme.bodySmall?.copyWith(
color: couleur,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
/// Widget d'indicateur de fiabilité
Widget _buildIndicateurFiabilite() {
final fiabilite = analyticsData.indicateurFiabilite;
Color couleur;
if (fiabilite >= 90) {
couleur = DesignSystem.successColor;
} else if (fiabilite >= 70) {
couleur = DesignSystem.warningColor;
} else {
couleur = DesignSystem.errorColor;
}
return Container(
padding: const EdgeInsets.symmetric(
horizontal: DesignSystem.spacing6,
vertical: DesignSystem.spacing2,
),
decoration: BoxDecoration(
color: couleur.withOpacity(0.1),
borderRadius: BorderRadius.circular(DesignSystem.radius8),
border: Border.all(
color: couleur.withOpacity(0.3),
width: 1,
),
),
child: Text(
'${fiabilite.toStringAsFixed(0)}%',
style: DesignSystem.textTheme.bodySmall?.copyWith(
color: couleur,
fontWeight: FontWeight.w600,
),
),
);
}
/// Widget des détails supplémentaires
Widget _buildDetails() {
return Column(
children: [
// Valeur précédente
if (analyticsData.valeurPrecedente != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Période précédente',
style: DesignSystem.textTheme.bodySmall?.copyWith(
color: DesignSystem.textSecondaryColor,
),
),
Text(
_formaterValeur(analyticsData.valeurPrecedente!),
style: DesignSystem.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: DesignSystem.spacing4),
// Éléments analysés
if (analyticsData.nombreElementsAnalyses != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Éléments analysés',
style: DesignSystem.textTheme.bodySmall?.copyWith(
color: DesignSystem.textSecondaryColor,
),
),
Text(
analyticsData.nombreElementsAnalyses.toString(),
style: DesignSystem.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: DesignSystem.spacing4),
// Temps de calcul
if (analyticsData.tempsCalculMs != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Temps de calcul',
style: DesignSystem.textTheme.bodySmall?.copyWith(
color: DesignSystem.textSecondaryColor,
),
),
Text(
'${analyticsData.tempsCalculMs}ms',
style: DesignSystem.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
],
),
],
);
}
/// Widget des informations de période
Widget _buildInfosPeriode() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
analyticsData.periodeAnalyse.libelle,
style: DesignSystem.textTheme.bodySmall?.copyWith(
color: DesignSystem.textSecondaryColor,
),
),
),
Text(
'Mis à jour ${AppFormatters.formatDateRelative(analyticsData.dateCalcul)}',
style: DesignSystem.textTheme.bodySmall?.copyWith(
color: DesignSystem.textSecondaryColor,
),
),
],
);
}
/// Obtient la couleur de la métrique
Color _getCouleurMetrique() {
return Color(int.parse(
analyticsData.couleur.replaceFirst('#', '0xFF'),
));
}
/// Obtient l'icône de la métrique
IconData _getIconeMetrique() {
switch (analyticsData.icone) {
case 'people':
return Icons.people;
case 'attach_money':
return Icons.attach_money;
case 'event':
return Icons.event;
case 'favorite':
return Icons.favorite;
case 'trending_up':
return Icons.trending_up;
case 'business':
return Icons.business;
case 'settings':
return Icons.settings;
default:
return Icons.analytics;
}
}
/// Formate une valeur selon le type de métrique
String _formaterValeur(double valeur) {
switch (analyticsData.typeMetrique.typeValeur) {
case 'amount':
return '${valeur.toStringAsFixed(0)} ${analyticsData.unite}';
case 'percentage':
return '${valeur.toStringAsFixed(1)}${analyticsData.unite}';
case 'average':
return valeur.toStringAsFixed(1);
default:
return valeur.toStringAsFixed(0);
}
}
}