import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; import 'package:otp/otp.dart'; import 'package:provider/provider.dart'; import '/data/session/session_storage.dart'; import '/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'; import 'package:uuid/uuid.dart'; import '../../services/services.dart'; import 'package:timezone/data/latest.dart' as timezone; import 'package:timezone/timezone.dart' as timezone; class PedidoForm extends StatefulWidget { final Pedido? pedidoExistente; const PedidoForm({Key? key, this.pedidoExistente}) : super(key: key); @override _PedidoFormState createState() => _PedidoFormState(); } class _PedidoFormState extends State { final _busqueda = TextEditingController(text: ''); final TextEditingController _descuentoController = TextEditingController(); 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(); ScrollController _categoryScrollController = ScrollController(); final _searchController = TextEditingController(); final NumberFormat _numberFormat = NumberFormat.decimalPattern('es_MX'); int? selectedDescuento = 0; double subtotal = 0; double precioDescuento = 0; double totalPedido = 0; bool efectivoSeleccionado = false; bool tarjetaSeleccionada = false; bool transferenciaSeleccionada = false; TextEditingController efectivoController = TextEditingController(); TextEditingController tarjetaController = TextEditingController(); TextEditingController transferenciaController = TextEditingController(); TextEditingController _propinaCantidadController = TextEditingController(); TextEditingController _propinaComentarioController = TextEditingController(); double cambio = 0.0; double faltante = 0.0; bool totalCompletado = false; bool _isMesasActive = false; double calcularTotalPedido() { double total = 0; for (var item in carrito) { total += 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 += (topping.precio ?? 0.0) * item.cantidad; } } }); } return total; } double aplicarDescuento(double total, int? descuento) { if (descuento != null && descuento > 0) { double totalPedido = total * (1 - descuento / 100); // print( // 'Total con descuento: $totalPedido (Descuento aplicado: $descuento%)'); return totalPedido; } // print('Sin descuento, total: $total'); return total; } @override void initState() { super.initState(); pedidoActual = widget.pedidoExistente ?? Pedido(id: 0); Future.microtask(() async { bool isMesasActive = await Provider.of(context, listen: false) .isVariableActive('MESAS'); setState(() { _isMesasActive = isMesasActive; }); if (pedidoActual != null && pedidoActual!.id! > 0) { await _cargarPedidoExistente(pedidoActual!.id!); } }); cargarCategoriasIniciales().then((_) { if (categorias.isNotEmpty) { categoriaSeleccionada = categorias.first; cargarProductosPorCategoria(categoriaSeleccionada!.id); } }); Provider.of(context, listen: false).cargarDescuentos(); } Future _cargarPedidoExistente(int pedidoId) async { Pedido? pedidoCompleto = await Provider.of(context, listen: false) .fetchPedidoConProductos(pedidoId); if (pedidoCompleto != null) { setState(() { pedidoActual = pedidoCompleto; carrito = pedidoCompleto.productos.map((producto) { return ItemCarrito( producto: producto.producto!, cantidad: producto.cantidad!, comentario: producto.comentario, selectedToppings: producto.toppings.fold>>( {}, (acc, topping) { acc[topping.idCategoria!] ??= {}; acc[topping.idCategoria]!.add(topping.idTopping!); return acc; }, ), ); }).toList(); }); selectedDescuento = pedidoCompleto.descuento ?? 0; _recalcularTotal(); } } void _onSearchChanged(String value) async { if (value.isEmpty) { cargarProductosPorCategoria( categoriaSeleccionada?.id ?? categorias.first.id); } else { setState(() { _estadoBusqueda = true; }); await Provider.of(context, listen: false) .fetchLocalByName(nombre: value); setState(() { productos = Provider.of(context, listen: false).productos; }); } } void _recalcularTotal() { subtotal = calcularTotalPedido(); // print('Subtotal: $subtotal'); // print('Descuento seleccionado: $selectedDescuento%'); precioDescuento = subtotal * (selectedDescuento! / 100); totalPedido = subtotal - precioDescuento; setState(() { pedidoActual = pedidoActual ?? Pedido(); pedidoActual!.totalPedido = totalPedido; pedidoActual!.descuento = selectedDescuento; }); // print('Precio descuento: $precioDescuento'); // print('Total con descuento aplicado: $totalPedido'); } bool validarMinimosSeleccionados() { for (var item in carrito) { for (var categoriaId in item.selectableToppings.keys) { final categoria = categorias.firstWhere((c) => c.id == categoriaId); final minimoRequerido = categoria.minimo ?? 0; final seleccionados = item.selectedToppings[categoriaId]?.length ?? 0; if (minimoRequerido > 0 && seleccionados < minimoRequerido) { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text('Faltan Toppings', style: TextStyle(fontWeight: FontWeight.w500, fontSize: 22)), content: Text( 'El producto ${item.producto.nombre} requiere que seleccione al menos $minimoRequerido topping en la categoría ${categoria.nombre}.', style: TextStyle(fontSize: 18)), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Aceptar'), style: ButtonStyle( padding: MaterialStatePropertyAll( EdgeInsets.fromLTRB(20, 10, 20, 10)), backgroundColor: MaterialStatePropertyAll(AppTheme.tertiary), foregroundColor: MaterialStatePropertyAll(AppTheme.quaternary))), ], ); }, ); return false; } } } return true; } 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; } if (!validarMinimosSeleccionados()) { return; } await _promptForCustomerName(); } void _mostrarDialogoPedidoVacio() { 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 crear un pedido sin productos. Por favor, agrega al menos un producto.', style: TextStyle(fontSize: 18)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), actions: [ TextButton( style: TextButton.styleFrom( padding: const 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)), ), ], ); }, ); } Future _crearPedidoConModal() async { if (carrito.isEmpty) { _mostrarDialogoPedidoVacio(); return; } TextEditingController nombreController = TextEditingController(); TextEditingController descripcionController = TextEditingController(); TextEditingController mesaSearchController = TextEditingController(); Mesa? mesaSeleccionada; // Inicializa las mesas disponibles await Provider.of(context, listen: false).fetchLocalAll(); List mesasDisponibles = Provider.of(context, listen: false).mesas; bool? shouldSave = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text( 'Crear Pedido', style: TextStyle(fontSize: 22, fontWeight: FontWeight.w500), ), content: Column( mainAxisSize: MainAxisSize.min, children: [ AppTextField( controller: nombreController, etiqueta: 'Nombre del Cliente', hintText: 'Nombre del Cliente', ), const SizedBox(height: 10), AppTextField( controller: descripcionController, etiqueta: 'Descripción', hintText: 'Descripción del Pedido', ), const SizedBox(height: 10), AppDropdownSearch( controller: mesaSearchController, etiqueta: 'Seleccionar Mesa', asyncItems: (String query) async { await Provider.of(context, listen: false) .fetchLocalByName(nombre: query); return Provider.of(context, listen: false) .mesas; }, itemAsString: (dynamic mesa) => (mesa as Mesa).nombre ?? 'Sin Nombre', selectedItem: mesaSeleccionada, onChanged: (dynamic nuevaMesa) { mesaSeleccionada = nuevaMesa as Mesa; }, ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Cancelar'), style: ButtonStyle( padding: MaterialStatePropertyAll( EdgeInsets.fromLTRB(20, 10, 20, 10), ), backgroundColor: MaterialStatePropertyAll(Colors.red), foregroundColor: MaterialStatePropertyAll(AppTheme.secondary), ), ), const SizedBox(width: 10), TextButton( onPressed: () { if (mesaSeleccionada != null) { Navigator.of(context).pop(true); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Seleccione una mesa por favor'), ), ); } }, child: const Text('Guardar'), style: ButtonStyle( padding: MaterialStatePropertyAll( EdgeInsets.fromLTRB(20, 10, 20, 10), ), backgroundColor: MaterialStatePropertyAll(Colors.black), foregroundColor: MaterialStatePropertyAll(AppTheme.quaternary), ), ), ], ); }, ); if (shouldSave ?? false) { Pedido nuevoPedido = Pedido( peticion: DateTime.now().toUtc().toIso8601String(), nombreCliente: nombreController.text, comentarios: descripcionController.text, estatus: 'NUEVO', idMesa: mesaSeleccionada?.id, totalPedido: totalPedido, descuento: selectedDescuento, uuid: Uuid().v4(), idUsuario: await SessionStorage().getId(), productos: carrito.map((item) { return PedidoProducto( idProducto: item.producto.id, producto: item.producto, costoUnitario: item.producto.precio.toString(), cantidad: item.cantidad, comentario: item.comentario, toppings: item.selectedToppings.entries.expand((entry) { return entry.value.map((toppingId) { return PedidoProductoTopping( idCategoria: entry.key, idTopping: toppingId, ); }); }).toList(), ); }).toList(), ); bool result = await Provider.of(context, listen: false) .guardarPedidoLocal(pedido: nuevoPedido); if (result) { Navigator.of(context).pop(); } else { print('Error al guardar el pedido'); } } } Future _guardarPedidoExistente({ double? cantEfectivo, double? cantTarjeta, double? cantTransferencia, String? tipoPago, String? estatus, }) async { if (pedidoActual == null) return; DateTime now = DateTime.now().toUtc(); pedidoActual!.totalPedido = totalPedido; pedidoActual!.descuento = selectedDescuento; pedidoActual!.modificado = now; pedidoActual!.sincronizado = null; if (cantEfectivo != null) pedidoActual!.cantEfectivo = cantEfectivo; if (cantTarjeta != null) pedidoActual!.cantTarjeta = cantTarjeta; if (cantTransferencia != null) pedidoActual!.cantTransferencia = cantTransferencia; if (tipoPago != null) pedidoActual!.tipoPago = tipoPago; if (estatus != null) pedidoActual!.estatus = estatus; bool result = await Provider.of(context, listen: false) .guardarPedidoConProductos( pedido: pedidoActual!, carrito: carrito, ); if (result) { Pedido? pedidoCompleto = await Provider.of(context, listen: false) .fetchPedidoConProductos(pedidoActual!.id); if (estatus == "TERMINADO") { imprimirTicketsJuntos(context, pedidoCompleto!); } print("Pedido actualizado correctamente"); Navigator.of(context).pop(); } else { print("Error al actualizar el pedido"); } } Future _promptForCustomerName() async { TextEditingController efectivoController = TextEditingController(); TextEditingController tarjetaController = TextEditingController(); TextEditingController transferenciaController = TextEditingController(); TextEditingController? nombreController; TextEditingController? comentarioController; if (pedidoActual == null || pedidoActual!.id == 0) { nombreController = TextEditingController(); comentarioController = TextEditingController(); } faltante = totalPedido; bool totalCompletado = false; bool efectivoCompleto = false; bool tarjetaCompleto = false; bool transferenciaCompleto = false; bool propinaExpandida = false; void _calcularCambio(StateSetter setState) { double totalPagado = (double.tryParse(efectivoController.text) ?? 0) + (double.tryParse(tarjetaController.text) ?? 0) + (double.tryParse(transferenciaController.text) ?? 0); setState(() { cambio = totalPagado - totalPedido; faltante = cambio < 0 ? totalPedido - totalPagado : 0; totalCompletado = cambio >= 0; }); } 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: SingleChildScrollView( child: AnimatedSize( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (nombreController != null) AppTextField( controller: nombreController, etiqueta: 'Nombre', hintText: "Nombre del Cliente", ), if (comentarioController != null) ...[ const SizedBox(height: 10), AppTextField( controller: comentarioController, etiqueta: 'Comentarios (opcional)', hintText: 'Comentarios', maxLines: 2, ), ], const SizedBox(height: 10), Align( alignment: Alignment.center, child: Text( 'Métodos de pago', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 20), ), ), const SizedBox(height: 10), // Efectivo _buildPaymentMethodRow( setState, label: 'Efectivo', selected: efectivoSeleccionado, exactSelected: efectivoCompleto, controller: efectivoController, onSelected: (value) { setState(() { efectivoSeleccionado = value; if (!efectivoSeleccionado) { efectivoCompleto = false; efectivoController.clear(); } _calcularCambio(setState); }); }, onExactSelected: (value) { setState(() { efectivoCompleto = value; if (efectivoCompleto) { efectivoController.text = totalPedido.toStringAsFixed(2); efectivoSeleccionado = true; tarjetaSeleccionada = false; transferenciaSeleccionada = false; tarjetaController.clear(); transferenciaController.clear(); } else { efectivoController.clear(); } _calcularCambio(setState); }); }, disableOtherMethods: tarjetaCompleto || transferenciaCompleto, onChangedMonto: () => _calcularCambio(setState), ), // Tarjeta _buildPaymentMethodRow( setState, label: 'Tarjeta', selected: tarjetaSeleccionada, exactSelected: tarjetaCompleto, controller: tarjetaController, sinCambio: true, onSelected: (value) { setState(() { tarjetaSeleccionada = value; if (!tarjetaSeleccionada) { tarjetaCompleto = false; tarjetaController.clear(); } _calcularCambio(setState); }); }, onExactSelected: (value) { setState(() { tarjetaCompleto = value; if (tarjetaCompleto) { tarjetaController.text = totalPedido.toStringAsFixed(2); tarjetaSeleccionada = true; efectivoSeleccionado = false; transferenciaSeleccionada = false; efectivoController.clear(); transferenciaController.clear(); } else { tarjetaController.clear(); } _calcularCambio(setState); }); }, disableOtherMethods: efectivoCompleto || transferenciaCompleto, onChangedMonto: () => _calcularCambio(setState), ), // Transferencia _buildPaymentMethodRow( setState, label: 'Transferencia', selected: transferenciaSeleccionada, exactSelected: transferenciaCompleto, controller: transferenciaController, sinCambio: true, onSelected: (value) { setState(() { transferenciaSeleccionada = value; if (!transferenciaSeleccionada) { transferenciaCompleto = false; transferenciaController.clear(); } _calcularCambio(setState); }); }, onExactSelected: (value) { setState(() { transferenciaCompleto = value; if (transferenciaCompleto) { transferenciaController.text = totalPedido.toStringAsFixed(2); transferenciaSeleccionada = true; efectivoSeleccionado = false; tarjetaSeleccionada = false; efectivoController.clear(); tarjetaController.clear(); } else { transferenciaController.clear(); } _calcularCambio(setState); }); }, disableOtherMethods: efectivoCompleto || tarjetaCompleto, onChangedMonto: () => _calcularCambio(setState), ), // Propina Expandable ExpansionTile( title: const Text( 'Agregar Propina', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold), ), initiallyExpanded: propinaExpandida, onExpansionChanged: (isExpanded) { setState(() { propinaExpandida = isExpanded; }); }, children: [ AppTextField( separarMiles: true, controller: _propinaCantidadController, etiqueta: 'Propina', hintText: '0.00', keyboardType: TextInputType.number, ), const SizedBox(height: 10), AppTextField( controller: _propinaComentarioController, etiqueta: 'Comentario', hintText: 'Comentario', maxLines: 2, ), ], ), const SizedBox(height: 10), Align( alignment: Alignment.centerRight, child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( 'Total del pedido: \$${totalPedido.toStringAsFixed(2)}', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 18), ), if (faltante > 0) Text( 'Faltante: \$${faltante.toStringAsFixed(2)}', style: const TextStyle( color: Colors.red, fontSize: 18, fontWeight: FontWeight.bold), ) else if (cambio > 0) Text( 'Cambio: \$${cambio.toStringAsFixed(2)}', style: const TextStyle( color: Colors.green, fontSize: 18, fontWeight: FontWeight.bold), ), ], ), ), ], ), ), ), actions: [ TextButton( child: const Text('Cancelar', style: TextStyle(fontSize: 18)), onPressed: () => Navigator.of(context).pop(false), style: ButtonStyle( padding: MaterialStateProperty.all( EdgeInsets.fromLTRB(30, 20, 30, 20)), backgroundColor: MaterialStateProperty.all(Colors.red), foregroundColor: MaterialStateProperty.all(AppTheme.secondary), ), ), const SizedBox(width: 100), TextButton( child: const Text('Guardar', style: TextStyle(fontSize: 18)), onPressed: totalCompletado ? () => Navigator.of(context).pop(true) : null, style: ButtonStyle( padding: MaterialStateProperty.all( EdgeInsets.fromLTRB(30, 20, 30, 20)), backgroundColor: MaterialStateProperty.all( totalCompletado ? AppTheme.tertiary : Colors.grey), foregroundColor: MaterialStateProperty.all(AppTheme.quaternary), ), ), ], ); }, ); }, ); if (shouldSave ?? false) { if (pedidoActual != null && pedidoActual!.id != 0) { await _guardarPedidoExistente( cantEfectivo: double.tryParse(efectivoController.text), cantTarjeta: double.tryParse(tarjetaController.text), cantTransferencia: double.tryParse(transferenciaController.text), tipoPago: _obtenerTipoPago(), estatus: "TERMINADO", ); await _guardarPropina(pedidoActual!.id!); } else { prepararPedidoActual( nombreController?.text ?? '', comentarioController?.text ?? '', efectivoController, tarjetaController, transferenciaController, ); } } } Future _guardarPropina(int idPedido) async { double? cantidad = double.tryParse(_propinaCantidadController.text.replaceAll(',', '')) ?? 0.0; String comentario = _propinaComentarioController.text.trim(); if (cantidad != null && cantidad > 0) { Propinas nuevaPropina = Propinas( idPedido: idPedido, cantidad: cantidad, comentario: comentario, sincronizado: null, creado: DateTime.now().toUtc(), ); await Provider.of(context, listen: false) .guardarPropina(nuevaPropina); print('Propina guardada correctamente'); } else { print('Propina no guardada (cantidad inválida)'); } } Widget _buildPaymentMethodRow( StateSetter setState, { required String label, required bool selected, required bool exactSelected, required TextEditingController controller, required Function(bool) onSelected, required Function(bool) onExactSelected, required bool disableOtherMethods, required Function() onChangedMonto, bool sinCambio = false, }) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ Row( children: [ Checkbox( activeColor: AppTheme.primary, value: selected, onChanged: disableOtherMethods ? null : (value) { onSelected(value ?? false); }, ), GestureDetector( onTap: disableOtherMethods ? null : () { onSelected(!selected); }, child: Text( label, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), ), ], ), SizedBox( width: 180, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( children: [ const Text( 'Exacto', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black), ), const SizedBox(height: 17), Checkbox( activeColor: AppTheme.primary, value: exactSelected, onChanged: !disableOtherMethods ? (value) { onExactSelected(value ?? false); if (value == true) { setState(() { disableOtherMethods = true; }); } } : null, ), ], ), const SizedBox(width: 5), Expanded( child: AppTextField( controller: controller, enabled: selected, etiqueta: 'Cantidad', hintText: '0.00', keyboardType: TextInputType.number, onChanged: (value) { if (sinCambio) { double? input = double.tryParse(value) ?? 0; if (input > totalPedido) { controller.text = totalPedido.toStringAsFixed(2); controller.selection = TextSelection.fromPosition( TextPosition(offset: controller.text.length), ); } } onChangedMonto(); }, ), ), ], ), ), ], ); } void prepararPedidoActual( String nombreCliente, String comentarios, TextEditingController efectivoController, TextEditingController tarjetaController, TextEditingController transferenciaController, ) async { String now = DateTime.now().toUtc().toIso8601String(); int? idUsuario = await SessionStorage().getId(); double cantEfectivo = efectivoSeleccionado ? double.tryParse(efectivoController.text) ?? 0 : 0; double cantTarjeta = tarjetaSeleccionada ? double.tryParse(tarjetaController.text) ?? 0 : 0; double cantTransferencia = transferenciaSeleccionada ? double.tryParse(transferenciaController.text) ?? 0 : 0; Pedido nuevoPedido = Pedido( peticion: now, nombreCliente: nombreCliente, comentarios: comentarios, estatus: "TERMINADO", totalPedido: totalPedido, descuento: pedidoActual?.descuento, idUsuario: idUsuario, tipoPago: _obtenerTipoPago(), cantEfectivo: cantEfectivo, cantTarjeta: cantTarjeta, cantTransferencia: cantTransferencia, uuid: Uuid().v4(), ); List listaPedidoProducto = carrito.map((item) { List selectedToppings = []; item.selectedToppings.forEach((categoryId, selectedToppingIds) { for (int toppingId in selectedToppingIds) { selectedToppings.add(PedidoProductoTopping( idCategoria: categoryId, idTopping: toppingId, )); } }); return PedidoProducto( idProducto: item.producto.id, producto: item.producto, costoUnitario: item.producto.precio.toString(), cantidad: item.cantidad, comentario: item.comentario, toppings: selectedToppings, ); }).toList(); nuevoPedido.productos = listaPedidoProducto; bool result = await Provider.of(context, listen: false) .guardarPedidoLocal(pedido: nuevoPedido); if (!mounted) return; if (result) { Pedido? pedidoCompleto = await Provider.of(context, listen: false) .fetchPedidoConProductos(nuevoPedido.id!); if (pedidoCompleto != null) { imprimirTicketsJuntos(context, pedidoCompleto); } Navigator.of(context).pop(); } else { print("Error al guardar el pedido"); } } String _obtenerTipoPago() { List tiposPago = []; if (efectivoSeleccionado) tiposPago.add('Efectivo'); if (tarjetaSeleccionada) tiposPago.add('Tarjeta'); if (transferenciaSeleccionada) tiposPago.add('Transferencia'); return tiposPago.isNotEmpty ? tiposPago.join(',') : 'No Definido'; } 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(); _categoryScrollController.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, )); }); } _recalcularTotal(); } 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; }); } } Widget _buildDiscountSection() { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ const Text( 'Descuento', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), ), const Spacer(), ConstrainedBox( constraints: const BoxConstraints( maxWidth: 150, ), child: Consumer( builder: (context, viewModel, child) { return AppDropdownModel( hint: 'Seleccionar', items: viewModel.descuentos .map( (descuento) => DropdownMenuItem( value: descuento.porcentaje, child: Text( '${descuento.porcentaje}%', style: const TextStyle(color: Colors.black), ), ), ) .toList(), selectedValue: selectedDescuento, onChanged: (value) async { if (value != null && value != selectedDescuento) { // Guardar el valor anterior final previousValue = selectedDescuento; // Actualizar el descuento temporalmente print('Descuento temporal seleccionado: $value'); setState(() { selectedDescuento = value; }); // Mostrar cuadro de confirmación final authenticated = await showDialog( context: context, builder: (context) { return TotpCuadroConfirmacion( title: "Aplicar Descuento", content: "Por favor, ingresa el código de autenticación para continuar.", onSuccess: () { // Confirmación exitosa: recalcular total y actualizar UI print( 'Autenticación exitosa. Aplicando descuento...'); setState(() { _recalcularTotal(); print('Descuento aplicado: $selectedDescuento'); print('Total recalculado: $totalPedido'); }); }, ); }, ); // Si la autenticación falla, revertir el descuento if (authenticated != true) { print( 'Autenticación fallida. Revirtiendo descuento...'); setState(() { selectedDescuento = previousValue; _recalcularTotal(); // Recalcular con el valor anterior print('Descuento revertido: $selectedDescuento'); print( 'Total recalculado tras revertir: $totalPedido'); }); } } }, ); }, ), ), ], ), ); } Widget _buildTotalSection() { String formattedsubtotal = _numberFormat.format(subtotal); String formattedPrecioDescuento = _numberFormat.format(precioDescuento); String formattedtotalPedido = _numberFormat.format(totalPedido); return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (precioDescuento > 0) Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Subtotal', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 18)), Text("\$$formattedsubtotal", style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 18)), ], ), const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Descuento', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 18)), Text("-\$$formattedPrecioDescuento", style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 18)), ], ), ], ), const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Total', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), Text("\$$formattedtotalPedido", style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 18)), ], ), ], ), ); } @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 _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: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(item.producto.nombre!, style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 16)), Text('\$${item.producto.precio}', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, color: Color(0xFF008000))), ], ), ), Row( children: [ IconButton( icon: const Icon(Icons.delete, color: Colors.red), onPressed: () => eliminarProductoDelCarrito(index), ), IconButton( icon: const Icon(Icons.remove), onPressed: () => quitarProductoDelCarrito(item), ), Text('${item.cantidad}', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14)), IconButton( icon: const Icon(Icons.add), onPressed: () => incrementarProducto(item), ), IconButton( icon: Icon(Icons.message, color: AppTheme.tertiary), onPressed: () { setState(() { item.expandido = !item.expandido; }); }, ), ], ), ], ), if (item.expandido) ...[ const SizedBox(height: 5), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: TextField( controller: item.comentarioController, decoration: const InputDecoration( hintText: 'Agregar un comentario...', border: OutlineInputBorder(), ), maxLines: 2, onChanged: (value) { item.comentario = value.trim(); }, ), ), ], // ExpansionTile para todos los toppings if (item.selectableToppings.isNotEmpty) ExpansionTile( initiallyExpanded: true, title: const Text( 'Toppings', style: 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 ExpansionTile( initiallyExpanded: true, title: Text( '${categoria.nombre}' ' (Hasta ${categoria.maximo ?? 0})' ' ${(categoria.minimo ?? 0) > 0 ? " (Mínimo ${categoria.minimo})" : ""}', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), children: availableToppings.map((topping) { bool isSelected = item .selectedToppings[categoryId] ?.contains(topping.id) ?? false; return CheckboxListTile( activeColor: AppTheme.primary, title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(topping.nombre!), if (topping.precio != 0.0) Text( '+\$${topping.precio}', style: const TextStyle( color: Colors.black, fontSize: 13, ), ), ], ), value: isSelected, onChanged: (bool? value) { final maximoToppings = categoria.maximo ?? 0; setState(() { 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); } } _recalcularTotal(); }); }, ); }).toList(), ); }).toList(), ), const Divider(), ], ); }, ), ), if (pedidoActual != null && pedidoActual!.id! > 0) _buildMesaSelector(), if (pedidoActual != null && pedidoActual!.id! > 0) const SizedBox(height: 5), _buildDiscountSection(), const Divider(thickness: 5), _buildTotalSection(), const SizedBox(height: 25), Padding( padding: const EdgeInsets.all(8.0), child: Column( children: [ if (pedidoActual != null && pedidoActual!.id! > 0) ...[ ElevatedButton( onPressed: () => _guardarPedidoExistente(), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.tertiary, textStyle: const TextStyle(fontSize: 22), fixedSize: const Size(250, 50), ), child: Text( 'Actualizar Pedido', style: TextStyle(color: AppTheme.quaternary), ), ), const SizedBox(height: 10), ElevatedButton( onPressed: () => _promptForCustomerName(), style: ElevatedButton.styleFrom( backgroundColor: Colors.green, textStyle: const TextStyle(fontSize: 22), fixedSize: const Size(250, 50), ), child: Text( 'Finalizar Pedido', style: TextStyle(color: Colors.white), ), ), ] else ...[ if (_isMesasActive) ElevatedButton( onPressed: () => _crearPedidoConModal(), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.tertiary, textStyle: const TextStyle(fontSize: 22), fixedSize: const Size(250, 50), ), child: Text( 'Iniciar Pedido', style: TextStyle(color: AppTheme.quaternary), ), ), const SizedBox(height: 5), ElevatedButton( onPressed: () => _promptForCustomerName(), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.rojo, textStyle: const TextStyle(fontSize: 18), fixedSize: const Size(250, 50), ), child: Text( 'Finalizar Pedido Sin Mesa', style: TextStyle(color: AppTheme.quaternary), ), ), ] ], ), ), ], ), ); } void _mostrarDialogoComentario( BuildContext context, ItemCarrito item, int index) { TextEditingController comentarioController = TextEditingController(text: carrito[index].comentario ?? ''); showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text( 'Comentario del Producto', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold), ), content: TextField( controller: comentarioController, maxLines: 3, decoration: const InputDecoration( hintText: 'Escribe un comentario...', border: OutlineInputBorder(), ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Cancelar'), ), TextButton( onPressed: () { setState(() { carrito[index].comentario = comentarioController.text.trim(); }); Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Comentario guardado')), ); }, child: const Text('Guardar'), ), ], ); }, ); } void eliminarProductoDelCarrito(int index) async { bool autorizado = true; // Solicitar autenticación solo si el pedido tiene un ID mayor a 0 if (pedidoActual != null && pedidoActual!.id! > 0) { autorizado = await autenticarConCodigo(context); } if (autorizado) { final producto = carrito[index].producto; // Verificar si el pedido actual tiene un ID mayor a 0 if (pedidoActual != null && pedidoActual!.id! > 0) { final pedidoProducto = pedidoActual!.productos .firstWhereOrNull((p) => p.idProducto == producto.id); if (pedidoProducto != null) { // Marcar como eliminado en la base de datos await Provider.of(context, listen: false) .eliminarProductoDelPedido(producto.id!, pedidoActual!.id!); } } // Remover el producto del carrito en la UI setState(() { carrito.removeAt(index); }); // Recalcular el total _recalcularTotal(); // Mostrar mensaje de confirmación ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("Producto eliminado del carrito.")), ); } } void incrementarProducto(ItemCarrito item) { setState(() { item.cantidad++; }); _recalcularTotal(); } void quitarProductoDelCarrito(ItemCarrito item) async { bool autorizado = true; if (pedidoActual != null && pedidoActual!.id! > 0) { autorizado = await autenticarConCodigo(context); } if (autorizado) { setState(() { if (item.cantidad > 1) { item.cantidad--; } else { if (pedidoActual != null && pedidoActual!.id! > 0) { final pedidoProducto = pedidoActual!.productos .firstWhereOrNull((p) => p.idProducto == item.producto.id); if (pedidoProducto != null) { Provider.of(context, listen: false) .eliminarProductoDelPedido( item.producto.id!, pedidoActual!.id!); } } carrito.remove(item); } }); _recalcularTotal(); // Mostrar mensaje de confirmación ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(item.cantidad > 0 ? "Cantidad del producto actualizada." : "Producto marcado como eliminado."), ), ); } } Widget _buildProductsSection() { return Column( children: [ const SizedBox(height: 5), _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]; // Si no se está buscando, aplicar el filtro de categoría if (!_estadoBusqueda && 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: 65, child: Scrollbar( thumbVisibility: true, trackVisibility: true, interactive: true, controller: _categoryScrollController, child: Padding( padding: EdgeInsets.fromLTRB(0, 0, 0, 15), child: ListView.builder( controller: _categoryScrollController, 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( backgroundColor: isSelected ? AppTheme.tertiary : Colors.grey, foregroundColor: isSelected ? AppTheme.quaternary : 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), suffixIcon: IconButton( icon: Icon(Icons.clear), onPressed: () { _searchController.clear(); _onSearchChanged(''); }, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12.0), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12.0), borderSide: BorderSide(width: 1.5)), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.black), borderRadius: BorderRadius.circular(12.0), ), ), onChanged: _onSearchChanged, ), ); } Future autenticarConCodigo(BuildContext context) async { final TextEditingController codeController = TextEditingController(); return await showDialog( context: context, builder: (context) { return AlertDialog( title: const Text("Autenticación requerida", style: TextStyle(fontWeight: FontWeight.w500, fontSize: 22)), content: Column( mainAxisSize: MainAxisSize.min, children: [ const Text( "Por favor, introduce el código de autenticación generado.", style: TextStyle(fontSize: 18), ), const SizedBox(height: 16), TextField( controller: codeController, decoration: const InputDecoration( labelText: "Código de autenticación", ), ), ], ), actions: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: Text('Cancelar', style: TextStyle( fontSize: 18, color: AppTheme.secondary)), style: ButtonStyle( padding: MaterialStatePropertyAll( EdgeInsets.fromLTRB(20, 10, 20, 10)), backgroundColor: MaterialStatePropertyAll(Colors.red), foregroundColor: MaterialStatePropertyAll(AppTheme.secondary)), ), TextButton( onPressed: () { final now = DateTime.now().toUtc(); timezone.initializeTimeZones(); final pacificTimeZone = timezone.getLocation('America/Los_Angeles'); final date = timezone.TZDateTime.from(now, pacificTimeZone); final codigoTotp = OTP.generateTOTPCodeString( 'TYSNE4CMT5LVLGWS', date.millisecondsSinceEpoch, algorithm: Algorithm.SHA1, isGoogle: true, ); String codigo = codeController.text.trim(); List codigosEstaticos = [ '172449', '827329', // Agregar más códigos estáticos si es necesario ]; bool esCodigoValido = codigosEstaticos.contains(codigo) || codigo == codigoTotp; if (!esCodigoValido) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('El código no es correcto'), duration: Duration(seconds: 2), ), ); return; } Navigator.of(context).pop(true); }, child: Text('Confirmar', style: TextStyle( fontSize: 18, color: AppTheme.quaternary)), style: ButtonStyle( padding: MaterialStatePropertyAll( EdgeInsets.fromLTRB(20, 10, 20, 10)), backgroundColor: MaterialStatePropertyAll(AppTheme.secondary), foregroundColor: MaterialStatePropertyAll(AppTheme.secondary)), ), ], ) ], ); }, ) ?? false; } Widget _buildMesaSelector() { return FutureBuilder( future: Provider.of(context, listen: false) .fetchLocalAll(sinLimite: true), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } List mesasDisponibles = Provider.of(context, listen: false).mesas; TextEditingController mesaSearchController = TextEditingController(); return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ const Text( 'Mesa:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), ), const SizedBox(width: 16), Expanded( child: AppDropdownSearch( controller: mesaSearchController, asyncItems: (String query) async { await Provider.of(context, listen: false) .fetchLocalByName(nombre: query); return Provider.of(context, listen: false) .mesas; }, itemAsString: (dynamic mesa) => (mesa as Mesa).nombre ?? 'Sin Nombre', selectedItem: mesasDisponibles.firstWhere( (mesa) => mesa.id == pedidoActual?.idMesa, orElse: () => null as Mesa, ), onChanged: (dynamic nuevaMesa) { setState(() { pedidoActual?.idMesa = (nuevaMesa as Mesa).id; }); }, items: mesasDisponibles, ), ), ], ), ); }, ); } }