pedido_mesa_form.dart 52 KB

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