pedido_form.dart 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833
  1. import 'dart:async';
  2. import 'package:flutter/foundation.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:provider/provider.dart';
  5. import 'package:yoshi_papas_app/widgets/widgets_components.dart';
  6. import '../../themes/themes.dart';
  7. import '../../models/models.dart';
  8. import '../../viewmodels/viewmodels.dart';
  9. import 'package:collection/collection.dart';
  10. import '../../widgets/widgets.dart';
  11. class PedidoForm extends StatefulWidget {
  12. @override
  13. _PedidoFormState createState() => _PedidoFormState();
  14. }
  15. class ItemCarrito {
  16. Producto producto;
  17. int cantidad;
  18. Map<String, dynamic>? customizaciones;
  19. ItemCarrito(
  20. {required this.producto, this.cantidad = 1, this.customizaciones});
  21. }
  22. class _PedidoFormState extends State<PedidoForm> {
  23. final _busqueda = TextEditingController(text: '');
  24. CategoriaProductoViewModel cvm = CategoriaProductoViewModel();
  25. ProductoViewModel pvm = ProductoViewModel();
  26. bool _isLoading = false;
  27. ScrollController horizontalScrollController = ScrollController();
  28. CategoriaProducto? categoriaSeleccionada;
  29. List<CategoriaProducto> categorias = [];
  30. List<Producto> productos = [];
  31. List<ItemCarrito> carrito = [];
  32. TopingViewModel _topingViewModel = TopingViewModel();
  33. TopingCategoriaViewModel _categoriaTopingViewModel =
  34. TopingCategoriaViewModel();
  35. List<Toping> _topingsDisponibles = []; // Lista de topings disponibles
  36. Producto? _productoActual; // Producto actual seleccionado para customización
  37. bool isCustomizingProduct = false;
  38. Map<String, dynamic>? currentProductForCustomization;
  39. String? selectedBase;
  40. List<String> selectedSauce = [];
  41. List<String> selectedDressing = [];
  42. List<String> selectedToppings = [];
  43. Map<String, bool> baseOptions = {};
  44. Map<String, bool> sauceOptions = {};
  45. Map<String, bool> dressingOptions = {};
  46. List<TopingCategoria> categoriasDeTopings = []; // Añade esta línea
  47. TopingCategoria? _currentTopingCategoriaSeleccionada; // Añade esta línea
  48. List<Toping> _topingsFiltrados = []; // Añade esta línea
  49. Map<int, List<int>> selectedToppingsByCategory = {};
  50. Map<String, bool> toppingOptions = {};
  51. List<int> _selectedToppings = [];
  52. bool _estadoBusqueda = false;
  53. double calcularTotalPedido() {
  54. double total = 0;
  55. for (var item in carrito) {
  56. total += double.parse(item.producto.precio!) * item.cantidad;
  57. }
  58. return total;
  59. }
  60. @override
  61. void initState() {
  62. super.initState();
  63. cargarCategoriasIniciales();
  64. _cargarCategoriasDeTopings();
  65. _cargarTopingsDisponibles();
  66. }
  67. void _limpiarBusqueda() async {
  68. setState(() {
  69. _busqueda.text = '';
  70. _estadoBusqueda = false;
  71. });
  72. // Carga nuevamente las categorías y productos iniciales.
  73. await cargarCategoriasIniciales();
  74. // Opcionalmente, puedes llamar a una función específica para cargar productos iniciales si tienes una.
  75. }
  76. void _cargarCategoriasDeTopings() async {
  77. var categoriasObtenidas = await _categoriaTopingViewModel.fetchRegistros();
  78. setState(() {
  79. categoriasDeTopings = categoriasObtenidas;
  80. });
  81. }
  82. // Función para cargar topings disponibles
  83. void _cargarTopingsDisponibles() async {
  84. _topingsDisponibles = await _topingViewModel.fetchRegistros(limitee: -1);
  85. var categorias = await _categoriaTopingViewModel.fetchRegistros();
  86. var categoriaBase = categorias.firstWhereOrNull(
  87. (cat) => cat.nombre == 'Base',
  88. );
  89. if (categoriaBase != null) {
  90. var topingsBase = _topingsDisponibles.where((toping) {
  91. return toping.idCategoria == categoriaBase.id;
  92. }).toList();
  93. setState(() {
  94. baseOptions = {
  95. for (var toping in topingsBase) toping.nombre!: false,
  96. };
  97. });
  98. }
  99. }
  100. void _onTopingCategoriaSelected(TopingCategoria categoriaSeleccionada) {
  101. setState(() {
  102. _currentTopingCategoriaSeleccionada = categoriaSeleccionada;
  103. // Filtrar y actualizar la lista de toppings para mostrar basada en la categoría seleccionada
  104. _topingsFiltrados = _topingsDisponibles
  105. .where((t) => t.idCategoria == categoriaSeleccionada.id)
  106. .toList();
  107. });
  108. }
  109. void _onToppingSelected(Toping topping) {
  110. // Verifica si el topping ya está seleccionado
  111. bool isSelected = _selectedToppings.contains(topping.id);
  112. final List<int> currentSelectedToppings =
  113. selectedToppingsByCategory[_currentTopingCategoriaSeleccionada!.id] ??
  114. [];
  115. if (isSelected) {
  116. // Si ya está seleccionado, lo deseleccionamos
  117. setState(() {
  118. _selectedToppings.remove(topping.id);
  119. currentSelectedToppings.remove(topping.id);
  120. selectedToppingsByCategory[_currentTopingCategoriaSeleccionada!.id] =
  121. List.from(currentSelectedToppings);
  122. });
  123. } else {
  124. // Si no está seleccionado, verifica las restricciones antes de seleccionar
  125. if (_currentTopingCategoriaSeleccionada!.id == 1 &&
  126. currentSelectedToppings.isNotEmpty) {
  127. // Si es la categoría 'Base', solo permite una selección
  128. setState(() {
  129. currentSelectedToppings.clear();
  130. currentSelectedToppings.add(topping.id);
  131. _selectedToppings.clear();
  132. _selectedToppings.add(topping.id);
  133. });
  134. } else {
  135. // Si la lista ya tiene dos toppings, quita el primero y agrega el nuevo
  136. if (currentSelectedToppings.length >= 2) {
  137. setState(() {
  138. _selectedToppings.remove(currentSelectedToppings.first);
  139. currentSelectedToppings
  140. .removeAt(0); // Elimina el primer topping seleccionado
  141. });
  142. }
  143. // Ahora agrega el nuevo topping seleccionado
  144. setState(() {
  145. currentSelectedToppings.add(topping.id);
  146. _selectedToppings.add(topping.id);
  147. });
  148. }
  149. selectedToppingsByCategory[_currentTopingCategoriaSeleccionada!.id] =
  150. List.from(currentSelectedToppings);
  151. }
  152. }
  153. Future<void> realizarBusqueda() async {
  154. setState(() {
  155. _isLoading = true;
  156. _estadoBusqueda = true; // Establecer estado de búsqueda
  157. });
  158. _busqueda.text = _busqueda.text.trim();
  159. // Buscar productos basados en el texto de búsqueda
  160. //var productosResultantes = await pvm.fetchRegistros(q: _busqueda.text);
  161. _busqueda.text = _busqueda.text.trim();
  162. await Provider.of<ProductoViewModel>(context, listen: false)
  163. .setIsLoading(true);
  164. await Provider.of<ProductoViewModel>(context, listen: false)
  165. .setBusqueda(_busqueda.text);
  166. var productosResultantes =
  167. await Provider.of<ProductoViewModel>(context, listen: false)
  168. .fetchRegistros(limitee: -1);
  169. await Provider.of<ProductoViewModel>(context, listen: false)
  170. .setBusqueda("");
  171. await Provider.of<ProductoViewModel>(context, listen: false)
  172. .setIsLoading(false);
  173. // Extraer los IDs únicos de las categorías de los productos resultantes
  174. var idsCategoriasUnicos =
  175. productosResultantes.map((p) => p.idCategoria).toSet();
  176. // Filtrar las categorías localmente para incluir solo las que coinciden con los IDs extraídos
  177. var categoriasFiltradas = cvm.categoriaProductos
  178. .where((categoria) => idsCategoriasUnicos.contains(categoria.id))
  179. .toList();
  180. setState(() {
  181. _isLoading = false;
  182. productos = productosResultantes;
  183. categorias = categoriasFiltradas;
  184. categoriaSeleccionada = categorias.isNotEmpty ? categorias.first : null;
  185. });
  186. }
  187. @override
  188. void dispose() {
  189. horizontalScrollController.dispose();
  190. super.dispose();
  191. }
  192. Future<void> cargarCategoriasIniciales() async {
  193. setState(() => _isLoading = true);
  194. var categoriasObtenidas = await cvm.getCategoriaProducto();
  195. setState(() {
  196. categorias = categoriasObtenidas;
  197. _isLoading = false;
  198. // Selecciona la primera categoría por defecto si hay categorías disponibles
  199. if (categorias.isNotEmpty) {
  200. categoriaSeleccionada = categorias.first;
  201. seleccionarCategoria(categoriaSeleccionada!);
  202. }
  203. });
  204. }
  205. Future<void> seleccionarCategoria(CategoriaProducto categoria) async {
  206. if (!mounted) return;
  207. setState(() {
  208. _isLoading = true;
  209. categoriaSeleccionada = categoria;
  210. });
  211. productos =
  212. await pvm.fetchRegistros(categoriaProducto: categoria, limitee: -1);
  213. if (!mounted) return;
  214. setState(() => _isLoading = false);
  215. }
  216. void agregarAlCarrito(Producto producto) {
  217. setState(() {
  218. var indice =
  219. carrito.indexWhere((item) => item.producto.id == producto.id);
  220. if (indice != -1) {
  221. carrito[indice].cantidad++;
  222. } else {
  223. // Se agrega el nuevo producto al carrito con cantidad inicial de 1
  224. carrito.add(ItemCarrito(producto: producto));
  225. }
  226. });
  227. }
  228. void quitarDelCarrito(Producto producto) {
  229. setState(() {
  230. // Comienza con setState por la misma razón
  231. var indice =
  232. carrito.indexWhere((item) => item.producto.id == producto.id);
  233. if (indice != -1) {
  234. if (carrito[indice].cantidad > 1) {
  235. carrito[indice].cantidad--;
  236. } else {
  237. carrito.removeAt(indice);
  238. }
  239. }
  240. });
  241. }
  242. void addToCart(Producto producto, {Map<String, dynamic>? customizations}) {
  243. // Revisa si hay un producto en el carrito con las mismas customizaciones
  244. var existingIndex = carrito.indexWhere((item) =>
  245. item.producto.id == producto.id &&
  246. mapEquals(item.customizaciones, customizations));
  247. if (existingIndex != -1) {
  248. carrito[existingIndex].cantidad++;
  249. } else {
  250. carrito.add(ItemCarrito(
  251. producto: producto, cantidad: 1, customizaciones: customizations));
  252. }
  253. setState(() {});
  254. }
  255. void customizeProduct(Producto producto) {
  256. // Asumimos que este producto requiere customización
  257. setState(() {
  258. _productoActual = producto;
  259. isCustomizingProduct = true;
  260. if (categoriasDeTopings.isNotEmpty) {
  261. // Selecciona la primera categoría de toppings automáticamente
  262. _currentTopingCategoriaSeleccionada = categoriasDeTopings.first;
  263. // Filtra los toppings basado en la primera categoría
  264. _topingsFiltrados = _topingsDisponibles
  265. .where((toping) =>
  266. toping.idCategoria == _currentTopingCategoriaSeleccionada!.id)
  267. .toList();
  268. }
  269. });
  270. }
  271. void finalizeCustomization() {
  272. if (_productoActual != null) {
  273. // Prepara el diccionario de customizaciones con listas vacías para cada categoría
  274. Map<String, List<String>> customizations = {
  275. 'BASE': [],
  276. 'SALSA': [],
  277. 'ADEREZO': [],
  278. 'TOPPINGS': [],
  279. };
  280. // Itera sobre los toppings seleccionados, clasificándolos por categoría
  281. selectedToppingsByCategory.forEach((categoryId, toppingIds) {
  282. var categoryName = _categoriaTopingViewModel.getById(categoryId)?.clave;
  283. toppingIds.forEach((toppingId) {
  284. var topping = _topingViewModel.getById(toppingId);
  285. if (topping != null &&
  286. categoryName != null &&
  287. customizations.containsKey(categoryName.toUpperCase())) {
  288. customizations[categoryName.toUpperCase()]!.add(topping.nombre!);
  289. }
  290. });
  291. });
  292. // Asegúrate de que la customización final se almacene como deseas, por ejemplo, como un solo mapa si es necesario
  293. Map<String, dynamic> finalCustomizations = {};
  294. customizations.forEach((key, value) {
  295. finalCustomizations[key] = value.join(', ');
  296. });
  297. addToCart(_productoActual!, customizations: finalCustomizations);
  298. // Limpia el estado para la siguiente personalización
  299. setState(() {
  300. isCustomizingProduct = false;
  301. _productoActual = null;
  302. _currentTopingCategoriaSeleccionada = null; // Agrega esta línea
  303. _topingsFiltrados.clear(); // Agrega esta línea
  304. selectedToppingsByCategory.clear(); // Agrega esta línea
  305. _selectedToppings.clear(); // Ya existe, asegúrate de que se llama
  306. });
  307. }
  308. }
  309. @override
  310. Widget build(BuildContext context) {
  311. return Scaffold(
  312. appBar: AppBar(
  313. title: Text("Crear Pedido"),
  314. ),
  315. body: Row(
  316. children: [
  317. Flexible(
  318. flex: 3,
  319. child: _buildCartSection(),
  320. ),
  321. SizedBox(width: 35),
  322. Flexible(
  323. flex: 7,
  324. child: isCustomizingProduct
  325. ? buildCustomizationOptions()
  326. : _buildProductsSection(),
  327. ),
  328. ],
  329. ),
  330. );
  331. }
  332. Widget _buildTotalSection() {
  333. double total =
  334. calcularTotalPedido(); // Aquí llamarías a la función calcularTotalPedido
  335. return Padding(
  336. padding: const EdgeInsets.symmetric(horizontal: 8.0),
  337. child: Row(
  338. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  339. children: [
  340. const Text('Total',
  341. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
  342. Text("\$${total.toStringAsFixed(2)}",
  343. style:
  344. const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
  345. ],
  346. ),
  347. );
  348. }
  349. List<Widget> _buildToppingList(Map<String, dynamic>? customizations) {
  350. List<Widget> list = [];
  351. customizations?.forEach((category, toppingsAsString) {
  352. if (toppingsAsString is String && toppingsAsString.isNotEmpty) {
  353. // Divide la string por comas para obtener los nombres individuales de los toppings
  354. List<String> toppingNames = toppingsAsString.split(', ');
  355. for (var toppingName in toppingNames) {
  356. list.add(ListTile(
  357. title: Text(toppingName),
  358. subtitle: Text(category), // Muestra la categoría como subtítulo
  359. ));
  360. }
  361. }
  362. });
  363. return list;
  364. }
  365. Widget _buildCartSection() {
  366. return Card(
  367. margin: const EdgeInsets.all(8.0),
  368. child: Column(
  369. children: [
  370. const Padding(
  371. padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0),
  372. child: Row(
  373. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  374. children: [
  375. Text('Producto',
  376. style:
  377. TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
  378. Text('Cantidad',
  379. style:
  380. TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
  381. ],
  382. ),
  383. ),
  384. Expanded(
  385. child: ListView.builder(
  386. itemCount: carrito.length,
  387. itemBuilder: (context, index) {
  388. final item = carrito[index];
  389. // Crear una lista de Widgets para las customizaciones.
  390. List<Widget> customizationWidgets = [];
  391. item.customizaciones?.forEach((category, toppingsAsString) {
  392. if (toppingsAsString is String &&
  393. toppingsAsString.isNotEmpty) {
  394. // Si hay más de un topping en la cadena, sepáralos y muéstralos en la misma línea
  395. customizationWidgets.add(
  396. Align(
  397. alignment: Alignment.centerLeft,
  398. child: Padding(
  399. padding: const EdgeInsets.only(left: 16.0, top: 4.0),
  400. child: Text(
  401. '- $category: $toppingsAsString', // Cambio clave aquí
  402. style: const TextStyle(
  403. fontWeight: FontWeight.w500,
  404. fontSize: 14.0,
  405. )),
  406. ),
  407. ),
  408. );
  409. }
  410. });
  411. return Column(
  412. children: [
  413. ListTile(
  414. title: Text(
  415. item.producto.nombre!,
  416. style: const TextStyle(fontWeight: FontWeight.w600),
  417. ),
  418. subtitle: Text(
  419. '\$${item.producto.precio}',
  420. style: const TextStyle(
  421. fontWeight: FontWeight.bold,
  422. color: Color(0xFF008000)),
  423. ),
  424. trailing: Row(
  425. mainAxisSize: MainAxisSize.min,
  426. children: [
  427. IconButton(
  428. icon: const Icon(Icons.delete, color: Colors.red),
  429. onPressed: () => eliminarProductoDelCarrito(index),
  430. ),
  431. IconButton(
  432. icon: const Icon(Icons.remove),
  433. onPressed: () => quitarDelCarrito(item.producto),
  434. ),
  435. const SizedBox(width: 5),
  436. Text(
  437. '${item.cantidad}',
  438. style: const TextStyle(
  439. fontWeight: FontWeight.bold, fontSize: 14),
  440. ),
  441. const SizedBox(width: 5),
  442. IconButton(
  443. icon: const Icon(Icons.add),
  444. onPressed: () => agregarAlCarrito(item.producto),
  445. ),
  446. ],
  447. ),
  448. ),
  449. // Coloca aquí las customizaciones directamente.
  450. ...customizationWidgets,
  451. Divider(), // Opcional: Un divisor visual entre los elementos.
  452. ],
  453. );
  454. },
  455. ),
  456. ),
  457. const Divider(
  458. thickness: 5,
  459. ),
  460. _buildTotalSection(),
  461. const SizedBox(height: 25),
  462. Padding(
  463. padding: const EdgeInsets.all(8.0),
  464. child: ElevatedButton(
  465. onPressed: () {
  466. // Aquí implementarías la lógica para finalizar el pedido
  467. },
  468. style: ElevatedButton.styleFrom(
  469. primary: AppTheme.primary,
  470. onPrimary: AppTheme.secondary,
  471. textStyle: const TextStyle(fontSize: 22),
  472. fixedSize: const Size(250, 50)),
  473. child: const Text('Finalizar Pedido'),
  474. ),
  475. ),
  476. ],
  477. ),
  478. );
  479. }
  480. void eliminarProductoDelCarrito(int index) {
  481. setState(() {
  482. carrito.removeAt(index);
  483. });
  484. }
  485. Widget _buildProductsSection() {
  486. // Asumiendo que tienes un método para obtener los productos según la categoría seleccionada
  487. return Column(
  488. children: [
  489. Row(
  490. children: [
  491. Expanded(
  492. flex: 8,
  493. child: AppTextField(
  494. prefixIcon: const Icon(Icons.search),
  495. etiqueta: 'Búsqueda por nombre...',
  496. controller: _busqueda,
  497. hintText: 'Búsqueda por nombre...',
  498. suffixIcon: IconButton(
  499. icon: Icon(
  500. Icons.cancel,
  501. color: AppTheme.primary,
  502. ),
  503. onPressed: () {
  504. _limpiarBusqueda();
  505. },
  506. ),
  507. ),
  508. ),
  509. const SizedBox(width: 10),
  510. Expanded(
  511. flex: 2,
  512. child: botonElevated(
  513. accion: () async {
  514. realizarBusqueda();
  515. },
  516. ),
  517. ),
  518. ],
  519. ),
  520. const SizedBox(height: 5),
  521. _buildCategoryButtons(),
  522. const SizedBox(height: 10),
  523. Expanded(
  524. child: GridView.builder(
  525. gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
  526. crossAxisCount: 3, // Número de columnas
  527. childAspectRatio: 3 / 2, // Proporción de cada tarjeta
  528. ),
  529. itemCount: productos.length,
  530. itemBuilder: (context, index) {
  531. final producto = productos[index];
  532. String nombreCategoria = categorias
  533. .firstWhere(
  534. (cat) => cat.id == producto.idCategoria,
  535. orElse: () => CategoriaProducto(nombre: "Desconocida"),
  536. )
  537. .nombre!;
  538. return Card(
  539. child: InkWell(
  540. onTap: () {
  541. // Modifica esta parte para verificar si el producto necesita customización
  542. if (producto.toping == 1) {
  543. // Asume que `toping` es 1 si necesita customización
  544. customizeProduct(producto);
  545. } else {
  546. // Si no requiere customización, agrega directamente al carrito
  547. agregarAlCarrito(producto);
  548. }
  549. },
  550. child: Column(
  551. mainAxisAlignment: MainAxisAlignment.center,
  552. children: [
  553. const Icon(Icons.fastfood, size: 80),
  554. const SizedBox(height: 8),
  555. Padding(
  556. padding: EdgeInsets.fromLTRB(30, 0, 30, 0),
  557. child: Text(
  558. producto.nombre ?? '',
  559. style: const TextStyle(
  560. fontSize: 16, fontWeight: FontWeight.bold),
  561. )),
  562. const SizedBox(height: 8),
  563. Text(
  564. '\$${producto.precio}',
  565. style: const TextStyle(
  566. fontSize: 16,
  567. fontWeight: FontWeight.bold,
  568. color: Color(0xFF008000)),
  569. ),
  570. _estadoBusqueda
  571. ? Text(
  572. nombreCategoria,
  573. style: const TextStyle(
  574. fontSize: 16, fontWeight: FontWeight.bold),
  575. )
  576. : Container()
  577. ],
  578. ),
  579. ),
  580. );
  581. },
  582. ),
  583. ),
  584. ],
  585. );
  586. }
  587. Widget _buildCategoryButtons() {
  588. return Column(
  589. mainAxisSize: MainAxisSize.min,
  590. children: [
  591. SizedBox(
  592. height: 50, // Altura para los botones
  593. child: Scrollbar(
  594. controller: horizontalScrollController,
  595. thumbVisibility: true,
  596. thickness: 5.0,
  597. child: ListView.builder(
  598. scrollDirection: Axis.horizontal,
  599. itemCount: categorias.length,
  600. controller: horizontalScrollController,
  601. itemBuilder: (context, index) {
  602. final categoria = categorias[index];
  603. bool esSeleccionada = categoriaSeleccionada?.id == categoria.id;
  604. return Padding(
  605. padding: const EdgeInsets.symmetric(horizontal: 4.0),
  606. child: ElevatedButton(
  607. onPressed: () => seleccionarCategoria(categoria),
  608. style: ElevatedButton.styleFrom(
  609. primary: esSeleccionada
  610. ? const Color(0xFFFF848F)
  611. : Colors.white,
  612. onPrimary: Colors.black,
  613. textStyle: const TextStyle(
  614. fontWeight: FontWeight.bold,
  615. ),
  616. shape: RoundedRectangleBorder(
  617. borderRadius: BorderRadius.circular(18.0),
  618. side: BorderSide(
  619. color: esSeleccionada ? Colors.black : Colors.grey),
  620. ),
  621. ),
  622. child: Text(categoria.nombre ?? 'Sin nombre'),
  623. ),
  624. );
  625. },
  626. ),
  627. ),
  628. ),
  629. ],
  630. );
  631. }
  632. Widget buildCustomizationOptions() {
  633. return Container(
  634. margin: const EdgeInsets.all(8.0),
  635. child: Card(
  636. child: Column(
  637. children: [
  638. Expanded(
  639. child: Row(
  640. children: [
  641. Expanded(
  642. flex: 4,
  643. child: ListView.builder(
  644. itemCount: categoriasDeTopings.length,
  645. itemBuilder: (context, index) {
  646. final categoria = categoriasDeTopings[index];
  647. return ListTile(
  648. title: Text(
  649. categoria.nombre ?? 'N/A',
  650. style: const TextStyle(
  651. fontSize: 16, fontWeight: FontWeight.bold),
  652. ),
  653. selected:
  654. _currentTopingCategoriaSeleccionada == categoria,
  655. trailing: Image.network(
  656. categoria.mediaTopingCategoria[0].media!.ruta!,
  657. width: 80,
  658. loadingBuilder: (BuildContext context, Widget child,
  659. ImageChunkEvent? loadingProgress) {
  660. if (loadingProgress == null) return child;
  661. return Center(
  662. child: CircularProgressIndicator(
  663. value: loadingProgress.expectedTotalBytes !=
  664. null
  665. ? loadingProgress.cumulativeBytesLoaded /
  666. loadingProgress.expectedTotalBytes!
  667. : null,
  668. ),
  669. );
  670. },
  671. errorBuilder: (BuildContext context,
  672. Object exception, StackTrace? stackTrace) {
  673. // Aquí puedes devolver un widget como un ícono o imagen local para indicar el error
  674. return const Icon(
  675. Icons.menu_book,
  676. size: 30,
  677. );
  678. },
  679. ),
  680. onTap: () {
  681. setState(() {
  682. _currentTopingCategoriaSeleccionada = categoria;
  683. _topingsFiltrados = _topingsDisponibles
  684. .where((t) => t.idCategoria == categoria.id)
  685. .toList();
  686. });
  687. },
  688. );
  689. },
  690. ),
  691. ),
  692. const VerticalDivider(width: 0),
  693. Expanded(
  694. flex: 6,
  695. child: GridView.builder(
  696. gridDelegate:
  697. const SliverGridDelegateWithFixedCrossAxisCount(
  698. crossAxisCount: 3,
  699. ),
  700. itemCount: _topingsFiltrados
  701. .length, // Usando _topingsFiltrados directamente
  702. itemBuilder: (context, index) {
  703. final topping = _topingsFiltrados[index];
  704. bool isSelected =
  705. _selectedToppings.contains(topping.id);
  706. return Card(
  707. color: isSelected
  708. ? const Color(0xFFFC8178)
  709. : Colors.white,
  710. child: InkWell(
  711. onTap: () => _onToppingSelected(topping),
  712. child: Column(
  713. mainAxisAlignment: MainAxisAlignment.center,
  714. children: [
  715. Image.network(
  716. topping.imagenes[0].media!.ruta!,
  717. width: 80,
  718. loadingBuilder: (BuildContext context,
  719. Widget child,
  720. ImageChunkEvent? loadingProgress) {
  721. if (loadingProgress == null) return child;
  722. return Center(
  723. child: CircularProgressIndicator(
  724. value: loadingProgress
  725. .expectedTotalBytes !=
  726. null
  727. ? loadingProgress
  728. .cumulativeBytesLoaded /
  729. loadingProgress
  730. .expectedTotalBytes!
  731. : null,
  732. ),
  733. );
  734. },
  735. errorBuilder: (BuildContext context,
  736. Object exception,
  737. StackTrace? stackTrace) {
  738. // Aquí puedes devolver un widget como un ícono o imagen local para indicar el error
  739. return const Icon(
  740. Icons.lunch_dining,
  741. size: 60,
  742. );
  743. },
  744. ),
  745. Text(topping.nombre!,
  746. style: TextStyle(
  747. color: isSelected
  748. ? Colors.white
  749. : Colors.black)),
  750. ],
  751. ),
  752. ),
  753. );
  754. },
  755. ),
  756. ),
  757. ],
  758. ),
  759. ),
  760. Container(
  761. padding: EdgeInsets.symmetric(vertical: 8.0),
  762. alignment: Alignment.centerRight,
  763. // El botón para finalizar la personalización
  764. child: _buildConfirmButton(),
  765. ),
  766. ],
  767. ),
  768. ),
  769. );
  770. }
  771. Widget _buildConfirmButton() {
  772. return ElevatedButton(
  773. style: ElevatedButton.styleFrom(
  774. primary: AppTheme.primary, // Color del botón
  775. onPrimary: Colors.black,
  776. textStyle: const TextStyle(fontSize: 22),
  777. padding: const EdgeInsets.fromLTRB(80, 20, 80, 20)),
  778. onPressed: () {
  779. finalizeCustomization(); // Este método creará el pedido personalizado
  780. },
  781. child: Text('Confirmar Pedido'),
  782. );
  783. }
  784. }