import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:yoshi_papas_app/views/pedido/pedido_ticket.dart'; import '../../themes/themes.dart'; import '../../models/models.dart'; import '../../viewmodels/viewmodels.dart'; import 'package:collection/collection.dart'; import '../../widgets/widgets.dart'; class PedidoForm extends StatefulWidget { @override _PedidoFormState createState() => _PedidoFormState(); } class _PedidoFormState extends State { final _busqueda = TextEditingController(text: ''); CategoriaProductoViewModel cvm = CategoriaProductoViewModel(); ProductoViewModel pvm = ProductoViewModel(); PedidoViewModel pedvm = PedidoViewModel(); bool _isLoading = false; CategoriaProducto? categoriaSeleccionada; List categorias = []; List productos = []; List carrito = []; Producto? _productoActual; bool _estadoBusqueda = false; Pedido? pedidoActual; ScrollController _gridViewController = ScrollController(); final _searchController = TextEditingController(); final NumberFormat _numberFormat = NumberFormat.decimalPattern('es_MX'); double calcularTotalPedido() { double total = 0; for (var item in carrito) { total += double.parse(item.producto.precio!) * item.cantidad; item.selectedToppings.forEach((categoryId, selectedToppingIds) { for (int toppingId in selectedToppingIds) { Producto? topping = item.selectableToppings[categoryId]?.firstWhere( (topping) => topping.id == toppingId, orElse: () => Producto(precio: '0')); if (topping != null) { total += (double.tryParse(topping.precio!) ?? 0) * item.cantidad; } } }); } return total; } @override void initState() { super.initState(); cargarCategoriasIniciales().then((_) { if (categorias.isNotEmpty) { categoriaSeleccionada = categorias.first; cargarProductosPorCategoria(categoriaSeleccionada!.id); } }); } _onSearchChanged(String value) { if (value.isEmpty) { cargarProductosIniciales(); } else { Provider.of(context, listen: false) .fetchLocalByName(nombre: value); } } void _finalizeOrder() async { if (carrito.isEmpty) { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text('Pedido vacĂ­o', style: TextStyle(fontWeight: FontWeight.w500, fontSize: 22)), content: const Text( 'No puedes finalizar un pedido sin productos. Por favor, agrega al menos un producto.', style: TextStyle(fontWeight: FontWeight.w500, fontSize: 18)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), actions: [ TextButton( style: TextButton.styleFrom( padding: EdgeInsets.fromLTRB(30, 20, 30, 20), foregroundColor: AppTheme.quaternary, backgroundColor: AppTheme.tertiary), onPressed: () { Navigator.of(context).pop(); }, child: const Text('Aceptar', style: TextStyle(fontSize: 18)), ), ], ); }, ); return; } else { int? pedidoId = pedidoActual?.id; if (pedidoId != null) { Navigator.of(context).pop(); } } await _promptForCustomerName(); } Future _promptForCustomerName() async { TextEditingController nombreController = TextEditingController(); TextEditingController comentarioController = TextEditingController(); String errorMessage = ''; bool? shouldSave = await showDialog( context: context, builder: (BuildContext context) { return StatefulBuilder( builder: (context, setState) { return AlertDialog( actionsPadding: EdgeInsets.fromLTRB(50, 10, 50, 30), title: const Text( 'Finalizar Pedido', style: TextStyle(fontSize: 22, fontWeight: FontWeight.w500), ), content: Column( mainAxisSize: MainAxisSize.min, children: [ AppTextField( controller: nombreController, etiqueta: 'Nombre', hintText: "Nombre del Cliente", errorText: errorMessage.isEmpty ? null : errorMessage, onChanged: (value) { if (value.trim().isEmpty) { setState(() => errorMessage = "El nombre del cliente es obligatorio."); } else { setState(() => errorMessage = ''); } }, ), const SizedBox(height: 10), AppTextField( controller: comentarioController, etiqueta: 'Comentarios (opcional)', hintText: 'Comentarios', maxLines: 3, ), ], ), actions: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ TextButton( child: const Text('Cancelar', style: TextStyle(fontSize: 18)), onPressed: () { Navigator.of(context).pop(false); }, style: ButtonStyle( padding: MaterialStatePropertyAll( EdgeInsets.fromLTRB(30, 20, 30, 20)), backgroundColor: MaterialStatePropertyAll(AppTheme.primary), foregroundColor: MaterialStatePropertyAll(AppTheme.secondary)), ), const SizedBox(width: 100), TextButton( child: const Text('Guardar', style: TextStyle(fontSize: 18)), onPressed: () { if (nombreController.text.trim().isEmpty) { setState(() => errorMessage = "El nombre del cliente es obligatorio."); return; } Navigator.of(context).pop(true); }, style: ButtonStyle( padding: MaterialStatePropertyAll( EdgeInsets.fromLTRB(30, 20, 30, 20)), backgroundColor: MaterialStatePropertyAll(AppTheme.tertiary), foregroundColor: MaterialStatePropertyAll(AppTheme.quaternary)), ), ], ) ], ); }, ); }, ); if (shouldSave ?? false) { prepararPedidoActual(nombreController.text, comentarioController.text); } } void prepararPedidoActual(String nombreCliente, String comentarios) async { DateTime now = DateTime.now(); String formattedDate = DateFormat('dd-MM-yyyy kk:mm:ss').format(now); double totalPedido = calcularTotalPedido(); Pedido nuevoPedido = Pedido( peticion: formattedDate, nombreCliente: nombreCliente, comentarios: comentarios, estatus: "NUEVO", totalPedido: totalPedido, ); List listaPedidoProducto = carrito.map((item) { List selectedToppings = []; item.selectedToppings.forEach((categoryId, selectedToppingIds) { for (int toppingId in selectedToppingIds) { selectedToppings.add(PedidoProductoTopping( idTopping: toppingId, )); } }); return PedidoProducto( idProducto: item.producto.id, producto: item.producto, costoUnitario: item.producto.precio, cantidad: item.cantidad, comentario: comentarios, toppings: selectedToppings, ); }).toList(); nuevoPedido.productos = listaPedidoProducto; bool result = await Provider.of(context, listen: false) .guardarPedidoLocal(pedido: nuevoPedido); if (result) { // Recuperar el pedido completo con detalles antes de imprimir Pedido? pedidoCompleto = await Provider.of(context, listen: false) .fetchPedidoConProductos(nuevoPedido.id!); if (pedidoCompleto != null) { imprimirTicketsJuntos(pedidoCompleto); } Navigator.of(context).pop(); } else { print("Error al guardar el pedido"); } } void _limpiarBusqueda() async { setState(() { _busqueda.text = ''; _estadoBusqueda = false; }); await cargarCategoriasIniciales(); } Future cargarProductosIniciales() async { setState(() => _isLoading = true); await Provider.of(context, listen: false) .fetchLocalAll(); productos = Provider.of(context, listen: false).productos; setState(() => _isLoading = false); } @override void dispose() { _gridViewController.dispose(); _searchController.dispose(); super.dispose(); } Future cargarCategoriasIniciales() async { setState(() => _isLoading = true); await Provider.of(context, listen: false) .fetchLocalAll(); categorias = Provider.of(context, listen: false) .categoriaProductos; if (categorias.isNotEmpty) { categoriaSeleccionada = categorias.first; cargarProductosPorCategoria(categoriaSeleccionada!.id); } setState(() => _isLoading = false); if (categorias.isNotEmpty) { categoriaSeleccionada = categorias.first; } } void cargarProductosPorCategoria(int categoriaId) async { setState(() => _isLoading = true); await Provider.of(context, listen: false) .fetchAllByCategory(categoriaId); productos = Provider.of(context, listen: false).productos; setState(() => _isLoading = false); } void agregarAlCarrito(Producto producto) async { var existente = carrito.firstWhereOrNull((item) => item.producto.id == producto.id && mapEquals(item.selectedToppings, {})); if (existente != null) { setState(() { existente.cantidad++; }); } else { Map> toppingsSeleccionables = await obtenerToppingsSeleccionables(producto); setState(() { carrito.add(ItemCarrito( producto: producto, cantidad: 1, selectableToppings: toppingsSeleccionables, )); }); } } Future>> obtenerToppingsSeleccionables( Producto producto) async { Map> toppingsSeleccionables = {}; final toppingCategories = await pvm.obtenerToppingsPorProducto(producto.id!); for (int toppingId in toppingCategories) { Producto? topping = await pvm.obtenerProductoPorId(toppingId); if (topping != null && topping.idCategoria != null) { toppingsSeleccionables[topping.idCategoria!] ??= []; toppingsSeleccionables[topping.idCategoria]!.add(topping); } } return toppingsSeleccionables; } void quitarDelCarrito(Producto producto) { setState(() { var indice = carrito.indexWhere((item) => item.producto.id == producto.id); if (indice != -1) { if (carrito[indice].cantidad > 1) { carrito[indice].cantidad--; } else { carrito.removeAt(indice); } } }); } void addToCart(Producto producto) { var existingIndex = carrito.indexWhere((item) => item.producto.id == producto.id && mapEquals(item.selectedToppings, {})); if (existingIndex != -1) { setState(() { carrito[existingIndex].cantidad++; }); } else { setState(() { carrito.add(ItemCarrito(producto: producto, cantidad: 1)); }); } } void finalizeCustomization() { if (_productoActual != null) { addToCart(_productoActual!); setState(() { _productoActual = null; }); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Crear Pedido", style: TextStyle(color: AppTheme.secondary)), iconTheme: IconThemeData(color: AppTheme.secondary)), body: Row( children: [ Flexible( flex: 3, child: _buildCartSection(), ), SizedBox(width: 35), Flexible(flex: 7, child: _buildProductsSection()), ], ), ); } Widget _buildTotalSection() { double total = calcularTotalPedido(); String formattedTotal = _numberFormat.format(total); return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Total', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), Text("\$$formattedTotal", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), ], ), ); } Widget _buildCartSection() { return Card( margin: const EdgeInsets.all(8.0), child: Column( children: [ const Padding( padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Producto', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), Text('Cantidad', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), ], ), ), Expanded( child: ListView.builder( itemCount: carrito.length, itemBuilder: (context, index) { final item = carrito[index]; return Column( children: [ ListTile( title: Text(item.producto.nombre!, style: const TextStyle(fontWeight: FontWeight.w600)), subtitle: Text('\$${item.producto.precio}', style: const TextStyle( fontWeight: FontWeight.bold, color: Color(0xFF008000))), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.delete, color: Colors.red), onPressed: () => eliminarProductoDelCarrito(index)), IconButton( icon: const Icon(Icons.remove), onPressed: () => quitarProductoDelCarrito(item)), const SizedBox(width: 5), Text('${item.cantidad}', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14)), const SizedBox(width: 5), IconButton( icon: const Icon(Icons.add), onPressed: () => incrementarProducto(item)), ], ), ), if (item.selectableToppings.isNotEmpty) ExpansionTile( title: Text('Toppings', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16)), children: item.selectableToppings.entries.map((entry) { final categoryId = entry.key; final availableToppings = entry.value; final categoria = categorias.firstWhere((c) => c.id == categoryId); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(left: 16.0), child: Text( categoria.descripcion!, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16), ), ), ...availableToppings.map((topping) { ValueNotifier isSelectedNotifier = ValueNotifier( item.selectedToppings[categoryId] ?.contains(topping.id) ?? false, ); return ValueListenableBuilder( valueListenable: isSelectedNotifier, builder: (context, isSelected, _) { return CheckboxListTile( activeColor: AppTheme.primary, title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(topping.nombre!), if (double.tryParse( topping.precio!) != 0.0) Text( '+\$${topping.precio}', style: TextStyle( color: Colors.black, fontSize: 13, ), ), ], ), value: isSelected, onChanged: (bool? value) { final maximoToppings = categoria.maximo ?? 0; if (value == true) { if ((item.selectedToppings[categoryId] ?.length ?? 0) >= maximoToppings) { item.selectedToppings[categoryId]! .remove(item .selectedToppings[ categoryId]! .first); } item.selectedToppings[categoryId] ??= {}; item.selectedToppings[categoryId]! .add(topping.id!); } else { item.selectedToppings[categoryId] ?.remove(topping.id!); if (item.selectedToppings[categoryId] ?.isEmpty ?? false) { item.selectedToppings .remove(categoryId); } } setState(() {}); }, ); }, ); }).toList(), ], ); }).toList(), ), Divider(), ], ); }, ), ), const Divider(thickness: 5), _buildTotalSection(), const SizedBox(height: 25), Padding( padding: const EdgeInsets.all(8.0), child: ElevatedButton( onPressed: _finalizeOrder, style: ElevatedButton.styleFrom( primary: AppTheme.tertiary, textStyle: const TextStyle(fontSize: 22), fixedSize: const Size(250, 50), ), child: Text('Finalizar Pedido', style: TextStyle(color: AppTheme.quaternary)), ), ), ], ), ); } void eliminarProductoDelCarrito(int index) { setState(() { carrito.removeAt(index); }); } void incrementarProducto(ItemCarrito item) { setState(() { item.cantidad++; }); } void quitarProductoDelCarrito(ItemCarrito item) { setState(() { if (item.cantidad > 1) { item.cantidad--; } else { carrito.remove(item); } }); } Widget _buildProductsSection() { return Column( children: [ // _buildSearchBar(), const SizedBox(height: 10), _buildCategoryButtons(), const SizedBox(height: 15), Expanded( child: Consumer(builder: (context, model, child) { productos = model.productos; return GridView.builder( controller: _gridViewController, key: ValueKey(categoriaSeleccionada?.id ?? 0), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, childAspectRatio: 3 / 2, ), itemCount: productos.length, itemBuilder: (context, index) { final producto = productos[index]; if (producto.idCategoria != categoriaSeleccionada?.id) { return Container(); } return Card( child: InkWell( onTap: () => agregarAlCarrito(producto), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (producto.imagen != null && File(producto.imagen!).existsSync()) Image.file( File(producto.imagen!), height: 120, fit: BoxFit.cover, ) else const Icon(Icons.fastfood, size: 80), const SizedBox(height: 8), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text( producto.nombre ?? '', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), ), const SizedBox(height: 8), Text( '\$${producto.precio}', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFF008000), ), ), ], ), ), ); }, ); }), ) ], ); } Widget _buildCategoryButtons() { List categoriasFiltradas = categorias.where((categoria) => categoria.esToping == 0).toList(); return Container( height: 50, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: categoriasFiltradas.length, itemBuilder: (context, index) { final categoria = categoriasFiltradas[index]; bool isSelected = categoriaSeleccionada?.id == categoria.id; return Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0), child: ElevatedButton( onPressed: () { cargarProductosPorCategoria(categoria.id); setState(() { categoriaSeleccionada = categoria; }); }, style: ElevatedButton.styleFrom( primary: isSelected ? AppTheme.tertiary : Colors.grey, foregroundColor: isSelected ? AppTheme.quaternary : AppTheme.secondary, onPrimary: AppTheme.secondary, ), child: Text(categoria.nombre!), ), ); }, ), ); } Widget _buildSearchBar() { return Padding( padding: const EdgeInsets.all(8.0), child: TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Buscar producto...', prefixIcon: const Icon(Icons.search), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12.0), ), ), onChanged: _onSearchChanged, ), ); } }