statistics_page.dart 7.5 KB

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