customer_detail_page.dart 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  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 '../../../database/database.dart';
  6. import '../../../providers/auth_provider.dart';
  7. import '../../../providers/database_provider.dart';
  8. import '../../../providers/customer_provider.dart';
  9. import '../../../database/tables/clienti.dart';
  10. import '../../../database/tables/filiali.dart';
  11. import '../../../database/tables/scadenze.dart';
  12. import '../../../core/responsive.dart';
  13. class CustomerDetailPage extends ConsumerWidget {
  14. final int id;
  15. const CustomerDetailPage({super.key, required this.id});
  16. @override
  17. Widget build(BuildContext context, WidgetRef ref) {
  18. final clientiDao = ref.read(clientiDaoProvider);
  19. final clienteAsync = clientiDao.getById(id);
  20. final theme = Theme.of(context);
  21. return DefaultTabController(
  22. length: 4,
  23. child: Scaffold(
  24. appBar: AppBar(
  25. title: FutureBuilder<Cliente?>(
  26. future: clienteAsync,
  27. builder: (_, snap) => Text(snap.data?.ragioneSociale ?? 'Cliente'),
  28. ),
  29. bottom: const TabBar(
  30. tabs: [
  31. Tab(text: 'Info', icon: Icon(Icons.info_outline)),
  32. Tab(text: 'Ordini', icon: Icon(Icons.receipt_long_outlined)),
  33. Tab(text: 'Destinazioni', icon: Icon(Icons.location_on_outlined)),
  34. Tab(
  35. text: 'Scadenze',
  36. icon: Icon(Icons.account_balance_wallet_outlined),
  37. ),
  38. ],
  39. ),
  40. ),
  41. body: SafeArea(
  42. child: FutureBuilder<Cliente?>(
  43. future: clienteAsync,
  44. builder: (_, snap) {
  45. final cliente = snap.data;
  46. if (cliente == null)
  47. return const Center(child: CircularProgressIndicator());
  48. return TabBarView(
  49. children: [
  50. _InfoTab(cliente: cliente, theme: theme),
  51. _OrdersTab(idCliente: id, theme: theme),
  52. _DestinationsTab(idCliente: id, theme: theme),
  53. _PaymentsTab(
  54. idCliente: cliente.idClienteRemoto ?? 0,
  55. theme: theme,
  56. ),
  57. ],
  58. );
  59. },
  60. ),
  61. ),
  62. ),
  63. );
  64. }
  65. }
  66. class _InfoTab extends StatelessWidget {
  67. final Cliente cliente;
  68. final ThemeData theme;
  69. const _InfoTab({required this.cliente, required this.theme});
  70. @override
  71. Widget build(BuildContext context) {
  72. return SingleChildScrollView(
  73. padding: const EdgeInsets.all(16),
  74. child: Column(
  75. crossAxisAlignment: CrossAxisAlignment.start,
  76. children: [
  77. _infoRow('Codice', cliente.codCliente ?? '-'),
  78. _infoRow('Ragione Sociale', cliente.ragioneSociale ?? '-'),
  79. _infoRow('Indirizzo', cliente.indirizzo ?? '-'),
  80. _infoRow(
  81. 'Città',
  82. '${cliente.citta ?? ""} ${cliente.provincia ?? ""} ${cliente.cap ?? ""}',
  83. ),
  84. _infoRow('Telefono', cliente.telefono ?? '-'),
  85. _infoRow('Cellulare', cliente.cellulare ?? '-'),
  86. _infoRow('Email', cliente.email ?? '-'),
  87. _infoRow('Partita IVA', cliente.partitaIva ?? '-'),
  88. _infoRow('Codice Fiscale', cliente.codiceFiscale ?? '-'),
  89. if (cliente.sdi != null && cliente.sdi!.isNotEmpty)
  90. _infoRow('SDI', cliente.sdi!),
  91. if (cliente.pec != null && cliente.pec!.isNotEmpty)
  92. _infoRow('PEC', cliente.pec!),
  93. if (cliente.note != null && cliente.note!.isNotEmpty)
  94. _infoRow('Note', cliente.note!),
  95. if (cliente.codBlocco != null && cliente.codBlocco!.isNotEmpty)
  96. Card(
  97. color: Colors.red.shade50,
  98. child: Padding(
  99. padding: const EdgeInsets.all(12),
  100. child: Row(
  101. children: [
  102. Icon(Icons.block, color: Colors.red),
  103. const SizedBox(width: 8),
  104. Text('Blocco: ${cliente.codBlocco}'),
  105. ],
  106. ),
  107. ),
  108. ),
  109. ],
  110. ),
  111. );
  112. }
  113. Widget _infoRow(String label, String value) {
  114. return Padding(
  115. padding: const EdgeInsets.symmetric(vertical: 4),
  116. child: Row(
  117. crossAxisAlignment: CrossAxisAlignment.start,
  118. children: [
  119. SizedBox(
  120. width: 120,
  121. child: Text(
  122. label,
  123. style: theme.textTheme.bodySmall?.copyWith(
  124. color: theme.colorScheme.outline,
  125. ),
  126. ),
  127. ),
  128. Expanded(child: Text(value)),
  129. ],
  130. ),
  131. );
  132. }
  133. }
  134. class _OrdersTab extends ConsumerWidget {
  135. final int idCliente;
  136. final ThemeData theme;
  137. const _OrdersTab({required this.idCliente, required this.theme});
  138. @override
  139. Widget build(BuildContext context, WidgetRef ref) {
  140. final ordiniDao = ref.read(ordiniDaoProvider);
  141. return Scaffold(
  142. body: SafeArea(
  143. child: FutureBuilder(
  144. future: ordiniDao.customerOrders(idCliente, false),
  145. builder: (_, snap) {
  146. if (!snap.hasData)
  147. return const Center(child: CircularProgressIndicator());
  148. final orders = snap.data!;
  149. if (orders.isEmpty) return const Center(child: Text('Nessun ordine'));
  150. return ListView.builder(
  151. itemCount: orders.length,
  152. itemBuilder: (_, i) => ListTile(
  153. title: Text('Ordine #${orders[i].numeroOrdine ?? ""}'),
  154. subtitle: Text(orders[i].dataOrdine ?? ''),
  155. onTap: () => context.push('/dashboard/orders/${orders[i].idOrdine}'),
  156. ),
  157. );
  158. },
  159. ),
  160. ),
  161. floatingActionButton: FloatingActionButton(
  162. heroTag: 'addOrder',
  163. child: const Icon(Icons.add),
  164. onPressed: () => context.push('/dashboard/orders/new?cliente=$idCliente'),
  165. ),
  166. );
  167. }
  168. }
  169. class _DestinationsTab extends ConsumerWidget {
  170. final int idCliente;
  171. final ThemeData theme;
  172. const _DestinationsTab({required this.idCliente, required this.theme});
  173. @override
  174. Widget build(BuildContext context, WidgetRef ref) {
  175. final destsAsync = ref.watch(customerDestinationsProvider(idCliente));
  176. return destsAsync.when(
  177. data: (dests) {
  178. if (dests.isEmpty)
  179. return const Center(child: Text('Nessuna destinazione'));
  180. return ListView.builder(
  181. itemCount: dests.length,
  182. itemBuilder: (_, i) => ListTile(
  183. leading: const Icon(Icons.location_on_outlined),
  184. title: Text(dests[i].descrizione ?? ''),
  185. subtitle: Text(
  186. '${dests[i].indirizzo ?? ""} ${dests[i].citta ?? ""}',
  187. ),
  188. ),
  189. );
  190. },
  191. loading: () => const Center(child: CircularProgressIndicator()),
  192. error: (e, _) => Center(child: Text('Errore: $e')),
  193. );
  194. }
  195. }
  196. class _PaymentsTab extends ConsumerWidget {
  197. final int idCliente;
  198. final ThemeData theme;
  199. const _PaymentsTab({required this.idCliente, required this.theme});
  200. @override
  201. Widget build(BuildContext context, WidgetRef ref) {
  202. final paysAsync = ref.watch(customerPaymentsProvider(idCliente));
  203. return paysAsync.when(
  204. data: (pays) {
  205. if (pays.isEmpty) return const Center(child: Text('Nessuna scadenza'));
  206. return ListView.builder(
  207. padding: const EdgeInsets.all(8),
  208. itemCount: pays.length,
  209. itemBuilder: (_, i) {
  210. final p = pays[i];
  211. return Card(
  212. margin: const EdgeInsets.symmetric(vertical: 4),
  213. child: Padding(
  214. padding: const EdgeInsets.all(12),
  215. child: Column(
  216. crossAxisAlignment: CrossAxisAlignment.start,
  217. children: [
  218. _row('Fattura N.', p.numDoc ?? '-'),
  219. _row('Data fatt.', _formatDate(p.dataDoc)),
  220. _row('Data scad', _formatDate(p.dataScadenza)),
  221. _row('Totale fattura', _formatAmount(p.importoFattura)),
  222. _row('Totale scad.', _formatAmount(p.importoScadenza)),
  223. _row('Saldo', _formatAmount(p.saldo)),
  224. ],
  225. ),
  226. ),
  227. );
  228. },
  229. );
  230. },
  231. loading: () => const Center(child: CircularProgressIndicator()),
  232. error: (e, _) => Center(child: Text('Errore: $e')),
  233. );
  234. }
  235. String _formatDate(String? dateStr) {
  236. if (dateStr == null) return '-';
  237. try {
  238. final dt = DateTime.parse(dateStr);
  239. return DateFormat('dd/MM/yyyy').format(dt);
  240. } catch (_) {
  241. return dateStr;
  242. }
  243. }
  244. String _formatAmount(double? amount) {
  245. if (amount == null) return '-';
  246. final fmt = NumberFormat('#,##0.00', 'it');
  247. return '€ ${fmt.format(amount)}';
  248. }
  249. Widget _row(String label, String value) {
  250. return Padding(
  251. padding: const EdgeInsets.symmetric(vertical: 2),
  252. child: Row(
  253. children: [
  254. SizedBox(
  255. width: 110,
  256. child: Text(
  257. label,
  258. style: theme.textTheme.bodySmall?.copyWith(
  259. color: theme.colorScheme.outline,
  260. ),
  261. ),
  262. ),
  263. Expanded(
  264. child: Text(value, style: theme.textTheme.bodyMedium),
  265. ),
  266. ],
  267. ),
  268. );
  269. }
  270. }