statistics_page.dart 8.0 KB

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