pedido_form.dart 61 KB

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