pedido_form.dart 56 KB

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