import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:intl/intl.dart'; import '../models/statistiche.dart'; import '../services/api_service.dart'; import '../utils/constants.dart'; class DashboardScreen extends StatefulWidget { const DashboardScreen({super.key}); @override State createState() => _DashboardScreenState(); } class _DashboardScreenState extends State { final _apiService = ApiService(); Statistiche? _statistiche; AllarmiPerGiornoResponse? _allarmiPerGiorno; bool _isLoading = true; String? _errorMessage; @override void initState() { super.initState(); _loadData(); } Future _loadData() async { setState(() => _isLoading = true); try { final stats = await _apiService.getStatistiche(); final allarmiGiorno = await _apiService.getAllarmiPerGiorno(giorni: 30); setState(() { _statistiche = stats; _allarmiPerGiorno = allarmiGiorno; _isLoading = false; _errorMessage = null; }); } catch (e) { setState(() { _errorMessage = 'Errore caricamento dati: $e'; _isLoading = false; }); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Dashboard'), Text('Panoramica generale', style: TextStyle(fontSize: 12)), ], ), actions: [ IconButton( icon: const Icon(Icons.refresh), onPressed: _loadData, ), ], ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : _errorMessage != null ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.error_outline, size: 64, color: AppColors.critical, ), const SizedBox(height: 16), Text(_errorMessage!), const SizedBox(height: 16), ElevatedButton.icon( onPressed: _loadData, icon: const Icon(Icons.refresh), label: const Text('Riprova'), ), ], ), ) : RefreshIndicator( onRefresh: _loadData, child: SingleChildScrollView( padding: const EdgeInsets.all(AppSizes.paddingL), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // KPI Cards _buildKPISection(), const SizedBox(height: 24), // Grafico Severità _buildSeveritaChart(), const SizedBox(height: 24), // Grafico Stati _buildStatiChart(), const SizedBox(height: 24), // Grafico Temporale _buildTimelineChart(), const SizedBox(height: 24), // Grafico Siti per Tipo _buildSitiPerTipoChart(), ], ), ), ), ); } Widget _buildKPISection() { if (_statistiche == null) return const SizedBox.shrink(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Indicatori Chiave', style: AppTextStyles.h2), const SizedBox(height: 12), Row( children: [ Expanded( child: _buildKPICard( 'Allarmi Totali', _statistiche!.totaleAllarmi.toString(), Icons.notifications_active, AppColors.primary, ), ), const SizedBox(width: AppSizes.paddingM), Expanded( child: _buildKPICard( 'Siti Monitorati', _statistiche!.totaleSiti.toString(), Icons.location_on, AppColors.secondary, ), ), ], ), const SizedBox(height: AppSizes.paddingM), Row( children: [ Expanded( child: _buildKPICard( 'Aperti', _statistiche!.allarmiAperti.toString(), Icons.warning, AppColors.warning, subtitle: '${_statistiche!.percentualeAperti.toStringAsFixed(1)}%', ), ), const SizedBox(width: AppSizes.paddingM), Expanded( child: _buildKPICard( 'Critici', _statistiche!.allarmiCritical.toString(), Icons.error, AppColors.critical, subtitle: '${_statistiche!.percentualeCritici.toStringAsFixed(1)}%', ), ), ], ), ], ); } Widget _buildKPICard( String label, String value, IconData icon, Color color, { String? subtitle, }) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [color, color.withOpacity(0.7)], ), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: color.withOpacity(0.3), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, color: Colors.white, size: 28), const SizedBox(height: 12), Text( value, style: const TextStyle( color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Text( label, style: TextStyle( color: Colors.white.withOpacity(0.9), fontSize: 14, ), ), if (subtitle != null) ...[ const SizedBox(height: 4), Text( subtitle, style: const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold, ), ), ], ], ), ); } Widget _buildSeveritaChart() { if (_statistiche == null) return const SizedBox.shrink(); final sections = [ if (_statistiche!.allarmiCritical > 0) PieChartSectionData( color: AppColors.critical, value: _statistiche!.allarmiCritical.toDouble(), title: '${_statistiche!.allarmiCritical}', radius: 50, titleStyle: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.white, ), ), if (_statistiche!.allarmiWarning > 0) PieChartSectionData( color: AppColors.warning, value: _statistiche!.allarmiWarning.toDouble(), title: '${_statistiche!.allarmiWarning}', radius: 50, titleStyle: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.white, ), ), if (_statistiche!.allarmiInfo > 0) PieChartSectionData( color: AppColors.info, value: _statistiche!.allarmiInfo.toDouble(), title: '${_statistiche!.allarmiInfo}', radius: 50, titleStyle: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.white, ), ), ]; if (sections.isEmpty) { return const SizedBox.shrink(); } return _buildChartCard( 'Allarmi per Severità', Column( children: [ SizedBox( height: 200, child: PieChart( PieChartData( sections: sections, sectionsSpace: 2, centerSpaceRadius: 40, ), ), ), const SizedBox(height: 16), _buildLegend([ _LegendItem('Critici', AppColors.critical, _statistiche!.allarmiCritical), _LegendItem('Avvisi', AppColors.warning, _statistiche!.allarmiWarning), _LegendItem('Info', AppColors.info, _statistiche!.allarmiInfo), ]), ], ), ); } Widget _buildStatiChart() { if (_statistiche == null) return const SizedBox.shrink(); return _buildChartCard( 'Allarmi per Stato', Column( children: [ SizedBox( height: 200, child: BarChart( BarChartData( alignment: BarChartAlignment.spaceAround, maxY: _statistiche!.totaleStato.toDouble() * 1.2, barTouchData: BarTouchData(enabled: false), titlesData: FlTitlesData( show: true, bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { switch (value.toInt()) { case 0: return const Text('Nuovi', style: TextStyle(fontSize: 12)); case 1: return const Text('In Gest.', style: TextStyle(fontSize: 12)); case 2: return const Text('Risolti', style: TextStyle(fontSize: 12)); default: return const Text(''); } }, ), ), leftTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), ), borderData: FlBorderData(show: false), barGroups: [ BarChartGroupData( x: 0, barRods: [ BarChartRodData( toY: _statistiche!.allarmiNuovo.toDouble(), color: AppColors.critical, width: 40, borderRadius: const BorderRadius.vertical(top: Radius.circular(4)), ), ], ), BarChartGroupData( x: 1, barRods: [ BarChartRodData( toY: _statistiche!.allarmiInGestione.toDouble(), color: AppColors.warning, width: 40, borderRadius: const BorderRadius.vertical(top: Radius.circular(4)), ), ], ), BarChartGroupData( x: 2, barRods: [ BarChartRodData( toY: _statistiche!.allarmiRisolto.toDouble(), color: AppColors.success, width: 40, borderRadius: const BorderRadius.vertical(top: Radius.circular(4)), ), ], ), ], ), ), ), ], ), ); } Widget _buildTimelineChart() { if (_allarmiPerGiorno == null || _allarmiPerGiorno!.dati.isEmpty) { return const SizedBox.shrink(); } final spots = _allarmiPerGiorno!.dati.asMap().entries.map((entry) { return FlSpot(entry.key.toDouble(), entry.value.count.toDouble()); }).toList(); final maxY = _allarmiPerGiorno!.dati .map((d) => d.count) .reduce((a, b) => a > b ? a : b) .toDouble(); return _buildChartCard( 'Andamento Temporale (30 giorni)', SizedBox( height: 200, child: LineChart( LineChartData( gridData: FlGridData( show: true, drawVerticalLine: false, horizontalInterval: 1, getDrawingHorizontalLine: (value) { return FlLine( color: Colors.grey[300], strokeWidth: 1, ); }, ), titlesData: FlTitlesData( show: true, bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 30, interval: 5, getTitlesWidget: (value, meta) { if (value.toInt() >= _allarmiPerGiorno!.dati.length) { return const Text(''); } final data = _allarmiPerGiorno!.dati[value.toInt()].data; return Text( DateFormat('d/M').format(data), style: const TextStyle(fontSize: 10), ); }, ), ), leftTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), ), borderData: FlBorderData(show: false), minX: 0, maxX: spots.length.toDouble() - 1, minY: 0, maxY: maxY * 1.2, lineBarsData: [ LineChartBarData( spots: spots, isCurved: true, color: AppColors.primary, barWidth: 3, isStrokeCapRound: true, dotData: const FlDotData(show: false), belowBarData: BarAreaData( show: true, color: AppColors.primary.withOpacity(0.1), ), ), ], ), ), ), ); } Widget _buildSitiPerTipoChart() { if (_statistiche == null) return const SizedBox.shrink(); final data = [ _ChartData('Ponti', _statistiche!.sitiPonte, Colors.blue), _ChartData('Gallerie', _statistiche!.sitiGalleria, Colors.brown), _ChartData('Dighe', _statistiche!.sitiDiga, Colors.cyan), _ChartData('Frane', _statistiche!.sitiFrana, Colors.orange), _ChartData('Versanti', _statistiche!.sitiVersante, Colors.green), _ChartData('Edifici', _statistiche!.sitiEdificio, Colors.purple), ].where((d) => d.value > 0).toList(); if (data.isEmpty) return const SizedBox.shrink(); return _buildChartCard( 'Siti per Tipologia', SizedBox( height: 200, child: BarChart( BarChartData( alignment: BarChartAlignment.spaceAround, maxY: data.map((d) => d.value).reduce((a, b) => a > b ? a : b).toDouble() * 1.2, barTouchData: BarTouchData(enabled: false), titlesData: FlTitlesData( show: true, bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { if (value.toInt() >= data.length) return const Text(''); return Text( data[value.toInt()].label.substring(0, 3), style: const TextStyle(fontSize: 12), ); }, ), ), leftTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), ), borderData: FlBorderData(show: false), barGroups: data.asMap().entries.map((entry) { return BarChartGroupData( x: entry.key, barRods: [ BarChartRodData( toY: entry.value.value.toDouble(), color: entry.value.color, width: 30, borderRadius: const BorderRadius.vertical(top: Radius.circular(4)), ), ], ); }).toList(), ), ), ), ); } Widget _buildChartCard(String title, Widget child) { return Container( padding: const EdgeInsets.all(AppSizes.paddingL), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: AppTextStyles.h3), const SizedBox(height: 16), child, ], ), ); } Widget _buildLegend(List<_LegendItem> items) { return Wrap( spacing: 16, runSpacing: 8, children: items.map((item) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 16, height: 16, decoration: BoxDecoration( color: item.color, shape: BoxShape.circle, ), ), const SizedBox(width: 8), Text( '${item.label}: ${item.value}', style: AppTextStyles.bodySmall, ), ], ); }).toList(), ); } } class _LegendItem { final String label; final Color color; final int value; _LegendItem(this.label, this.color, this.value); } class _ChartData { final String label; final int value; final Color color; _ChartData(this.label, this.value, this.color); }