pedido_form.dart 62 KB

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