pedido_form.dart 44 KB

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