dashboard_page.dart 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:go_router/go_router.dart';
  4. import 'package:intl/intl.dart';
  5. import 'package:ristoris/providers/database_provider.dart';
  6. import '../../../providers/auth_provider.dart';
  7. import '../../../providers/dashboard_provider.dart';
  8. import '../../../core/responsive.dart';
  9. import '../../../services/database_export_service.dart';
  10. import '../../widgets/persistent_sidebar.dart';
  11. class DashboardPage extends ConsumerWidget {
  12. const DashboardPage({super.key});
  13. @override
  14. Widget build(BuildContext context, WidgetRef ref) {
  15. final authState = ref.watch(authProvider);
  16. final agente = authState.agente;
  17. final dashboardAsync =
  18. agente != null ? ref.watch(dashboardProvider(agente.idAgente)) : null;
  19. final agentiAsync = ref.watch(agentiListProvider);
  20. final theme = Theme.of(context);
  21. final dashboardBody = SafeArea(
  22. child: dashboardAsync?.when(
  23. data: (data) {
  24. final agentiList = agentiAsync.asData?.value;
  25. final hasMultipleAgents =
  26. agentiList != null && agentiList.length > 1;
  27. return _DashboardContent(
  28. data: data,
  29. theme: theme,
  30. hasMultipleAgents: hasMultipleAgents,
  31. );
  32. },
  33. loading: () => const Center(child: CircularProgressIndicator()),
  34. error: (e, _) => Center(child: Text('Errore: $e')),
  35. ) ??
  36. const Center(child: CircularProgressIndicator()),
  37. );
  38. if (ResponsiveBreakpoints.showPersistentSidebar(context)) {
  39. return Row(
  40. children: [
  41. PersistentSidebar(
  42. onExportDatabase: () => DatabaseExportService.export(context),
  43. ),
  44. Expanded(
  45. child: Scaffold(
  46. appBar: AppBar(
  47. title: Text(
  48. 'Benvenuto, ${agente?.ragioneSociale ?? ''}',
  49. ),
  50. ),
  51. body: dashboardBody,
  52. ),
  53. ),
  54. ],
  55. );
  56. }
  57. return Scaffold(
  58. appBar: AppBar(title: Text('Benvenuto, ${agente?.ragioneSociale ?? ''}')),
  59. drawer: Drawer(
  60. child: PersistentSidebar(
  61. onExportDatabase: () => DatabaseExportService.export(context),
  62. ),
  63. ),
  64. body: dashboardBody,
  65. );
  66. }
  67. }
  68. class _DashboardContent extends StatelessWidget {
  69. final DashboardData data;
  70. final ThemeData theme;
  71. final bool hasMultipleAgents;
  72. const _DashboardContent({
  73. required this.data,
  74. required this.theme,
  75. this.hasMultipleAgents = false,
  76. });
  77. @override
  78. Widget build(BuildContext context) {
  79. return SingleChildScrollView(
  80. padding: ResponsiveBreakpoints.padding(context),
  81. child: Column(
  82. crossAxisAlignment: CrossAxisAlignment.start,
  83. children: [
  84. // Quick action cards
  85. Wrap(
  86. spacing: 12,
  87. runSpacing: 12,
  88. children: [
  89. _ActionCard(
  90. icon: Icons.receipt_long,
  91. label: 'Nuovo Ordine',
  92. color: theme.colorScheme.primary,
  93. onTap: () => context.go('/dashboard/orders/new'),
  94. ),
  95. _ActionCard(
  96. icon: Icons.people,
  97. label: 'Clienti',
  98. color: theme.colorScheme.tertiary,
  99. onTap: () => context.go('/dashboard/customers'),
  100. ),
  101. _ActionCard(
  102. icon: Icons.inventory_2,
  103. label: 'Catalogo',
  104. color: theme.colorScheme.secondary,
  105. onTap: () => context.go('/dashboard/catalog'),
  106. ),
  107. _ActionCard(
  108. icon: Icons.sync,
  109. label: 'Sincronizza',
  110. color: theme.colorScheme.error,
  111. onTap: () => context.go('/dashboard/sync'),
  112. ),
  113. ],
  114. ),
  115. const SizedBox(height: 24),
  116. // Stats cards
  117. Wrap(
  118. spacing: 12,
  119. runSpacing: 12,
  120. children: [
  121. _StatCard(
  122. label: 'Ordini',
  123. value: data.newOrdersCount.toString(),
  124. icon: Icons.receipt_long,
  125. color: theme.colorScheme.primary,
  126. onTap: () => context.go('/dashboard/orders'),
  127. ),
  128. _StatCard(
  129. label: 'Ordini in Sospeso',
  130. value: data.ordersOnHoldCount.toString(),
  131. icon: Icons.pause_circle,
  132. color: theme.colorScheme.tertiary,
  133. ),
  134. _StatCard(
  135. label: 'Nuovi Clienti',
  136. value: data.newCustomersCount.toString(),
  137. icon: Icons.person_add,
  138. color: theme.colorScheme.secondary,
  139. ),
  140. ],
  141. ),
  142. const SizedBox(height: 24),
  143. // Top items
  144. if (data.topItems.isNotEmpty) ...[
  145. Text(
  146. 'Articoli Più Venduti',
  147. style: theme.textTheme.titleMedium?.copyWith(
  148. fontWeight: FontWeight.bold,
  149. ),
  150. ),
  151. const SizedBox(height: 8),
  152. Card(
  153. child: Column(
  154. children: data.topItems
  155. .map(
  156. (item) => ListTile(
  157. leading: CircleAvatar(
  158. child: Text(
  159. item.codArticolo?.substring(0, 2) ?? '??',
  160. ),
  161. ),
  162. title: Text(item.descrizione ?? ''),
  163. trailing: Text(
  164. NumberFormat.currency(
  165. locale: 'it',
  166. symbol: '€',
  167. ).format(item.totale ?? 0),
  168. ),
  169. ),
  170. )
  171. .toList(),
  172. ),
  173. ),
  174. ],
  175. // Top customers
  176. if (data.topCustomers.isNotEmpty) ...[
  177. const SizedBox(height: 16),
  178. Text(
  179. 'Top Clienti',
  180. style: theme.textTheme.titleMedium?.copyWith(
  181. fontWeight: FontWeight.bold,
  182. ),
  183. ),
  184. const SizedBox(height: 8),
  185. Card(
  186. child: Column(
  187. children: data.topCustomers
  188. .map(
  189. (c) => ListTile(
  190. leading: CircleAvatar(
  191. child: Text(c.codCliente?.substring(0, 2) ?? '??'),
  192. ),
  193. title: Text(c.ragioneSociale ?? ''),
  194. trailing: Text(
  195. NumberFormat.currency(
  196. locale: 'it',
  197. symbol: '€',
  198. ).format(c.totale ?? 0),
  199. ),
  200. ),
  201. )
  202. .toList(),
  203. ),
  204. ),
  205. ],
  206. if (hasMultipleAgents) ...[
  207. const SizedBox(height: 24),
  208. SizedBox(
  209. width: double.infinity,
  210. child: OutlinedButton.icon(
  211. onPressed: () => context.go('/dashboard/switch-agent'),
  212. icon: const Icon(Icons.swap_horiz),
  213. label: const Text('Cambia agente'),
  214. ),
  215. ),
  216. const SizedBox(height: 16),
  217. ],
  218. ],
  219. ),
  220. );
  221. }
  222. }
  223. class _ActionCard extends StatelessWidget {
  224. final IconData icon;
  225. final String label;
  226. final Color color;
  227. final VoidCallback onTap;
  228. const _ActionCard({
  229. required this.icon,
  230. required this.label,
  231. required this.color,
  232. required this.onTap,
  233. });
  234. @override
  235. Widget build(BuildContext context) {
  236. return SizedBox(
  237. width: ResponsiveBreakpoints.isPhone(context)
  238. ? (MediaQuery.of(context).size.width - 48) / 2
  239. : 160,
  240. child: Card(
  241. child: InkWell(
  242. borderRadius: BorderRadius.circular(12),
  243. onTap: onTap,
  244. child: Padding(
  245. padding: const EdgeInsets.all(16),
  246. child: Column(
  247. mainAxisSize: MainAxisSize.min,
  248. children: [
  249. Icon(icon, size: 32, color: color),
  250. const SizedBox(height: 8),
  251. Text(
  252. label,
  253. textAlign: TextAlign.center,
  254. style: Theme.of(context).textTheme.labelLarge,
  255. ),
  256. ],
  257. ),
  258. ),
  259. ),
  260. ),
  261. );
  262. }
  263. }
  264. class _StatCard extends StatelessWidget {
  265. final String label;
  266. final String value;
  267. final IconData icon;
  268. final Color color;
  269. final VoidCallback? onTap;
  270. const _StatCard({
  271. required this.label,
  272. required this.value,
  273. required this.icon,
  274. required this.color,
  275. this.onTap,
  276. });
  277. @override
  278. Widget build(BuildContext context) {
  279. return Card(
  280. child: InkWell(
  281. borderRadius: BorderRadius.circular(12),
  282. onTap: onTap,
  283. child: Padding(
  284. padding: const EdgeInsets.all(16),
  285. child: Row(
  286. mainAxisSize: MainAxisSize.min,
  287. children: [
  288. Icon(icon, color: color),
  289. const SizedBox(width: 12),
  290. Column(
  291. crossAxisAlignment: CrossAxisAlignment.start,
  292. children: [
  293. Text(
  294. value,
  295. style: Theme.of(context).textTheme.headlineSmall?.copyWith(
  296. fontWeight: FontWeight.bold,
  297. ),
  298. ),
  299. Text(label, style: Theme.of(context).textTheme.bodySmall),
  300. ],
  301. ),
  302. ],
  303. ),
  304. ),
  305. ),
  306. );
  307. }
  308. }