pedido_form.dart 61 KB

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