pedido_form.dart 45 KB

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