order_edit_page.dart 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:intl/intl.dart';
  4. import 'package:ristoris/core/credit_score_color.dart';
  5. import '../../../database/daos/sconti_dao.dart';
  6. import '../../../providers/auth_provider.dart';
  7. import '../../widgets/cod_desc_dropdown_item.dart';
  8. import '../../../providers/order_provider.dart';
  9. import '../../../providers/database_provider.dart';
  10. import '../../../database/database.dart';
  11. import '../../../database/daos/listini_dao.dart';
  12. import '../../widgets/article_image.dart';
  13. import 'selezione_cliente_page.dart';
  14. class OrderEditPage extends ConsumerStatefulWidget {
  15. final int? id;
  16. final int? idCliente;
  17. const OrderEditPage({super.key, this.id, this.idCliente});
  18. @override
  19. ConsumerState<OrderEditPage> createState() => _OrderEditPageState();
  20. }
  21. class _OrderEditPageState extends ConsumerState<OrderEditPage>
  22. with SingleTickerProviderStateMixin {
  23. late TabController _tabController;
  24. final _artCtrl = TextEditingController();
  25. final _noteClienteCtrl = TextEditingController();
  26. final _noteOrdineCtrl = TextEditingController();
  27. final _riferimentoCtrl = TextEditingController();
  28. List<Articolo> _searchResults = [];
  29. bool _searching = false;
  30. bool _usaListinoCliente = true;
  31. List<Filiale> _filiali = [];
  32. List<Listino> _listini = [];
  33. List<Pagamento> _pagamenti = [];
  34. Cliente? _cliente;
  35. int? _selectedFiliale;
  36. int? _selectedListino;
  37. int? _selectedPagamento;
  38. bool _dataLoaded = false;
  39. @override
  40. void initState() {
  41. super.initState();
  42. _tabController = TabController(length: 2, vsync: this);
  43. _tabController.addListener(() {
  44. if (_tabController.index == 1 && _searchResults.isEmpty && _artCtrl.text.isEmpty) {
  45. _searchArticles('');
  46. }
  47. });
  48. WidgetsBinding.instance.addPostFrameCallback((_) => _init());
  49. }
  50. @override
  51. void dispose() {
  52. _tabController.dispose();
  53. _artCtrl.dispose();
  54. _noteClienteCtrl.dispose();
  55. _noteOrdineCtrl.dispose();
  56. _riferimentoCtrl.dispose();
  57. super.dispose();
  58. }
  59. Future<void> _init() async {
  60. final agente = ref.read(authProvider).agente;
  61. if (agente == null) return;
  62. if (widget.id != null) {
  63. await ref.read(orderProvider.notifier).loadOrder(widget.id!);
  64. } else if (widget.idCliente != null) {
  65. await ref
  66. .read(orderProvider.notifier)
  67. .createOrder(agente.idAgente, idCliente: widget.idCliente!);
  68. } else {
  69. await ref.read(orderProvider.notifier).createOrder(agente.idAgente);
  70. }
  71. final orderState = ref.read(orderProvider);
  72. final order = orderState.order;
  73. if (order == null) return;
  74. final cli = order.idCliente;
  75. final clientiDao = ref.read(clientiDaoProvider);
  76. final filialiDao = ref.read(filialiDaoProvider);
  77. final listiniDao = ref.read(listiniDaoProvider);
  78. final pagamentiDao = ref.read(pagamentiDaoProvider);
  79. if (cli != null) {
  80. _cliente = await clientiDao.getById(cli);
  81. _filiali = await filialiDao.getDestinationsForCustomer(cli);
  82. }
  83. _listini = await listiniDao.list();
  84. _pagamenti = await pagamentiDao.searchActive();
  85. setState(() {
  86. _selectedFiliale = order.idFiliale;
  87. _selectedListino = order.idListino ?? _cliente?.idListino;
  88. _selectedPagamento = order.idPagamento ?? _cliente?.idPagamento;
  89. _noteClienteCtrl.text = order.noteCliente ?? _cliente?.noteOrdine ?? '';
  90. _noteOrdineCtrl.text = order.noteOrdine ?? '';
  91. _riferimentoCtrl.text = order.riferimento ?? '';
  92. _dataLoaded = true;
  93. });
  94. }
  95. Future<void> _searchArticles(String q) async {
  96. setState(() => _searching = true);
  97. final dao = ref.read(articoliDaoProvider);
  98. final order = ref.read(orderProvider).order;
  99. final dataOrdine = order?.dataOrdine ?? DateTime.now().toIso8601String();
  100. List<Articolo> results;
  101. if (_usaListinoCliente && _cliente != null) {
  102. results = await dao.getArticoliPerListinoCliente(_cliente!.idCliente, dataOrdine);
  103. } else {
  104. results = await dao.getArticoliPerListinoAttivo(dataOrdine);
  105. }
  106. if (q.isNotEmpty) {
  107. final lower = q.toLowerCase();
  108. results = results.where((a) {
  109. final cod = (a.codArticolo ?? '').toLowerCase();
  110. final desc = (a.descrizione ?? '').toLowerCase();
  111. final ean = (a.ean ?? '').toLowerCase();
  112. return cod.contains(lower) || desc.contains(lower) || ean.contains(lower);
  113. }).toList();
  114. }
  115. setState(() {
  116. _searchResults = results;
  117. _searching = false;
  118. });
  119. }
  120. void _showArticlePicker(Articolo art) async {
  121. final agente = ref.read(authProvider).agente;
  122. final canEditPrice = agente?.modificaPrezzi == 1;
  123. final order = ref.read(orderProvider).order;
  124. final dataOrdine = order?.dataOrdine ?? DateTime.now().toIso8601String();
  125. final listini = await ref
  126. .read(listiniDaoProvider)
  127. .getListiniPerArticolo(art.idArticolo, dataOrdine);
  128. final listiniDao = ref.read(listiniDaoProvider);
  129. final scontiDao = ref.read(scontiDaoProvider);
  130. if (!context.mounted) return;
  131. showModalBottomSheet(
  132. context: context,
  133. isScrollControlled: true,
  134. builder: (_) => _ArticlePickerSheet(
  135. articolo: art,
  136. listini: listini,
  137. listiniDao: listiniDao,
  138. scontiDao: scontiDao,
  139. dataOrdine: dataOrdine,
  140. canEditPrezzo: canEditPrice,
  141. idListinoDefault: _selectedListino,
  142. onConfirm: (
  143. colli,
  144. quantita,
  145. prezzoLordo,
  146. prezzoNetto,
  147. listinoId, {
  148. double sconto1 = 0,
  149. double sconto2 = 0,
  150. double sconto3 = 0,
  151. double sconto4 = 0,
  152. double sconto5 = 0,
  153. }) {
  154. _addArticleWithDetails(
  155. art: art,
  156. idListino: listinoId,
  157. colli: colli,
  158. quantita: quantita,
  159. prezzoLordo: prezzoLordo,
  160. prezzoNetto: prezzoNetto,
  161. sconto1: sconto1,
  162. sconto2: sconto2,
  163. sconto3: sconto3,
  164. sconto4: sconto4,
  165. sconto5: sconto5,
  166. );
  167. },
  168. ),
  169. );
  170. }
  171. Future<void> _addArticleWithDetails({
  172. required Articolo art,
  173. required int idListino,
  174. required double colli,
  175. required double quantita,
  176. required double prezzoLordo,
  177. required double prezzoNetto,
  178. double? sconto1 = 0,
  179. double? sconto2 = 0,
  180. double? sconto3 = 0,
  181. double? sconto4 = 0,
  182. double? sconto5 = 0,
  183. }) async {
  184. final orderState = ref.read(orderProvider);
  185. var idOrdine = orderState.order?.idOrdine;
  186. if (idOrdine == null || idOrdine == 0) {
  187. await ref.read(orderProvider.notifier).saveOrder();
  188. idOrdine = ref.read(orderProvider).order?.idOrdine;
  189. if (idOrdine == null || idOrdine == 0) return;
  190. }
  191. await ref
  192. .read(orderProvider.notifier)
  193. .addLine(
  194. RigaOrdine(
  195. idRigaOrdine: 0,
  196. idOrdine: idOrdine,
  197. idArticolo: art.idArticolo,
  198. idListino: idListino,
  199. quantita: quantita,
  200. prezzoLordo: prezzoLordo,
  201. prezzoNetto: prezzoNetto,
  202. sconto1: sconto1,
  203. sconto2: sconto2,
  204. sconto3: sconto3,
  205. sconto4: sconto4,
  206. sconto5: sconto5,
  207. totale: prezzoNetto * quantita,
  208. promozione: 0,
  209. rigaPromozione: 0,
  210. colli: colli,
  211. ),
  212. );
  213. _artCtrl.clear();
  214. setState(() => _searchResults = []);
  215. }
  216. Future<void> _selezionaCliente() async {
  217. final agente = ref.read(authProvider).agente;
  218. if (agente == null) return;
  219. final cliente = await Navigator.push<Cliente>(
  220. context,
  221. MaterialPageRoute(
  222. builder: (_) => SelezioneClientePage(idAgente: agente.idAgente),
  223. ),
  224. );
  225. if (cliente != null) _onClienteSelezionato(cliente);
  226. }
  227. void _onClienteSelezionato(Cliente cliente) async {
  228. ref.read(orderProvider.notifier).setIdCliente(cliente.idCliente);
  229. final filialiDao = ref.read(filialiDaoProvider);
  230. final filiali = await filialiDao.getDestinationsForCustomer(
  231. cliente.idCliente,
  232. );
  233. setState(() {
  234. _cliente = cliente;
  235. _selectedFiliale = null;
  236. _selectedPagamento = cliente.idPagamento;
  237. _selectedListino = cliente.idListino;
  238. _filiali = filiali;
  239. _noteClienteCtrl.text = cliente.noteOrdine ?? '';
  240. });
  241. }
  242. Future<void> _salva() async {
  243. final notifier = ref.read(orderProvider.notifier);
  244. notifier.setIdFiliale(_selectedFiliale);
  245. notifier.setIdListino(_selectedListino);
  246. notifier.setIdPagamento(_selectedPagamento);
  247. notifier.setNoteCliente(_noteClienteCtrl.text);
  248. notifier.setNoteOrdine(_noteOrdineCtrl.text);
  249. notifier.setRiferimento(_riferimentoCtrl.text);
  250. await notifier.saveOrder();
  251. if (context.mounted) Navigator.pop(context);
  252. }
  253. @override
  254. Widget build(BuildContext context) {
  255. final orderState = ref.watch(orderProvider);
  256. final theme = Theme.of(context);
  257. return Scaffold(
  258. appBar: AppBar(
  259. title: Text('Ordine #${orderState.order?.numeroOrdine ?? 'nuovo'}'),
  260. bottom: TabBar(
  261. controller: _tabController,
  262. tabs: const [
  263. Tab(text: 'Testata'),
  264. Tab(text: 'Righe'),
  265. ],
  266. ),
  267. ),
  268. body: SafeArea(
  269. child: orderState.isLoading || !_dataLoaded
  270. ? const Center(child: CircularProgressIndicator())
  271. : TabBarView(
  272. controller: _tabController,
  273. children: [
  274. _buildHeaderTab(theme),
  275. _buildLinesTab(theme, orderState),
  276. ],
  277. ),
  278. ),
  279. floatingActionButton:
  280. _tabController.index == 1 && orderState.order?.idOrdine != null
  281. ? FloatingActionButton.extended(
  282. onPressed: _salva,
  283. icon: const Icon(Icons.save),
  284. label: const Text('Salva'),
  285. )
  286. : null,
  287. );
  288. }
  289. Widget _buildHeaderTab(ThemeData theme) {
  290. final cs = theme.colorScheme;
  291. final bgColor = creditScoreBg(_cliente?.creditScoreColor);
  292. return SingleChildScrollView(
  293. padding: const EdgeInsets.all(16),
  294. child: Column(
  295. crossAxisAlignment: CrossAxisAlignment.start,
  296. children: [
  297. Card(
  298. elevation: 0,
  299. color: Colors.white,
  300. shape: RoundedRectangleBorder(
  301. borderRadius: BorderRadius.circular(12),
  302. side: BorderSide(color: cs.outlineVariant),
  303. ),
  304. child: Padding(
  305. padding: const EdgeInsets.all(12),
  306. child: Row(
  307. children: [
  308. Expanded(
  309. child: Column(
  310. crossAxisAlignment: CrossAxisAlignment.start,
  311. children: [
  312. Text(
  313. 'Cliente',
  314. style: TextStyle(
  315. fontSize: 12,
  316. fontWeight: FontWeight.w600,
  317. color: cs.onSurfaceVariant,
  318. ),
  319. ),
  320. const SizedBox(height: 4),
  321. if (_cliente != null)
  322. Row(
  323. children: [
  324. Container(
  325. padding: const EdgeInsets.symmetric(
  326. horizontal: 8,
  327. vertical: 4,
  328. ),
  329. decoration: BoxDecoration(
  330. color: bgColor ?? cs.surfaceVariant,
  331. borderRadius: BorderRadius.circular(4),
  332. ),
  333. child: Text(
  334. _cliente!.codCliente ?? '',
  335. style: TextStyle(
  336. fontSize: 12,
  337. height: 16 / 12,
  338. fontWeight: FontWeight.w500,
  339. color: bgColor != null
  340. ? Colors.white
  341. : cs.onSurfaceVariant,
  342. ),
  343. ),
  344. ),
  345. const SizedBox(width: 8),
  346. Expanded(
  347. child: Text(
  348. _cliente!.ragioneSociale ?? '',
  349. style: theme.textTheme.bodyMedium?.copyWith(
  350. fontWeight: FontWeight.w600,
  351. ),
  352. maxLines: 1,
  353. overflow: TextOverflow.ellipsis,
  354. ),
  355. ),
  356. ],
  357. )
  358. else
  359. Text(
  360. 'Nessun cliente selezionato',
  361. style: theme.textTheme.bodyMedium?.copyWith(
  362. color: cs.onSurfaceVariant,
  363. ),
  364. ),
  365. ],
  366. ),
  367. ),
  368. IconButton(
  369. icon: Icon(Icons.search, color: cs.primary),
  370. onPressed: _selezionaCliente,
  371. ),
  372. ],
  373. ),
  374. ),
  375. ),
  376. const SizedBox(height: 16),
  377. DropdownButtonFormField<int>(
  378. value: _selectedFiliale,
  379. decoration: const InputDecoration(labelText: 'Filiale'),
  380. items: _filiali
  381. .map(
  382. (f) => DropdownMenuItem(
  383. value: f.idFiliale,
  384. child: CodDescDropdownItem(
  385. codice: f.codFiliale,
  386. descrizione: f.descrizione,
  387. ),
  388. ),
  389. )
  390. .toList(),
  391. onChanged: (v) => setState(() => _selectedFiliale = v),
  392. ),
  393. const SizedBox(height: 16),
  394. DropdownButtonFormField<int>(
  395. initialValue: _selectedListino,
  396. decoration: const InputDecoration(labelText: 'Listino'),
  397. items: _listini
  398. .map(
  399. (l) => DropdownMenuItem(
  400. value: l.idListino,
  401. child: CodDescDropdownItem(
  402. codice: l.codListino,
  403. descrizione: l.descrizione,
  404. ),
  405. ),
  406. )
  407. .toList(),
  408. onChanged: (v) => setState(() => _selectedListino = v),
  409. ),
  410. const SizedBox(height: 16),
  411. DropdownButtonFormField<int>(
  412. initialValue: _selectedPagamento,
  413. decoration: const InputDecoration(labelText: 'Pagamento'),
  414. items: _pagamenti
  415. .map(
  416. (p) => DropdownMenuItem(
  417. value: p.idPagamento,
  418. child: CodDescDropdownItem(
  419. codice: p.codPagamento,
  420. descrizione: p.descrizione,
  421. ),
  422. ),
  423. )
  424. .toList(),
  425. onChanged: (v) => setState(() => _selectedPagamento = v),
  426. ),
  427. const SizedBox(height: 16),
  428. TextFormField(
  429. controller: _noteClienteCtrl,
  430. decoration: const InputDecoration(
  431. labelText: 'Note Cliente',
  432. alignLabelWithHint: true,
  433. ),
  434. maxLines: 3,
  435. ),
  436. const SizedBox(height: 16),
  437. TextFormField(
  438. controller: _noteOrdineCtrl,
  439. decoration: const InputDecoration(
  440. labelText: 'Note Ordine',
  441. alignLabelWithHint: true,
  442. ),
  443. maxLines: 3,
  444. ),
  445. const SizedBox(height: 16),
  446. TextFormField(
  447. controller: _riferimentoCtrl,
  448. decoration: const InputDecoration(labelText: 'Riferimento'),
  449. maxLines: 1,
  450. ),
  451. ],
  452. ),
  453. );
  454. }
  455. Widget _buildLinesTab(ThemeData theme, OrderState orderState) {
  456. return Column(
  457. children: [
  458. Padding(
  459. padding: const EdgeInsets.all(12),
  460. child: Row(
  461. children: [
  462. Expanded(
  463. child: TextField(
  464. controller: _artCtrl,
  465. decoration: InputDecoration(
  466. hintText: 'Cerca articolo...',
  467. prefixIcon: const Icon(Icons.search),
  468. suffixIcon: _searching
  469. ? const SizedBox(
  470. width: 20,
  471. height: 20,
  472. child: CircularProgressIndicator(strokeWidth: 2),
  473. )
  474. : null,
  475. ),
  476. onChanged: _searchArticles,
  477. ),
  478. ),
  479. const SizedBox(width: 8),
  480. FilterChip(
  481. label: const Text('Listino cliente'),
  482. selected: _usaListinoCliente,
  483. onSelected: (v) {
  484. setState(() => _usaListinoCliente = v);
  485. _searchArticles(_artCtrl.text);
  486. },
  487. ),
  488. ],
  489. ),
  490. ),
  491. if (_searchResults.isNotEmpty)
  492. SizedBox(
  493. height: 200,
  494. child: ListView.builder(
  495. itemCount: _searchResults.length,
  496. itemBuilder: (_, i) => ListTile(
  497. leading: ArticleImage(
  498. idArticolo: _searchResults[i].idArticolo,
  499. codArticolo: _searchResults[i].codArticolo,
  500. ),
  501. title: Text(_searchResults[i].descrizione ?? ''),
  502. subtitle: Text(_searchResults[i].codArticolo ?? ''),
  503. trailing: TextButton(
  504. onPressed: () => _showArticlePicker(_searchResults[i]),
  505. child: const Text('Aggiungi'),
  506. ),
  507. ),
  508. ),
  509. ),
  510. Expanded(
  511. child: orderState.lines.isEmpty
  512. ? const Center(child: Text('Nessun articolo'))
  513. : ListView.builder(
  514. itemCount: orderState.lines.length,
  515. itemBuilder: (_, i) => _LineCard(
  516. line: orderState.lines[i],
  517. onDelete: () => ref
  518. .read(orderProvider.notifier)
  519. .removeLine(orderState.lines[i].idRigaOrdine),
  520. theme: theme,
  521. ),
  522. ),
  523. ),
  524. Container(
  525. padding: const EdgeInsets.all(16),
  526. decoration: BoxDecoration(
  527. color: theme.colorScheme.surfaceContainerHighest,
  528. border: Border(top: BorderSide(color: theme.dividerColor)),
  529. ),
  530. child: Row(
  531. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  532. children: [
  533. Text('Qtà: ${orderState.totQta.toStringAsFixed(1)}'),
  534. Text(
  535. 'Totale: ${NumberFormat.currency(locale: 'it', symbol: '€').format(orderState.totale)}',
  536. style: theme.textTheme.titleMedium?.copyWith(
  537. fontWeight: FontWeight.bold,
  538. ),
  539. ),
  540. ],
  541. ),
  542. ),
  543. ],
  544. );
  545. }
  546. }
  547. class _LineCard extends StatelessWidget {
  548. final RigaOrdine line;
  549. final VoidCallback onDelete;
  550. final ThemeData theme;
  551. const _LineCard({
  552. required this.line,
  553. required this.onDelete,
  554. required this.theme,
  555. });
  556. @override
  557. Widget build(BuildContext context) {
  558. final isPromo = line.rigaPromozione == 1;
  559. return Card(
  560. margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
  561. color: isPromo ? Colors.green.shade50 : null,
  562. child: ListTile(
  563. title: Text(
  564. 'Articolo #${line.idArticolo ?? ''}',
  565. style: TextStyle(fontWeight: isPromo ? FontWeight.bold : null),
  566. ),
  567. subtitle: Text(
  568. 'Qtà: ${line.quantita?.toStringAsFixed(1) ?? "0"} Prezzo: ${line.prezzoNetto?.toStringAsFixed(2) ?? "0,00"}€',
  569. ),
  570. trailing: Row(
  571. mainAxisSize: MainAxisSize.min,
  572. children: [
  573. if (isPromo)
  574. Container(
  575. padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
  576. decoration: BoxDecoration(
  577. color: Colors.green,
  578. borderRadius: BorderRadius.circular(8),
  579. ),
  580. child: const Text(
  581. 'PROMO',
  582. style: TextStyle(color: Colors.white, fontSize: 10),
  583. ),
  584. ),
  585. if (!isPromo)
  586. IconButton(
  587. icon: const Icon(Icons.delete_outline, size: 20),
  588. onPressed: onDelete,
  589. ),
  590. ],
  591. ),
  592. ),
  593. );
  594. }
  595. }
  596. class _ArticlePickerSheet extends StatefulWidget {
  597. final Articolo articolo;
  598. final List<Listino> listini;
  599. final ListiniDao listiniDao;
  600. final ScontiDao scontiDao;
  601. final String dataOrdine;
  602. final bool canEditPrezzo;
  603. final int? idListinoDefault;
  604. final void Function(
  605. double colli,
  606. double quantita,
  607. double prezzoLordo,
  608. double prezzoNetto,
  609. int listinoId, {
  610. double sconto1,
  611. double sconto2,
  612. double sconto3,
  613. double sconto4,
  614. double sconto5,
  615. })
  616. onConfirm;
  617. const _ArticlePickerSheet({
  618. required this.articolo,
  619. required this.listini,
  620. required this.listiniDao,
  621. required this.scontiDao,
  622. required this.dataOrdine,
  623. required this.canEditPrezzo,
  624. this.idListinoDefault,
  625. required this.onConfirm,
  626. });
  627. @override
  628. State<_ArticlePickerSheet> createState() => _ArticlePickerSheetState();
  629. }
  630. class _ArticlePickerSheetState extends State<_ArticlePickerSheet> {
  631. final _colliCtrl = TextEditingController();
  632. final _qtaCtrl = TextEditingController();
  633. final _prezzoCtrl = TextEditingController();
  634. final _sconto1Ctrl = TextEditingController();
  635. final _sconto2Ctrl = TextEditingController();
  636. final _sconto3Ctrl = TextEditingController();
  637. final _sconto4Ctrl = TextEditingController();
  638. final _sconto5Ctrl = TextEditingController();
  639. int? _selectedListino;
  640. double _prezzo = 0;
  641. double _prezzoNetto = 0;
  642. double _sconto1 = 0;
  643. double _sconto2 = 0;
  644. double _sconto3 = 0;
  645. double _sconto4 = 0;
  646. double _sconto5 = 0;
  647. bool _updatingColli = false;
  648. bool _updatingQta = false;
  649. @override
  650. void initState() {
  651. super.initState();
  652. _selectedListino = widget.idListinoDefault;
  653. if (_selectedListino != null) {
  654. _loadPrezzo(_selectedListino!);
  655. }
  656. }
  657. @override
  658. void dispose() {
  659. _colliCtrl.dispose();
  660. _qtaCtrl.dispose();
  661. _prezzoCtrl.dispose();
  662. _sconto1Ctrl.dispose();
  663. _sconto2Ctrl.dispose();
  664. _sconto3Ctrl.dispose();
  665. _sconto4Ctrl.dispose();
  666. _sconto5Ctrl.dispose();
  667. super.dispose();
  668. }
  669. Future<void> _loadPrezzo(int idListino) async {
  670. final lp = await widget.listiniDao.findListinoArticolo(
  671. idListino,
  672. widget.articolo.idArticolo,
  673. );
  674. if (lp != null) {
  675. if (lp.prezzo != null) {
  676. setState(() => _prezzo = lp.prezzo!);
  677. _prezzoCtrl.text = _prezzo.toStringAsFixed(2);
  678. }
  679. if (lp.idSconto != null) {
  680. final sconto = await widget.scontiDao.findById(lp.idSconto!);
  681. setState(() {
  682. _sconto1 = sconto?.sconto1 ?? 0;
  683. _sconto2 = sconto?.sconto2 ?? 0;
  684. _sconto3 = sconto?.sconto3 ?? 0;
  685. _sconto4 = sconto?.sconto4 ?? 0;
  686. _sconto5 = sconto?.sconto5 ?? 0;
  687. });
  688. _sconto1Ctrl.text = _sconto1 > 0 ? _sconto1.toStringAsFixed(1) : '';
  689. _sconto2Ctrl.text = _sconto2 > 0 ? _sconto2.toStringAsFixed(1) : '';
  690. _sconto3Ctrl.text = _sconto3 > 0 ? _sconto3.toStringAsFixed(1) : '';
  691. _sconto4Ctrl.text = _sconto4 > 0 ? _sconto4.toStringAsFixed(1) : '';
  692. _sconto5Ctrl.text = _sconto5 > 0 ? _sconto5.toStringAsFixed(1) : '';
  693. } else {
  694. _resetSconti();
  695. }
  696. _calcolaPrezzoNetto();
  697. }
  698. }
  699. void _resetSconti() {
  700. setState(() {
  701. _sconto1 = 0;
  702. _sconto2 = 0;
  703. _sconto3 = 0;
  704. _sconto4 = 0;
  705. _sconto5 = 0;
  706. });
  707. _sconto1Ctrl.clear();
  708. _sconto2Ctrl.clear();
  709. _sconto3Ctrl.clear();
  710. _sconto4Ctrl.clear();
  711. _sconto5Ctrl.clear();
  712. }
  713. void _calcolaPrezzoNetto() {
  714. double n = _prezzo;
  715. n *= (100 - _sconto1) / 100;
  716. n *= (100 - _sconto2) / 100;
  717. n *= (100 - _sconto3) / 100;
  718. n *= (100 - _sconto4) / 100;
  719. n *= (100 - _sconto5) / 100;
  720. setState(() => _prezzoNetto = n);
  721. }
  722. void _onPrezzoChanged(String v) {
  723. setState(() => _prezzo = double.tryParse(v) ?? 0);
  724. _calcolaPrezzoNetto();
  725. }
  726. void _onScontoChanged(int index, String v) {
  727. final val = double.tryParse(v) ?? 0;
  728. setState(() {
  729. switch (index) {
  730. case 1:
  731. _sconto1 = val;
  732. case 2:
  733. _sconto2 = val;
  734. case 3:
  735. _sconto3 = val;
  736. case 4:
  737. _sconto4 = val;
  738. case 5:
  739. _sconto5 = val;
  740. }
  741. });
  742. _calcolaPrezzoNetto();
  743. }
  744. void _onColliChanged(String v) {
  745. if (_updatingColli) return;
  746. _updatingQta = true;
  747. final colli = double.tryParse(v) ?? 0;
  748. final pezzi = widget.articolo.pezziConfezione ?? 0;
  749. if (colli > 0 && pezzi > 0) {
  750. _qtaCtrl.text = (colli * pezzi).toStringAsFixed(1);
  751. } else if (colli == 0) {
  752. _qtaCtrl.text = '';
  753. }
  754. _updatingQta = false;
  755. }
  756. void _onQtaChanged(String v) {
  757. if (_updatingQta) return;
  758. _updatingColli = true;
  759. final qta = double.tryParse(v) ?? 0;
  760. if (qta > 0) {
  761. _colliCtrl.text = '0';
  762. } else if (qta == 0) {
  763. _colliCtrl.text = '';
  764. }
  765. _updatingColli = false;
  766. }
  767. void _confirm() {
  768. final colli = double.tryParse(_colliCtrl.text) ?? 0;
  769. final qta = double.tryParse(_qtaCtrl.text) ?? 0;
  770. final prezzoLordo = double.tryParse(_prezzoCtrl.text) ?? 0;
  771. final listinoId = _selectedListino ?? 0;
  772. widget.onConfirm(
  773. colli,
  774. qta,
  775. prezzoLordo,
  776. _prezzoNetto,
  777. listinoId,
  778. sconto1: _sconto1,
  779. sconto2: _sconto2,
  780. sconto3: _sconto3,
  781. sconto4: _sconto4,
  782. sconto5: _sconto5,
  783. );
  784. Navigator.pop(context);
  785. }
  786. @override
  787. Widget build(BuildContext context) {
  788. final theme = Theme.of(context);
  789. return Padding(
  790. padding: EdgeInsets.only(
  791. bottom: MediaQuery.of(context).viewInsets.bottom,
  792. left: 16,
  793. right: 16,
  794. top: 16,
  795. ),
  796. child: SingleChildScrollView(
  797. child: Column(
  798. mainAxisSize: MainAxisSize.min,
  799. crossAxisAlignment: CrossAxisAlignment.start,
  800. children: [
  801. Text(
  802. widget.articolo.descrizione ?? '',
  803. style: theme.textTheme.titleMedium,
  804. ),
  805. const SizedBox(height: 4),
  806. Text(
  807. widget.articolo.codArticolo ?? '',
  808. style: theme.textTheme.bodySmall,
  809. ),
  810. if (widget.articolo.pezziConfezione != null)
  811. Text(
  812. 'Pezzi/confezione: ${widget.articolo.pezziConfezione}',
  813. style: theme.textTheme.bodySmall,
  814. ),
  815. const SizedBox(height: 16),
  816. DropdownButtonFormField<int>(
  817. value: _selectedListino,
  818. decoration: const InputDecoration(labelText: 'Listino'),
  819. isExpanded: true,
  820. items: widget.listini
  821. .map(
  822. (l) => DropdownMenuItem(
  823. value: l.idListino,
  824. child: CodDescDropdownItem(
  825. codice: l.codListino,
  826. descrizione: l.descrizione,
  827. ),
  828. ),
  829. )
  830. .toList(),
  831. onChanged: (v) {
  832. setState(() => _selectedListino = v);
  833. if (v != null) _loadPrezzo(v);
  834. },
  835. ),
  836. const SizedBox(height: 12),
  837. TextField(
  838. controller: _prezzoCtrl,
  839. decoration: const InputDecoration(labelText: 'Prezzo lordo'),
  840. keyboardType: const TextInputType.numberWithOptions(
  841. decimal: true,
  842. ),
  843. enabled: widget.canEditPrezzo,
  844. onChanged: _onPrezzoChanged,
  845. ),
  846. const SizedBox(height: 12),
  847. Row(
  848. children: [
  849. Expanded(
  850. child: TextField(
  851. controller: _sconto1Ctrl,
  852. decoration: const InputDecoration(labelText: 'Sconto 1 %'),
  853. keyboardType: const TextInputType.numberWithOptions(
  854. decimal: true,
  855. ),
  856. onChanged: (v) => _onScontoChanged(1, v),
  857. ),
  858. ),
  859. const SizedBox(width: 12),
  860. Expanded(
  861. child: TextField(
  862. controller: _sconto2Ctrl,
  863. decoration: const InputDecoration(labelText: 'Sconto 2 %'),
  864. keyboardType: const TextInputType.numberWithOptions(
  865. decimal: true,
  866. ),
  867. onChanged: (v) => _onScontoChanged(2, v),
  868. ),
  869. ),
  870. ],
  871. ),
  872. const SizedBox(height: 12),
  873. Row(
  874. children: [
  875. Expanded(
  876. child: TextField(
  877. controller: _sconto3Ctrl,
  878. decoration: const InputDecoration(labelText: 'Sconto 3 %'),
  879. keyboardType: const TextInputType.numberWithOptions(
  880. decimal: true,
  881. ),
  882. onChanged: (v) => _onScontoChanged(3, v),
  883. ),
  884. ),
  885. const SizedBox(width: 12),
  886. Expanded(
  887. child: TextField(
  888. controller: _sconto4Ctrl,
  889. decoration: const InputDecoration(labelText: 'Sconto 4 %'),
  890. keyboardType: const TextInputType.numberWithOptions(
  891. decimal: true,
  892. ),
  893. onChanged: (v) => _onScontoChanged(4, v),
  894. ),
  895. ),
  896. ],
  897. ),
  898. const SizedBox(height: 12),
  899. SizedBox(
  900. width: 140,
  901. child: TextField(
  902. controller: _sconto5Ctrl,
  903. decoration: const InputDecoration(labelText: 'Sconto 5 %'),
  904. keyboardType: const TextInputType.numberWithOptions(
  905. decimal: true,
  906. ),
  907. onChanged: (v) => _onScontoChanged(5, v),
  908. ),
  909. ),
  910. const SizedBox(height: 16),
  911. Row(
  912. children: [
  913. Text('Prezzo netto: ', style: theme.textTheme.bodyLarge),
  914. Text(
  915. _prezzoNetto.toStringAsFixed(2),
  916. style: theme.textTheme.bodyLarge?.copyWith(
  917. fontWeight: FontWeight.bold,
  918. ),
  919. ),
  920. ],
  921. ),
  922. const SizedBox(height: 12),
  923. Row(
  924. children: [
  925. Expanded(
  926. child: TextField(
  927. controller: _colliCtrl,
  928. decoration: const InputDecoration(labelText: 'Colli'),
  929. keyboardType: TextInputType.number,
  930. onChanged: _onColliChanged,
  931. ),
  932. ),
  933. const SizedBox(width: 12),
  934. Expanded(
  935. child: TextField(
  936. controller: _qtaCtrl,
  937. decoration: const InputDecoration(labelText: 'Quantità'),
  938. keyboardType: TextInputType.number,
  939. onChanged: _onQtaChanged,
  940. ),
  941. ),
  942. ],
  943. ),
  944. const SizedBox(height: 20),
  945. SizedBox(
  946. width: double.infinity,
  947. child: ElevatedButton(
  948. onPressed: _confirm,
  949. child: const Text('Aggiungi'),
  950. ),
  951. ),
  952. const SizedBox(height: 8),
  953. ],
  954. ),
  955. ),
  956. );
  957. }
  958. }