customer_detail_page.dart 9.4 KB

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