pedido_mesa_form.dart 49 KB

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