selezione_articolo_page.dart 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:ristoris/database/daos/articoli_dao.dart';
  4. import '../../../database/daos/sconti_dao.dart';
  5. import '../../../providers/auth_provider.dart';
  6. import '../../widgets/cod_desc_dropdown_item.dart';
  7. import '../../../providers/database_provider.dart';
  8. import '../../../database/database.dart';
  9. import '../../widgets/article_image.dart';
  10. class ArticleSelectionResult {
  11. final Articolo articolo;
  12. final int idListino;
  13. final double colli;
  14. final double quantita;
  15. final double prezzoLordo;
  16. final double prezzoNetto;
  17. final double sconto1;
  18. final double sconto2;
  19. final double sconto3;
  20. final double sconto4;
  21. final double sconto5;
  22. const ArticleSelectionResult({
  23. required this.articolo,
  24. required this.idListino,
  25. required this.colli,
  26. required this.quantita,
  27. required this.prezzoLordo,
  28. required this.prezzoNetto,
  29. this.sconto1 = 0,
  30. this.sconto2 = 0,
  31. this.sconto3 = 0,
  32. this.sconto4 = 0,
  33. this.sconto5 = 0,
  34. });
  35. }
  36. class SelezioneArticoloPage extends ConsumerStatefulWidget {
  37. final int idCliente;
  38. final String dataOrdine;
  39. final int? idListinoDefault;
  40. const SelezioneArticoloPage({
  41. super.key,
  42. required this.idCliente,
  43. required this.dataOrdine,
  44. this.idListinoDefault,
  45. });
  46. @override
  47. ConsumerState<SelezioneArticoloPage> createState() =>
  48. _SelezioneArticoloPageState();
  49. }
  50. class _SelezioneArticoloPageState extends ConsumerState<SelezioneArticoloPage> {
  51. final _artCtrl = TextEditingController();
  52. List<Articolo> _searchResults = [];
  53. bool _searching = false;
  54. bool _usaListinoCliente = true;
  55. Map<int, Listino> _mapListini = {};
  56. @override
  57. void initState() {
  58. super.initState();
  59. _loadListini();
  60. _searchArticles('');
  61. }
  62. @override
  63. void dispose() {
  64. _artCtrl.dispose();
  65. super.dispose();
  66. }
  67. Future<void> _loadListini() async {
  68. final listini = await ref.read(listiniDaoProvider).listSenzaFiltro();
  69. setState(() {
  70. _mapListini = {for (final l in listini) l.idListino: l};
  71. });
  72. }
  73. Future<void> _searchArticles(String q) async {
  74. setState(() => _searching = true);
  75. final dao = ref.read(articoliDaoProvider);
  76. List<Articolo> results;
  77. if (_usaListinoCliente && widget.idCliente != 0) {
  78. results = await dao.getArticoliPerListinoCliente(
  79. widget.idCliente,
  80. widget.dataOrdine,
  81. );
  82. } else {
  83. results = await dao.getArticoliPerListinoAttivo(widget.dataOrdine, q);
  84. }
  85. if (q.isNotEmpty) {
  86. final lower = q.toLowerCase();
  87. results = results.where((a) {
  88. final cod = (a.codArticolo ?? '').toLowerCase();
  89. final desc = (a.descrizione ?? '').toLowerCase();
  90. final ean = (a.ean ?? '').toLowerCase();
  91. return cod.contains(lower) ||
  92. desc.contains(lower) ||
  93. ean.contains(lower);
  94. }).toList();
  95. }
  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
  105. .read(articoliDaoProvider)
  106. .getListiniArticoloValidi(art.idArticolo, widget.dataOrdine);
  107. /*
  108. final seen = <int>{};
  109. final listini = listiniArticolo
  110. .map((la) => _mapListini[la.idListino])
  111. .whereType<Listino>()
  112. .where((l) => seen.add(l.idListino))
  113. .toList();
  114. */
  115. final scontiDao = ref.read(scontiDaoProvider);
  116. final articoliDao = ref.read(articoliDaoProvider);
  117. if (!context.mounted) return;
  118. final sheetResult = await showModalBottomSheet<ArticleSelectionResult>(
  119. context: context,
  120. isScrollControlled: true,
  121. builder: (_) => _ArticlePickerSheet(
  122. articolo: art,
  123. listini: listini,
  124. articoliDao: articoliDao,
  125. scontiDao: scontiDao,
  126. dataOrdine: widget.dataOrdine,
  127. canEditPrezzo: canEditPrice,
  128. idListinoDefault: widget.idListinoDefault,
  129. onConfirm: (
  130. colli,
  131. quantita,
  132. prezzoLordo,
  133. prezzoNetto,
  134. listinoId, {
  135. double sconto1 = 0,
  136. double sconto2 = 0,
  137. double sconto3 = 0,
  138. double sconto4 = 0,
  139. double sconto5 = 0,
  140. }) {
  141. Navigator.pop(
  142. context,
  143. ArticleSelectionResult(
  144. articolo: art,
  145. idListino: listinoId,
  146. colli: colli,
  147. quantita: quantita,
  148. prezzoLordo: prezzoLordo,
  149. prezzoNetto: prezzoNetto,
  150. sconto1: sconto1,
  151. sconto2: sconto2,
  152. sconto3: sconto3,
  153. sconto4: sconto4,
  154. sconto5: sconto5,
  155. ),
  156. );
  157. },
  158. ),
  159. );
  160. if (sheetResult != null && context.mounted) {
  161. Navigator.pop(context, sheetResult);
  162. }
  163. }
  164. @override
  165. Widget build(BuildContext context) {
  166. return Scaffold(
  167. appBar: AppBar(
  168. title: const Text('Selezione articolo'),
  169. ),
  170. body: SafeArea(
  171. child: Column(
  172. children: [
  173. Padding(
  174. padding: const EdgeInsets.all(12),
  175. child: Row(
  176. children: [
  177. Expanded(
  178. child: TextField(
  179. controller: _artCtrl,
  180. decoration: InputDecoration(
  181. hintText: 'Cerca articolo...',
  182. prefixIcon: const Icon(Icons.search),
  183. suffixIcon: _searching
  184. ? const SizedBox(
  185. width: 20,
  186. height: 20,
  187. child: CircularProgressIndicator(
  188. strokeWidth: 2,
  189. ),
  190. )
  191. : null,
  192. ),
  193. onChanged: _searchArticles,
  194. ),
  195. ),
  196. const SizedBox(width: 8),
  197. FilterChip(
  198. label: const Text('Listino cliente'),
  199. selected: _usaListinoCliente,
  200. onSelected: (v) {
  201. setState(() => _usaListinoCliente = v);
  202. _searchArticles(_artCtrl.text);
  203. },
  204. ),
  205. ],
  206. ),
  207. ),
  208. Expanded(
  209. child: _searchResults.isEmpty
  210. ? Center(
  211. child: Text(
  212. _searching ? 'Ricerca...' : 'Nessun articolo trovato',
  213. ),
  214. )
  215. : ListView.builder(
  216. itemCount: _searchResults.length,
  217. itemBuilder: (_, i) => ListTile(
  218. leading: ArticleImage(
  219. idArticolo: _searchResults[i].idArticolo,
  220. codArticolo: _searchResults[i].codArticolo,
  221. ),
  222. title: Text(_searchResults[i].descrizione ?? ''),
  223. subtitle: Text(_searchResults[i].codArticolo ?? ''),
  224. trailing: TextButton(
  225. onPressed: () =>
  226. _showArticlePicker(_searchResults[i]),
  227. child: const Text('Aggiungi'),
  228. ),
  229. ),
  230. ),
  231. ),
  232. ],
  233. ),
  234. ),
  235. );
  236. }
  237. }
  238. class _ArticlePickerSheet extends StatefulWidget {
  239. final Articolo articolo;
  240. // final List<ListinoArticolo> listiniArticolo;
  241. final List<Listino> listini;
  242. final ArticoliDao articoliDao;
  243. final ScontiDao scontiDao;
  244. final String dataOrdine;
  245. final bool canEditPrezzo;
  246. final int? idListinoDefault;
  247. final void Function(
  248. double colli,
  249. double quantita,
  250. double prezzoLordo,
  251. double prezzoNetto,
  252. int listinoId, {
  253. double sconto1,
  254. double sconto2,
  255. double sconto3,
  256. double sconto4,
  257. double sconto5,
  258. }) onConfirm;
  259. const _ArticlePickerSheet({
  260. required this.articolo,
  261. required this.listini,
  262. required this.articoliDao,
  263. required this.scontiDao,
  264. required this.dataOrdine,
  265. required this.canEditPrezzo,
  266. this.idListinoDefault,
  267. required this.onConfirm,
  268. });
  269. @override
  270. State<_ArticlePickerSheet> createState() => _ArticlePickerSheetState();
  271. }
  272. class _ArticlePickerSheetState extends State<_ArticlePickerSheet> {
  273. final _colliCtrl = TextEditingController();
  274. final _qtaCtrl = TextEditingController();
  275. final _prezzoCtrl = TextEditingController();
  276. final _sconto1Ctrl = TextEditingController();
  277. final _sconto2Ctrl = TextEditingController();
  278. final _sconto3Ctrl = TextEditingController();
  279. final _sconto4Ctrl = TextEditingController();
  280. final _sconto5Ctrl = TextEditingController();
  281. int? _selectedListino;
  282. double _prezzo = 0;
  283. double _prezzoNetto = 0;
  284. double _sconto1 = 0;
  285. double _sconto2 = 0;
  286. double _sconto3 = 0;
  287. double _sconto4 = 0;
  288. double _sconto5 = 0;
  289. bool _updatingColli = false;
  290. bool _updatingQta = false;
  291. @override
  292. void initState() {
  293. super.initState();
  294. _selectedListino = widget.idListinoDefault;
  295. if (_selectedListino != null &&
  296. !widget.listini.any((l) => l.idListino == _selectedListino)) {
  297. _selectedListino = null;
  298. }
  299. if (_selectedListino != null) {
  300. _loadPrezzo(_selectedListino!, widget.articolo.idArticolo);
  301. }
  302. }
  303. @override
  304. void dispose() {
  305. _colliCtrl.dispose();
  306. _qtaCtrl.dispose();
  307. _prezzoCtrl.dispose();
  308. _sconto1Ctrl.dispose();
  309. _sconto2Ctrl.dispose();
  310. _sconto3Ctrl.dispose();
  311. _sconto4Ctrl.dispose();
  312. _sconto5Ctrl.dispose();
  313. super.dispose();
  314. }
  315. void _loadPrezzo(int idListino, int idaArticolo) async {
  316. final lp = await widget.articoliDao
  317. .getListPrice(idListino, idaArticolo, widget.dataOrdine);
  318. /*
  319. final lp = widget.listiniArticolo.cast<ListinoArticolo?>().firstWhere(
  320. (la) => la!.idListino == idListino,
  321. orElse: () => null,
  322. );
  323. */
  324. if (lp != null) {
  325. if (lp.prezzo != null) {
  326. setState(() => _prezzo = lp.prezzo!);
  327. _prezzoCtrl.text = _prezzo.toStringAsFixed(2);
  328. }
  329. if (lp.idSconto != null) {
  330. _loadSconti(lp.idSconto!);
  331. } else {
  332. _resetSconti();
  333. }
  334. _calcolaPrezzoNetto();
  335. }
  336. }
  337. Future<void> _loadSconti(int idSconto) async {
  338. final sconto = await widget.scontiDao.findById(idSconto);
  339. setState(() {
  340. _sconto1 = sconto?.sconto1 ?? 0;
  341. _sconto2 = sconto?.sconto2 ?? 0;
  342. _sconto3 = sconto?.sconto3 ?? 0;
  343. _sconto4 = sconto?.sconto4 ?? 0;
  344. _sconto5 = sconto?.sconto5 ?? 0;
  345. });
  346. _sconto1Ctrl.text = _sconto1 > 0 ? _sconto1.toStringAsFixed(1) : '';
  347. _sconto2Ctrl.text = _sconto2 > 0 ? _sconto2.toStringAsFixed(1) : '';
  348. _sconto3Ctrl.text = _sconto3 > 0 ? _sconto3.toStringAsFixed(1) : '';
  349. _sconto4Ctrl.text = _sconto4 > 0 ? _sconto4.toStringAsFixed(1) : '';
  350. _sconto5Ctrl.text = _sconto5 > 0 ? _sconto5.toStringAsFixed(1) : '';
  351. }
  352. void _resetSconti() {
  353. setState(() {
  354. _sconto1 = 0;
  355. _sconto2 = 0;
  356. _sconto3 = 0;
  357. _sconto4 = 0;
  358. _sconto5 = 0;
  359. });
  360. _sconto1Ctrl.clear();
  361. _sconto2Ctrl.clear();
  362. _sconto3Ctrl.clear();
  363. _sconto4Ctrl.clear();
  364. _sconto5Ctrl.clear();
  365. }
  366. void _calcolaPrezzoNetto() {
  367. double n = _prezzo;
  368. n *= (100 - _sconto1) / 100;
  369. n *= (100 - _sconto2) / 100;
  370. n *= (100 - _sconto3) / 100;
  371. n *= (100 - _sconto4) / 100;
  372. n *= (100 - _sconto5) / 100;
  373. setState(() => _prezzoNetto = n);
  374. }
  375. void _onPrezzoChanged(String v) {
  376. setState(() => _prezzo = double.tryParse(v) ?? 0);
  377. _calcolaPrezzoNetto();
  378. }
  379. void _onScontoChanged(int index, String v) {
  380. final val = double.tryParse(v) ?? 0;
  381. setState(() {
  382. switch (index) {
  383. case 1:
  384. _sconto1 = val;
  385. case 2:
  386. _sconto2 = val;
  387. case 3:
  388. _sconto3 = val;
  389. case 4:
  390. _sconto4 = val;
  391. case 5:
  392. _sconto5 = val;
  393. }
  394. });
  395. _calcolaPrezzoNetto();
  396. }
  397. void _onColliChanged(String v) {
  398. if (_updatingColli) return;
  399. _updatingQta = true;
  400. final colli = double.tryParse(v) ?? 0;
  401. final pezzi = widget.articolo.pezziConfezione ?? 0;
  402. if (colli > 0 && pezzi > 0) {
  403. _qtaCtrl.text = (colli * pezzi).toStringAsFixed(1);
  404. } else if (colli == 0) {
  405. _qtaCtrl.text = '';
  406. }
  407. _updatingQta = false;
  408. }
  409. void _onQtaChanged(String v) {
  410. if (_updatingQta) return;
  411. _updatingColli = true;
  412. final qta = double.tryParse(v) ?? 0;
  413. if (qta > 0) {
  414. _colliCtrl.text = '0';
  415. } else if (qta == 0) {
  416. _colliCtrl.text = '';
  417. }
  418. _updatingColli = false;
  419. }
  420. void _confirm() {
  421. final colli = double.tryParse(_colliCtrl.text) ?? 0;
  422. final qta = double.tryParse(_qtaCtrl.text) ?? 0;
  423. final prezzoLordo = double.tryParse(_prezzoCtrl.text) ?? 0;
  424. final listinoId = _selectedListino ?? 0;
  425. widget.onConfirm(
  426. colli,
  427. qta,
  428. prezzoLordo,
  429. _prezzoNetto,
  430. listinoId,
  431. sconto1: _sconto1,
  432. sconto2: _sconto2,
  433. sconto3: _sconto3,
  434. sconto4: _sconto4,
  435. sconto5: _sconto5,
  436. );
  437. }
  438. @override
  439. Widget build(BuildContext context) {
  440. final theme = Theme.of(context);
  441. return Padding(
  442. padding: EdgeInsets.only(
  443. bottom: MediaQuery.of(context).viewInsets.bottom,
  444. left: 16,
  445. right: 16,
  446. top: 16,
  447. ),
  448. child: SingleChildScrollView(
  449. child: Column(
  450. mainAxisSize: MainAxisSize.min,
  451. crossAxisAlignment: CrossAxisAlignment.start,
  452. children: [
  453. Text(
  454. widget.articolo.descrizione ?? '',
  455. style: theme.textTheme.titleMedium,
  456. ),
  457. const SizedBox(height: 4),
  458. Text(
  459. widget.articolo.codArticolo ?? '',
  460. style: theme.textTheme.bodySmall,
  461. ),
  462. if (widget.articolo.pezziConfezione != null)
  463. Text(
  464. 'Pezzi/confezione: ${widget.articolo.pezziConfezione}',
  465. style: theme.textTheme.bodySmall,
  466. ),
  467. const SizedBox(height: 16),
  468. DropdownButtonFormField<int>(
  469. // initialValue: _selectedListino,
  470. decoration: const InputDecoration(labelText: 'Listino'),
  471. isExpanded: true,
  472. items: widget.listini.map((l) {
  473. print(l);
  474. return DropdownMenuItem(
  475. value: l.idListino,
  476. child: CodDescDropdownItem(
  477. codice: l.codListino,
  478. descrizione: l.descrizione,
  479. ),
  480. );
  481. }).toList(),
  482. onChanged: (v) {
  483. setState(() => _selectedListino = v);
  484. if (v != null) _loadPrezzo(v, widget.articolo.idArticolo);
  485. },
  486. ),
  487. const SizedBox(height: 12),
  488. TextField(
  489. controller: _prezzoCtrl,
  490. decoration: const InputDecoration(labelText: 'Prezzo lordo'),
  491. keyboardType: const TextInputType.numberWithOptions(
  492. decimal: true,
  493. ),
  494. enabled: widget.canEditPrezzo,
  495. onChanged: _onPrezzoChanged,
  496. ),
  497. const SizedBox(height: 12),
  498. Row(
  499. children: [
  500. Expanded(
  501. child: TextField(
  502. controller: _sconto1Ctrl,
  503. decoration: const InputDecoration(labelText: 'Sconto 1 %'),
  504. keyboardType: const TextInputType.numberWithOptions(
  505. decimal: true,
  506. ),
  507. onChanged: (v) => _onScontoChanged(1, v),
  508. ),
  509. ),
  510. const SizedBox(width: 12),
  511. Expanded(
  512. child: TextField(
  513. controller: _sconto2Ctrl,
  514. decoration: const InputDecoration(labelText: 'Sconto 2 %'),
  515. keyboardType: const TextInputType.numberWithOptions(
  516. decimal: true,
  517. ),
  518. onChanged: (v) => _onScontoChanged(2, v),
  519. ),
  520. ),
  521. ],
  522. ),
  523. const SizedBox(height: 12),
  524. Row(
  525. children: [
  526. Expanded(
  527. child: TextField(
  528. controller: _sconto3Ctrl,
  529. decoration: const InputDecoration(labelText: 'Sconto 3 %'),
  530. keyboardType: const TextInputType.numberWithOptions(
  531. decimal: true,
  532. ),
  533. onChanged: (v) => _onScontoChanged(3, v),
  534. ),
  535. ),
  536. const SizedBox(width: 12),
  537. Expanded(
  538. child: TextField(
  539. controller: _sconto4Ctrl,
  540. decoration: const InputDecoration(labelText: 'Sconto 4 %'),
  541. keyboardType: const TextInputType.numberWithOptions(
  542. decimal: true,
  543. ),
  544. onChanged: (v) => _onScontoChanged(4, v),
  545. ),
  546. ),
  547. ],
  548. ),
  549. const SizedBox(height: 12),
  550. SizedBox(
  551. width: 140,
  552. child: TextField(
  553. controller: _sconto5Ctrl,
  554. decoration: const InputDecoration(labelText: 'Sconto 5 %'),
  555. keyboardType: const TextInputType.numberWithOptions(
  556. decimal: true,
  557. ),
  558. onChanged: (v) => _onScontoChanged(5, v),
  559. ),
  560. ),
  561. const SizedBox(height: 16),
  562. Row(
  563. children: [
  564. Text('Prezzo netto: ', style: theme.textTheme.bodyLarge),
  565. Text(
  566. _prezzoNetto.toStringAsFixed(2),
  567. style: theme.textTheme.bodyLarge?.copyWith(
  568. fontWeight: FontWeight.bold,
  569. ),
  570. ),
  571. ],
  572. ),
  573. const SizedBox(height: 12),
  574. Row(
  575. children: [
  576. Expanded(
  577. child: TextField(
  578. controller: _colliCtrl,
  579. decoration: const InputDecoration(labelText: 'Colli'),
  580. keyboardType: TextInputType.number,
  581. onChanged: _onColliChanged,
  582. ),
  583. ),
  584. const SizedBox(width: 12),
  585. Expanded(
  586. child: TextField(
  587. controller: _qtaCtrl,
  588. decoration: const InputDecoration(labelText: 'Quantità'),
  589. keyboardType: TextInputType.number,
  590. onChanged: _onQtaChanged,
  591. ),
  592. ),
  593. ],
  594. ),
  595. const SizedBox(height: 20),
  596. SizedBox(
  597. width: double.infinity,
  598. child: ElevatedButton(
  599. onPressed: _confirm,
  600. child: const Text('Aggiungi'),
  601. ),
  602. ),
  603. const SizedBox(height: 8),
  604. ],
  605. ),
  606. ),
  607. );
  608. }
  609. }