statistics_page.dart 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:fl_chart/fl_chart.dart';
  4. import 'package:intl/intl.dart';
  5. import '../../../providers/auth_provider.dart';
  6. import '../../../providers/database_provider.dart';
  7. import '../../../business/statistics_service.dart';
  8. class StatisticsPage extends ConsumerStatefulWidget {
  9. const StatisticsPage({super.key});
  10. @override
  11. ConsumerState<StatisticsPage> createState() => _StatisticsPageState();
  12. }
  13. class _StatisticsPageState extends ConsumerState<StatisticsPage>
  14. with SingleTickerProviderStateMixin {
  15. late TabController _tabCtrl;
  16. @override
  17. void initState() {
  18. super.initState();
  19. _tabCtrl = TabController(length: 2, vsync: this);
  20. }
  21. @override
  22. void dispose() {
  23. _tabCtrl.dispose();
  24. super.dispose();
  25. }
  26. @override
  27. Widget build(BuildContext context) {
  28. return Scaffold(
  29. appBar: AppBar(
  30. title: const Text('Statistiche'),
  31. bottom: TabBar(
  32. controller: _tabCtrl,
  33. tabs: const [
  34. Tab(text: 'Andamento Mensile'),
  35. Tab(text: 'Top Clienti'),
  36. ],
  37. ),
  38. ),
  39. body: SafeArea(
  40. child: TabBarView(
  41. controller: _tabCtrl,
  42. children: [
  43. _MonthlyChart(),
  44. _TopClientsChart(),
  45. ],
  46. ),
  47. ),
  48. );
  49. }
  50. }
  51. class _MonthlyChart extends ConsumerWidget {
  52. @override
  53. Widget build(BuildContext context, WidgetRef ref) {
  54. final agente = ref.watch(authProvider).agente;
  55. if (agente == null) return const Center(child: Text('Login richiesto'));
  56. final statsService = StatisticsService(
  57. ref.read(statisticheDaoProvider),
  58. ref.read(articoliDaoProvider),
  59. ref.read(clientiDaoProvider),
  60. ref.read(agentiDaoProvider),
  61. );
  62. return FutureBuilder(
  63. future: statsService.getMonthlyTotals(agente.idAgente),
  64. builder: (_, snap) {
  65. if (!snap.hasData) return const Center(child: CircularProgressIndicator());
  66. final data = snap.data!;
  67. if (data.isEmpty) return const Center(child: Text('Nessun dato statistico'));
  68. final sortedKeys = data.keys.toList()..sort();
  69. final maxVal = data.values.reduce((a, b) => a > b ? a : b);
  70. return Padding(
  71. padding: const EdgeInsets.all(16),
  72. child: Column(
  73. crossAxisAlignment: CrossAxisAlignment.start,
  74. children: [
  75. Text('Andamento Vendite', style: Theme.of(context).textTheme.titleMedium),
  76. const SizedBox(height: 16),
  77. Expanded(
  78. child: BarChart(
  79. BarChartData(
  80. alignment: BarChartAlignment.spaceAround,
  81. maxY: maxVal * 1.2,
  82. barGroups: sortedKeys.asMap().entries.map((e) => BarChartGroupData(
  83. x: e.key,
  84. barRods: [BarChartRodData(
  85. toY: data[e.value]!,
  86. color: Theme.of(context).colorScheme.primary,
  87. width: 20,
  88. borderRadius: const BorderRadius.vertical(top: Radius.circular(4)),
  89. )],
  90. )).toList(),
  91. titlesData: FlTitlesData(
  92. bottomTitles: AxisTitles(
  93. sideTitles: SideTitles(
  94. showTitles: true,
  95. getTitlesWidget: (value, _) {
  96. final idx = value.toInt();
  97. if (idx < 0 || idx >= sortedKeys.length) return const SizedBox();
  98. final label = sortedKeys[idx];
  99. return Text(label.length >= 7 ? label.substring(5, 7) : label, style: const TextStyle(fontSize: 10));
  100. },
  101. ),
  102. ),
  103. leftTitles: AxisTitles(
  104. sideTitles: SideTitles(
  105. showTitles: true,
  106. reservedSize: 60,
  107. getTitlesWidget: (value, _) => Text(
  108. NumberFormat.compact(locale: 'it').format(value),
  109. style: const TextStyle(fontSize: 10),
  110. ),
  111. ),
  112. ),
  113. topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
  114. rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
  115. ),
  116. gridData: FlGridData(show: true, drawVerticalLine: false),
  117. borderData: FlBorderData(show: false),
  118. ),
  119. ),
  120. ),
  121. ],
  122. ),
  123. );
  124. },
  125. );
  126. }
  127. }
  128. class _TopClientsChart extends ConsumerWidget {
  129. @override
  130. Widget build(BuildContext context, WidgetRef ref) {
  131. final agente = ref.watch(authProvider).agente;
  132. if (agente == null) return const Center(child: Text('Login richiesto'));
  133. final statsService = StatisticsService(
  134. ref.read(statisticheDaoProvider),
  135. ref.read(articoliDaoProvider),
  136. ref.read(clientiDaoProvider),
  137. ref.read(agentiDaoProvider),
  138. );
  139. return FutureBuilder(
  140. future: statsService.getTotalsByClient(agente.idAgente),
  141. builder: (_, snap) {
  142. if (!snap.hasData) return const Center(child: CircularProgressIndicator());
  143. final data = snap.data!;
  144. if (data.isEmpty) return const Center(child: Text('Nessun dato'));
  145. final sorted = data.entries.toList()..sort((a, b) => b.value.compareTo(a.value));
  146. final top5 = sorted.take(5).toList();
  147. // final total = top5.fold(0.0, (s, e) => s + e.value);
  148. return Padding(
  149. padding: const EdgeInsets.all(16),
  150. child: Column(
  151. crossAxisAlignment: CrossAxisAlignment.start,
  152. children: [
  153. Text('Top 5 Clienti per Fatturato', style: Theme.of(context).textTheme.titleMedium),
  154. const SizedBox(height: 16),
  155. Expanded(
  156. child: PieChart(
  157. PieChartData(
  158. sections: top5.asMap().entries.map((e) => PieChartSectionData(
  159. value: e.value.value,
  160. title: '${e.key + 1}',
  161. color: _colors[e.key % _colors.length],
  162. radius: 60,
  163. titleStyle: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
  164. )).toList(),
  165. sectionsSpace: 2,
  166. centerSpaceRadius: 40,
  167. ),
  168. ),
  169. ),
  170. const SizedBox(height: 16),
  171. ...top5.asMap().entries.map((e) => Padding(
  172. padding: const EdgeInsets.symmetric(vertical: 2),
  173. child: Row(
  174. children: [
  175. Container(width: 12, height: 12, decoration: BoxDecoration(color: _colors[e.key % _colors.length], shape: BoxShape.circle)),
  176. const SizedBox(width: 8),
  177. Text('Cliente #${e.value.key}', style: Theme.of(context).textTheme.bodySmall),
  178. const Spacer(),
  179. Text(NumberFormat.currency(locale: 'it', symbol: '€').format(e.value.value),
  180. style: Theme.of(context).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.bold)),
  181. ],
  182. ),
  183. )),
  184. ],
  185. ),
  186. );
  187. },
  188. );
  189. }
  190. }
  191. const _colors = [
  192. Colors.blue,
  193. Colors.red,
  194. Colors.green,
  195. Colors.orange,
  196. Colors.purple,
  197. Colors.teal,
  198. Colors.pink,
  199. Colors.indigo,
  200. ];