pedido_form.dart 57 KB

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