pedido_form.dart 43 KB

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