394 lines
12 KiB
Dart
394 lines
12 KiB
Dart
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);
|
|
}
|
|
}
|