pedido_form.dart 73 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999
  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:otp/otp.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. import 'package:uuid/uuid.dart';
  16. import '../../services/services.dart';
  17. import 'package:timezone/data/latest.dart' as timezone;
  18. import 'package:timezone/timezone.dart' as timezone;
  19. class PedidoForm extends StatefulWidget {
  20. final Pedido? pedidoExistente;
  21. const PedidoForm({Key? key, this.pedidoExistente}) : super(key: key);
  22. @override
  23. _PedidoFormState createState() => _PedidoFormState();
  24. }
  25. class _PedidoFormState extends State<PedidoForm> {
  26. final _busqueda = TextEditingController(text: '');
  27. final TextEditingController _descuentoController = TextEditingController();
  28. CategoriaProductoViewModel cvm = CategoriaProductoViewModel();
  29. ProductoViewModel pvm = ProductoViewModel();
  30. PedidoViewModel pedvm = PedidoViewModel();
  31. bool _isLoading = false;
  32. CategoriaProducto? categoriaSeleccionada;
  33. List<CategoriaProducto> categorias = [];
  34. List<Producto> productos = [];
  35. List<ItemCarrito> carrito = [];
  36. Producto? _productoActual;
  37. bool _estadoBusqueda = false;
  38. Pedido? pedidoActual;
  39. ScrollController _gridViewController = ScrollController();
  40. ScrollController _categoryScrollController = ScrollController();
  41. final _searchController = TextEditingController();
  42. final NumberFormat _numberFormat = NumberFormat.decimalPattern('es_MX');
  43. int? selectedDescuento = 0;
  44. double subtotal = 0;
  45. double precioDescuento = 0;
  46. double totalPedido = 0;
  47. bool efectivoSeleccionado = false;
  48. bool tarjetaSeleccionada = false;
  49. bool transferenciaSeleccionada = false;
  50. TextEditingController efectivoController = TextEditingController();
  51. TextEditingController tarjetaController = TextEditingController();
  52. TextEditingController transferenciaController = TextEditingController();
  53. TextEditingController _propinaCantidadController = TextEditingController();
  54. TextEditingController _propinaComentarioController = TextEditingController();
  55. double cambio = 0.0;
  56. double faltante = 0.0;
  57. bool totalCompletado = false;
  58. bool _isMesasActive = false;
  59. double calcularTotalPedido() {
  60. double total = 0;
  61. for (var item in carrito) {
  62. total += item.producto.precio! * item.cantidad;
  63. item.selectedToppings.forEach((categoryId, selectedToppingIds) {
  64. for (int toppingId in selectedToppingIds) {
  65. Producto? topping = item.selectableToppings[categoryId]?.firstWhere(
  66. (topping) => topping.id == toppingId,
  67. orElse: () => Producto(precio: 0));
  68. if (topping != null) {
  69. total += (topping.precio ?? 0.0) * item.cantidad;
  70. }
  71. }
  72. });
  73. }
  74. return total;
  75. }
  76. double aplicarDescuento(double total, int? descuento) {
  77. if (descuento != null && descuento > 0) {
  78. double totalPedido = total * (1 - descuento / 100);
  79. // print(
  80. // 'Total con descuento: $totalPedido (Descuento aplicado: $descuento%)');
  81. return totalPedido;
  82. }
  83. // print('Sin descuento, total: $total');
  84. return total;
  85. }
  86. @override
  87. void initState() {
  88. super.initState();
  89. pedidoActual = widget.pedidoExistente ?? Pedido(id: 0);
  90. Future.microtask(() async {
  91. bool isMesasActive =
  92. await Provider.of<VariableViewModel>(context, listen: false)
  93. .isVariableActive('MESAS');
  94. setState(() {
  95. _isMesasActive = isMesasActive;
  96. });
  97. if (pedidoActual != null && pedidoActual!.id! > 0) {
  98. await _cargarPedidoExistente(pedidoActual!.id!);
  99. }
  100. });
  101. cargarCategoriasIniciales().then((_) {
  102. if (categorias.isNotEmpty) {
  103. categoriaSeleccionada = categorias.first;
  104. cargarProductosPorCategoria(categoriaSeleccionada!.id);
  105. }
  106. });
  107. Provider.of<DescuentoViewModel>(context, listen: false).cargarDescuentos();
  108. }
  109. Future<void> _cargarPedidoExistente(int pedidoId) async {
  110. Pedido? pedidoCompleto =
  111. await Provider.of<PedidoViewModel>(context, listen: false)
  112. .fetchPedidoConProductos(pedidoId);
  113. if (pedidoCompleto != null) {
  114. setState(() {
  115. pedidoActual = pedidoCompleto;
  116. carrito = pedidoCompleto.productos.map((producto) {
  117. return ItemCarrito(
  118. producto: producto.producto!,
  119. cantidad: producto.cantidad!,
  120. comentario: producto.comentario,
  121. selectedToppings: producto.toppings.fold<Map<int, Set<int>>>(
  122. {},
  123. (acc, topping) {
  124. acc[topping.idCategoria!] ??= {};
  125. acc[topping.idCategoria]!.add(topping.idTopping!);
  126. return acc;
  127. },
  128. ),
  129. );
  130. }).toList();
  131. });
  132. selectedDescuento = pedidoCompleto.descuento ?? 0;
  133. _recalcularTotal();
  134. }
  135. }
  136. void _onSearchChanged(String value) async {
  137. if (value.isEmpty) {
  138. cargarProductosPorCategoria(
  139. categoriaSeleccionada?.id ?? categorias.first.id);
  140. } else {
  141. setState(() {
  142. _estadoBusqueda = true;
  143. });
  144. await Provider.of<ProductoViewModel>(context, listen: false)
  145. .fetchLocalByName(nombre: value);
  146. setState(() {
  147. productos =
  148. Provider.of<ProductoViewModel>(context, listen: false).productos;
  149. });
  150. }
  151. }
  152. void _recalcularTotal() {
  153. subtotal = calcularTotalPedido();
  154. // print('Subtotal: $subtotal');
  155. // print('Descuento seleccionado: $selectedDescuento%');
  156. precioDescuento = subtotal * (selectedDescuento! / 100);
  157. totalPedido = subtotal - precioDescuento;
  158. setState(() {
  159. pedidoActual = pedidoActual ?? Pedido();
  160. pedidoActual!.totalPedido = totalPedido;
  161. pedidoActual!.descuento = selectedDescuento;
  162. });
  163. // print('Precio descuento: $precioDescuento');
  164. // print('Total con descuento aplicado: $totalPedido');
  165. }
  166. bool validarMinimosSeleccionados() {
  167. for (var item in carrito) {
  168. for (var categoriaId in item.selectableToppings.keys) {
  169. final categoria = categorias.firstWhere((c) => c.id == categoriaId);
  170. final minimoRequerido = categoria.minimo ?? 0;
  171. final seleccionados = item.selectedToppings[categoriaId]?.length ?? 0;
  172. if (minimoRequerido > 0 && seleccionados < minimoRequerido) {
  173. showDialog(
  174. context: context,
  175. builder: (BuildContext context) {
  176. return AlertDialog(
  177. title: const Text('Faltan Toppings',
  178. style:
  179. TextStyle(fontWeight: FontWeight.w500, fontSize: 22)),
  180. content: Text(
  181. 'El producto ${item.producto.nombre} requiere que seleccione al menos $minimoRequerido topping en la categoría ${categoria.nombre}.',
  182. style: TextStyle(fontSize: 18)),
  183. actions: <Widget>[
  184. TextButton(
  185. onPressed: () => Navigator.of(context).pop(),
  186. child: const Text('Aceptar'),
  187. style: ButtonStyle(
  188. padding: WidgetStatePropertyAll(
  189. EdgeInsets.fromLTRB(20, 10, 20, 10)),
  190. backgroundColor:
  191. WidgetStatePropertyAll(AppTheme.tertiary),
  192. foregroundColor:
  193. WidgetStatePropertyAll(AppTheme.quaternary))),
  194. ],
  195. );
  196. },
  197. );
  198. return false;
  199. }
  200. }
  201. }
  202. return true;
  203. }
  204. void _finalizeOrder() async {
  205. if (carrito.isEmpty) {
  206. showDialog(
  207. context: context,
  208. builder: (BuildContext context) {
  209. return AlertDialog(
  210. title: const Text('Pedido vacío',
  211. style: TextStyle(fontWeight: FontWeight.w500, fontSize: 22)),
  212. content: const Text(
  213. 'No puedes finalizar un pedido sin productos. Por favor, agrega al menos un producto.',
  214. style: TextStyle(fontWeight: FontWeight.w500, fontSize: 18)),
  215. shape:
  216. RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
  217. actions: <Widget>[
  218. TextButton(
  219. style: TextButton.styleFrom(
  220. padding: EdgeInsets.fromLTRB(30, 20, 30, 20),
  221. foregroundColor: AppTheme.quaternary,
  222. backgroundColor: AppTheme.tertiary),
  223. onPressed: () {
  224. Navigator.of(context).pop();
  225. },
  226. child: const Text('Aceptar', style: TextStyle(fontSize: 18)),
  227. ),
  228. ],
  229. );
  230. },
  231. );
  232. return;
  233. }
  234. if (!validarMinimosSeleccionados()) {
  235. return;
  236. }
  237. await _promptForCustomerName();
  238. }
  239. void _mostrarDialogoPedidoVacio() {
  240. showDialog(
  241. context: context,
  242. builder: (BuildContext context) {
  243. return AlertDialog(
  244. title: const Text('Pedido vacío',
  245. style: TextStyle(fontWeight: FontWeight.w500, fontSize: 22)),
  246. content: const Text(
  247. 'No puedes crear un pedido sin productos. Por favor, agrega al menos un producto.',
  248. style: TextStyle(fontSize: 18)),
  249. shape:
  250. RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
  251. actions: <Widget>[
  252. TextButton(
  253. style: TextButton.styleFrom(
  254. padding: const EdgeInsets.fromLTRB(30, 20, 30, 20),
  255. foregroundColor: AppTheme.quaternary,
  256. backgroundColor: AppTheme.tertiary),
  257. onPressed: () {
  258. Navigator.of(context).pop();
  259. },
  260. child: const Text('Aceptar', style: TextStyle(fontSize: 18)),
  261. ),
  262. ],
  263. );
  264. },
  265. );
  266. }
  267. Future<void> _crearPedidoConModal() async {
  268. if (carrito.isEmpty) {
  269. _mostrarDialogoPedidoVacio();
  270. return;
  271. }
  272. TextEditingController nombreController = TextEditingController();
  273. TextEditingController descripcionController = TextEditingController();
  274. TextEditingController mesaSearchController = TextEditingController();
  275. Mesa? mesaSeleccionada;
  276. // Inicializa las mesas disponibles
  277. await Provider.of<MesaViewModel>(context, listen: false).fetchLocalAll();
  278. List<Mesa> mesasDisponibles =
  279. Provider.of<MesaViewModel>(context, listen: false).mesas;
  280. bool? shouldSave = await showDialog<bool>(
  281. context: context,
  282. builder: (BuildContext context) {
  283. return AlertDialog(
  284. title: const Text(
  285. 'Crear Pedido',
  286. style: TextStyle(fontSize: 22, fontWeight: FontWeight.w500),
  287. ),
  288. content: Column(
  289. mainAxisSize: MainAxisSize.min,
  290. children: [
  291. AppTextField(
  292. controller: nombreController,
  293. etiqueta: 'Nombre del Cliente',
  294. hintText: 'Nombre del Cliente',
  295. ),
  296. const SizedBox(height: 10),
  297. AppTextField(
  298. controller: descripcionController,
  299. etiqueta: 'Descripción',
  300. hintText: 'Descripción del Pedido',
  301. ),
  302. const SizedBox(height: 10),
  303. AppDropdownSearch(
  304. controller: mesaSearchController,
  305. etiqueta: 'Seleccionar Mesa',
  306. asyncItems: (String query) async {
  307. await Provider.of<MesaViewModel>(context, listen: false)
  308. .fetchLocalByName(nombre: query);
  309. return Provider.of<MesaViewModel>(context, listen: false)
  310. .mesas;
  311. },
  312. itemAsString: (dynamic mesa) =>
  313. (mesa as Mesa).nombre ?? 'Sin Nombre',
  314. selectedItem: mesaSeleccionada,
  315. onChanged: (dynamic nuevaMesa) {
  316. mesaSeleccionada = nuevaMesa as Mesa;
  317. },
  318. ),
  319. ],
  320. ),
  321. actions: [
  322. TextButton(
  323. onPressed: () => Navigator.of(context).pop(false),
  324. child: const Text('Cancelar'),
  325. style: ButtonStyle(
  326. padding: WidgetStatePropertyAll(
  327. EdgeInsets.fromLTRB(20, 10, 20, 10),
  328. ),
  329. backgroundColor: WidgetStatePropertyAll(Colors.red),
  330. foregroundColor: WidgetStatePropertyAll(AppTheme.secondary),
  331. ),
  332. ),
  333. const SizedBox(width: 10),
  334. TextButton(
  335. onPressed: () {
  336. if (mesaSeleccionada != null) {
  337. Navigator.of(context).pop(true);
  338. } else {
  339. ScaffoldMessenger.of(context).showSnackBar(
  340. const SnackBar(
  341. content: Text('Seleccione una mesa por favor'),
  342. ),
  343. );
  344. }
  345. },
  346. child: const Text('Guardar'),
  347. style: ButtonStyle(
  348. padding: WidgetStatePropertyAll(
  349. EdgeInsets.fromLTRB(20, 10, 20, 10),
  350. ),
  351. backgroundColor: WidgetStatePropertyAll(Colors.black),
  352. foregroundColor: WidgetStatePropertyAll(AppTheme.quaternary),
  353. ),
  354. ),
  355. ],
  356. );
  357. },
  358. );
  359. if (shouldSave ?? false) {
  360. Pedido nuevoPedido = Pedido(
  361. peticion: DateTime.now().toUtc().toIso8601String(),
  362. nombreCliente: nombreController.text,
  363. comentarios: descripcionController.text,
  364. estatus: 'NUEVO',
  365. idMesa: mesaSeleccionada?.id,
  366. totalPedido: totalPedido,
  367. descuento: selectedDescuento,
  368. uuid: Uuid().v4(),
  369. idUsuario: await SessionStorage().getId(),
  370. productos: carrito.map((item) {
  371. return PedidoProducto(
  372. idProducto: item.producto.id,
  373. producto: item.producto,
  374. costoUnitario: item.producto.precio.toString(),
  375. cantidad: item.cantidad,
  376. comentario: item.comentario,
  377. toppings: item.selectedToppings.entries.expand((entry) {
  378. return entry.value.map((toppingId) {
  379. return PedidoProductoTopping(
  380. idCategoria: entry.key,
  381. idTopping: toppingId,
  382. );
  383. });
  384. }).toList(),
  385. );
  386. }).toList(),
  387. );
  388. final corteViewModel =
  389. Provider.of<CorteCajaViewModel>(context, listen: false);
  390. CorteCaja? corteActivo = corteViewModel.cortes.firstWhereOrNull(
  391. (corte) => corte.fechaCorte == null,
  392. );
  393. if (corteActivo != null) {
  394. nuevoPedido.idCorteCaja = corteActivo.id;
  395. }
  396. bool result = await Provider.of<PedidoViewModel>(context, listen: false)
  397. .guardarPedidoLocal(pedido: nuevoPedido);
  398. if (result) {
  399. Navigator.of(context).pop();
  400. } else {
  401. print('Error al guardar el pedido');
  402. }
  403. }
  404. }
  405. Future<void> _guardarPedidoExistente({
  406. double? cantEfectivo,
  407. double? cantTarjeta,
  408. double? cantTransferencia,
  409. String? tipoPago,
  410. String? estatus,
  411. }) async {
  412. if (pedidoActual == null) return;
  413. DateTime now = DateTime.now().toUtc();
  414. pedidoActual!.totalPedido = totalPedido;
  415. pedidoActual!.descuento = selectedDescuento;
  416. pedidoActual!.modificado = now;
  417. pedidoActual!.sincronizado = null;
  418. if (cantEfectivo != null) pedidoActual!.cantEfectivo = cantEfectivo;
  419. if (cantTarjeta != null) pedidoActual!.cantTarjeta = cantTarjeta;
  420. if (cantTransferencia != null)
  421. pedidoActual!.cantTransferencia = cantTransferencia;
  422. if (tipoPago != null) pedidoActual!.tipoPago = tipoPago;
  423. if (estatus != null) pedidoActual!.estatus = estatus;
  424. bool result = await Provider.of<PedidoViewModel>(context, listen: false)
  425. .guardarPedidoConProductos(
  426. pedido: pedidoActual!,
  427. carrito: carrito,
  428. );
  429. if (result) {
  430. Pedido? pedidoCompleto =
  431. await Provider.of<PedidoViewModel>(context, listen: false)
  432. .fetchPedidoConProductos(pedidoActual!.id);
  433. if (estatus == "TERMINADO") {
  434. imprimirTicketsJuntos(context, pedidoCompleto!);
  435. }
  436. print("Pedido actualizado correctamente");
  437. Navigator.of(context).pop();
  438. } else {
  439. print("Error al actualizar el pedido");
  440. }
  441. }
  442. Future<void> _promptForCustomerName() async {
  443. TextEditingController efectivoController = TextEditingController();
  444. TextEditingController tarjetaController = TextEditingController();
  445. TextEditingController transferenciaController = TextEditingController();
  446. TextEditingController? nombreController;
  447. TextEditingController? comentarioController;
  448. if (pedidoActual == null || pedidoActual!.id == 0) {
  449. nombreController = TextEditingController();
  450. comentarioController = TextEditingController();
  451. }
  452. faltante = totalPedido;
  453. bool totalCompletado = false;
  454. bool efectivoCompleto = false;
  455. bool tarjetaCompleto = false;
  456. bool transferenciaCompleto = false;
  457. bool propinaExpandida = false;
  458. void _calcularCambio(StateSetter setState) {
  459. double totalPagado = (double.tryParse(efectivoController.text) ?? 0) +
  460. (double.tryParse(tarjetaController.text) ?? 0) +
  461. (double.tryParse(transferenciaController.text) ?? 0);
  462. setState(() {
  463. cambio = totalPagado - totalPedido;
  464. faltante = cambio < 0 ? totalPedido - totalPagado : 0;
  465. totalCompletado = cambio >= 0;
  466. });
  467. }
  468. bool? shouldSave = await showDialog<bool>(
  469. context: context,
  470. builder: (BuildContext context) {
  471. return StatefulBuilder(
  472. builder: (context, setState) {
  473. return AlertDialog(
  474. actionsPadding: EdgeInsets.fromLTRB(50, 10, 50, 30),
  475. title: const Text(
  476. 'Finalizar Pedido',
  477. style: TextStyle(fontSize: 22, fontWeight: FontWeight.w500),
  478. ),
  479. content: SingleChildScrollView(
  480. child: AnimatedSize(
  481. duration: const Duration(milliseconds: 300),
  482. curve: Curves.easeInOut,
  483. child: Column(
  484. crossAxisAlignment: CrossAxisAlignment.stretch,
  485. children: [
  486. if (nombreController != null)
  487. AppTextField(
  488. controller: nombreController,
  489. etiqueta: 'Nombre',
  490. hintText: "Nombre del Cliente",
  491. ),
  492. if (comentarioController != null) ...[
  493. const SizedBox(height: 10),
  494. AppTextField(
  495. controller: comentarioController,
  496. etiqueta: 'Comentarios (opcional)',
  497. hintText: 'Comentarios',
  498. maxLines: 2,
  499. ),
  500. ],
  501. const SizedBox(height: 10),
  502. Align(
  503. alignment: Alignment.center,
  504. child: Text(
  505. 'Métodos de pago',
  506. style: TextStyle(
  507. fontWeight: FontWeight.bold, fontSize: 20),
  508. ),
  509. ),
  510. const SizedBox(height: 10),
  511. // Efectivo
  512. _buildPaymentMethodRow(
  513. setState,
  514. label: 'Efectivo',
  515. selected: efectivoSeleccionado,
  516. exactSelected: efectivoCompleto,
  517. controller: efectivoController,
  518. onSelected: (value) {
  519. setState(() {
  520. efectivoSeleccionado = value;
  521. if (!efectivoSeleccionado) {
  522. efectivoCompleto = false;
  523. efectivoController.clear();
  524. }
  525. _calcularCambio(setState);
  526. });
  527. },
  528. onExactSelected: (value) {
  529. setState(() {
  530. efectivoCompleto = value;
  531. if (efectivoCompleto) {
  532. efectivoController.text =
  533. totalPedido.toStringAsFixed(2);
  534. efectivoSeleccionado = true;
  535. tarjetaSeleccionada = false;
  536. transferenciaSeleccionada = false;
  537. tarjetaController.clear();
  538. transferenciaController.clear();
  539. } else {
  540. efectivoController.clear();
  541. }
  542. _calcularCambio(setState);
  543. });
  544. },
  545. disableOtherMethods:
  546. tarjetaCompleto || transferenciaCompleto,
  547. onChangedMonto: () => _calcularCambio(setState),
  548. ),
  549. // Tarjeta
  550. _buildPaymentMethodRow(
  551. setState,
  552. label: 'Tarjeta',
  553. selected: tarjetaSeleccionada,
  554. exactSelected: tarjetaCompleto,
  555. controller: tarjetaController,
  556. sinCambio: true,
  557. onSelected: (value) {
  558. setState(() {
  559. tarjetaSeleccionada = value;
  560. if (!tarjetaSeleccionada) {
  561. tarjetaCompleto = false;
  562. tarjetaController.clear();
  563. }
  564. _calcularCambio(setState);
  565. });
  566. },
  567. onExactSelected: (value) {
  568. setState(() {
  569. tarjetaCompleto = value;
  570. if (tarjetaCompleto) {
  571. tarjetaController.text =
  572. totalPedido.toStringAsFixed(2);
  573. tarjetaSeleccionada = true;
  574. efectivoSeleccionado = false;
  575. transferenciaSeleccionada = false;
  576. efectivoController.clear();
  577. transferenciaController.clear();
  578. } else {
  579. tarjetaController.clear();
  580. }
  581. _calcularCambio(setState);
  582. });
  583. },
  584. disableOtherMethods:
  585. efectivoCompleto || transferenciaCompleto,
  586. onChangedMonto: () => _calcularCambio(setState),
  587. ),
  588. // Transferencia
  589. _buildPaymentMethodRow(
  590. setState,
  591. label: 'Transferencia',
  592. selected: transferenciaSeleccionada,
  593. exactSelected: transferenciaCompleto,
  594. controller: transferenciaController,
  595. sinCambio: true,
  596. onSelected: (value) {
  597. setState(() {
  598. transferenciaSeleccionada = value;
  599. if (!transferenciaSeleccionada) {
  600. transferenciaCompleto = false;
  601. transferenciaController.clear();
  602. }
  603. _calcularCambio(setState);
  604. });
  605. },
  606. onExactSelected: (value) {
  607. setState(() {
  608. transferenciaCompleto = value;
  609. if (transferenciaCompleto) {
  610. transferenciaController.text =
  611. totalPedido.toStringAsFixed(2);
  612. transferenciaSeleccionada = true;
  613. efectivoSeleccionado = false;
  614. tarjetaSeleccionada = false;
  615. efectivoController.clear();
  616. tarjetaController.clear();
  617. } else {
  618. transferenciaController.clear();
  619. }
  620. _calcularCambio(setState);
  621. });
  622. },
  623. disableOtherMethods:
  624. efectivoCompleto || tarjetaCompleto,
  625. onChangedMonto: () => _calcularCambio(setState),
  626. ),
  627. // Propina Expandable
  628. ExpansionTile(
  629. title: const Text(
  630. 'Agregar Propina',
  631. style: TextStyle(
  632. fontSize: 18, fontWeight: FontWeight.bold),
  633. ),
  634. initiallyExpanded: propinaExpandida,
  635. onExpansionChanged: (isExpanded) {
  636. setState(() {
  637. propinaExpandida = isExpanded;
  638. });
  639. },
  640. children: [
  641. AppTextField(
  642. separarMiles: true,
  643. controller: _propinaCantidadController,
  644. etiqueta: 'Propina',
  645. hintText: '0.00',
  646. keyboardType: TextInputType.number,
  647. ),
  648. const SizedBox(height: 10),
  649. AppTextField(
  650. controller: _propinaComentarioController,
  651. etiqueta: 'Comentario',
  652. hintText: 'Comentario',
  653. maxLines: 2,
  654. ),
  655. ],
  656. ),
  657. const SizedBox(height: 10),
  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. ),
  691. actions: [
  692. TextButton(
  693. child: const Text('Cancelar', style: TextStyle(fontSize: 18)),
  694. onPressed: () => Navigator.of(context).pop(false),
  695. style: ButtonStyle(
  696. padding: WidgetStatePropertyAll(
  697. EdgeInsets.fromLTRB(30, 20, 30, 20)),
  698. backgroundColor: WidgetStatePropertyAll(Colors.red),
  699. foregroundColor: WidgetStatePropertyAll(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: WidgetStatePropertyAll(
  710. EdgeInsets.fromLTRB(30, 20, 30, 20)),
  711. backgroundColor: WidgetStatePropertyAll(
  712. totalCompletado ? AppTheme.tertiary : Colors.grey),
  713. foregroundColor:
  714. WidgetStatePropertyAll(AppTheme.quaternary),
  715. ),
  716. ),
  717. ],
  718. );
  719. },
  720. );
  721. },
  722. );
  723. if (shouldSave ?? false) {
  724. if (pedidoActual != null && pedidoActual!.id != 0) {
  725. await _guardarPedidoExistente(
  726. cantEfectivo: double.tryParse(efectivoController.text),
  727. cantTarjeta: double.tryParse(tarjetaController.text),
  728. cantTransferencia: double.tryParse(transferenciaController.text),
  729. tipoPago: _obtenerTipoPago(),
  730. estatus: "TERMINADO",
  731. );
  732. await _guardarPropina(pedidoActual!.id!);
  733. } else {
  734. prepararPedidoActual(
  735. nombreController?.text ?? '',
  736. comentarioController?.text ?? '',
  737. efectivoController,
  738. tarjetaController,
  739. transferenciaController,
  740. );
  741. }
  742. }
  743. }
  744. Future<void> _guardarPropina(int idPedido) async {
  745. double? cantidad =
  746. double.tryParse(_propinaCantidadController.text.replaceAll(',', '')) ??
  747. 0.0;
  748. String comentario = _propinaComentarioController.text.trim();
  749. if (cantidad != null && cantidad > 0) {
  750. Propinas nuevaPropina = Propinas(
  751. idPedido: idPedido,
  752. cantidad: cantidad,
  753. comentario: comentario,
  754. sincronizado: null,
  755. creado: DateTime.now().toUtc(),
  756. );
  757. await Provider.of<PropinaViewModel>(context, listen: false)
  758. .guardarPropina(nuevaPropina);
  759. print('Propina guardada correctamente');
  760. } else {
  761. print('Propina no guardada (cantidad inválida)');
  762. }
  763. }
  764. Widget _buildPaymentMethodRow(
  765. StateSetter setState, {
  766. required String label,
  767. required bool selected,
  768. required bool exactSelected,
  769. required TextEditingController controller,
  770. required Function(bool) onSelected,
  771. required Function(bool) onExactSelected,
  772. required bool disableOtherMethods,
  773. required Function() onChangedMonto,
  774. bool sinCambio = false,
  775. }) {
  776. return Row(
  777. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  778. crossAxisAlignment: CrossAxisAlignment.center,
  779. children: [
  780. Row(
  781. children: [
  782. Checkbox(
  783. activeColor: AppTheme.primary,
  784. value: selected,
  785. onChanged: disableOtherMethods
  786. ? null
  787. : (value) {
  788. onSelected(value ?? false);
  789. },
  790. ),
  791. GestureDetector(
  792. onTap: disableOtherMethods
  793. ? null
  794. : () {
  795. onSelected(!selected);
  796. },
  797. child: Text(
  798. label,
  799. style:
  800. const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
  801. ),
  802. ),
  803. ],
  804. ),
  805. SizedBox(
  806. width: 180,
  807. child: Row(
  808. crossAxisAlignment: CrossAxisAlignment.start,
  809. children: [
  810. Column(
  811. children: [
  812. const Text(
  813. 'Exacto',
  814. style: TextStyle(
  815. fontSize: 18,
  816. fontWeight: FontWeight.bold,
  817. color: Colors.black),
  818. ),
  819. const SizedBox(height: 17),
  820. Checkbox(
  821. activeColor: AppTheme.primary,
  822. value: exactSelected,
  823. onChanged: !disableOtherMethods
  824. ? (value) {
  825. onExactSelected(value ?? false);
  826. if (value == true) {
  827. setState(() {
  828. disableOtherMethods = true;
  829. });
  830. }
  831. }
  832. : null,
  833. ),
  834. ],
  835. ),
  836. const SizedBox(width: 5),
  837. Expanded(
  838. child: AppTextField(
  839. controller: controller,
  840. enabled: selected,
  841. etiqueta: 'Cantidad',
  842. hintText: '0.00',
  843. keyboardType: TextInputType.number,
  844. onChanged: (value) {
  845. if (sinCambio) {
  846. double? input = double.tryParse(value) ?? 0;
  847. if (input > totalPedido) {
  848. controller.text = totalPedido.toStringAsFixed(2);
  849. controller.selection = TextSelection.fromPosition(
  850. TextPosition(offset: controller.text.length),
  851. );
  852. }
  853. }
  854. onChangedMonto();
  855. },
  856. ),
  857. ),
  858. ],
  859. ),
  860. ),
  861. ],
  862. );
  863. }
  864. void prepararPedidoActual(
  865. String nombreCliente,
  866. String comentarios,
  867. TextEditingController efectivoController,
  868. TextEditingController tarjetaController,
  869. TextEditingController transferenciaController,
  870. ) async {
  871. String now = DateTime.now().toUtc().toIso8601String();
  872. int? idUsuario = await SessionStorage().getId();
  873. double cantEfectivo = efectivoSeleccionado
  874. ? double.tryParse(efectivoController.text) ?? 0
  875. : 0;
  876. double cantTarjeta =
  877. tarjetaSeleccionada ? double.tryParse(tarjetaController.text) ?? 0 : 0;
  878. double cantTransferencia = transferenciaSeleccionada
  879. ? double.tryParse(transferenciaController.text) ?? 0
  880. : 0;
  881. Pedido nuevoPedido = Pedido(
  882. peticion: now,
  883. nombreCliente: nombreCliente,
  884. comentarios: comentarios,
  885. estatus: "TERMINADO",
  886. totalPedido: totalPedido,
  887. descuento: pedidoActual?.descuento,
  888. idUsuario: idUsuario,
  889. tipoPago: _obtenerTipoPago(),
  890. cantEfectivo: cantEfectivo,
  891. cantTarjeta: cantTarjeta,
  892. cantTransferencia: cantTransferencia,
  893. uuid: Uuid().v4(),
  894. );
  895. final corteViewModel =
  896. Provider.of<CorteCajaViewModel>(context, listen: false);
  897. CorteCaja? corteActivo = corteViewModel.cortes.firstWhereOrNull(
  898. (corte) => corte.fechaCorte == null,
  899. );
  900. if (corteActivo != null) {
  901. nuevoPedido.idCorteCaja = corteActivo.id;
  902. }
  903. List<PedidoProducto> listaPedidoProducto = carrito.map((item) {
  904. List<PedidoProductoTopping> selectedToppings = [];
  905. item.selectedToppings.forEach((categoryId, selectedToppingIds) {
  906. for (int toppingId in selectedToppingIds) {
  907. selectedToppings.add(PedidoProductoTopping(
  908. idCategoria: categoryId,
  909. idTopping: toppingId,
  910. ));
  911. }
  912. });
  913. return PedidoProducto(
  914. idProducto: item.producto.id,
  915. producto: item.producto,
  916. costoUnitario: item.producto.precio.toString(),
  917. cantidad: item.cantidad,
  918. comentario: item.comentario,
  919. toppings: selectedToppings,
  920. );
  921. }).toList();
  922. nuevoPedido.productos = listaPedidoProducto;
  923. bool result = await Provider.of<PedidoViewModel>(context, listen: false)
  924. .guardarPedidoLocal(pedido: nuevoPedido);
  925. if (!mounted) return;
  926. if (result) {
  927. Pedido? pedidoCompleto =
  928. await Provider.of<PedidoViewModel>(context, listen: false)
  929. .fetchPedidoConProductos(nuevoPedido.id!);
  930. if (pedidoCompleto != null) {
  931. imprimirTicketsJuntos(context, pedidoCompleto);
  932. }
  933. Navigator.of(context).pop();
  934. } else {
  935. print("Error al guardar el pedido");
  936. }
  937. }
  938. String _obtenerTipoPago() {
  939. List<String> tiposPago = [];
  940. if (efectivoSeleccionado) tiposPago.add('Efectivo');
  941. if (tarjetaSeleccionada) tiposPago.add('Tarjeta');
  942. if (transferenciaSeleccionada) tiposPago.add('Transferencia');
  943. return tiposPago.isNotEmpty ? tiposPago.join(',') : 'No Definido';
  944. }
  945. void _limpiarBusqueda() async {
  946. setState(() {
  947. _busqueda.text = '';
  948. _estadoBusqueda = false;
  949. });
  950. await cargarCategoriasIniciales();
  951. }
  952. Future<void> cargarProductosIniciales() async {
  953. setState(() => _isLoading = true);
  954. await Provider.of<ProductoViewModel>(context, listen: false)
  955. .fetchLocalAll();
  956. productos =
  957. Provider.of<ProductoViewModel>(context, listen: false).productos;
  958. setState(() => _isLoading = false);
  959. }
  960. @override
  961. void dispose() {
  962. _gridViewController.dispose();
  963. _searchController.dispose();
  964. _categoryScrollController.dispose();
  965. super.dispose();
  966. }
  967. Future<void> cargarCategoriasIniciales() async {
  968. setState(() => _isLoading = true);
  969. await Provider.of<CategoriaProductoViewModel>(context, listen: false)
  970. .fetchLocalAll();
  971. categorias = Provider.of<CategoriaProductoViewModel>(context, listen: false)
  972. .categoriaProductos;
  973. if (categorias.isNotEmpty) {
  974. categoriaSeleccionada = categorias.first;
  975. cargarProductosPorCategoria(categoriaSeleccionada!.id);
  976. }
  977. setState(() => _isLoading = false);
  978. if (categorias.isNotEmpty) {
  979. categoriaSeleccionada = categorias.first;
  980. }
  981. }
  982. void cargarProductosPorCategoria(int categoriaId) async {
  983. setState(() => _isLoading = true);
  984. await Provider.of<ProductoViewModel>(context, listen: false)
  985. .fetchAllByCategory(categoriaId);
  986. productos =
  987. Provider.of<ProductoViewModel>(context, listen: false).productos;
  988. setState(() => _isLoading = false);
  989. }
  990. void agregarAlCarrito(Producto producto) async {
  991. var existente = carrito.firstWhereOrNull((item) =>
  992. item.producto.id == producto.id &&
  993. mapEquals(item.selectedToppings, {}));
  994. if (existente != null) {
  995. setState(() {
  996. existente.cantidad++;
  997. });
  998. } else {
  999. Map<int, List<Producto>> toppingsSeleccionables =
  1000. await obtenerToppingsSeleccionables(producto);
  1001. setState(() {
  1002. carrito.add(ItemCarrito(
  1003. producto: producto,
  1004. cantidad: 1,
  1005. selectableToppings: toppingsSeleccionables,
  1006. ));
  1007. });
  1008. }
  1009. _recalcularTotal();
  1010. }
  1011. Future<Map<int, List<Producto>>> obtenerToppingsSeleccionables(
  1012. Producto producto) async {
  1013. Map<int, List<Producto>> toppingsSeleccionables = {};
  1014. final toppingCategories =
  1015. await pvm.obtenerToppingsPorProducto(producto.id!);
  1016. for (int toppingId in toppingCategories) {
  1017. Producto? topping = await pvm.obtenerProductoPorId(toppingId);
  1018. if (topping != null && topping.idCategoria != null) {
  1019. toppingsSeleccionables[topping.idCategoria!] ??= [];
  1020. toppingsSeleccionables[topping.idCategoria]!.add(topping);
  1021. }
  1022. }
  1023. return toppingsSeleccionables;
  1024. }
  1025. void quitarDelCarrito(Producto producto) {
  1026. setState(() {
  1027. var indice =
  1028. carrito.indexWhere((item) => item.producto.id == producto.id);
  1029. if (indice != -1) {
  1030. if (carrito[indice].cantidad > 1) {
  1031. carrito[indice].cantidad--;
  1032. } else {
  1033. carrito.removeAt(indice);
  1034. }
  1035. }
  1036. });
  1037. }
  1038. void addToCart(Producto producto) {
  1039. var existingIndex = carrito.indexWhere((item) =>
  1040. item.producto.id == producto.id &&
  1041. mapEquals(item.selectedToppings, {}));
  1042. if (existingIndex != -1) {
  1043. setState(() {
  1044. carrito[existingIndex].cantidad++;
  1045. });
  1046. } else {
  1047. setState(() {
  1048. carrito.add(ItemCarrito(producto: producto, cantidad: 1));
  1049. });
  1050. }
  1051. }
  1052. void finalizeCustomization() {
  1053. if (_productoActual != null) {
  1054. addToCart(_productoActual!);
  1055. setState(() {
  1056. _productoActual = null;
  1057. });
  1058. }
  1059. }
  1060. Widget _buildDiscountSection() {
  1061. return Padding(
  1062. padding: const EdgeInsets.symmetric(horizontal: 8.0),
  1063. child: Row(
  1064. crossAxisAlignment: CrossAxisAlignment.center,
  1065. children: [
  1066. const Text(
  1067. 'Descuento',
  1068. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
  1069. ),
  1070. const Spacer(),
  1071. ConstrainedBox(
  1072. constraints: const BoxConstraints(
  1073. maxWidth: 150,
  1074. ),
  1075. child: Consumer<DescuentoViewModel>(
  1076. builder: (context, viewModel, child) {
  1077. return AppDropdownModel<int>(
  1078. hint: 'Seleccionar',
  1079. items: viewModel.descuentos
  1080. .map(
  1081. (descuento) => DropdownMenuItem<int>(
  1082. value: descuento.porcentaje,
  1083. child: Text(
  1084. '${descuento.porcentaje}%',
  1085. style: const TextStyle(color: Colors.black),
  1086. ),
  1087. ),
  1088. )
  1089. .toList(),
  1090. selectedValue: selectedDescuento,
  1091. onChanged: (value) async {
  1092. if (value != null && value != selectedDescuento) {
  1093. // Guardar el valor anterior
  1094. final previousValue = selectedDescuento;
  1095. // Actualizar el descuento temporalmente
  1096. print('Descuento temporal seleccionado: $value');
  1097. setState(() {
  1098. selectedDescuento = value;
  1099. });
  1100. // Mostrar cuadro de confirmación
  1101. final authenticated = await showDialog<bool>(
  1102. context: context,
  1103. builder: (context) {
  1104. return TotpCuadroConfirmacion(
  1105. title: "Aplicar Descuento",
  1106. content:
  1107. "Por favor, ingresa el código de autenticación para continuar.",
  1108. onSuccess: () {
  1109. // Confirmación exitosa: recalcular total y actualizar UI
  1110. print(
  1111. 'Autenticación exitosa. Aplicando descuento...');
  1112. setState(() {
  1113. _recalcularTotal();
  1114. print('Descuento aplicado: $selectedDescuento');
  1115. print('Total recalculado: $totalPedido');
  1116. });
  1117. },
  1118. );
  1119. },
  1120. );
  1121. // Si la autenticación falla, revertir el descuento
  1122. if (authenticated != true) {
  1123. print(
  1124. 'Autenticación fallida. Revirtiendo descuento...');
  1125. setState(() {
  1126. selectedDescuento = previousValue;
  1127. _recalcularTotal(); // Recalcular con el valor anterior
  1128. print('Descuento revertido: $selectedDescuento');
  1129. print(
  1130. 'Total recalculado tras revertir: $totalPedido');
  1131. });
  1132. }
  1133. }
  1134. },
  1135. );
  1136. },
  1137. ),
  1138. ),
  1139. ],
  1140. ),
  1141. );
  1142. }
  1143. Widget _buildTotalSection() {
  1144. String formattedsubtotal = _numberFormat.format(subtotal);
  1145. String formattedPrecioDescuento = _numberFormat.format(precioDescuento);
  1146. String formattedtotalPedido = _numberFormat.format(totalPedido);
  1147. return Padding(
  1148. padding: const EdgeInsets.symmetric(horizontal: 8.0),
  1149. child: Column(
  1150. crossAxisAlignment: CrossAxisAlignment.start,
  1151. children: [
  1152. if (precioDescuento > 0)
  1153. Column(
  1154. children: [
  1155. Row(
  1156. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  1157. children: [
  1158. const Text('Subtotal',
  1159. style: TextStyle(
  1160. fontWeight: FontWeight.bold, fontSize: 18)),
  1161. Text("\$$formattedsubtotal",
  1162. style: const TextStyle(
  1163. fontWeight: FontWeight.bold, fontSize: 18)),
  1164. ],
  1165. ),
  1166. const SizedBox(height: 10),
  1167. Row(
  1168. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  1169. children: [
  1170. const Text('Descuento',
  1171. style: TextStyle(
  1172. fontWeight: FontWeight.bold, fontSize: 18)),
  1173. Text("-\$$formattedPrecioDescuento",
  1174. style: const TextStyle(
  1175. fontWeight: FontWeight.bold, fontSize: 18)),
  1176. ],
  1177. ),
  1178. ],
  1179. ),
  1180. const SizedBox(height: 10),
  1181. Row(
  1182. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  1183. children: [
  1184. const Text('Total',
  1185. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
  1186. Text("\$$formattedtotalPedido",
  1187. style: const TextStyle(
  1188. fontWeight: FontWeight.bold, fontSize: 18)),
  1189. ],
  1190. ),
  1191. ],
  1192. ),
  1193. );
  1194. }
  1195. @override
  1196. Widget build(BuildContext context) {
  1197. return Scaffold(
  1198. appBar: AppBar(
  1199. title:
  1200. Text("Crear Pedido", style: TextStyle(color: AppTheme.secondary)),
  1201. iconTheme: IconThemeData(color: AppTheme.secondary)),
  1202. body: Row(
  1203. children: [
  1204. Flexible(
  1205. flex: 3,
  1206. child: _buildCartSection(),
  1207. ),
  1208. SizedBox(width: 35),
  1209. Flexible(flex: 7, child: _buildProductsSection()),
  1210. ],
  1211. ),
  1212. );
  1213. }
  1214. Widget _buildCartSection() {
  1215. return Card(
  1216. margin: const EdgeInsets.all(8.0),
  1217. child: Column(
  1218. children: [
  1219. const Padding(
  1220. padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0),
  1221. child: Row(
  1222. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  1223. children: [
  1224. Text('Producto',
  1225. style:
  1226. TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
  1227. Text('Cantidad',
  1228. style:
  1229. TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
  1230. ],
  1231. ),
  1232. ),
  1233. Expanded(
  1234. child: ListView.builder(
  1235. itemCount: carrito.length,
  1236. itemBuilder: (context, index) {
  1237. final item = carrito[index];
  1238. return Column(
  1239. children: [
  1240. Row(
  1241. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  1242. children: [
  1243. Expanded(
  1244. child: Column(
  1245. crossAxisAlignment: CrossAxisAlignment.start,
  1246. children: [
  1247. Text(item.producto.nombre!,
  1248. style: const TextStyle(
  1249. fontWeight: FontWeight.w600,
  1250. fontSize: 16)),
  1251. Text('\$${item.producto.precio}',
  1252. style: const TextStyle(
  1253. fontWeight: FontWeight.bold,
  1254. fontSize: 14,
  1255. color: Color(0xFF008000))),
  1256. ],
  1257. ),
  1258. ),
  1259. Row(
  1260. children: [
  1261. IconButton(
  1262. icon: const Icon(Icons.delete, color: Colors.red),
  1263. onPressed: () =>
  1264. eliminarProductoDelCarrito(index),
  1265. ),
  1266. IconButton(
  1267. icon: const Icon(Icons.remove),
  1268. onPressed: () => quitarProductoDelCarrito(item),
  1269. ),
  1270. Text('${item.cantidad}',
  1271. style: const TextStyle(
  1272. fontWeight: FontWeight.bold, fontSize: 14)),
  1273. IconButton(
  1274. icon: const Icon(Icons.add),
  1275. onPressed: () => incrementarProducto(item),
  1276. ),
  1277. IconButton(
  1278. icon:
  1279. Icon(Icons.message, color: AppTheme.tertiary),
  1280. onPressed: () {
  1281. setState(() {
  1282. item.expandido = !item.expandido;
  1283. });
  1284. },
  1285. ),
  1286. ],
  1287. ),
  1288. ],
  1289. ),
  1290. if (item.expandido) ...[
  1291. const SizedBox(height: 5),
  1292. Padding(
  1293. padding: const EdgeInsets.symmetric(horizontal: 16.0),
  1294. child: TextField(
  1295. controller: item.comentarioController,
  1296. decoration: const InputDecoration(
  1297. hintText: 'Agregar un comentario...',
  1298. border: OutlineInputBorder(),
  1299. ),
  1300. maxLines: 2,
  1301. onChanged: (value) {
  1302. item.comentario = value.trim();
  1303. },
  1304. ),
  1305. ),
  1306. ],
  1307. // ExpansionTile para todos los toppings
  1308. if (item.selectableToppings.isNotEmpty)
  1309. ExpansionTile(
  1310. initiallyExpanded: true,
  1311. title: const Text(
  1312. 'Toppings',
  1313. style: TextStyle(
  1314. fontWeight: FontWeight.bold, fontSize: 16),
  1315. ),
  1316. children: item.selectableToppings.entries.map((entry) {
  1317. final categoryId = entry.key;
  1318. final availableToppings = entry.value;
  1319. final categoria =
  1320. categorias.firstWhere((c) => c.id == categoryId);
  1321. return ExpansionTile(
  1322. initiallyExpanded: true,
  1323. title: Text(
  1324. '${categoria.nombre}'
  1325. ' (Hasta ${categoria.maximo ?? 0})'
  1326. ' ${(categoria.minimo ?? 0) > 0 ? " (Mínimo ${categoria.minimo})" : ""}',
  1327. style: const TextStyle(
  1328. fontWeight: FontWeight.bold,
  1329. fontSize: 16,
  1330. ),
  1331. ),
  1332. children: availableToppings.map((topping) {
  1333. bool isSelected = item
  1334. .selectedToppings[categoryId]
  1335. ?.contains(topping.id) ??
  1336. false;
  1337. return CheckboxListTile(
  1338. activeColor: AppTheme.primary,
  1339. title: Row(
  1340. mainAxisAlignment:
  1341. MainAxisAlignment.spaceBetween,
  1342. children: [
  1343. Text(topping.nombre!),
  1344. if (topping.precio != 0.0)
  1345. Text(
  1346. '+\$${topping.precio}',
  1347. style: const TextStyle(
  1348. color: Colors.black,
  1349. fontSize: 13,
  1350. ),
  1351. ),
  1352. ],
  1353. ),
  1354. value: isSelected,
  1355. onChanged: (bool? value) {
  1356. final maximoToppings = categoria.maximo ?? 0;
  1357. setState(() {
  1358. if (value == true) {
  1359. if ((item.selectedToppings[categoryId]
  1360. ?.length ??
  1361. 0) >=
  1362. maximoToppings) {
  1363. item.selectedToppings[categoryId]!
  1364. .remove(item
  1365. .selectedToppings[categoryId]!
  1366. .first);
  1367. }
  1368. item.selectedToppings[categoryId] ??=
  1369. <int>{};
  1370. item.selectedToppings[categoryId]!
  1371. .add(topping.id!);
  1372. } else {
  1373. item.selectedToppings[categoryId]
  1374. ?.remove(topping.id!);
  1375. if (item.selectedToppings[categoryId]
  1376. ?.isEmpty ??
  1377. false) {
  1378. item.selectedToppings
  1379. .remove(categoryId);
  1380. }
  1381. }
  1382. _recalcularTotal();
  1383. });
  1384. },
  1385. );
  1386. }).toList(),
  1387. );
  1388. }).toList(),
  1389. ),
  1390. const Divider(),
  1391. ],
  1392. );
  1393. },
  1394. ),
  1395. ),
  1396. if (pedidoActual != null && pedidoActual!.id! > 0)
  1397. _buildMesaSelector(),
  1398. if (pedidoActual != null && pedidoActual!.id! > 0)
  1399. const SizedBox(height: 5),
  1400. _buildDiscountSection(),
  1401. const Divider(thickness: 5),
  1402. _buildTotalSection(),
  1403. const SizedBox(height: 25),
  1404. Padding(
  1405. padding: const EdgeInsets.all(8.0),
  1406. child: Column(
  1407. children: [
  1408. if (pedidoActual != null && pedidoActual!.id! > 0) ...[
  1409. ElevatedButton(
  1410. onPressed: () => _guardarPedidoExistente(),
  1411. style: ElevatedButton.styleFrom(
  1412. backgroundColor: AppTheme.tertiary,
  1413. textStyle: const TextStyle(fontSize: 22),
  1414. fixedSize: const Size(250, 50),
  1415. ),
  1416. child: Text(
  1417. 'Actualizar Pedido',
  1418. style: TextStyle(color: AppTheme.quaternary),
  1419. ),
  1420. ),
  1421. const SizedBox(height: 10),
  1422. ElevatedButton(
  1423. onPressed: () => _promptForCustomerName(),
  1424. style: ElevatedButton.styleFrom(
  1425. backgroundColor: Colors.green,
  1426. textStyle: const TextStyle(fontSize: 22),
  1427. fixedSize: const Size(250, 50),
  1428. ),
  1429. child: Text(
  1430. 'Finalizar Pedido',
  1431. style: TextStyle(color: Colors.white),
  1432. ),
  1433. ),
  1434. ] else ...[
  1435. if (_isMesasActive)
  1436. ElevatedButton(
  1437. onPressed: () => _crearPedidoConModal(),
  1438. style: ElevatedButton.styleFrom(
  1439. backgroundColor: AppTheme.tertiary,
  1440. textStyle: const TextStyle(fontSize: 22),
  1441. fixedSize: const Size(250, 50),
  1442. ),
  1443. child: Text(
  1444. 'Iniciar Pedido',
  1445. style: TextStyle(color: AppTheme.quaternary),
  1446. ),
  1447. ),
  1448. const SizedBox(height: 5),
  1449. ElevatedButton(
  1450. onPressed: () => _promptForCustomerName(),
  1451. style: ElevatedButton.styleFrom(
  1452. backgroundColor: AppTheme.rojo,
  1453. textStyle: const TextStyle(fontSize: 18),
  1454. fixedSize: const Size(250, 50),
  1455. ),
  1456. child: Text(
  1457. 'Finalizar Pedido Sin Mesa',
  1458. style: TextStyle(color: AppTheme.quaternary),
  1459. ),
  1460. ),
  1461. ]
  1462. ],
  1463. ),
  1464. ),
  1465. ],
  1466. ),
  1467. );
  1468. }
  1469. void _mostrarDialogoComentario(
  1470. BuildContext context, ItemCarrito item, int index) {
  1471. TextEditingController comentarioController =
  1472. TextEditingController(text: carrito[index].comentario ?? '');
  1473. showDialog(
  1474. context: context,
  1475. builder: (BuildContext context) {
  1476. return AlertDialog(
  1477. title: const Text(
  1478. 'Comentario del Producto',
  1479. style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
  1480. ),
  1481. content: TextField(
  1482. controller: comentarioController,
  1483. maxLines: 3,
  1484. decoration: const InputDecoration(
  1485. hintText: 'Escribe un comentario...',
  1486. border: OutlineInputBorder(),
  1487. ),
  1488. ),
  1489. actions: [
  1490. TextButton(
  1491. onPressed: () => Navigator.of(context).pop(),
  1492. child: const Text('Cancelar'),
  1493. ),
  1494. TextButton(
  1495. onPressed: () {
  1496. setState(() {
  1497. carrito[index].comentario = comentarioController.text.trim();
  1498. });
  1499. Navigator.of(context).pop();
  1500. ScaffoldMessenger.of(context).showSnackBar(
  1501. const SnackBar(content: Text('Comentario guardado')),
  1502. );
  1503. },
  1504. child: const Text('Guardar'),
  1505. ),
  1506. ],
  1507. );
  1508. },
  1509. );
  1510. }
  1511. void eliminarProductoDelCarrito(int index) async {
  1512. bool autorizado = true;
  1513. // Solicitar autenticación solo si el pedido tiene un ID mayor a 0
  1514. if (pedidoActual != null && pedidoActual!.id! > 0) {
  1515. autorizado = await autenticarConCodigo(context);
  1516. }
  1517. if (autorizado) {
  1518. final producto = carrito[index].producto;
  1519. // Verificar si el pedido actual tiene un ID mayor a 0
  1520. if (pedidoActual != null && pedidoActual!.id! > 0) {
  1521. final pedidoProducto = pedidoActual!.productos
  1522. .firstWhereOrNull((p) => p.idProducto == producto.id);
  1523. if (pedidoProducto != null) {
  1524. // Marcar como eliminado en la base de datos
  1525. await Provider.of<PedidoViewModel>(context, listen: false)
  1526. .eliminarProductoDelPedido(producto.id!, pedidoActual!.id!);
  1527. }
  1528. }
  1529. // Remover el producto del carrito en la UI
  1530. setState(() {
  1531. carrito.removeAt(index);
  1532. });
  1533. // Recalcular el total
  1534. _recalcularTotal();
  1535. // Mostrar mensaje de confirmación
  1536. ScaffoldMessenger.of(context).showSnackBar(
  1537. const SnackBar(content: Text("Producto eliminado del carrito.")),
  1538. );
  1539. }
  1540. }
  1541. void incrementarProducto(ItemCarrito item) {
  1542. setState(() {
  1543. item.cantidad++;
  1544. });
  1545. _recalcularTotal();
  1546. }
  1547. void quitarProductoDelCarrito(ItemCarrito item) async {
  1548. bool autorizado = true;
  1549. if (pedidoActual != null && pedidoActual!.id! > 0) {
  1550. autorizado = await autenticarConCodigo(context);
  1551. }
  1552. if (autorizado) {
  1553. setState(() {
  1554. if (item.cantidad > 1) {
  1555. item.cantidad--;
  1556. } else {
  1557. if (pedidoActual != null && pedidoActual!.id! > 0) {
  1558. final pedidoProducto = pedidoActual!.productos
  1559. .firstWhereOrNull((p) => p.idProducto == item.producto.id);
  1560. if (pedidoProducto != null) {
  1561. Provider.of<PedidoViewModel>(context, listen: false)
  1562. .eliminarProductoDelPedido(
  1563. item.producto.id!, pedidoActual!.id!);
  1564. }
  1565. }
  1566. carrito.remove(item);
  1567. }
  1568. });
  1569. _recalcularTotal();
  1570. // Mostrar mensaje de confirmación
  1571. ScaffoldMessenger.of(context).showSnackBar(
  1572. SnackBar(
  1573. content: Text(item.cantidad > 0
  1574. ? "Cantidad del producto actualizada."
  1575. : "Producto marcado como eliminado."),
  1576. ),
  1577. );
  1578. }
  1579. }
  1580. Widget _buildProductsSection() {
  1581. return Column(
  1582. children: [
  1583. const SizedBox(height: 5),
  1584. _buildSearchBar(),
  1585. const SizedBox(height: 10),
  1586. _buildCategoryButtons(),
  1587. const SizedBox(height: 15),
  1588. Expanded(
  1589. child: Consumer<ProductoViewModel>(builder: (context, model, child) {
  1590. productos = model.productos;
  1591. return GridView.builder(
  1592. controller: _gridViewController,
  1593. key: ValueKey<int>(categoriaSeleccionada?.id ?? 0),
  1594. gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
  1595. crossAxisCount: 3,
  1596. childAspectRatio: 3 / 2,
  1597. ),
  1598. itemCount: productos.length,
  1599. itemBuilder: (context, index) {
  1600. final producto = productos[index];
  1601. // Si no se está buscando, aplicar el filtro de categoría
  1602. if (!_estadoBusqueda &&
  1603. producto.idCategoria != categoriaSeleccionada?.id) {
  1604. return Container();
  1605. }
  1606. return Card(
  1607. child: InkWell(
  1608. onTap: () => agregarAlCarrito(producto),
  1609. child: Column(
  1610. mainAxisAlignment: MainAxisAlignment.center,
  1611. children: [
  1612. if (producto.imagen != null &&
  1613. File(producto.imagen!).existsSync())
  1614. Image.file(
  1615. File(producto.imagen!),
  1616. height: 120,
  1617. fit: BoxFit.cover,
  1618. )
  1619. else
  1620. const Icon(Icons.fastfood, size: 80),
  1621. const SizedBox(height: 8),
  1622. Padding(
  1623. padding: const EdgeInsets.symmetric(horizontal: 8.0),
  1624. child: Text(
  1625. producto.nombre ?? '',
  1626. style: const TextStyle(
  1627. fontSize: 16,
  1628. fontWeight: FontWeight.bold,
  1629. ),
  1630. textAlign: TextAlign.center,
  1631. ),
  1632. ),
  1633. const SizedBox(height: 8),
  1634. Text(
  1635. '\$${producto.precio}',
  1636. style: const TextStyle(
  1637. fontSize: 16,
  1638. fontWeight: FontWeight.bold,
  1639. color: Color(0xFF008000),
  1640. ),
  1641. ),
  1642. ],
  1643. ),
  1644. ),
  1645. );
  1646. },
  1647. );
  1648. }),
  1649. )
  1650. ],
  1651. );
  1652. }
  1653. Widget _buildCategoryButtons() {
  1654. List<CategoriaProducto> categoriasFiltradas =
  1655. categorias.where((categoria) => categoria.esToping == 0).toList();
  1656. return Container(
  1657. height: 65,
  1658. child: Scrollbar(
  1659. thumbVisibility: true,
  1660. trackVisibility: true,
  1661. interactive: true,
  1662. controller: _categoryScrollController,
  1663. child: Padding(
  1664. padding: EdgeInsets.fromLTRB(0, 0, 0, 15),
  1665. child: ListView.builder(
  1666. controller: _categoryScrollController,
  1667. scrollDirection: Axis.horizontal,
  1668. itemCount: categoriasFiltradas.length,
  1669. itemBuilder: (context, index) {
  1670. final categoria = categoriasFiltradas[index];
  1671. bool isSelected = categoriaSeleccionada?.id == categoria.id;
  1672. return Padding(
  1673. padding: const EdgeInsets.symmetric(horizontal: 4.0),
  1674. child: ElevatedButton(
  1675. onPressed: () {
  1676. cargarProductosPorCategoria(categoria.id);
  1677. setState(() {
  1678. categoriaSeleccionada = categoria;
  1679. });
  1680. },
  1681. style: ElevatedButton.styleFrom(
  1682. backgroundColor:
  1683. isSelected ? AppTheme.tertiary : Colors.grey,
  1684. foregroundColor:
  1685. isSelected ? AppTheme.quaternary : AppTheme.secondary,
  1686. ),
  1687. child: Text(categoria.nombre!),
  1688. ),
  1689. );
  1690. },
  1691. ),
  1692. ),
  1693. ),
  1694. );
  1695. }
  1696. Widget _buildSearchBar() {
  1697. return Padding(
  1698. padding: const EdgeInsets.all(8.0),
  1699. child: TextField(
  1700. controller: _searchController,
  1701. decoration: InputDecoration(
  1702. hintText: 'Buscar producto...',
  1703. prefixIcon: const Icon(Icons.search),
  1704. suffixIcon: IconButton(
  1705. icon: Icon(Icons.clear),
  1706. onPressed: () {
  1707. _searchController.clear();
  1708. _onSearchChanged('');
  1709. },
  1710. ),
  1711. border: OutlineInputBorder(
  1712. borderRadius: BorderRadius.circular(12.0),
  1713. ),
  1714. enabledBorder: OutlineInputBorder(
  1715. borderRadius: BorderRadius.circular(12.0),
  1716. borderSide: BorderSide(width: 1.5)),
  1717. focusedBorder: OutlineInputBorder(
  1718. borderSide: BorderSide(color: Colors.black),
  1719. borderRadius: BorderRadius.circular(12.0),
  1720. ),
  1721. ),
  1722. onChanged: _onSearchChanged,
  1723. ),
  1724. );
  1725. }
  1726. Future<bool> autenticarConCodigo(BuildContext context) async {
  1727. final TextEditingController codeController = TextEditingController();
  1728. return await showDialog<bool>(
  1729. context: context,
  1730. builder: (context) {
  1731. return AlertDialog(
  1732. title: const Text("Autenticación requerida",
  1733. style: TextStyle(fontWeight: FontWeight.w500, fontSize: 22)),
  1734. content: Column(
  1735. mainAxisSize: MainAxisSize.min,
  1736. children: [
  1737. const Text(
  1738. "Por favor, introduce el código de autenticación generado.",
  1739. style: TextStyle(fontSize: 18),
  1740. ),
  1741. const SizedBox(height: 16),
  1742. TextField(
  1743. controller: codeController,
  1744. decoration: const InputDecoration(
  1745. labelText: "Código de autenticación",
  1746. ),
  1747. ),
  1748. ],
  1749. ),
  1750. actions: [
  1751. Row(
  1752. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  1753. children: [
  1754. TextButton(
  1755. onPressed: () => Navigator.of(context).pop(false),
  1756. child: Text('Cancelar',
  1757. style: TextStyle(
  1758. fontSize: 18, color: AppTheme.secondary)),
  1759. style: ButtonStyle(
  1760. padding: WidgetStatePropertyAll(
  1761. EdgeInsets.fromLTRB(20, 10, 20, 10)),
  1762. backgroundColor: WidgetStatePropertyAll(Colors.red),
  1763. foregroundColor:
  1764. WidgetStatePropertyAll(AppTheme.secondary)),
  1765. ),
  1766. TextButton(
  1767. onPressed: () {
  1768. final now = DateTime.now().toUtc();
  1769. timezone.initializeTimeZones();
  1770. final pacificTimeZone =
  1771. timezone.getLocation('America/Los_Angeles');
  1772. final date =
  1773. timezone.TZDateTime.from(now, pacificTimeZone);
  1774. final codigoTotp = OTP.generateTOTPCodeString(
  1775. 'TYSNE4CMT5LVLGWS',
  1776. date.millisecondsSinceEpoch,
  1777. algorithm: Algorithm.SHA1,
  1778. isGoogle: true,
  1779. );
  1780. String codigo = codeController.text.trim();
  1781. List<String> codigosEstaticos = [
  1782. '172449',
  1783. '827329',
  1784. // Agregar más códigos estáticos si es necesario
  1785. ];
  1786. bool esCodigoValido =
  1787. codigosEstaticos.contains(codigo) ||
  1788. codigo == codigoTotp;
  1789. if (!esCodigoValido) {
  1790. ScaffoldMessenger.of(context).showSnackBar(
  1791. const SnackBar(
  1792. content: Text('El código no es correcto'),
  1793. duration: Duration(seconds: 2),
  1794. ),
  1795. );
  1796. return;
  1797. }
  1798. Navigator.of(context).pop(true);
  1799. },
  1800. child: Text('Confirmar',
  1801. style: TextStyle(
  1802. fontSize: 18, color: AppTheme.quaternary)),
  1803. style: ButtonStyle(
  1804. padding: WidgetStatePropertyAll(
  1805. EdgeInsets.fromLTRB(20, 10, 20, 10)),
  1806. backgroundColor:
  1807. WidgetStatePropertyAll(AppTheme.secondary),
  1808. foregroundColor:
  1809. WidgetStatePropertyAll(AppTheme.secondary)),
  1810. ),
  1811. ],
  1812. )
  1813. ],
  1814. );
  1815. },
  1816. ) ??
  1817. false;
  1818. }
  1819. Widget _buildMesaSelector() {
  1820. return FutureBuilder(
  1821. future: Provider.of<MesaViewModel>(context, listen: false)
  1822. .fetchLocalAll(sinLimite: true),
  1823. builder: (context, snapshot) {
  1824. if (snapshot.connectionState == ConnectionState.waiting) {
  1825. return const Center(child: CircularProgressIndicator());
  1826. }
  1827. List<Mesa> mesasDisponibles =
  1828. Provider.of<MesaViewModel>(context, listen: false).mesas;
  1829. TextEditingController mesaSearchController = TextEditingController();
  1830. return Padding(
  1831. padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
  1832. child: Row(
  1833. crossAxisAlignment: CrossAxisAlignment.center,
  1834. children: [
  1835. const Text(
  1836. 'Mesa:',
  1837. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
  1838. ),
  1839. const SizedBox(width: 16),
  1840. Expanded(
  1841. child: AppDropdownSearch(
  1842. controller: mesaSearchController,
  1843. asyncItems: (String query) async {
  1844. await Provider.of<MesaViewModel>(context, listen: false)
  1845. .fetchLocalByName(nombre: query);
  1846. return Provider.of<MesaViewModel>(context, listen: false)
  1847. .mesas;
  1848. },
  1849. itemAsString: (dynamic mesa) =>
  1850. (mesa as Mesa).nombre ?? 'Sin Nombre',
  1851. selectedItem: mesasDisponibles.firstWhere(
  1852. (mesa) => mesa.id == pedidoActual?.idMesa,
  1853. orElse: () => null as Mesa,
  1854. ),
  1855. onChanged: (dynamic nuevaMesa) {
  1856. setState(() {
  1857. pedidoActual?.idMesa = (nuevaMesa as Mesa).id;
  1858. });
  1859. },
  1860. items: mesasDisponibles,
  1861. ),
  1862. ),
  1863. ],
  1864. ),
  1865. );
  1866. },
  1867. );
  1868. }
  1869. }