Files
unionflow-server-api/unionflow-mobile-apps/lib/shared/widgets/sections/unified_kpi_section.dart
2025-09-17 17:54:06 +00:00

263 lines
6.5 KiB
Dart

import 'package:flutter/material.dart';
import '../../theme/app_theme.dart';
import '../cards/unified_card_widget.dart';
/// Section KPI unifiée pour afficher des indicateurs clés
///
/// Fournit :
/// - Cartes KPI avec animations
/// - Layouts adaptatifs (grille ou liste)
/// - Indicateurs de tendance
/// - Couleurs thématiques
class UnifiedKPISection extends StatelessWidget {
/// Liste des KPI à afficher
final List<UnifiedKPIData> kpis;
/// Titre de la section
final String? title;
/// Nombre de colonnes dans la grille (par défaut : 2)
final int crossAxisCount;
/// Espacement entre les cartes
final double spacing;
/// Callback lors du tap sur un KPI
final void Function(UnifiedKPIData kpi)? onKPITap;
const UnifiedKPISection({
super.key,
required this.kpis,
this.title,
this.crossAxisCount = 2,
this.spacing = 16.0,
this.onKPITap,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title != null) ...[
Text(
title!,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 16),
],
_buildKPIGrid(),
],
);
}
Widget _buildKPIGrid() {
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: spacing,
mainAxisSpacing: spacing,
childAspectRatio: 1.4,
),
itemCount: kpis.length,
itemBuilder: (context, index) {
final kpi = kpis[index];
return UnifiedCard.kpi(
onTap: onKPITap != null ? () => onKPITap!(kpi) : null,
child: _buildKPIContent(kpi),
);
},
);
}
Widget _buildKPIContent(UnifiedKPIData kpi) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// En-tête avec icône et titre
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: kpi.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
kpi.icon,
color: kpi.color,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
kpi.title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.textSecondary,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 12),
// Valeur principale
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Text(
kpi.value,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
color: AppTheme.textPrimary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (kpi.trend != null) ...[
const SizedBox(width: 8),
_buildTrendIndicator(kpi.trend!),
],
],
),
// Sous-titre ou description
if (kpi.subtitle != null) ...[
const SizedBox(height: 4),
Text(
kpi.subtitle!,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
],
);
}
Widget _buildTrendIndicator(UnifiedKPITrend trend) {
IconData icon;
Color color;
switch (trend.direction) {
case UnifiedKPITrendDirection.up:
icon = Icons.trending_up;
color = AppTheme.successColor;
break;
case UnifiedKPITrendDirection.down:
icon = Icons.trending_down;
color = AppTheme.errorColor;
break;
case UnifiedKPITrendDirection.stable:
icon = Icons.trending_flat;
color = AppTheme.textSecondary;
break;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 12,
color: color,
),
const SizedBox(width: 2),
Text(
trend.value,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: color,
),
),
],
),
);
}
}
/// Données pour un KPI unifié
class UnifiedKPIData {
/// Titre du KPI
final String title;
/// Valeur principale à afficher
final String value;
/// Sous-titre ou description optionnelle
final String? subtitle;
/// Icône représentative
final IconData icon;
/// Couleur thématique
final Color color;
/// Indicateur de tendance optionnel
final UnifiedKPITrend? trend;
/// Données supplémentaires pour les callbacks
final Map<String, dynamic>? metadata;
const UnifiedKPIData({
required this.title,
required this.value,
required this.icon,
required this.color,
this.subtitle,
this.trend,
this.metadata,
});
}
/// Indicateur de tendance pour les KPI
class UnifiedKPITrend {
/// Direction de la tendance
final UnifiedKPITrendDirection direction;
/// Valeur de la tendance (ex: "+12%", "-5", "stable")
final String value;
/// Label descriptif de la tendance (ex: "ce mois", "vs mois dernier")
final String? label;
const UnifiedKPITrend({
required this.direction,
required this.value,
this.label,
});
}
/// Direction de tendance disponibles
enum UnifiedKPITrendDirection {
up,
down,
stable,
}