Refactoring
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../shared/widgets/common/unified_page_layout.dart';
|
||||
import '../../../../shared/widgets/common/unified_card.dart';
|
||||
import '../../../../shared/theme/design_system.dart';
|
||||
import '../../../../core/utils/constants.dart';
|
||||
import '../bloc/analytics_bloc.dart';
|
||||
import '../widgets/kpi_card_widget.dart';
|
||||
import '../widgets/trend_chart_widget.dart';
|
||||
import '../widgets/period_selector_widget.dart';
|
||||
import '../widgets/metrics_grid_widget.dart';
|
||||
import '../widgets/performance_gauge_widget.dart';
|
||||
import '../widgets/alerts_panel_widget.dart';
|
||||
import '../../domain/entities/analytics_data.dart';
|
||||
|
||||
/// Page principale du tableau de bord analytics
|
||||
class AnalyticsDashboardPage extends StatefulWidget {
|
||||
const AnalyticsDashboardPage({super.key});
|
||||
|
||||
@override
|
||||
State<AnalyticsDashboardPage> createState() => _AnalyticsDashboardPageState();
|
||||
}
|
||||
|
||||
class _AnalyticsDashboardPageState extends State<AnalyticsDashboardPage>
|
||||
with TickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
PeriodeAnalyse _periodeSelectionnee = PeriodeAnalyse.ceMois;
|
||||
String? _organisationId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 4, vsync: this);
|
||||
_chargerDonneesInitiales();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _chargerDonneesInitiales() {
|
||||
context.read<AnalyticsBloc>().add(
|
||||
ChargerTableauBordEvent(
|
||||
periodeAnalyse: _periodeSelectionnee,
|
||||
organisationId: _organisationId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onPeriodeChanged(PeriodeAnalyse nouvellePeriode) {
|
||||
setState(() {
|
||||
_periodeSelectionnee = nouvellePeriode;
|
||||
});
|
||||
_chargerDonneesInitiales();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UnifiedPageLayout(
|
||||
title: 'Analytics',
|
||||
subtitle: 'Tableau de bord et métriques',
|
||||
showBackButton: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: _chargerDonneesInitiales,
|
||||
tooltip: 'Actualiser',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
onPressed: () => _ouvrirParametres(context),
|
||||
tooltip: 'Paramètres',
|
||||
),
|
||||
],
|
||||
body: Column(
|
||||
children: [
|
||||
// Sélecteur de période
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(DesignSystem.spacing16),
|
||||
child: PeriodSelectorWidget(
|
||||
periodeSelectionnee: _periodeSelectionnee,
|
||||
onPeriodeChanged: _onPeriodeChanged,
|
||||
),
|
||||
),
|
||||
|
||||
// Onglets
|
||||
TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: DesignSystem.primaryColor,
|
||||
unselectedLabelColor: DesignSystem.textSecondaryColor,
|
||||
indicatorColor: DesignSystem.primaryColor,
|
||||
tabs: const [
|
||||
Tab(
|
||||
icon: Icon(Icons.dashboard),
|
||||
text: 'Vue d\'ensemble',
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(Icons.trending_up),
|
||||
text: 'Tendances',
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(Icons.analytics),
|
||||
text: 'Détails',
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(Icons.warning),
|
||||
text: 'Alertes',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Contenu des onglets
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildVueEnsemble(),
|
||||
_buildTendances(),
|
||||
_buildDetails(),
|
||||
_buildAlertes(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Vue d'ensemble avec KPI principaux
|
||||
Widget _buildVueEnsemble() {
|
||||
return BlocBuilder<AnalyticsBloc, AnalyticsState>(
|
||||
builder: (context, state) {
|
||||
if (state is AnalyticsLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is AnalyticsError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: DesignSystem.errorColor,
|
||||
),
|
||||
const SizedBox(height: DesignSystem.spacing16),
|
||||
Text(
|
||||
'Erreur lors du chargement',
|
||||
style: DesignSystem.textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: DesignSystem.spacing8),
|
||||
Text(
|
||||
state.message,
|
||||
style: DesignSystem.textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: DesignSystem.spacing16),
|
||||
ElevatedButton(
|
||||
onPressed: _chargerDonneesInitiales,
|
||||
child: const Text('Réessayer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is AnalyticsLoaded) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(DesignSystem.spacing16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Performance globale
|
||||
if (state.performanceGlobale != null)
|
||||
UnifiedCard(
|
||||
variant: UnifiedCardVariant.elevated,
|
||||
child: PerformanceGaugeWidget(
|
||||
score: state.performanceGlobale!,
|
||||
periode: _periodeSelectionnee,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: DesignSystem.spacing16),
|
||||
|
||||
// KPI principaux
|
||||
Text(
|
||||
'Indicateurs clés',
|
||||
style: DesignSystem.textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: DesignSystem.spacing12),
|
||||
|
||||
MetricsGridWidget(
|
||||
metriques: state.metriques,
|
||||
onMetriquePressed: (metrique) => _ouvrirDetailMetrique(
|
||||
context,
|
||||
metrique,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: DesignSystem.spacing24),
|
||||
|
||||
// Graphiques de tendance rapide
|
||||
Text(
|
||||
'Évolutions récentes',
|
||||
style: DesignSystem.textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: DesignSystem.spacing12),
|
||||
|
||||
if (state.tendances.isNotEmpty)
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: state.tendances.length,
|
||||
itemBuilder: (context, index) {
|
||||
final tendance = state.tendances[index];
|
||||
return Container(
|
||||
width: 300,
|
||||
margin: const EdgeInsets.only(
|
||||
right: DesignSystem.spacing12,
|
||||
),
|
||||
child: UnifiedCard(
|
||||
variant: UnifiedCardVariant.outlined,
|
||||
child: TrendChartWidget(
|
||||
trend: tendance,
|
||||
compact: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Onglet des tendances détaillées
|
||||
Widget _buildTendances() {
|
||||
return BlocBuilder<AnalyticsBloc, AnalyticsState>(
|
||||
builder: (context, state) {
|
||||
if (state is AnalyticsLoaded && state.tendances.isNotEmpty) {
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(DesignSystem.spacing16),
|
||||
itemCount: state.tendances.length,
|
||||
itemBuilder: (context, index) {
|
||||
final tendance = state.tendances[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: DesignSystem.spacing16,
|
||||
),
|
||||
child: UnifiedCard(
|
||||
variant: UnifiedCardVariant.elevated,
|
||||
child: TrendChartWidget(
|
||||
trend: tendance,
|
||||
compact: false,
|
||||
showPredictions: true,
|
||||
showAnomalies: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return const Center(
|
||||
child: Text('Aucune tendance disponible'),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Onglet des détails par métrique
|
||||
Widget _buildDetails() {
|
||||
return BlocBuilder<AnalyticsBloc, AnalyticsState>(
|
||||
builder: (context, state) {
|
||||
if (state is AnalyticsLoaded) {
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(DesignSystem.spacing16),
|
||||
itemCount: TypeMetrique.values.length,
|
||||
itemBuilder: (context, index) {
|
||||
final typeMetrique = TypeMetrique.values[index];
|
||||
final metrique = state.metriques.firstWhere(
|
||||
(m) => m.typeMetrique == typeMetrique,
|
||||
orElse: () => AnalyticsData(
|
||||
id: 'placeholder_$index',
|
||||
typeMetrique: typeMetrique,
|
||||
periodeAnalyse: _periodeSelectionnee,
|
||||
valeur: 0,
|
||||
dateDebut: DateTime.now().subtract(const Duration(days: 30)),
|
||||
dateFin: DateTime.now(),
|
||||
dateCalcul: DateTime.now(),
|
||||
),
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: DesignSystem.spacing12,
|
||||
),
|
||||
child: KPICardWidget(
|
||||
analyticsData: metrique,
|
||||
onTap: () => _ouvrirDetailMetrique(context, metrique),
|
||||
showTrend: true,
|
||||
showDetails: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return const Center(
|
||||
child: Text('Aucun détail disponible'),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Onglet des alertes
|
||||
Widget _buildAlertes() {
|
||||
return BlocBuilder<AnalyticsBloc, AnalyticsState>(
|
||||
builder: (context, state) {
|
||||
if (state is AnalyticsLoaded) {
|
||||
final alertes = state.metriques
|
||||
.where((m) => m.isCritique || !m.isDonneesFiables)
|
||||
.toList();
|
||||
|
||||
if (alertes.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle_outline,
|
||||
size: 64,
|
||||
color: DesignSystem.successColor,
|
||||
),
|
||||
const SizedBox(height: DesignSystem.spacing16),
|
||||
Text(
|
||||
'Aucune alerte active',
|
||||
style: DesignSystem.textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: DesignSystem.spacing8),
|
||||
Text(
|
||||
'Toutes les métriques sont dans les normes',
|
||||
style: DesignSystem.textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return AlertsPanelWidget(
|
||||
alertes: alertes,
|
||||
onAlertePressed: (alerte) => _ouvrirDetailMetrique(
|
||||
context,
|
||||
alerte,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const Center(
|
||||
child: Text('Aucune alerte disponible'),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _ouvrirDetailMetrique(BuildContext context, AnalyticsData metrique) {
|
||||
Navigator.of(context).pushNamed(
|
||||
AppRoutes.analyticsDetail,
|
||||
arguments: {
|
||||
'metrique': metrique,
|
||||
'periode': _periodeSelectionnee,
|
||||
'organisationId': _organisationId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _ouvrirParametres(BuildContext context) {
|
||||
Navigator.of(context).pushNamed(AppRoutes.analyticsSettings);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../shared/widgets/common/unified_card.dart';
|
||||
import '../../../../shared/theme/design_system.dart';
|
||||
import '../../domain/entities/analytics_data.dart';
|
||||
|
||||
/// Widget de sélection de période pour les analytics
|
||||
class PeriodSelectorWidget extends StatelessWidget {
|
||||
const PeriodSelectorWidget({
|
||||
super.key,
|
||||
required this.periodeSelectionnee,
|
||||
required this.onPeriodeChanged,
|
||||
this.compact = false,
|
||||
});
|
||||
|
||||
final PeriodeAnalyse periodeSelectionnee;
|
||||
final ValueChanged<PeriodeAnalyse> onPeriodeChanged;
|
||||
final bool compact;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (compact) {
|
||||
return _buildCompactSelector(context);
|
||||
} else {
|
||||
return _buildFullSelector(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sélecteur compact avec dropdown
|
||||
Widget _buildCompactSelector(BuildContext context) {
|
||||
return UnifiedCard(
|
||||
variant: UnifiedCardVariant.outlined,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: DesignSystem.spacing16,
|
||||
vertical: DesignSystem.spacing8,
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<PeriodeAnalyse>(
|
||||
value: periodeSelectionnee,
|
||||
onChanged: (periode) {
|
||||
if (periode != null) {
|
||||
onPeriodeChanged(periode);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.expand_more),
|
||||
isExpanded: true,
|
||||
items: PeriodeAnalyse.values.map((periode) {
|
||||
return DropdownMenuItem<PeriodeAnalyse>(
|
||||
value: periode,
|
||||
child: Text(
|
||||
periode.libelle,
|
||||
style: DesignSystem.textTheme.bodyMedium,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Sélecteur complet avec chips
|
||||
Widget _buildFullSelector(BuildContext context) {
|
||||
final periodesRapides = [
|
||||
PeriodeAnalyse.aujourdHui,
|
||||
PeriodeAnalyse.hier,
|
||||
PeriodeAnalyse.cetteSemaine,
|
||||
PeriodeAnalyse.ceMois,
|
||||
PeriodeAnalyse.troisDerniersMois,
|
||||
PeriodeAnalyse.cetteAnnee,
|
||||
];
|
||||
|
||||
final periodesPersonnalisees = [
|
||||
PeriodeAnalyse.septDerniersJours,
|
||||
PeriodeAnalyse.trenteDerniersJours,
|
||||
PeriodeAnalyse.sixDerniersMois,
|
||||
PeriodeAnalyse.anneeDerniere,
|
||||
PeriodeAnalyse.periodePersonnalisee,
|
||||
];
|
||||
|
||||
return UnifiedCard(
|
||||
variant: UnifiedCardVariant.outlined,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(DesignSystem.spacing16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Titre
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.date_range,
|
||||
size: 20,
|
||||
color: DesignSystem.primaryColor,
|
||||
),
|
||||
const SizedBox(width: DesignSystem.spacing8),
|
||||
Text(
|
||||
'Période d\'analyse',
|
||||
style: DesignSystem.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: DesignSystem.spacing12),
|
||||
|
||||
// Périodes rapides
|
||||
Text(
|
||||
'Accès rapide',
|
||||
style: DesignSystem.textTheme.bodySmall?.copyWith(
|
||||
color: DesignSystem.textSecondaryColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DesignSystem.spacing8),
|
||||
|
||||
Wrap(
|
||||
spacing: DesignSystem.spacing8,
|
||||
runSpacing: DesignSystem.spacing8,
|
||||
children: periodesRapides.map((periode) {
|
||||
return _buildPeriodeChip(periode, isRapide: true);
|
||||
}).toList(),
|
||||
),
|
||||
|
||||
const SizedBox(height: DesignSystem.spacing16),
|
||||
|
||||
// Périodes personnalisées
|
||||
Text(
|
||||
'Autres périodes',
|
||||
style: DesignSystem.textTheme.bodySmall?.copyWith(
|
||||
color: DesignSystem.textSecondaryColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DesignSystem.spacing8),
|
||||
|
||||
Wrap(
|
||||
spacing: DesignSystem.spacing8,
|
||||
runSpacing: DesignSystem.spacing8,
|
||||
children: periodesPersonnalisees.map((periode) {
|
||||
return _buildPeriodeChip(periode, isRapide: false);
|
||||
}).toList(),
|
||||
),
|
||||
|
||||
// Informations sur la période sélectionnée
|
||||
if (periodeSelectionnee != PeriodeAnalyse.periodePersonnalisee) ...[
|
||||
const SizedBox(height: DesignSystem.spacing16),
|
||||
_buildInfosPeriode(),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Chip de sélection de période
|
||||
Widget _buildPeriodeChip(PeriodeAnalyse periode, {required bool isRapide}) {
|
||||
final isSelected = periode == periodeSelectionnee;
|
||||
|
||||
return FilterChip(
|
||||
label: Text(
|
||||
periode.libelle,
|
||||
style: DesignSystem.textTheme.bodySmall?.copyWith(
|
||||
color: isSelected
|
||||
? Colors.white
|
||||
: isRapide
|
||||
? DesignSystem.primaryColor
|
||||
: DesignSystem.textSecondaryColor,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
selected: isSelected,
|
||||
onSelected: (_) => onPeriodeChanged(periode),
|
||||
backgroundColor: isRapide
|
||||
? DesignSystem.primaryColor.withOpacity(0.1)
|
||||
: DesignSystem.surfaceColor,
|
||||
selectedColor: isRapide
|
||||
? DesignSystem.primaryColor
|
||||
: DesignSystem.secondaryColor,
|
||||
checkmarkColor: Colors.white,
|
||||
side: BorderSide(
|
||||
color: isSelected
|
||||
? Colors.transparent
|
||||
: isRapide
|
||||
? DesignSystem.primaryColor.withOpacity(0.3)
|
||||
: DesignSystem.borderColor,
|
||||
width: 1,
|
||||
),
|
||||
elevation: isSelected ? 2 : 0,
|
||||
pressElevation: 4,
|
||||
);
|
||||
}
|
||||
|
||||
/// Informations sur la période sélectionnée
|
||||
Widget _buildInfosPeriode() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DesignSystem.spacing12),
|
||||
decoration: BoxDecoration(
|
||||
color: DesignSystem.primaryColor.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(DesignSystem.radius8),
|
||||
border: Border.all(
|
||||
color: DesignSystem.primaryColor.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 16,
|
||||
color: DesignSystem.primaryColor,
|
||||
),
|
||||
const SizedBox(width: DesignSystem.spacing8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Période sélectionnée : ${periodeSelectionnee.libelle}',
|
||||
style: DesignSystem.textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DesignSystem.spacing2),
|
||||
Text(
|
||||
_getDescriptionPeriode(),
|
||||
style: DesignSystem.textTheme.bodySmall?.copyWith(
|
||||
color: DesignSystem.textSecondaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Description de la période sélectionnée
|
||||
String _getDescriptionPeriode() {
|
||||
switch (periodeSelectionnee) {
|
||||
case PeriodeAnalyse.aujourdHui:
|
||||
return 'Données du jour en cours';
|
||||
case PeriodeAnalyse.hier:
|
||||
return 'Données de la journée précédente';
|
||||
case PeriodeAnalyse.cetteSemaine:
|
||||
return 'Du lundi au dimanche de cette semaine';
|
||||
case PeriodeAnalyse.semaineDerniere:
|
||||
return 'Du lundi au dimanche de la semaine passée';
|
||||
case PeriodeAnalyse.ceMois:
|
||||
return 'Du 1er au dernier jour de ce mois';
|
||||
case PeriodeAnalyse.moisDernier:
|
||||
return 'Du 1er au dernier jour du mois passé';
|
||||
case PeriodeAnalyse.troisDerniersMois:
|
||||
return 'Les 3 derniers mois complets';
|
||||
case PeriodeAnalyse.sixDerniersMois:
|
||||
return 'Les 6 derniers mois complets';
|
||||
case PeriodeAnalyse.cetteAnnee:
|
||||
return 'Du 1er janvier à aujourd\'hui';
|
||||
case PeriodeAnalyse.anneeDerniere:
|
||||
return 'Du 1er janvier au 31 décembre de l\'année passée';
|
||||
case PeriodeAnalyse.septDerniersJours:
|
||||
return 'Les 7 derniers jours glissants';
|
||||
case PeriodeAnalyse.trenteDerniersJours:
|
||||
return 'Les 30 derniers jours glissants';
|
||||
case PeriodeAnalyse.periodePersonnalisee:
|
||||
return 'Définissez vos propres dates de début et fin';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user