pedido_form.dart 61 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'package:flutter/foundation.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:intl/intl.dart';
  6. import 'package:provider/provider.dart';
  7. import '../pedido/pedido_ticket.dart';
  8. import '../../themes/themes.dart';
  9. import '../../models/models.dart';
  10. import '../../viewmodels/viewmodels.dart';
  11. import 'package:collection/collection.dart';
  12. import 'package:flutter/services.dart';
  13. import '../../widgets/widgets.dart';
  14. class PedidoForm extends StatefulWidget {
  15. @override
  16. _PedidoFormState createState() => _PedidoFormState();
  17. }
  18. class _PedidoFormState extends State<PedidoForm> {
  19. final _busqueda = TextEditingController(text: '');
  20. final TextEditingController _descuentoController = TextEditingController();
  21. CategoriaProductoViewModel cvm = CategoriaProductoViewModel();
  22. ProductoViewModel pvm = ProductoViewModel();
  23. PedidoViewModel pedvm = PedidoViewModel();
  24. bool _isLoading = false;
  25. CategoriaProducto? categoriaSeleccionada;
  26. List<CategoriaProducto> categorias = [];
  27. List<Producto> productos = [];
  28. List<ItemCarrito> carrito = [];
  29. Producto? _productoActual;
  30. bool _estadoBusqueda = false;
  31. Pedido? pedidoActual;
  32. ScrollController _listViewController = ScrollController();
  33. ScrollController _categoryScrollController = ScrollController();
  34. final _searchController = TextEditingController();
  35. final NumberFormat _numberFormat = NumberFormat.decimalPattern('es_MX');
  36. int? selectedDescuento = 0;
  37. double subtotal = 0;
  38. double precioDescuento = 0;
  39. double totalPedido = 0;
  40. bool efectivoSeleccionado = false;
  41. bool tarjetaSeleccionada = false;
  42. bool transferenciaSeleccionada = false;
  43. TextEditingController efectivoController = TextEditingController();
  44. TextEditingController tarjetaController = TextEditingController();
  45. TextEditingController transferenciaController = TextEditingController();
  46. double cambio = 0.0;
  47. double calcularTotalPedido() {
  48. double total = 0;
  49. for (var item in carrito) {
  50. total += double.parse(item.producto.precio!) * item.cantidad;
  51. item.selectedToppings.forEach((categoryId, selectedToppingIds) {
  52. for (int toppingId in selectedToppingIds) {
  53. Producto? topping = item.selectableToppings[categoryId]?.firstWhere(
  54. (topping) => topping.id == toppingId,
  55. orElse: () => Producto(precio: '0'));
  56. if (topping != null) {
  57. total += (double.tryParse(topping.precio!) ?? 0) * item.cantidad;
  58. }
  59. }
  60. });
  61. }
  62. return total;
  63. }
  64. double aplicarDescuento(double total, int? descuento) {
  65. if (descuento != null && descuento > 0) {
  66. double totalPedido = total * (1 - descuento / 100);
  67. // print(
  68. // 'Total con descuento: $totalPedido (Descuento aplicado: $descuento%)');
  69. return totalPedido;
  70. }
  71. // print('Sin descuento, total: $total');
  72. return total;
  73. }
  74. @override
  75. void initState() {
  76. super.initState();
  77. cargarCategoriasIniciales().then((_) {
  78. if (categorias.isNotEmpty) {
  79. categoriaSeleccionada = categorias.first;
  80. cargarProductosPorCategoria(categoriaSeleccionada!.id);
  81. }
  82. });
  83. Provider.of<DescuentoViewModel>(context, listen: false).cargarDescuentos();
  84. }
  85. void _onSearchChanged(String value) {
  86. if (value.isEmpty) {
  87. // Si no hay valor de búsqueda, cargar los productos iniciales y restaurar la categoría seleccionada.
  88. cargarProductosPorCategoria(
  89. categoriaSeleccionada?.id ?? categorias.first.id);
  90. } else {
  91. // Realiza la búsqueda y desactiva la categoría seleccionada para mostrar productos de todas las categorías.
  92. setState(() {
  93. _estadoBusqueda = true; // Indicamos que hay una búsqueda activa
  94. categoriaSeleccionada = null; // Ignoramos la categoría seleccionada
  95. });
  96. Provider.of<ProductoViewModel>(context, listen: false)
  97. .fetchLocalByName(nombre: value);
  98. }
  99. }
  100. void _recalcularTotal() {
  101. subtotal = calcularTotalPedido();
  102. // print('Subtotal: $subtotal');
  103. // print('Descuento seleccionado: $selectedDescuento%');
  104. precioDescuento = subtotal * (selectedDescuento! / 100);
  105. totalPedido = subtotal - precioDescuento;
  106. setState(() {
  107. pedidoActual = pedidoActual ?? Pedido();
  108. pedidoActual!.totalPedido = totalPedido;
  109. pedidoActual!.descuento = selectedDescuento;
  110. });
  111. // print('Precio descuento: $precioDescuento');
  112. // print('Total con descuento aplicado: $totalPedido');
  113. }
  114. void _finalizeOrder() async {
  115. if (carrito.isEmpty) {
  116. showDialog(
  117. context: context,
  118. builder: (BuildContext context) {
  119. return AlertDialog(
  120. title: const Text('Pedido vacío',
  121. style: TextStyle(fontWeight: FontWeight.w500, fontSize: 22)),
  122. content: const Text(
  123. 'No puedes finalizar un pedido sin productos. Por favor, agrega al menos un producto.',
  124. style: TextStyle(fontWeight: FontWeight.w500, fontSize: 18)),
  125. shape:
  126. RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
  127. actions: <Widget>[
  128. TextButton(
  129. style: TextButton.styleFrom(
  130. padding: EdgeInsets.fromLTRB(30, 20, 30, 20),
  131. foregroundColor: AppTheme.quaternary,
  132. backgroundColor: AppTheme.tertiary),
  133. onPressed: () {
  134. Navigator.of(context).pop();
  135. },
  136. child: const Text('Aceptar', style: TextStyle(fontSize: 18)),
  137. ),
  138. ],
  139. );
  140. },
  141. );
  142. return;
  143. }
  144. await _promptForCustomerName();
  145. }
  146. Future<void> _promptForCustomerName() async {
  147. TextEditingController nombreController = TextEditingController();
  148. TextEditingController comentarioController = TextEditingController();
  149. String errorMessage = '';
  150. double faltante = totalPedido;
  151. bool totalCompletado = false;
  152. bool efectivoCompleto = false;
  153. bool tarjetaCompleto = false;
  154. bool transferenciaCompleto = false;
  155. void _calcularCambio(StateSetter setState) {
  156. double totalPagado = (double.tryParse(efectivoController.text) ?? 0) +
  157. (double.tryParse(tarjetaController.text) ?? 0) +
  158. (double.tryParse(transferenciaController.text) ?? 0);
  159. setState(() {
  160. cambio = totalPagado - totalPedido;
  161. if (cambio < 0) {
  162. faltante = totalPedido - totalPagado;
  163. cambio = 0;
  164. totalCompletado = false;
  165. } else {
  166. faltante = 0;
  167. totalCompletado = true;
  168. }
  169. // Si el total ha sido alcanzado o excedido, desactivar otros métodos de pago
  170. if (totalPagado >= totalPedido) {
  171. if (!efectivoSeleccionado) efectivoSeleccionado = false;
  172. if (!tarjetaSeleccionada) tarjetaSeleccionada = false;
  173. if (!transferenciaSeleccionada) transferenciaSeleccionada = false;
  174. }
  175. });
  176. }
  177. void _validarCantidad(
  178. StateSetter setState, TextEditingController controller) {
  179. double cantidad = double.tryParse(controller.text) ?? 0;
  180. if (cantidad > totalPedido) {
  181. setState(() {
  182. controller.text = totalPedido.toStringAsFixed(2);
  183. });
  184. }
  185. _calcularCambio(setState);
  186. }
  187. bool _isPaymentOptionEnabled(bool isSelected) {
  188. return !totalCompletado || isSelected;
  189. }
  190. bool? shouldSave = await showDialog<bool>(
  191. context: context,
  192. builder: (BuildContext context) {
  193. return StatefulBuilder(
  194. builder: (context, setState) {
  195. return RawKeyboardListener(
  196. focusNode: FocusNode(),
  197. onKey: (RawKeyEvent event) {
  198. if (event.isKeyPressed(LogicalKeyboardKey.enter) &&
  199. totalCompletado) {
  200. Navigator.of(context).pop(true);
  201. }
  202. },
  203. child: AlertDialog(
  204. actionsPadding: EdgeInsets.fromLTRB(50, 10, 50, 30),
  205. title: const Text(
  206. 'Finalizar Pedido',
  207. style: TextStyle(fontSize: 22, fontWeight: FontWeight.w500),
  208. ),
  209. content: Container(
  210. height: 600,
  211. child: Column(
  212. children: [
  213. Expanded(
  214. child: SingleChildScrollView(
  215. child: Column(
  216. children: [
  217. AppTextField(
  218. controller: nombreController,
  219. etiqueta: 'Nombre',
  220. hintText: "Nombre del Cliente",
  221. ),
  222. const SizedBox(height: 10),
  223. AppTextField(
  224. controller: comentarioController,
  225. etiqueta: 'Comentarios (opcional)',
  226. hintText: 'Comentarios',
  227. maxLines: 2,
  228. ),
  229. const SizedBox(height: 10),
  230. Align(
  231. alignment: Alignment.center,
  232. child: Text(
  233. 'Métodos de pago',
  234. style: TextStyle(
  235. fontWeight: FontWeight.bold,
  236. fontSize: 20),
  237. ),
  238. ),
  239. const SizedBox(height: 10),
  240. // Efectivo
  241. GestureDetector(
  242. onTap: () {
  243. if (_isPaymentOptionEnabled(
  244. efectivoSeleccionado)) {
  245. setState(() {
  246. efectivoSeleccionado =
  247. !efectivoSeleccionado;
  248. if (!efectivoSeleccionado) {
  249. efectivoCompleto = false;
  250. efectivoController.clear();
  251. _calcularCambio(setState);
  252. } else if (efectivoCompleto) {
  253. efectivoController.text =
  254. totalPedido.toStringAsFixed(2);
  255. _calcularCambio(setState);
  256. }
  257. });
  258. }
  259. },
  260. child: Row(
  261. mainAxisAlignment:
  262. MainAxisAlignment.spaceBetween,
  263. crossAxisAlignment:
  264. CrossAxisAlignment.center,
  265. children: [
  266. Row(
  267. children: [
  268. Checkbox(
  269. activeColor: AppTheme.primary,
  270. value: efectivoSeleccionado,
  271. onChanged: _isPaymentOptionEnabled(
  272. efectivoSeleccionado)
  273. ? (bool? value) {
  274. setState(() {
  275. efectivoSeleccionado =
  276. value ?? false;
  277. if (!efectivoSeleccionado) {
  278. efectivoCompleto =
  279. false;
  280. efectivoController
  281. .clear();
  282. _calcularCambio(
  283. setState);
  284. } else if (efectivoCompleto) {
  285. efectivoController
  286. .text =
  287. totalPedido
  288. .toStringAsFixed(
  289. 2);
  290. _calcularCambio(
  291. setState);
  292. }
  293. });
  294. }
  295. : null,
  296. ),
  297. const Text(
  298. "Efectivo",
  299. style: TextStyle(
  300. fontSize: 18,
  301. fontWeight: FontWeight.bold),
  302. ),
  303. ],
  304. ),
  305. if (efectivoSeleccionado)
  306. SizedBox(
  307. width: 180,
  308. child: Row(
  309. crossAxisAlignment:
  310. CrossAxisAlignment.start,
  311. children: [
  312. Column(
  313. children: [
  314. const Text('Exacto',
  315. style: TextStyle(
  316. fontSize: 18,
  317. fontWeight:
  318. FontWeight.bold,
  319. color: Colors.black)),
  320. const SizedBox(
  321. height: 17,
  322. ),
  323. Checkbox(
  324. activeColor:
  325. AppTheme.primary,
  326. value: efectivoCompleto,
  327. onChanged:
  328. efectivoSeleccionado
  329. ? (bool? value) {
  330. setState(() {
  331. efectivoCompleto =
  332. value ??
  333. false;
  334. if (efectivoCompleto) {
  335. efectivoController
  336. .text =
  337. totalPedido
  338. .toStringAsFixed(2);
  339. _calcularCambio(
  340. setState);
  341. } else {
  342. efectivoController
  343. .clear();
  344. _calcularCambio(
  345. setState);
  346. }
  347. });
  348. }
  349. : null,
  350. ),
  351. ],
  352. ),
  353. const SizedBox(
  354. width: 5,
  355. ),
  356. Expanded(
  357. child: AppTextField(
  358. controller:
  359. efectivoController,
  360. etiqueta: 'Cantidad',
  361. hintText: '0.00',
  362. keyboardType:
  363. TextInputType.number,
  364. onChanged: (value) =>
  365. _calcularCambio(setState),
  366. ),
  367. ),
  368. ],
  369. ),
  370. ),
  371. ],
  372. ),
  373. ),
  374. const SizedBox(height: 10),
  375. // Tarjeta
  376. GestureDetector(
  377. onTap: () {
  378. if (_isPaymentOptionEnabled(
  379. tarjetaSeleccionada)) {
  380. setState(() {
  381. tarjetaSeleccionada =
  382. !tarjetaSeleccionada;
  383. if (!tarjetaSeleccionada) {
  384. tarjetaController.clear();
  385. _calcularCambio(setState);
  386. }
  387. });
  388. }
  389. },
  390. child: Row(
  391. mainAxisAlignment:
  392. MainAxisAlignment.spaceBetween,
  393. crossAxisAlignment:
  394. CrossAxisAlignment.center,
  395. children: [
  396. Row(
  397. children: [
  398. Checkbox(
  399. activeColor: AppTheme.primary,
  400. value: tarjetaSeleccionada,
  401. onChanged: _isPaymentOptionEnabled(
  402. tarjetaSeleccionada)
  403. ? (bool? value) {
  404. setState(() {
  405. tarjetaSeleccionada =
  406. value ?? false;
  407. if (!tarjetaSeleccionada) {
  408. tarjetaController
  409. .clear();
  410. _calcularCambio(
  411. setState);
  412. }
  413. });
  414. }
  415. : null,
  416. ),
  417. const Text(
  418. "Tarjeta",
  419. style: TextStyle(
  420. fontSize: 18,
  421. fontWeight: FontWeight.bold),
  422. ),
  423. ],
  424. ),
  425. if (tarjetaSeleccionada)
  426. SizedBox(
  427. width: 180,
  428. child: Row(
  429. crossAxisAlignment:
  430. CrossAxisAlignment.start,
  431. children: [
  432. Column(
  433. children: [
  434. const Text('Exacto',
  435. style: TextStyle(
  436. fontSize: 18,
  437. fontWeight:
  438. FontWeight.bold,
  439. color: Colors.black)),
  440. const SizedBox(
  441. height: 17,
  442. ),
  443. Checkbox(
  444. activeColor:
  445. AppTheme.primary,
  446. value: tarjetaCompleto,
  447. onChanged:
  448. tarjetaSeleccionada
  449. ? (bool? value) {
  450. setState(() {
  451. tarjetaCompleto =
  452. value ??
  453. false;
  454. if (tarjetaCompleto) {
  455. tarjetaController
  456. .text =
  457. totalPedido
  458. .toStringAsFixed(2);
  459. _calcularCambio(
  460. setState);
  461. } else {
  462. tarjetaController
  463. .clear();
  464. _calcularCambio(
  465. setState);
  466. }
  467. });
  468. }
  469. : null,
  470. ),
  471. ],
  472. ),
  473. const SizedBox(
  474. width: 5,
  475. ),
  476. Expanded(
  477. child: AppTextField(
  478. controller: tarjetaController,
  479. etiqueta: 'Cantidad',
  480. hintText: '0.00',
  481. keyboardType:
  482. TextInputType.number,
  483. onChanged: (value) {
  484. _validarCantidad(setState,
  485. tarjetaController);
  486. },
  487. ),
  488. ),
  489. ],
  490. ),
  491. ),
  492. ],
  493. ),
  494. ),
  495. const SizedBox(height: 10),
  496. // Transferencia
  497. GestureDetector(
  498. onTap: () {
  499. if (_isPaymentOptionEnabled(
  500. transferenciaSeleccionada)) {
  501. setState(() {
  502. transferenciaSeleccionada =
  503. !transferenciaSeleccionada;
  504. if (!transferenciaSeleccionada) {
  505. transferenciaController.clear();
  506. _calcularCambio(setState);
  507. }
  508. });
  509. }
  510. },
  511. child: Row(
  512. mainAxisAlignment:
  513. MainAxisAlignment.spaceBetween,
  514. crossAxisAlignment:
  515. CrossAxisAlignment.center,
  516. children: [
  517. Row(
  518. children: [
  519. Checkbox(
  520. activeColor: AppTheme.primary,
  521. value: transferenciaSeleccionada,
  522. onChanged: _isPaymentOptionEnabled(
  523. transferenciaSeleccionada)
  524. ? (bool? value) {
  525. setState(() {
  526. transferenciaSeleccionada =
  527. value ?? false;
  528. if (!transferenciaSeleccionada) {
  529. transferenciaController
  530. .clear();
  531. _calcularCambio(
  532. setState);
  533. }
  534. });
  535. }
  536. : null,
  537. ),
  538. const Text(
  539. "Transferencia",
  540. style: TextStyle(
  541. fontSize: 18,
  542. fontWeight: FontWeight.bold),
  543. ),
  544. ],
  545. ),
  546. if (transferenciaSeleccionada)
  547. SizedBox(
  548. width: 180,
  549. child: Row(
  550. crossAxisAlignment:
  551. CrossAxisAlignment.start,
  552. children: [
  553. Column(
  554. children: [
  555. const Text('Exacto',
  556. style: TextStyle(
  557. fontSize: 18,
  558. fontWeight:
  559. FontWeight.bold,
  560. color: Colors.black)),
  561. const SizedBox(
  562. height: 17,
  563. ),
  564. Checkbox(
  565. activeColor:
  566. AppTheme.primary,
  567. value:
  568. transferenciaCompleto,
  569. onChanged:
  570. transferenciaSeleccionada
  571. ? (bool? value) {
  572. setState(() {
  573. transferenciaCompleto =
  574. value ??
  575. false;
  576. if (transferenciaCompleto) {
  577. transferenciaController
  578. .text =
  579. totalPedido
  580. .toStringAsFixed(2);
  581. _calcularCambio(
  582. setState);
  583. } else {
  584. transferenciaController
  585. .clear();
  586. _calcularCambio(
  587. setState);
  588. }
  589. });
  590. }
  591. : null,
  592. ),
  593. ],
  594. ),
  595. const SizedBox(
  596. width: 5,
  597. ),
  598. Expanded(
  599. child: AppTextField(
  600. controller:
  601. transferenciaController,
  602. etiqueta: 'Cantidad',
  603. hintText: '0.00',
  604. keyboardType:
  605. TextInputType.number,
  606. onChanged: (value) {
  607. _validarCantidad(setState,
  608. transferenciaController);
  609. },
  610. ),
  611. ),
  612. ],
  613. ),
  614. ),
  615. ],
  616. ),
  617. ),
  618. const SizedBox(height: 10),
  619. // Mostrar el total del pedido y la cantidad faltante
  620. Align(
  621. alignment: Alignment.centerRight,
  622. child: Column(
  623. crossAxisAlignment:
  624. CrossAxisAlignment.end,
  625. children: [
  626. Text(
  627. 'Total del pedido: \$${totalPedido.toStringAsFixed(2)}',
  628. style: const TextStyle(
  629. fontWeight: FontWeight.bold,
  630. fontSize: 18),
  631. ),
  632. if (faltante > 0)
  633. Text(
  634. 'Faltante: \$${faltante.toStringAsFixed(2)}',
  635. style: const TextStyle(
  636. color: Colors.red,
  637. fontSize: 18,
  638. fontWeight: FontWeight.bold),
  639. )
  640. else if (cambio > 0)
  641. Text(
  642. 'Cambio: \$${cambio.toStringAsFixed(2)}',
  643. style: const TextStyle(
  644. color: Colors.green,
  645. fontSize: 18,
  646. fontWeight: FontWeight.bold)),
  647. ]),
  648. ),
  649. ],
  650. ),
  651. ),
  652. ),
  653. // Aquí mantenemos los botones fijos
  654. Row(
  655. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  656. children: [
  657. TextButton(
  658. child: const Text('Cancelar',
  659. style: TextStyle(fontSize: 18)),
  660. onPressed: () {
  661. Navigator.of(context).pop(false);
  662. },
  663. style: ButtonStyle(
  664. padding: MaterialStatePropertyAll(
  665. EdgeInsets.fromLTRB(30, 20, 30, 20)),
  666. backgroundColor:
  667. MaterialStatePropertyAll(Colors.red),
  668. foregroundColor: MaterialStatePropertyAll(
  669. AppTheme.secondary)),
  670. ),
  671. const SizedBox(width: 100),
  672. TextButton(
  673. child: const Text('Guardar',
  674. style: TextStyle(fontSize: 18)),
  675. onPressed: totalCompletado
  676. ? () {
  677. Navigator.of(context).pop(true);
  678. }
  679. : null,
  680. style: ButtonStyle(
  681. padding: MaterialStatePropertyAll(
  682. EdgeInsets.fromLTRB(30, 20, 30, 20)),
  683. backgroundColor: MaterialStatePropertyAll(
  684. totalCompletado
  685. ? AppTheme.tertiary
  686. : Colors.grey),
  687. foregroundColor: MaterialStatePropertyAll(
  688. AppTheme.quaternary)),
  689. ),
  690. ],
  691. ),
  692. ],
  693. ),
  694. ),
  695. ));
  696. },
  697. );
  698. },
  699. );
  700. if (shouldSave ?? false) {
  701. prepararPedidoActual(nombreController.text, comentarioController.text);
  702. }
  703. }
  704. void prepararPedidoActual(String nombreCliente, String comentarios) async {
  705. DateTime now = DateTime.now();
  706. String formattedDate = DateFormat('dd-MM-yyyy kk:mm:ss').format(now);
  707. Pedido nuevoPedido = Pedido(
  708. peticion: formattedDate,
  709. nombreCliente: nombreCliente,
  710. comentarios: comentarios,
  711. estatus: "NUEVO",
  712. totalPedido: totalPedido,
  713. descuento: pedidoActual?.descuento,
  714. tipoPago: _obtenerTipoPago(),
  715. cantEfectivo:
  716. efectivoSeleccionado ? double.tryParse(efectivoController.text) : 0,
  717. cantTarjeta:
  718. tarjetaSeleccionada ? double.tryParse(tarjetaController.text) : 0,
  719. cantTransferencia: transferenciaSeleccionada
  720. ? double.tryParse(transferenciaController.text)
  721. : 0,
  722. );
  723. List<PedidoProducto> listaPedidoProducto = carrito.map((item) {
  724. List<PedidoProductoTopping> selectedToppings = [];
  725. item.selectedToppings.forEach((categoryId, selectedToppingIds) {
  726. for (int toppingId in selectedToppingIds) {
  727. selectedToppings.add(PedidoProductoTopping(
  728. idTopping: toppingId,
  729. ));
  730. }
  731. });
  732. return PedidoProducto(
  733. idProducto: item.producto.id,
  734. producto: item.producto,
  735. costoUnitario: item.producto.precio,
  736. cantidad: item.cantidad,
  737. comentario: comentarios,
  738. toppings: selectedToppings,
  739. );
  740. }).toList();
  741. nuevoPedido.productos = listaPedidoProducto;
  742. bool result = await Provider.of<PedidoViewModel>(context, listen: false)
  743. .guardarPedidoLocal(pedido: nuevoPedido);
  744. if (!mounted) return;
  745. if (result) {
  746. Pedido? pedidoCompleto =
  747. await Provider.of<PedidoViewModel>(context, listen: false)
  748. .fetchPedidoConProductos(nuevoPedido.id!);
  749. if (pedidoCompleto != null) {
  750. imprimirTicketsJuntos(context, pedidoCompleto);
  751. }
  752. Navigator.of(context).pop();
  753. } else {
  754. print("Error al guardar el pedido");
  755. }
  756. }
  757. String _obtenerTipoPago() {
  758. List<String> tiposPago = [];
  759. if (efectivoSeleccionado) tiposPago.add('Efectivo');
  760. if (tarjetaSeleccionada) tiposPago.add('Tarjeta');
  761. if (transferenciaSeleccionada) tiposPago.add('Transferencia');
  762. return tiposPago.join(',');
  763. }
  764. void _limpiarBusqueda() async {
  765. setState(() {
  766. _busqueda.text = '';
  767. _estadoBusqueda = false;
  768. });
  769. await cargarCategoriasIniciales();
  770. }
  771. Future<void> cargarProductosIniciales() async {
  772. setState(() => _isLoading = true);
  773. await Provider.of<ProductoViewModel>(context, listen: false)
  774. .fetchLocalAll();
  775. productos =
  776. Provider.of<ProductoViewModel>(context, listen: false).productos;
  777. setState(() => _isLoading = false);
  778. }
  779. @override
  780. void dispose() {
  781. _listViewController.dispose();
  782. _searchController.dispose();
  783. _categoryScrollController.dispose();
  784. super.dispose();
  785. }
  786. Future<void> cargarCategoriasIniciales() async {
  787. setState(() => _isLoading = true);
  788. await Provider.of<CategoriaProductoViewModel>(context, listen: false)
  789. .fetchLocalAll();
  790. categorias = Provider.of<CategoriaProductoViewModel>(context, listen: false)
  791. .categoriaProductos;
  792. if (categorias.isNotEmpty) {
  793. categoriaSeleccionada = categorias.first;
  794. cargarProductosPorCategoria(categoriaSeleccionada!.id);
  795. }
  796. setState(() => _isLoading = false);
  797. if (categorias.isNotEmpty) {
  798. categoriaSeleccionada = categorias.first;
  799. }
  800. }
  801. void cargarProductosPorCategoria(int categoriaId) async {
  802. setState(() => _isLoading = true);
  803. await Provider.of<ProductoViewModel>(context, listen: false)
  804. .fetchAllByCategory(categoriaId);
  805. productos =
  806. Provider.of<ProductoViewModel>(context, listen: false).productos;
  807. setState(() => _isLoading = false);
  808. }
  809. void agregarAlCarrito(Producto producto) async {
  810. var existente = carrito.firstWhereOrNull((item) =>
  811. item.producto.id == producto.id &&
  812. mapEquals(item.selectedToppings, {}));
  813. if (existente != null) {
  814. setState(() {
  815. existente.cantidad++;
  816. });
  817. } else {
  818. Map<int, List<Producto>> toppingsSeleccionables =
  819. await obtenerToppingsSeleccionables(producto);
  820. setState(() {
  821. carrito.add(ItemCarrito(
  822. producto: producto,
  823. cantidad: 1,
  824. selectableToppings: toppingsSeleccionables,
  825. ));
  826. });
  827. }
  828. _recalcularTotal();
  829. }
  830. Future<Map<int, List<Producto>>> obtenerToppingsSeleccionables(
  831. Producto producto) async {
  832. Map<int, List<Producto>> toppingsSeleccionables = {};
  833. final toppingCategories =
  834. await pvm.obtenerToppingsPorProducto(producto.id!);
  835. for (int toppingId in toppingCategories) {
  836. Producto? topping = await pvm.obtenerProductoPorId(toppingId);
  837. if (topping != null && topping.idCategoria != null) {
  838. toppingsSeleccionables[topping.idCategoria!] ??= [];
  839. toppingsSeleccionables[topping.idCategoria]!.add(topping);
  840. }
  841. }
  842. return toppingsSeleccionables;
  843. }
  844. void quitarDelCarrito(Producto producto) {
  845. setState(() {
  846. var indice =
  847. carrito.indexWhere((item) => item.producto.id == producto.id);
  848. if (indice != -1) {
  849. if (carrito[indice].cantidad > 1) {
  850. carrito[indice].cantidad--;
  851. } else {
  852. carrito.removeAt(indice);
  853. }
  854. }
  855. });
  856. }
  857. void addToCart(Producto producto) {
  858. var existingIndex = carrito.indexWhere((item) =>
  859. item.producto.id == producto.id &&
  860. mapEquals(item.selectedToppings, {}));
  861. if (existingIndex != -1) {
  862. setState(() {
  863. carrito[existingIndex].cantidad++;
  864. });
  865. } else {
  866. setState(() {
  867. carrito.add(ItemCarrito(producto: producto, cantidad: 1));
  868. });
  869. }
  870. }
  871. void finalizeCustomization() {
  872. if (_productoActual != null) {
  873. addToCart(_productoActual!);
  874. setState(() {
  875. _productoActual = null;
  876. });
  877. }
  878. }
  879. Widget _buildDiscountSection() {
  880. return Padding(
  881. padding: const EdgeInsets.symmetric(horizontal: 8.0),
  882. child: Row(
  883. crossAxisAlignment: CrossAxisAlignment.center,
  884. children: [
  885. const Text(
  886. 'Descuento',
  887. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
  888. ),
  889. const Spacer(),
  890. ConstrainedBox(
  891. constraints: const BoxConstraints(
  892. maxWidth: 150,
  893. ),
  894. child: Consumer<DescuentoViewModel>(
  895. builder: (context, viewModel, child) {
  896. return AppDropdownModel<int>(
  897. hint: 'Seleccionar',
  898. items: viewModel.descuentos
  899. .map(
  900. (descuento) => DropdownMenuItem<int>(
  901. value: descuento.porcentaje,
  902. child: Text(
  903. '${descuento.porcentaje}%',
  904. style: const TextStyle(color: Colors.black),
  905. ),
  906. ),
  907. )
  908. .toList(),
  909. selectedValue: selectedDescuento,
  910. onChanged: (value) {
  911. setState(() {
  912. selectedDescuento = value;
  913. _recalcularTotal();
  914. });
  915. },
  916. );
  917. },
  918. ),
  919. ),
  920. ],
  921. ),
  922. );
  923. }
  924. Widget _buildTotalSection() {
  925. String formattedsubtotal = _numberFormat.format(subtotal);
  926. String formattedPrecioDescuento = _numberFormat.format(precioDescuento);
  927. String formattedtotalPedido = _numberFormat.format(totalPedido);
  928. return Padding(
  929. padding: const EdgeInsets.symmetric(horizontal: 8.0),
  930. child: Column(
  931. crossAxisAlignment: CrossAxisAlignment.start,
  932. children: [
  933. if (precioDescuento > 0)
  934. Column(
  935. children: [
  936. Row(
  937. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  938. children: [
  939. const Text('Subtotal',
  940. style: TextStyle(
  941. fontWeight: FontWeight.bold, fontSize: 18)),
  942. Text("\$$formattedsubtotal",
  943. style: const TextStyle(
  944. fontWeight: FontWeight.bold, fontSize: 18)),
  945. ],
  946. ),
  947. const SizedBox(height: 10),
  948. Row(
  949. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  950. children: [
  951. const Text('Descuento',
  952. style: TextStyle(
  953. fontWeight: FontWeight.bold, fontSize: 18)),
  954. Text("-\$$formattedPrecioDescuento",
  955. style: const TextStyle(
  956. fontWeight: FontWeight.bold, fontSize: 18)),
  957. ],
  958. ),
  959. ],
  960. ),
  961. const SizedBox(height: 10),
  962. Row(
  963. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  964. children: [
  965. const Text('Total',
  966. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
  967. Text("\$$formattedtotalPedido",
  968. style: const TextStyle(
  969. fontWeight: FontWeight.bold, fontSize: 18)),
  970. ],
  971. ),
  972. ],
  973. ),
  974. );
  975. }
  976. @override
  977. Widget build(BuildContext context) {
  978. return Scaffold(
  979. appBar: AppBar(
  980. title:
  981. Text("Crear Pedido", style: TextStyle(color: AppTheme.secondary)),
  982. iconTheme: IconThemeData(color: AppTheme.secondary)),
  983. body: Row(
  984. children: [
  985. Flexible(
  986. flex: 3,
  987. child: _buildCartSection(),
  988. ),
  989. SizedBox(width: 35),
  990. Flexible(flex: 7, child: _buildProductsSection()),
  991. ],
  992. ),
  993. );
  994. }
  995. Widget _buildCartSection() {
  996. return Card(
  997. margin: const EdgeInsets.all(8.0),
  998. child: Column(
  999. children: [
  1000. const Padding(
  1001. padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0),
  1002. child: Row(
  1003. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  1004. children: [
  1005. Text('Producto',
  1006. style:
  1007. TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
  1008. Text('Cantidad',
  1009. style:
  1010. TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
  1011. ],
  1012. ),
  1013. ),
  1014. Expanded(
  1015. child: ListView.builder(
  1016. itemCount: carrito.length,
  1017. itemBuilder: (context, index) {
  1018. final item = carrito[index];
  1019. return Column(
  1020. children: [
  1021. ListTile(
  1022. title: Text(item.producto.nombre!,
  1023. style: const TextStyle(fontWeight: FontWeight.w600)),
  1024. subtitle: Text('\$${item.producto.precio}',
  1025. style: const TextStyle(
  1026. fontWeight: FontWeight.bold,
  1027. color: Color(0xFF008000))),
  1028. trailing: Row(
  1029. mainAxisSize: MainAxisSize.min,
  1030. children: [
  1031. IconButton(
  1032. icon: const Icon(Icons.delete, color: Colors.red),
  1033. onPressed: () =>
  1034. eliminarProductoDelCarrito(index)),
  1035. IconButton(
  1036. icon: const Icon(Icons.remove),
  1037. onPressed: () => quitarProductoDelCarrito(item)),
  1038. const SizedBox(width: 5),
  1039. Text('${item.cantidad}',
  1040. style: const TextStyle(
  1041. fontWeight: FontWeight.bold, fontSize: 14)),
  1042. const SizedBox(width: 5),
  1043. IconButton(
  1044. icon: const Icon(Icons.add),
  1045. onPressed: () => incrementarProducto(item)),
  1046. ],
  1047. ),
  1048. ),
  1049. if (item.selectableToppings.isNotEmpty)
  1050. ExpansionTile(
  1051. title: Text('Toppings',
  1052. style: const TextStyle(
  1053. fontWeight: FontWeight.bold, fontSize: 16)),
  1054. children: item.selectableToppings.entries.map((entry) {
  1055. final categoryId = entry.key;
  1056. final availableToppings = entry.value;
  1057. final categoria =
  1058. categorias.firstWhere((c) => c.id == categoryId);
  1059. return Column(
  1060. crossAxisAlignment: CrossAxisAlignment.start,
  1061. children: [
  1062. Padding(
  1063. padding: const EdgeInsets.only(left: 16.0),
  1064. child: Text(
  1065. categoria.descripcion!,
  1066. style: const TextStyle(
  1067. fontWeight: FontWeight.bold,
  1068. fontSize: 16),
  1069. ),
  1070. ),
  1071. ...availableToppings.map((topping) {
  1072. ValueNotifier<bool> isSelectedNotifier =
  1073. ValueNotifier<bool>(
  1074. item.selectedToppings[categoryId]
  1075. ?.contains(topping.id) ??
  1076. false,
  1077. );
  1078. return ValueListenableBuilder<bool>(
  1079. valueListenable: isSelectedNotifier,
  1080. builder: (context, isSelected, _) {
  1081. return CheckboxListTile(
  1082. activeColor: AppTheme.primary,
  1083. title: Row(
  1084. mainAxisAlignment:
  1085. MainAxisAlignment.spaceBetween,
  1086. children: [
  1087. Text(topping.nombre!),
  1088. if (double.tryParse(
  1089. topping.precio!) !=
  1090. 0.0)
  1091. Text(
  1092. '+\$${topping.precio}',
  1093. style: TextStyle(
  1094. color: Colors.black,
  1095. fontSize: 13,
  1096. ),
  1097. ),
  1098. ],
  1099. ),
  1100. value: isSelected,
  1101. onChanged: (bool? value) {
  1102. final maximoToppings =
  1103. categoria.maximo ?? 0;
  1104. if (value == true) {
  1105. if ((item.selectedToppings[categoryId]
  1106. ?.length ??
  1107. 0) >=
  1108. maximoToppings) {
  1109. item.selectedToppings[categoryId]!
  1110. .remove(item
  1111. .selectedToppings[
  1112. categoryId]!
  1113. .first);
  1114. }
  1115. item.selectedToppings[categoryId] ??=
  1116. <int>{};
  1117. item.selectedToppings[categoryId]!
  1118. .add(topping.id!);
  1119. } else {
  1120. item.selectedToppings[categoryId]
  1121. ?.remove(topping.id!);
  1122. if (item.selectedToppings[categoryId]
  1123. ?.isEmpty ??
  1124. false) {
  1125. item.selectedToppings
  1126. .remove(categoryId);
  1127. }
  1128. }
  1129. setState(() {});
  1130. _recalcularTotal();
  1131. },
  1132. );
  1133. },
  1134. );
  1135. }).toList(),
  1136. ],
  1137. );
  1138. }).toList(),
  1139. ),
  1140. Divider(),
  1141. ],
  1142. );
  1143. },
  1144. ),
  1145. ),
  1146. _buildDiscountSection(),
  1147. const Divider(thickness: 5),
  1148. _buildTotalSection(),
  1149. const SizedBox(height: 25),
  1150. Padding(
  1151. padding: const EdgeInsets.all(8.0),
  1152. child: ElevatedButton(
  1153. onPressed: _finalizeOrder,
  1154. style: ElevatedButton.styleFrom(
  1155. primary: AppTheme.tertiary,
  1156. textStyle: const TextStyle(fontSize: 22),
  1157. fixedSize: const Size(250, 50),
  1158. ),
  1159. child: Text('Finalizar Pedido',
  1160. style: TextStyle(color: AppTheme.quaternary)),
  1161. ),
  1162. ),
  1163. ],
  1164. ),
  1165. );
  1166. }
  1167. void eliminarProductoDelCarrito(int index) {
  1168. setState(() {
  1169. carrito.removeAt(index);
  1170. });
  1171. _recalcularTotal();
  1172. }
  1173. void incrementarProducto(ItemCarrito item) {
  1174. setState(() {
  1175. item.cantidad++;
  1176. });
  1177. _recalcularTotal();
  1178. }
  1179. void quitarProductoDelCarrito(ItemCarrito item) {
  1180. setState(() {
  1181. if (item.cantidad > 1) {
  1182. item.cantidad--;
  1183. } else {
  1184. carrito.remove(item);
  1185. }
  1186. });
  1187. _recalcularTotal();
  1188. }
  1189. Widget _buildProductsSection() {
  1190. return Column(
  1191. children: [
  1192. const SizedBox(height: 5),
  1193. _buildSearchBar(),
  1194. const SizedBox(height: 10),
  1195. _buildCategoryButtons(),
  1196. const SizedBox(height: 15),
  1197. Expanded(
  1198. child: Consumer<ProductoViewModel>(builder: (context, model, child) {
  1199. productos = model.productos;
  1200. return Scrollbar(
  1201. controller: _listViewController,
  1202. thumbVisibility: true,
  1203. trackVisibility: true,
  1204. child: ListView.builder(
  1205. controller: _listViewController,
  1206. itemCount: productos.length,
  1207. itemBuilder: (context, index) {
  1208. final producto = productos[index];
  1209. return Card(
  1210. child: ListTile(
  1211. title: Row(
  1212. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  1213. children: [
  1214. Text(
  1215. producto.nombre ?? '',
  1216. style: const TextStyle(
  1217. fontSize: 16,
  1218. fontWeight: FontWeight.bold,
  1219. ),
  1220. textAlign: TextAlign.left,
  1221. ),
  1222. Text(
  1223. '\$${producto.precio}',
  1224. style: const TextStyle(
  1225. fontSize: 16,
  1226. fontWeight: FontWeight.bold,
  1227. color: Colors.green,
  1228. ),
  1229. textAlign: TextAlign.right,
  1230. ),
  1231. ],
  1232. ),
  1233. onTap: () => agregarAlCarrito(producto),
  1234. ),
  1235. );
  1236. },
  1237. ),
  1238. );
  1239. }),
  1240. ),
  1241. ],
  1242. );
  1243. }
  1244. Widget _buildCategoryButtons() {
  1245. List<CategoriaProducto> categoriasFiltradas =
  1246. categorias.where((categoria) => categoria.esToping == 0).toList();
  1247. return Column(
  1248. children: [
  1249. Container(
  1250. height: 50,
  1251. child: Stack(
  1252. children: [
  1253. ListView.builder(
  1254. controller: _categoryScrollController,
  1255. scrollDirection: Axis.horizontal,
  1256. itemCount: categoriasFiltradas.length,
  1257. itemBuilder: (context, index) {
  1258. final categoria = categoriasFiltradas[index];
  1259. bool isSelected = categoriaSeleccionada?.id == categoria.id;
  1260. return Padding(
  1261. padding: const EdgeInsets.symmetric(horizontal: 4.0),
  1262. child: ElevatedButton(
  1263. onPressed: () {
  1264. cargarProductosPorCategoria(categoria.id);
  1265. setState(() {
  1266. categoriaSeleccionada = categoria;
  1267. });
  1268. },
  1269. style: ElevatedButton.styleFrom(
  1270. backgroundColor:
  1271. isSelected ? AppTheme.tertiary : Colors.grey,
  1272. foregroundColor: isSelected
  1273. ? AppTheme.quaternary
  1274. : AppTheme.secondary,
  1275. ),
  1276. child: Text(categoria.nombre!),
  1277. ),
  1278. );
  1279. },
  1280. ),
  1281. ],
  1282. ),
  1283. ),
  1284. ],
  1285. );
  1286. }
  1287. Widget _buildSearchBar() {
  1288. return Padding(
  1289. padding: const EdgeInsets.all(8.0),
  1290. child: TextField(
  1291. controller: _searchController,
  1292. decoration: InputDecoration(
  1293. hintText: 'Buscar producto...',
  1294. prefixIcon: const Icon(Icons.search),
  1295. suffixIcon: IconButton(
  1296. icon: Icon(Icons.clear),
  1297. onPressed: () {
  1298. _searchController.clear();
  1299. _onSearchChanged('');
  1300. },
  1301. ),
  1302. border: OutlineInputBorder(
  1303. borderRadius: BorderRadius.circular(12.0),
  1304. ),
  1305. enabledBorder: OutlineInputBorder(
  1306. borderRadius: BorderRadius.circular(12.0),
  1307. borderSide: BorderSide(width: 1.5)),
  1308. focusedBorder: OutlineInputBorder(
  1309. borderSide: BorderSide(color: Colors.black),
  1310. borderRadius: BorderRadius.circular(12.0),
  1311. ),
  1312. ),
  1313. onChanged: _onSearchChanged,
  1314. ),
  1315. );
  1316. }
  1317. }