pedido_mesa_form.dart 71 KB

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