pedido_form.dart 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'package:flutter/foundation.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:intl/intl.dart';
  6. import 'package:provider/provider.dart';
  7. import 'package:yoshi_papas_app/views/pedido/pedido_ticket.dart';
  8. import '../../themes/themes.dart';
  9. import '../../models/models.dart';
  10. import '../../viewmodels/viewmodels.dart';
  11. import 'package:collection/collection.dart';
  12. import '../../widgets/widgets.dart';
  13. class PedidoForm extends StatefulWidget {
  14. @override
  15. _PedidoFormState createState() => _PedidoFormState();
  16. }
  17. class _PedidoFormState extends State<PedidoForm> {
  18. final _busqueda = TextEditingController(text: '');
  19. CategoriaProductoViewModel cvm = CategoriaProductoViewModel();
  20. ProductoViewModel pvm = ProductoViewModel();
  21. PedidoViewModel pedvm = PedidoViewModel();
  22. bool _isLoading = false;
  23. CategoriaProducto? categoriaSeleccionada;
  24. List<CategoriaProducto> categorias = [];
  25. List<Producto> productos = [];
  26. List<ItemCarrito> carrito = [];
  27. Producto? _productoActual;
  28. bool _estadoBusqueda = false;
  29. Pedido? pedidoActual;
  30. ScrollController _gridViewController = ScrollController();
  31. final _searchController = TextEditingController();
  32. final NumberFormat _numberFormat = NumberFormat.decimalPattern('es_MX');
  33. double calcularTotalPedido() {
  34. double total = 0;
  35. for (var item in carrito) {
  36. total += double.parse(item.producto.precio!) * item.cantidad;
  37. item.selectedToppings.forEach((categoryId, selectedToppingIds) {
  38. for (int toppingId in selectedToppingIds) {
  39. Producto? topping = item.selectableToppings[categoryId]?.firstWhere(
  40. (topping) => topping.id == toppingId,
  41. orElse: () => Producto(precio: '0'));
  42. if (topping != null) {
  43. total += (double.tryParse(topping.precio!) ?? 0) * item.cantidad;
  44. }
  45. }
  46. });
  47. }
  48. return total;
  49. }
  50. @override
  51. void initState() {
  52. super.initState();
  53. cargarCategoriasIniciales().then((_) {
  54. if (categorias.isNotEmpty) {
  55. categoriaSeleccionada = categorias.first;
  56. cargarProductosPorCategoria(categoriaSeleccionada!.id);
  57. }
  58. });
  59. }
  60. _onSearchChanged(String value) {
  61. if (value.isEmpty) {
  62. cargarProductosIniciales();
  63. } else {
  64. Provider.of<ProductoViewModel>(context, listen: false)
  65. .fetchLocalByName(nombre: value);
  66. }
  67. }
  68. void _finalizeOrder() async {
  69. if (carrito.isEmpty) {
  70. showDialog(
  71. context: context,
  72. builder: (BuildContext context) {
  73. return AlertDialog(
  74. title: const Text('Pedido vacío',
  75. style: TextStyle(fontWeight: FontWeight.w500, fontSize: 22)),
  76. content: const Text(
  77. 'No puedes finalizar un pedido sin productos. Por favor, agrega al menos un producto.',
  78. style: TextStyle(fontWeight: FontWeight.w500, fontSize: 18)),
  79. shape:
  80. RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
  81. actions: <Widget>[
  82. TextButton(
  83. style: TextButton.styleFrom(
  84. padding: EdgeInsets.fromLTRB(30, 20, 30, 20),
  85. foregroundColor: AppTheme.quaternary,
  86. backgroundColor: AppTheme.tertiary),
  87. onPressed: () {
  88. Navigator.of(context).pop();
  89. },
  90. child: const Text('Aceptar', style: TextStyle(fontSize: 18)),
  91. ),
  92. ],
  93. );
  94. },
  95. );
  96. return;
  97. } else {
  98. int? pedidoId = pedidoActual?.id;
  99. if (pedidoId != null) {
  100. Navigator.of(context).pop();
  101. }
  102. }
  103. await _promptForCustomerName();
  104. }
  105. Future<void> _promptForCustomerName() async {
  106. TextEditingController nombreController = TextEditingController();
  107. TextEditingController comentarioController = TextEditingController();
  108. String errorMessage = '';
  109. bool? shouldSave = await showDialog<bool>(
  110. context: context,
  111. builder: (BuildContext context) {
  112. return StatefulBuilder(
  113. builder: (context, setState) {
  114. return AlertDialog(
  115. actionsPadding: EdgeInsets.fromLTRB(50, 10, 50, 30),
  116. title: const Text(
  117. 'Finalizar Pedido',
  118. style: TextStyle(fontSize: 22, fontWeight: FontWeight.w500),
  119. ),
  120. content: Column(
  121. mainAxisSize: MainAxisSize.min,
  122. children: [
  123. AppTextField(
  124. controller: nombreController,
  125. etiqueta: 'Nombre',
  126. hintText: "Nombre del Cliente",
  127. errorText: errorMessage.isEmpty ? null : errorMessage,
  128. onChanged: (value) {
  129. if (value.trim().isEmpty) {
  130. setState(() => errorMessage =
  131. "El nombre del cliente es obligatorio.");
  132. } else {
  133. setState(() => errorMessage = '');
  134. }
  135. },
  136. ),
  137. const SizedBox(height: 10),
  138. AppTextField(
  139. controller: comentarioController,
  140. etiqueta: 'Comentarios (opcional)',
  141. hintText: 'Comentarios',
  142. maxLines: 3,
  143. ),
  144. ],
  145. ),
  146. actions: <Widget>[
  147. Row(
  148. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  149. children: [
  150. TextButton(
  151. child: const Text('Cancelar',
  152. style: TextStyle(fontSize: 18)),
  153. onPressed: () {
  154. Navigator.of(context).pop(false);
  155. },
  156. style: ButtonStyle(
  157. padding: MaterialStatePropertyAll(
  158. EdgeInsets.fromLTRB(30, 20, 30, 20)),
  159. backgroundColor:
  160. MaterialStatePropertyAll(AppTheme.primary),
  161. foregroundColor:
  162. MaterialStatePropertyAll(AppTheme.secondary)),
  163. ),
  164. const SizedBox(width: 100),
  165. TextButton(
  166. child:
  167. const Text('Guardar', style: TextStyle(fontSize: 18)),
  168. onPressed: () {
  169. if (nombreController.text.trim().isEmpty) {
  170. setState(() => errorMessage =
  171. "El nombre del cliente es obligatorio.");
  172. return;
  173. }
  174. Navigator.of(context).pop(true);
  175. },
  176. style: ButtonStyle(
  177. padding: MaterialStatePropertyAll(
  178. EdgeInsets.fromLTRB(30, 20, 30, 20)),
  179. backgroundColor:
  180. MaterialStatePropertyAll(AppTheme.tertiary),
  181. foregroundColor:
  182. MaterialStatePropertyAll(AppTheme.quaternary)),
  183. ),
  184. ],
  185. )
  186. ],
  187. );
  188. },
  189. );
  190. },
  191. );
  192. if (shouldSave ?? false) {
  193. prepararPedidoActual(nombreController.text, comentarioController.text);
  194. }
  195. }
  196. void prepararPedidoActual(String nombreCliente, String comentarios) async {
  197. DateTime now = DateTime.now();
  198. String formattedDate = DateFormat('dd-MM-yyyy kk:mm:ss').format(now);
  199. double totalPedido = calcularTotalPedido();
  200. Pedido nuevoPedido = Pedido(
  201. peticion: formattedDate,
  202. nombreCliente: nombreCliente,
  203. comentarios: comentarios,
  204. estatus: "NUEVO",
  205. totalPedido: totalPedido,
  206. );
  207. List<PedidoProducto> listaPedidoProducto = carrito.map((item) {
  208. List<PedidoProductoTopping> selectedToppings = [];
  209. item.selectedToppings.forEach((categoryId, selectedToppingIds) {
  210. for (int toppingId in selectedToppingIds) {
  211. selectedToppings.add(PedidoProductoTopping(
  212. idTopping: toppingId,
  213. ));
  214. }
  215. });
  216. return PedidoProducto(
  217. idProducto: item.producto.id,
  218. producto: item.producto,
  219. costoUnitario: item.producto.precio,
  220. cantidad: item.cantidad,
  221. comentario: comentarios,
  222. toppings: selectedToppings,
  223. );
  224. }).toList();
  225. nuevoPedido.productos = listaPedidoProducto;
  226. bool result = await Provider.of<PedidoViewModel>(context, listen: false)
  227. .guardarPedidoLocal(pedido: nuevoPedido);
  228. if (result) {
  229. // Recuperar el pedido completo con detalles antes de imprimir
  230. Pedido? pedidoCompleto =
  231. await Provider.of<PedidoViewModel>(context, listen: false)
  232. .fetchPedidoConProductos(nuevoPedido.id!);
  233. if (pedidoCompleto != null) {
  234. imprimirTicketsJuntos(pedidoCompleto);
  235. }
  236. Navigator.of(context).pop();
  237. } else {
  238. print("Error al guardar el pedido");
  239. }
  240. }
  241. void _limpiarBusqueda() async {
  242. setState(() {
  243. _busqueda.text = '';
  244. _estadoBusqueda = false;
  245. });
  246. await cargarCategoriasIniciales();
  247. }
  248. Future<void> cargarProductosIniciales() async {
  249. setState(() => _isLoading = true);
  250. await Provider.of<ProductoViewModel>(context, listen: false)
  251. .fetchLocalAll();
  252. productos =
  253. Provider.of<ProductoViewModel>(context, listen: false).productos;
  254. setState(() => _isLoading = false);
  255. }
  256. @override
  257. void dispose() {
  258. _gridViewController.dispose();
  259. _searchController.dispose();
  260. super.dispose();
  261. }
  262. Future<void> cargarCategoriasIniciales() async {
  263. setState(() => _isLoading = true);
  264. await Provider.of<CategoriaProductoViewModel>(context, listen: false)
  265. .fetchLocalAll();
  266. categorias = Provider.of<CategoriaProductoViewModel>(context, listen: false)
  267. .categoriaProductos;
  268. if (categorias.isNotEmpty) {
  269. categoriaSeleccionada = categorias.first;
  270. cargarProductosPorCategoria(categoriaSeleccionada!.id);
  271. }
  272. setState(() => _isLoading = false);
  273. if (categorias.isNotEmpty) {
  274. categoriaSeleccionada = categorias.first;
  275. }
  276. }
  277. void cargarProductosPorCategoria(int categoriaId) async {
  278. setState(() => _isLoading = true);
  279. await Provider.of<ProductoViewModel>(context, listen: false)
  280. .fetchAllByCategory(categoriaId);
  281. productos =
  282. Provider.of<ProductoViewModel>(context, listen: false).productos;
  283. setState(() => _isLoading = false);
  284. }
  285. void agregarAlCarrito(Producto producto) async {
  286. var existente = carrito.firstWhereOrNull((item) =>
  287. item.producto.id == producto.id &&
  288. mapEquals(item.selectedToppings, {}));
  289. if (existente != null) {
  290. setState(() {
  291. existente.cantidad++;
  292. });
  293. } else {
  294. Map<int, List<Producto>> toppingsSeleccionables =
  295. await obtenerToppingsSeleccionables(producto);
  296. setState(() {
  297. carrito.add(ItemCarrito(
  298. producto: producto,
  299. cantidad: 1,
  300. selectableToppings: toppingsSeleccionables,
  301. ));
  302. });
  303. }
  304. }
  305. Future<Map<int, List<Producto>>> obtenerToppingsSeleccionables(
  306. Producto producto) async {
  307. Map<int, List<Producto>> toppingsSeleccionables = {};
  308. final toppingCategories =
  309. await pvm.obtenerToppingsPorProducto(producto.id!);
  310. for (int toppingId in toppingCategories) {
  311. Producto? topping = await pvm.obtenerProductoPorId(toppingId);
  312. if (topping != null && topping.idCategoria != null) {
  313. toppingsSeleccionables[topping.idCategoria!] ??= [];
  314. toppingsSeleccionables[topping.idCategoria]!.add(topping);
  315. }
  316. }
  317. return toppingsSeleccionables;
  318. }
  319. void quitarDelCarrito(Producto producto) {
  320. setState(() {
  321. var indice =
  322. carrito.indexWhere((item) => item.producto.id == producto.id);
  323. if (indice != -1) {
  324. if (carrito[indice].cantidad > 1) {
  325. carrito[indice].cantidad--;
  326. } else {
  327. carrito.removeAt(indice);
  328. }
  329. }
  330. });
  331. }
  332. void addToCart(Producto producto) {
  333. var existingIndex = carrito.indexWhere((item) =>
  334. item.producto.id == producto.id &&
  335. mapEquals(item.selectedToppings, {}));
  336. if (existingIndex != -1) {
  337. setState(() {
  338. carrito[existingIndex].cantidad++;
  339. });
  340. } else {
  341. setState(() {
  342. carrito.add(ItemCarrito(producto: producto, cantidad: 1));
  343. });
  344. }
  345. }
  346. void finalizeCustomization() {
  347. if (_productoActual != null) {
  348. addToCart(_productoActual!);
  349. setState(() {
  350. _productoActual = null;
  351. });
  352. }
  353. }
  354. @override
  355. Widget build(BuildContext context) {
  356. return Scaffold(
  357. appBar: AppBar(
  358. title:
  359. Text("Crear Pedido", style: TextStyle(color: AppTheme.secondary)),
  360. iconTheme: IconThemeData(color: AppTheme.secondary)),
  361. body: Row(
  362. children: [
  363. Flexible(
  364. flex: 3,
  365. child: _buildCartSection(),
  366. ),
  367. SizedBox(width: 35),
  368. Flexible(flex: 7, child: _buildProductsSection()),
  369. ],
  370. ),
  371. );
  372. }
  373. Widget _buildTotalSection() {
  374. double total = calcularTotalPedido();
  375. String formattedTotal = _numberFormat.format(total);
  376. return Padding(
  377. padding: const EdgeInsets.symmetric(horizontal: 8.0),
  378. child: Row(
  379. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  380. children: [
  381. const Text('Total',
  382. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
  383. Text("\$$formattedTotal",
  384. style:
  385. const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
  386. ],
  387. ),
  388. );
  389. }
  390. Widget _buildCartSection() {
  391. return Card(
  392. margin: const EdgeInsets.all(8.0),
  393. child: Column(
  394. children: [
  395. const Padding(
  396. padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0),
  397. child: Row(
  398. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  399. children: [
  400. Text('Producto',
  401. style:
  402. TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
  403. Text('Cantidad',
  404. style:
  405. TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
  406. ],
  407. ),
  408. ),
  409. Expanded(
  410. child: ListView.builder(
  411. itemCount: carrito.length,
  412. itemBuilder: (context, index) {
  413. final item = carrito[index];
  414. return Column(
  415. children: [
  416. ListTile(
  417. title: Text(item.producto.nombre!,
  418. style: const TextStyle(fontWeight: FontWeight.w600)),
  419. subtitle: Text('\$${item.producto.precio}',
  420. style: const TextStyle(
  421. fontWeight: FontWeight.bold,
  422. color: Color(0xFF008000))),
  423. trailing: Row(
  424. mainAxisSize: MainAxisSize.min,
  425. children: [
  426. IconButton(
  427. icon: const Icon(Icons.delete, color: Colors.red),
  428. onPressed: () =>
  429. eliminarProductoDelCarrito(index)),
  430. IconButton(
  431. icon: const Icon(Icons.remove),
  432. onPressed: () => quitarProductoDelCarrito(item)),
  433. const SizedBox(width: 5),
  434. Text('${item.cantidad}',
  435. style: const TextStyle(
  436. fontWeight: FontWeight.bold, fontSize: 14)),
  437. const SizedBox(width: 5),
  438. IconButton(
  439. icon: const Icon(Icons.add),
  440. onPressed: () => incrementarProducto(item)),
  441. ],
  442. ),
  443. ),
  444. if (item.selectableToppings.isNotEmpty)
  445. ExpansionTile(
  446. title: Text('Toppings',
  447. style: const TextStyle(
  448. fontWeight: FontWeight.bold, fontSize: 16)),
  449. children: item.selectableToppings.entries.map((entry) {
  450. final categoryId = entry.key;
  451. final availableToppings = entry.value;
  452. final categoria =
  453. categorias.firstWhere((c) => c.id == categoryId);
  454. return Column(
  455. crossAxisAlignment: CrossAxisAlignment.start,
  456. children: [
  457. Padding(
  458. padding: const EdgeInsets.only(left: 16.0),
  459. child: Text(
  460. categoria.descripcion!,
  461. style: const TextStyle(
  462. fontWeight: FontWeight.bold,
  463. fontSize: 16),
  464. ),
  465. ),
  466. ...availableToppings.map((topping) {
  467. ValueNotifier<bool> isSelectedNotifier =
  468. ValueNotifier<bool>(
  469. item.selectedToppings[categoryId]
  470. ?.contains(topping.id) ??
  471. false,
  472. );
  473. return ValueListenableBuilder<bool>(
  474. valueListenable: isSelectedNotifier,
  475. builder: (context, isSelected, _) {
  476. return CheckboxListTile(
  477. activeColor: AppTheme.primary,
  478. title: Row(
  479. mainAxisAlignment:
  480. MainAxisAlignment.spaceBetween,
  481. children: [
  482. Text(topping.nombre!),
  483. if (double.tryParse(
  484. topping.precio!) !=
  485. 0.0)
  486. Text(
  487. '+\$${topping.precio}',
  488. style: TextStyle(
  489. color: Colors.black,
  490. fontSize: 13,
  491. ),
  492. ),
  493. ],
  494. ),
  495. value: isSelected,
  496. onChanged: (bool? value) {
  497. final maximoToppings =
  498. categoria.maximo ?? 0;
  499. if (value == true) {
  500. if ((item.selectedToppings[categoryId]
  501. ?.length ??
  502. 0) >=
  503. maximoToppings) {
  504. item.selectedToppings[categoryId]!
  505. .remove(item
  506. .selectedToppings[
  507. categoryId]!
  508. .first);
  509. }
  510. item.selectedToppings[categoryId] ??=
  511. <int>{};
  512. item.selectedToppings[categoryId]!
  513. .add(topping.id!);
  514. } else {
  515. item.selectedToppings[categoryId]
  516. ?.remove(topping.id!);
  517. if (item.selectedToppings[categoryId]
  518. ?.isEmpty ??
  519. false) {
  520. item.selectedToppings
  521. .remove(categoryId);
  522. }
  523. }
  524. setState(() {});
  525. },
  526. );
  527. },
  528. );
  529. }).toList(),
  530. ],
  531. );
  532. }).toList(),
  533. ),
  534. Divider(),
  535. ],
  536. );
  537. },
  538. ),
  539. ),
  540. const Divider(thickness: 5),
  541. _buildTotalSection(),
  542. const SizedBox(height: 25),
  543. Padding(
  544. padding: const EdgeInsets.all(8.0),
  545. child: ElevatedButton(
  546. onPressed: _finalizeOrder,
  547. style: ElevatedButton.styleFrom(
  548. primary: AppTheme.tertiary,
  549. textStyle: const TextStyle(fontSize: 22),
  550. fixedSize: const Size(250, 50),
  551. ),
  552. child: Text('Finalizar Pedido',
  553. style: TextStyle(color: AppTheme.quaternary)),
  554. ),
  555. ),
  556. ],
  557. ),
  558. );
  559. }
  560. void eliminarProductoDelCarrito(int index) {
  561. setState(() {
  562. carrito.removeAt(index);
  563. });
  564. }
  565. void incrementarProducto(ItemCarrito item) {
  566. setState(() {
  567. item.cantidad++;
  568. });
  569. }
  570. void quitarProductoDelCarrito(ItemCarrito item) {
  571. setState(() {
  572. if (item.cantidad > 1) {
  573. item.cantidad--;
  574. } else {
  575. carrito.remove(item);
  576. }
  577. });
  578. }
  579. Widget _buildProductsSection() {
  580. return Column(
  581. children: [
  582. // _buildSearchBar(),
  583. const SizedBox(height: 10),
  584. _buildCategoryButtons(),
  585. const SizedBox(height: 15),
  586. Expanded(
  587. child: Consumer<ProductoViewModel>(builder: (context, model, child) {
  588. productos = model.productos;
  589. return GridView.builder(
  590. controller: _gridViewController,
  591. key: ValueKey<int>(categoriaSeleccionada?.id ?? 0),
  592. gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
  593. crossAxisCount: 3,
  594. childAspectRatio: 3 / 2,
  595. ),
  596. itemCount: productos.length,
  597. itemBuilder: (context, index) {
  598. final producto = productos[index];
  599. if (producto.idCategoria != categoriaSeleccionada?.id) {
  600. return Container();
  601. }
  602. return Card(
  603. child: InkWell(
  604. onTap: () => agregarAlCarrito(producto),
  605. child: Column(
  606. mainAxisAlignment: MainAxisAlignment.center,
  607. children: [
  608. if (producto.imagen != null &&
  609. File(producto.imagen!).existsSync())
  610. Image.file(
  611. File(producto.imagen!),
  612. height: 120,
  613. fit: BoxFit.cover,
  614. )
  615. else
  616. const Icon(Icons.fastfood, size: 80),
  617. const SizedBox(height: 8),
  618. Padding(
  619. padding: const EdgeInsets.symmetric(horizontal: 8.0),
  620. child: Text(
  621. producto.nombre ?? '',
  622. style: const TextStyle(
  623. fontSize: 16,
  624. fontWeight: FontWeight.bold,
  625. ),
  626. textAlign: TextAlign.center,
  627. ),
  628. ),
  629. const SizedBox(height: 8),
  630. Text(
  631. '\$${producto.precio}',
  632. style: const TextStyle(
  633. fontSize: 16,
  634. fontWeight: FontWeight.bold,
  635. color: Color(0xFF008000),
  636. ),
  637. ),
  638. ],
  639. ),
  640. ),
  641. );
  642. },
  643. );
  644. }),
  645. )
  646. ],
  647. );
  648. }
  649. Widget _buildCategoryButtons() {
  650. List<CategoriaProducto> categoriasFiltradas =
  651. categorias.where((categoria) => categoria.esToping == 0).toList();
  652. return Container(
  653. height: 50,
  654. child: ListView.builder(
  655. scrollDirection: Axis.horizontal,
  656. itemCount: categoriasFiltradas.length,
  657. itemBuilder: (context, index) {
  658. final categoria = categoriasFiltradas[index];
  659. bool isSelected = categoriaSeleccionada?.id == categoria.id;
  660. return Padding(
  661. padding: const EdgeInsets.symmetric(horizontal: 4.0),
  662. child: ElevatedButton(
  663. onPressed: () {
  664. cargarProductosPorCategoria(categoria.id);
  665. setState(() {
  666. categoriaSeleccionada = categoria;
  667. });
  668. },
  669. style: ElevatedButton.styleFrom(
  670. primary: isSelected ? AppTheme.tertiary : Colors.grey,
  671. foregroundColor:
  672. isSelected ? AppTheme.quaternary : AppTheme.secondary,
  673. onPrimary: AppTheme.secondary,
  674. ),
  675. child: Text(categoria.nombre!),
  676. ),
  677. );
  678. },
  679. ),
  680. );
  681. }
  682. Widget _buildSearchBar() {
  683. return Padding(
  684. padding: const EdgeInsets.all(8.0),
  685. child: TextField(
  686. controller: _searchController,
  687. decoration: InputDecoration(
  688. hintText: 'Buscar producto...',
  689. prefixIcon: const Icon(Icons.search),
  690. border: OutlineInputBorder(
  691. borderRadius: BorderRadius.circular(12.0),
  692. ),
  693. ),
  694. onChanged: _onSearchChanged,
  695. ),
  696. );
  697. }
  698. }