order_edit_page.dart 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:intl/intl.dart';
  4. import '../../../providers/auth_provider.dart';
  5. import '../../../providers/order_provider.dart';
  6. import '../../../providers/database_provider.dart';
  7. import '../../../database/database.dart';
  8. import '../../../database/daos/listini_dao.dart';
  9. import '../../widgets/article_image.dart';
  10. class OrderEditPage extends ConsumerStatefulWidget {
  11. final int? id;
  12. final int? idCliente;
  13. const OrderEditPage({super.key, this.id, this.idCliente});
  14. @override
  15. ConsumerState<OrderEditPage> createState() => _OrderEditPageState();
  16. }
  17. class _OrderEditPageState extends ConsumerState<OrderEditPage>
  18. with SingleTickerProviderStateMixin {
  19. late TabController _tabController;
  20. final _artCtrl = TextEditingController();
  21. final _noteClienteCtrl = TextEditingController();
  22. final _noteOrdineCtrl = TextEditingController();
  23. final _riferimentoCtrl = TextEditingController();
  24. List<Articolo> _searchResults = [];
  25. bool _searching = false;
  26. List<Filiale> _filiali = [];
  27. List<Listino> _listini = [];
  28. List<Pagamento> _pagamenti = [];
  29. Cliente? _cliente;
  30. int? _selectedFiliale;
  31. int? _selectedListino;
  32. int? _selectedPagamento;
  33. bool _dataLoaded = false;
  34. @override
  35. void initState() {
  36. super.initState();
  37. _tabController = TabController(length: 2, vsync: this);
  38. WidgetsBinding.instance.addPostFrameCallback((_) => _init());
  39. }
  40. @override
  41. void dispose() {
  42. _tabController.dispose();
  43. _artCtrl.dispose();
  44. _noteClienteCtrl.dispose();
  45. _noteOrdineCtrl.dispose();
  46. _riferimentoCtrl.dispose();
  47. super.dispose();
  48. }
  49. Future<void> _init() async {
  50. final agente = ref.read(authProvider).agente;
  51. if (agente == null) return;
  52. if (widget.id != null) {
  53. await ref.read(orderProvider.notifier).loadOrder(widget.id!);
  54. } else if (widget.idCliente != null) {
  55. await ref
  56. .read(orderProvider.notifier)
  57. .createOrder(agente.idAgente, idCliente: widget.idCliente!);
  58. } else {
  59. await ref.read(orderProvider.notifier).createOrder(agente.idAgente);
  60. }
  61. final orderState = ref.read(orderProvider);
  62. final order = orderState.order;
  63. if (order == null) return;
  64. final cli = order.idCliente;
  65. final clientiDao = ref.read(clientiDaoProvider);
  66. final filialiDao = ref.read(filialiDaoProvider);
  67. final listiniDao = ref.read(listiniDaoProvider);
  68. final pagamentiDao = ref.read(pagamentiDaoProvider);
  69. if (cli != null) {
  70. _cliente = await clientiDao.getById(cli);
  71. _filiali = await filialiDao.getDestinationsForCustomer(cli);
  72. }
  73. _listini = await listiniDao.list();
  74. _pagamenti = await pagamentiDao.searchActive();
  75. setState(() {
  76. _selectedFiliale = order.idFiliale;
  77. _selectedListino = order.idListino ?? _cliente?.idListino;
  78. _selectedPagamento = order.idPagamento ?? _cliente?.idPagamento;
  79. _noteClienteCtrl.text = order.noteCliente ?? _cliente?.noteOrdine ?? '';
  80. _noteOrdineCtrl.text = order.noteOrdine ?? '';
  81. _riferimentoCtrl.text = order.riferimento ?? '';
  82. _dataLoaded = true;
  83. });
  84. }
  85. Future<void> _searchArticles(String q) async {
  86. if (q.length < 2) {
  87. setState(() {
  88. _searchResults = [];
  89. _searching = false;
  90. });
  91. return;
  92. }
  93. setState(() => _searching = true);
  94. final dao = ref.read(articoliDaoProvider);
  95. final results = await dao.getItems(q);
  96. setState(() {
  97. _searchResults = results;
  98. _searching = false;
  99. });
  100. }
  101. void _showArticlePicker(Articolo art) async {
  102. final agente = ref.read(authProvider).agente;
  103. final canEditPrice = agente?.modificaPrezzi == 1;
  104. final listini = await ref.read(articoliDaoProvider).getPriceCodes(art.idArticolo);
  105. final listiniDao = ref.read(listiniDaoProvider);
  106. if (!context.mounted) return;
  107. showModalBottomSheet(
  108. context: context,
  109. isScrollControlled: true,
  110. builder: (_) => _ArticlePickerSheet(
  111. articolo: art,
  112. listini: listini,
  113. listiniDao: listiniDao,
  114. canEditPrezzo: canEditPrice,
  115. idListinoDefault: _selectedListino,
  116. onConfirm: (colli, quantita, prezzo, listinoId) {
  117. _addArticleWithDetails(
  118. art: art,
  119. colli: colli,
  120. quantita: quantita,
  121. prezzo: prezzo,
  122. );
  123. },
  124. ),
  125. );
  126. }
  127. Future<void> _addArticleWithDetails({
  128. required Articolo art,
  129. required double colli,
  130. required double quantita,
  131. required double prezzo,
  132. }) async {
  133. final orderState = ref.read(orderProvider);
  134. final idOrdine = orderState.order?.idOrdine;
  135. if (idOrdine == null || idOrdine == 0) return;
  136. await ref.read(orderProvider.notifier).addLine(RigaOrdine(
  137. idRigaOrdine: 0,
  138. idOrdine: idOrdine,
  139. idArticolo: art.idArticolo,
  140. quantita: quantita,
  141. prezzoLordo: prezzo,
  142. prezzoNetto: prezzo,
  143. totale: prezzo * quantita,
  144. promozione: 0,
  145. rigaPromozione: 0,
  146. colli: colli,
  147. ));
  148. _artCtrl.clear();
  149. setState(() => _searchResults = []);
  150. }
  151. Future<void> _salva() async {
  152. final notifier = ref.read(orderProvider.notifier);
  153. notifier.setIdFiliale(_selectedFiliale);
  154. notifier.setIdListino(_selectedListino);
  155. notifier.setIdPagamento(_selectedPagamento);
  156. notifier.setNoteCliente(_noteClienteCtrl.text);
  157. notifier.setNoteOrdine(_noteOrdineCtrl.text);
  158. notifier.setRiferimento(_riferimentoCtrl.text);
  159. await notifier.saveOrder();
  160. if (context.mounted) Navigator.pop(context);
  161. }
  162. @override
  163. Widget build(BuildContext context) {
  164. final orderState = ref.watch(orderProvider);
  165. final theme = Theme.of(context);
  166. return Scaffold(
  167. appBar: AppBar(
  168. title:
  169. Text('Ordine #${orderState.order?.numeroOrdine ?? 'nuovo'}'),
  170. bottom: TabBar(
  171. controller: _tabController,
  172. tabs: const [
  173. Tab(text: 'Testata'),
  174. Tab(text: 'Righe'),
  175. ],
  176. ),
  177. ),
  178. body: orderState.isLoading || !_dataLoaded
  179. ? const Center(child: CircularProgressIndicator())
  180. : TabBarView(
  181. controller: _tabController,
  182. children: [
  183. _buildHeaderTab(theme),
  184. _buildLinesTab(theme, orderState),
  185. ],
  186. ),
  187. floatingActionButton: _tabController.index == 1 &&
  188. orderState.order?.idOrdine != null
  189. ? FloatingActionButton.extended(
  190. onPressed: _salva,
  191. icon: const Icon(Icons.save),
  192. label: const Text('Salva'),
  193. )
  194. : null,
  195. );
  196. }
  197. Widget _buildHeaderTab(ThemeData theme) {
  198. return SingleChildScrollView(
  199. padding: const EdgeInsets.all(16),
  200. child: Column(
  201. crossAxisAlignment: CrossAxisAlignment.start,
  202. children: [
  203. DropdownButtonFormField<int>(
  204. value: _selectedFiliale,
  205. decoration: const InputDecoration(labelText: 'Filiale'),
  206. items: _filiali
  207. .map((f) => DropdownMenuItem(
  208. value: f.idFiliale,
  209. child: Text(f.descrizione ?? ''),
  210. ))
  211. .toList(),
  212. onChanged: (v) => setState(() => _selectedFiliale = v),
  213. ),
  214. const SizedBox(height: 16),
  215. DropdownButtonFormField<int>(
  216. value: _selectedListino,
  217. decoration: const InputDecoration(labelText: 'Listino'),
  218. items: _listini
  219. .map((l) => DropdownMenuItem(
  220. value: l.idListino,
  221. child: Text(l.descrizione ?? ''),
  222. ))
  223. .toList(),
  224. onChanged: (v) => setState(() => _selectedListino = v),
  225. ),
  226. const SizedBox(height: 16),
  227. DropdownButtonFormField<int>(
  228. value: _selectedPagamento,
  229. decoration: const InputDecoration(labelText: 'Pagamento'),
  230. items: _pagamenti
  231. .map((p) => DropdownMenuItem(
  232. value: p.idPagamento,
  233. child: Text(p.descrizione ?? ''),
  234. ))
  235. .toList(),
  236. onChanged: (v) => setState(() => _selectedPagamento = v),
  237. ),
  238. const SizedBox(height: 16),
  239. TextFormField(
  240. controller: _noteClienteCtrl,
  241. decoration: const InputDecoration(
  242. labelText: 'Note Cliente',
  243. alignLabelWithHint: true,
  244. ),
  245. maxLines: 3,
  246. ),
  247. const SizedBox(height: 16),
  248. TextFormField(
  249. controller: _noteOrdineCtrl,
  250. decoration: const InputDecoration(
  251. labelText: 'Note Ordine',
  252. alignLabelWithHint: true,
  253. ),
  254. maxLines: 3,
  255. ),
  256. const SizedBox(height: 16),
  257. TextFormField(
  258. controller: _riferimentoCtrl,
  259. decoration: const InputDecoration(labelText: 'Riferimento'),
  260. maxLines: 1,
  261. ),
  262. ],
  263. ),
  264. );
  265. }
  266. Widget _buildLinesTab(ThemeData theme, OrderState orderState) {
  267. return Column(
  268. children: [
  269. Padding(
  270. padding: const EdgeInsets.all(12),
  271. child: TextField(
  272. controller: _artCtrl,
  273. decoration: InputDecoration(
  274. hintText: 'Cerca articolo...',
  275. prefixIcon: const Icon(Icons.search),
  276. suffixIcon: _searching
  277. ? const SizedBox(
  278. width: 20,
  279. height: 20,
  280. child:
  281. CircularProgressIndicator(strokeWidth: 2))
  282. : null,
  283. ),
  284. onChanged: _searchArticles,
  285. ),
  286. ),
  287. if (_searchResults.isNotEmpty)
  288. SizedBox(
  289. height: 200,
  290. child: ListView.builder(
  291. itemCount: _searchResults.length,
  292. itemBuilder: (_, i) => ListTile(
  293. leading: ArticleImage(
  294. idArticolo: _searchResults[i].idArticolo,
  295. codArticolo: _searchResults[i].codArticolo),
  296. title: Text(_searchResults[i].descrizione ?? ''),
  297. subtitle: Text(_searchResults[i].codArticolo ?? ''),
  298. trailing: TextButton(
  299. onPressed: () => _showArticlePicker(_searchResults[i]),
  300. child: const Text('Aggiungi')),
  301. ),
  302. ),
  303. ),
  304. Expanded(
  305. child: orderState.lines.isEmpty
  306. ? const Center(child: Text('Nessun articolo'))
  307. : ListView.builder(
  308. itemCount: orderState.lines.length,
  309. itemBuilder: (_, i) => _LineCard(
  310. line: orderState.lines[i],
  311. onDelete: () => ref
  312. .read(orderProvider.notifier)
  313. .removeLine(orderState.lines[i].idRigaOrdine),
  314. theme: theme,
  315. ),
  316. ),
  317. ),
  318. Container(
  319. padding: const EdgeInsets.all(16),
  320. decoration: BoxDecoration(
  321. color: theme.colorScheme.surfaceContainerHighest,
  322. border: Border(
  323. top: BorderSide(color: theme.dividerColor)),
  324. ),
  325. child: Row(
  326. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  327. children: [
  328. Text('Qtà: ${orderState.totQta.toStringAsFixed(1)}'),
  329. Text(
  330. 'Totale: ${NumberFormat.currency(locale: 'it', symbol: '€').format(orderState.totale)}',
  331. style: theme.textTheme.titleMedium
  332. ?.copyWith(fontWeight: FontWeight.bold),
  333. ),
  334. ],
  335. ),
  336. ),
  337. ],
  338. );
  339. }
  340. }
  341. class _LineCard extends StatelessWidget {
  342. final RigaOrdine line;
  343. final VoidCallback onDelete;
  344. final ThemeData theme;
  345. const _LineCard({required this.line, required this.onDelete, required this.theme});
  346. @override
  347. Widget build(BuildContext context) {
  348. final isPromo = line.rigaPromozione == 1;
  349. return Card(
  350. margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
  351. color: isPromo ? Colors.green.shade50 : null,
  352. child: ListTile(
  353. title: Text('Articolo #${line.idArticolo ?? ''}',
  354. style: TextStyle(
  355. fontWeight: isPromo ? FontWeight.bold : null)),
  356. subtitle: Text(
  357. 'Qtà: ${line.quantita?.toStringAsFixed(1) ?? "0"} Prezzo: ${line.prezzoNetto?.toStringAsFixed(2) ?? "0,00"}€'),
  358. trailing: Row(
  359. mainAxisSize: MainAxisSize.min,
  360. children: [
  361. if (isPromo)
  362. Container(
  363. padding: const EdgeInsets.symmetric(
  364. horizontal: 6, vertical: 2),
  365. decoration: BoxDecoration(
  366. color: Colors.green,
  367. borderRadius: BorderRadius.circular(8)),
  368. child: const Text('PROMO',
  369. style: TextStyle(
  370. color: Colors.white, fontSize: 10)),
  371. ),
  372. if (!isPromo)
  373. IconButton(
  374. icon: const Icon(Icons.delete_outline, size: 20),
  375. onPressed: onDelete),
  376. ],
  377. ),
  378. ),
  379. );
  380. }
  381. }
  382. class _ArticlePickerSheet extends StatefulWidget {
  383. final Articolo articolo;
  384. final List<Listino> listini;
  385. final ListiniDao listiniDao;
  386. final bool canEditPrezzo;
  387. final int? idListinoDefault;
  388. final void Function(double colli, double quantita, double prezzo, int listinoId)
  389. onConfirm;
  390. const _ArticlePickerSheet({
  391. required this.articolo,
  392. required this.listini,
  393. required this.listiniDao,
  394. required this.canEditPrezzo,
  395. this.idListinoDefault,
  396. required this.onConfirm,
  397. });
  398. @override
  399. State<_ArticlePickerSheet> createState() => _ArticlePickerSheetState();
  400. }
  401. class _ArticlePickerSheetState extends State<_ArticlePickerSheet> {
  402. final _colliCtrl = TextEditingController();
  403. final _qtaCtrl = TextEditingController();
  404. final _prezzoCtrl = TextEditingController();
  405. int? _selectedListino;
  406. double _prezzo = 0;
  407. bool _updatingColli = false;
  408. bool _updatingQta = false;
  409. @override
  410. void initState() {
  411. super.initState();
  412. _selectedListino = widget.idListinoDefault;
  413. if (_selectedListino != null) {
  414. _loadPrezzo(_selectedListino!);
  415. }
  416. }
  417. @override
  418. void dispose() {
  419. _colliCtrl.dispose();
  420. _qtaCtrl.dispose();
  421. _prezzoCtrl.dispose();
  422. super.dispose();
  423. }
  424. Future<void> _loadPrezzo(int idListino) async {
  425. final lp = await widget.listiniDao.findListinoArticolo(
  426. idListino, widget.articolo.idArticolo);
  427. if (lp?.prezzo != null) {
  428. setState(() => _prezzo = lp!.prezzo!);
  429. _prezzoCtrl.text = _prezzo.toStringAsFixed(2);
  430. }
  431. }
  432. void _onColliChanged(String v) {
  433. if (_updatingColli) return;
  434. _updatingQta = true;
  435. final colli = double.tryParse(v) ?? 0;
  436. final pezzi = widget.articolo.pezziConfezione ?? 0;
  437. if (colli > 0 && pezzi > 0) {
  438. _qtaCtrl.text = (colli * pezzi).toStringAsFixed(1);
  439. } else if (colli == 0) {
  440. _qtaCtrl.text = '';
  441. }
  442. _updatingQta = false;
  443. }
  444. void _onQtaChanged(String v) {
  445. if (_updatingQta) return;
  446. _updatingColli = true;
  447. final qta = double.tryParse(v) ?? 0;
  448. if (qta > 0) {
  449. _colliCtrl.text = '0';
  450. } else if (qta == 0) {
  451. _colliCtrl.text = '';
  452. }
  453. _updatingColli = false;
  454. }
  455. void _confirm() {
  456. final colli = double.tryParse(_colliCtrl.text) ?? 0;
  457. final qta = double.tryParse(_qtaCtrl.text) ?? 0;
  458. final prezzo = double.tryParse(_prezzoCtrl.text) ?? 0;
  459. final listinoId = _selectedListino ?? 0;
  460. widget.onConfirm(colli, qta, prezzo, listinoId);
  461. Navigator.pop(context);
  462. }
  463. @override
  464. Widget build(BuildContext context) {
  465. final theme = Theme.of(context);
  466. return Padding(
  467. padding: EdgeInsets.only(
  468. bottom: MediaQuery.of(context).viewInsets.bottom,
  469. left: 16, right: 16, top: 16,
  470. ),
  471. child: SingleChildScrollView(
  472. child: Column(
  473. mainAxisSize: MainAxisSize.min,
  474. crossAxisAlignment: CrossAxisAlignment.start,
  475. children: [
  476. Text(widget.articolo.descrizione ?? '',
  477. style: theme.textTheme.titleMedium),
  478. const SizedBox(height: 4),
  479. Text(widget.articolo.codArticolo ?? '',
  480. style: theme.textTheme.bodySmall),
  481. if (widget.articolo.pezziConfezione != null)
  482. Text('Pezzi/confezione: ${widget.articolo.pezziConfezione}',
  483. style: theme.textTheme.bodySmall),
  484. const SizedBox(height: 16),
  485. DropdownButtonFormField<int>(
  486. value: _selectedListino,
  487. decoration: const InputDecoration(labelText: 'Listino'),
  488. isExpanded: true,
  489. items: widget.listini
  490. .map((l) => DropdownMenuItem(
  491. value: l.idListino,
  492. child: Text(l.descrizione ?? ''),
  493. ))
  494. .toList(),
  495. onChanged: (v) {
  496. setState(() => _selectedListino = v);
  497. if (v != null) _loadPrezzo(v);
  498. },
  499. ),
  500. const SizedBox(height: 12),
  501. Row(
  502. children: [
  503. Expanded(
  504. child: TextField(
  505. controller: _colliCtrl,
  506. decoration: const InputDecoration(
  507. labelText: 'Colli'),
  508. keyboardType: TextInputType.number,
  509. onChanged: _onColliChanged,
  510. ),
  511. ),
  512. const SizedBox(width: 12),
  513. Expanded(
  514. child: TextField(
  515. controller: _qtaCtrl,
  516. decoration: const InputDecoration(
  517. labelText: 'Quantità'),
  518. keyboardType: TextInputType.number,
  519. onChanged: _onQtaChanged,
  520. ),
  521. ),
  522. ],
  523. ),
  524. const SizedBox(height: 12),
  525. TextField(
  526. controller: _prezzoCtrl,
  527. decoration: const InputDecoration(labelText: 'Prezzo'),
  528. keyboardType:
  529. const TextInputType.numberWithOptions(decimal: true),
  530. enabled: widget.canEditPrezzo,
  531. ),
  532. const SizedBox(height: 20),
  533. SizedBox(
  534. width: double.infinity,
  535. child: ElevatedButton(
  536. onPressed: _confirm,
  537. child: const Text('Aggiungi'),
  538. ),
  539. ),
  540. const SizedBox(height: 8),
  541. ],
  542. ),
  543. ),
  544. );
  545. }
  546. }