2 次代码提交 7e01fdcb7d ... 35d5bc8b8b

作者 SHA1 备注 提交日期
  ElPoteito 35d5bc8b8b Avance pedidos por día 2 月之前
  ElPoteito bbeb86fdc6 15358: Poner rango en selector de Pedidos de dia , ajustar dos columnas incluirlas, fecha y propina, cambiar etiqueta del final de dia por rango y en ticket agregar total propinas https://prnt.sc/RFz4_6F7uIHS 2 月之前
共有 2 个文件被更改,包括 348 次插入66 次删除
  1. 102 0
      lib/views/venta/venta_csv.dart
  2. 246 66
      lib/views/venta/venta_screen.dart

+ 102 - 0
lib/views/venta/venta_csv.dart

@@ -0,0 +1,102 @@
+import 'dart:convert';
+import 'package:csv/csv.dart';
+import 'package:intl/intl.dart';
+import 'package:path_provider/path_provider.dart';
+import 'dart:io';
+import 'package:path/path.dart' as p;
+import 'package:provider/provider.dart';
+import 'package:turquessa_app/viewmodels/propina_view_model.dart';
+import '../../models/models.dart';
+
+Future<void> exportarVentasACSV(List<Pedido> pedidos, List<Propinas> propinas, String fileName) async {
+  List<List<dynamic>> rows = [
+    [
+      "Folio",
+      "Cliente",
+      "Producto",
+      "Cantidad",
+      "Precio Unitario",
+      "Toppings",
+      "Descuento (%)",
+      "Estado",
+      "Fecha",
+      "Total con Descuento",
+      "Tipo de Pago",
+      "Cantidad Efectivo",
+      "Cantidad Tarjeta",
+      "Cantidad Transferencia",
+      "Propina"
+    ]
+  ];
+
+  for (var pedido in pedidos) {
+    for (var producto in pedido.productos) {
+      // Convertir toppings a una cadena de texto y calcular el total adicional de los toppings
+      double totalToppingsPrecio = 0.0;
+      String toppingsText = producto.toppings.isNotEmpty
+          ? producto.toppings.map((t) {
+              String toppingNombre =
+                  t.topping?.nombre ?? 'Topping no especificado';
+              double toppingPrecio = t.topping?.precio ?? 0.0;
+
+              if (toppingPrecio > 0) {
+                toppingNombre += "(+\$${formatCurrency(toppingPrecio)})";
+                totalToppingsPrecio += toppingPrecio;
+              }
+
+              return toppingNombre;
+            }).join(', ')
+          : 'Sin toppings';
+
+      // Calcular el total con descuento para este producto
+      double precioUnitario = producto.producto?.precio ?? 0.0;
+      double subtotal =
+          (precioUnitario + totalToppingsPrecio) * (producto.cantidad ?? 1);
+      double descuento = pedido.descuento?.toDouble() ?? 0.0;
+      double precioDescuento = subtotal * (descuento / 100);
+      double totalConDescuento = subtotal - precioDescuento;
+      double propina = 0.0;
+
+      for (var prop in propinas) {
+        if (prop.idPedido == pedido.id) {
+          propina = propina + prop.cantidad!;
+        }
+      }
+
+      List<dynamic> row = [
+        pedido.folio,
+        pedido.nombreCliente,
+        producto.producto?.nombre ?? 'No especificado',
+        producto.cantidad,
+        formatCurrency(precioUnitario),
+        toppingsText,
+        descuento,
+        pedido.estatus,
+        pedido.peticion ?? '',
+        formatCurrency(totalConDescuento),
+        pedido.tipoPago ?? 'No especificado',
+        formatCurrency(pedido.cantEfectivo ?? 0.0),
+        formatCurrency(pedido.cantTarjeta ?? 0.0),
+        formatCurrency(pedido.cantTransferencia ?? 0.0),
+        formatCurrency(propina ?? 0.0),
+      ];
+      rows.add(row);
+    }
+  }
+
+  String csv = const ListToCsvConverter().convert(rows);
+
+  final directory = await getApplicationDocumentsDirectory();
+  final path = p.join(directory.path, fileName);
+  final file = File(path);
+
+  final utf8Csv = utf8.encode('\uFEFF' + csv);
+  await file.writeAsBytes(utf8Csv, flush: true);
+
+  print('Archivo CSV guardado en $path');
+}
+
+String formatCurrency(double amount) {
+  final format = NumberFormat("#,##0.00", "es_MX");
+  return format.format(amount);
+}

+ 246 - 66
lib/views/venta/venta_screen.dart

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
 import 'package:intl/intl.dart';
 import 'package:omni_datetime_picker/omni_datetime_picker.dart';
 import 'package:provider/provider.dart';
+import 'package:turquessa_app/views/venta/venta_csv.dart';
 import '../../widgets/widgets_components.dart';
 import '../../models/models.dart';
 import '../../viewmodels/viewmodels.dart';
@@ -15,11 +16,14 @@ class VentaScreen extends StatefulWidget {
 }
 
 class _VentaScreenState extends State<VentaScreen> {
-  DateTime? fechaSeleccionada;
+  DateTime? fechaInicialSeleccionada;
+  DateTime? fechaFinalSeleccionada;
   List<Pedido> pedidosNoCancelados = [];
   List<Pedido> pedidosCancelados = [];
+  List<Propinas> listaPropinas = [];
   final _busqueda = TextEditingController(text: '');
   double totalDelDia = 0.0;
+  double totalPropinas = 0.0;
   double totalCancelados = 0.0;
 
   double totalEfectivoDelDia = 0.0;
@@ -36,7 +40,8 @@ class _VentaScreenState extends State<VentaScreen> {
   void clearSearchAndReset() {
     setState(() {
       _busqueda.clear();
-      fechaSeleccionada = null;
+      fechaInicialSeleccionada = null;
+      fechaFinalSeleccionada = null;
     });
   }
 
@@ -45,9 +50,18 @@ class _VentaScreenState extends State<VentaScreen> {
     return Scaffold(
       appBar: AppBar(
           title: Text(
-            "Resumen de Pedidos por Día",
+            "Resumen de Pedidos por Periodo",
             style: TextStyle(color: AppTheme.secondary),
           ),
+          actions: <Widget>[
+            IconButton(
+              icon: const Icon(Icons.save_alt),
+              onPressed: () {
+                exportCSV(fechaInicialSeleccionada, fechaFinalSeleccionada);
+              },
+              tooltip: 'Exportar a CSV',
+            ),
+          ],
           iconTheme: IconThemeData(color: AppTheme.secondary)),
       body: Padding(
         padding: const EdgeInsets.all(16.0),
@@ -62,16 +76,16 @@ class _VentaScreenState extends State<VentaScreen> {
                       child: tarjeta(
                         ListTile(
                             title: Text(
-                              "Fecha",
+                              "Fecha Inicial",
                               style: TextStyle(
                                   color: AppTheme.quaternary,
                                   fontWeight: FontWeight.bold),
                             ),
                             subtitle: Text(
-                              fechaSeleccionada == null
+                              fechaInicialSeleccionada == null
                                   ? ""
                                   : DateFormat("dd/MM/yyyy")
-                                      .format(fechaSeleccionada!),
+                                      .format(fechaInicialSeleccionada!),
                               style: TextStyle(
                                   color: AppTheme.quaternary,
                                   fontWeight: FontWeight.bold),
@@ -80,15 +94,58 @@ class _VentaScreenState extends State<VentaScreen> {
                                 color: AppTheme.quaternary),
                             onTap: () async {
                               DateTime? d = await showDatetimePicker(
-                                  context, fechaSeleccionada,
-                                  inicia: fechaSeleccionada,
+                                  context, fechaInicialSeleccionada,
+                                  inicia: fechaInicialSeleccionada,
                                   tipo: OmniDateTimePickerType.date,
                                   solofecha: true);
                               if (d == null) return;
                               setState(() {
-                                fechaSeleccionada = d;
+                                fechaInicialSeleccionada = d;
                               });
-                              cargarPedidos(fechaSeleccionada!);
+                              cargarPedidos(fechaInicialSeleccionada,
+                                  fechaFinalSeleccionada);
+                            }),
+                        color: AppTheme.tertiary,
+                      ),
+                    )),
+                const SizedBox(
+                  width: 10,
+                ),
+                Expanded(
+                    flex: 3,
+                    child: Padding(
+                      padding: const EdgeInsets.symmetric(horizontal: 0.0),
+                      child: tarjeta(
+                        ListTile(
+                            title: Text(
+                              "Fecha Final",
+                              style: TextStyle(
+                                  color: AppTheme.quaternary,
+                                  fontWeight: FontWeight.bold),
+                            ),
+                            subtitle: Text(
+                              fechaFinalSeleccionada == null
+                                  ? ""
+                                  : DateFormat("dd/MM/yyyy")
+                                      .format(fechaFinalSeleccionada!),
+                              style: TextStyle(
+                                  color: AppTheme.quaternary,
+                                  fontWeight: FontWeight.bold),
+                            ),
+                            trailing: Icon(Icons.calendar_month,
+                                color: AppTheme.quaternary),
+                            onTap: () async {
+                              DateTime? d = await showDatetimePicker(
+                                  context, fechaFinalSeleccionada,
+                                  inicia: fechaFinalSeleccionada,
+                                  tipo: OmniDateTimePickerType.date,
+                                  solofecha: true);
+                              if (d == null) return;
+                              setState(() {
+                                fechaFinalSeleccionada = d;
+                              });
+                              cargarPedidos(fechaInicialSeleccionada,
+                                  fechaFinalSeleccionada);
                             }),
                         color: AppTheme.tertiary,
                       ),
@@ -136,6 +193,16 @@ class _VentaScreenState extends State<VentaScreen> {
                                 fontSize: 20, fontWeight: FontWeight.w500),
                           ),
                           Text(
+                            "Fecha/Hora: ${_formatDateTime(pedido.peticion)}",
+                            style: TextStyle(
+                                fontSize: 20, fontWeight: FontWeight.w500),
+                          ),
+                          Text(
+                            "Propina: \$${formatCurrency(_propinaPedido(pedido.id) ?? 0)}",
+                            style: TextStyle(
+                                fontSize: 20, fontWeight: FontWeight.w500),
+                          ),
+                          Text(
                               "Total: \$${formatCurrency(pedido.totalPedido ?? 0)}",
                               style: TextStyle(
                                   fontSize: 20, fontWeight: FontWeight.w500)),
@@ -155,6 +222,16 @@ class _VentaScreenState extends State<VentaScreen> {
                                 color: Colors.red),
                           ),
                           Text(
+                            "Fecha/Hora: ${_formatDateTime(pedido.peticion)}",
+                            style: TextStyle(
+                                fontSize: 20, fontWeight: FontWeight.w500),
+                          ),
+                          Text(
+                            "Propina: \$${formatCurrency(_propinaPedido(pedido.id) ?? 0)}",
+                            style: TextStyle(
+                                fontSize: 20, fontWeight: FontWeight.w500),
+                          ),
+                          Text(
                               "Total: \$${formatCurrency(pedido.totalPedido ?? 0)}",
                               style: TextStyle(
                                   fontSize: 20,
@@ -194,13 +271,25 @@ class _VentaScreenState extends State<VentaScreen> {
                 Column(
                   crossAxisAlignment: CrossAxisAlignment.end,
                   children: [
-                    Padding(
-                      padding: const EdgeInsets.all(16.0),
-                      child: Text(
-                        "Total del día: \$${formatCurrency(totalDelDia)}",
-                        style: TextStyle(
-                            fontSize: 20, fontWeight: FontWeight.bold),
-                      ),
+                    Row(
+                      children: [
+                        Padding(
+                          padding: const EdgeInsets.all(16.0),
+                          child: Text(
+                            "Total Propina: \$${formatCurrency(totalPropinas)}",
+                            style: TextStyle(
+                                fontSize: 20, fontWeight: FontWeight.bold),
+                          ),
+                        ),
+                        Padding(
+                          padding: const EdgeInsets.all(16.0),
+                          child: Text(
+                            "Total: \$${formatCurrency(totalDelDia)}",
+                            style: TextStyle(
+                                fontSize: 20, fontWeight: FontWeight.bold),
+                          ),
+                        ),
+                      ],
                     ),
                     Row(
                       children: [
@@ -263,62 +352,153 @@ class _VentaScreenState extends State<VentaScreen> {
     );
   }
 
-  void cargarPedidos(DateTime fecha) async {
-    // Convertir el inicio y fin del día local a UTC
-    final inicioDelDia = DateTime(fecha.year, fecha.month, fecha.day)
-        .toUtc(); // Convierte la fecha local de inicio del día a UTC
-
-    final finDelDia = DateTime(fecha.year, fecha.month, fecha.day, 23, 59, 59)
-        .toUtc(); // Convierte la fecha local de fin del día a UTC
-
-    print('Buscando pedidos desde: $inicioDelDia hasta: $finDelDia (en UTC)');
-
-    // Realizar la búsqueda en UTC
-    final pedidos = await Provider.of<PedidoViewModel>(context, listen: false)
-        .buscarPorFecha(inicioDelDia, finDelDia);
-
-    print('Pedidos obtenidos: ${pedidos.length}');
-    pedidos.forEach((pedido) => print(
-        'Pedido: ${pedido.folio}, Total: ${pedido.totalPedido}, Estatus: ${pedido.estatus}'));
-
-    final pedidosNoCancelados =
-        pedidos.where((p) => p.estatus != "CANCELADO").toList();
-    final pedidosCancelados =
-        pedidos.where((p) => p.estatus == "CANCELADO").toList();
-
-    totalDelDia = 0.0;
-    totalCancelados = 0.0;
-    totalEfectivoDelDia = 0.0;
-    totalTarjetaDelDia = 0.0;
-    totalTransferenciaDelDia = 0.0;
-    cambio = 0.0;
-    totalSinCambio = 0.0;
-
-    for (var pedido in pedidosNoCancelados) {
-      totalDelDia += pedido.totalPedido ?? 0.0;
-      totalEfectivoDelDia += pedido.cantEfectivo ?? 0.0;
-      totalTarjetaDelDia += pedido.cantTarjeta ?? 0.0;
-      totalTransferenciaDelDia += pedido.cantTransferencia ?? 0.0;
-      totalSinCambio =
-          totalEfectivoDelDia + totalTarjetaDelDia + totalTransferenciaDelDia;
-
-      cambio = totalSinCambio - totalDelDia;
+  void cargarPedidos(DateTime? fechaInicial, DateTime? fechaFinal) async {
+    if (fechaInicial != null && fechaFinal != null) {
+      // Convertir el inicio y fin del día local a UTC
+      final inicioDelDia =
+          DateTime(fechaInicial.year, fechaInicial.month, fechaInicial.day)
+              .toUtc(); // Convierte la fecha local de inicio del día a UTC
+
+      final finDelDia = DateTime(
+              fechaFinal.year, fechaFinal.month, fechaFinal.day, 23, 59, 59)
+          .toUtc(); // Convierte la fecha local de fin del día a UTC
+
+      print('Buscando pedidos desde: $inicioDelDia hasta: $finDelDia (en UTC)');
+
+      // Realizar la búsqueda en UTC
+      final pedidos = await Provider.of<PedidoViewModel>(context, listen: false)
+          .buscarPorFecha(inicioDelDia, finDelDia);
+
+      print('Pedidos obtenidos: ${pedidos.length}');
+      pedidos.forEach((pedido) => print(
+          'Pedido: ${pedido.folio}, Total: ${pedido.totalPedido}, Estatus: ${pedido.estatus}, Propinas: ${pedido.propinas.length}'));
+
+      final pedidosNoCancelados =
+          pedidos.where((p) => p.estatus != "CANCELADO").toList();
+      final pedidosCancelados =
+          pedidos.where((p) => p.estatus == "CANCELADO").toList();
+
+      totalDelDia = 0.0;
+      totalPropinas = 0.0;
+      totalCancelados = 0.0;
+      totalEfectivoDelDia = 0.0;
+      totalTarjetaDelDia = 0.0;
+      totalTransferenciaDelDia = 0.0;
+      cambio = 0.0;
+      totalSinCambio = 0.0;
+
+      for (var pedido in pedidosNoCancelados) {
+        totalDelDia += pedido.totalPedido ?? 0.0;
+        totalEfectivoDelDia += pedido.cantEfectivo ?? 0.0;
+        totalTarjetaDelDia += pedido.cantTarjeta ?? 0.0;
+        totalTransferenciaDelDia += pedido.cantTransferencia ?? 0.0;
+        totalSinCambio =
+            totalEfectivoDelDia + totalTarjetaDelDia + totalTransferenciaDelDia;
+
+        cambio = totalSinCambio - totalDelDia;
+
+        print("Propinas: ${pedido.propinas.length}");
+        // if (pedido.propinas.length > 0) {
+        //   for (var propina in pedido.propinas) {
+        //     totalPropinas += propina.cantidad ?? 0.0;
+        //   }
+        // }
+
+        final propinas =
+            await Provider.of<PropinaViewModel>(context, listen: false)
+                .obtenerPropinasPorPedido(pedido.id);
+
+        listaPropinas = propinas;
+        for (var propina in propinas) {
+          totalPropinas += propina.cantidad ?? 0.0;
+        }
+        print("Total propinas: ${totalPropinas}");
+      }
+
+      totalCancelados = pedidosCancelados.fold(
+          0.0, (sum, current) => sum + (current.totalPedido ?? 0.0));
+
+      setState(() {
+        this.pedidosNoCancelados = pedidosNoCancelados;
+        this.pedidosCancelados = pedidosCancelados;
+      });
     }
+  }
 
-    totalCancelados = pedidosCancelados.fold(
-        0.0, (sum, current) => sum + (current.totalPedido ?? 0.0));
+  void exportCSV(DateTime? fechaInicial, DateTime? fechaFinal) async {
+    final pedidosViewModel =
+        Provider.of<PedidoViewModel>(context, listen: false);
+    List<Pedido> pedidosConProductos = [];
+    List<Propinas> propinas = [];
+    List<Propinas> altPropinas = [];
 
-    setState(() {
-      this.pedidosNoCancelados = pedidosNoCancelados;
-      this.pedidosCancelados = pedidosCancelados;
-    });
+    for (Pedido pedido in pedidosViewModel.pedidos) {
+      Pedido? pedidoConProductos =
+          await pedidosViewModel.fetchPedidoConProductos(pedido.id);
+      if (pedidoConProductos != null) {
+        pedidosConProductos.add(pedidoConProductos);
+      }
+    }
+
+    if (pedidosConProductos.isNotEmpty) {
+      String fileName = 'Ventas_Turquessa_POS';
+      if (fechaInicial != null && fechaFinal != null) {
+        String startDateStr = DateFormat('dd-MM-yyyy').format(fechaInicial!);
+        String endDateStr = DateFormat('dd-MM-yyyy').format(fechaFinal!);
+        fileName += '_${startDateStr}_al_${endDateStr}';
+      }
+      fileName += '.csv';
+
+      for (var pedido in pedidosConProductos) {
+        altPropinas =
+            await Provider.of<PropinaViewModel>(context, listen: false)
+                .obtenerPropinasPorPedido(pedido.id);
+      }
+
+      for (var alt in altPropinas) {
+        propinas.add(alt);
+      }
+
+      await exportarVentasACSV(pedidosConProductos, propinas, fileName);
+      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
+          content: Text('Archivo CSV descargado! Archivo: $fileName')));
+    } else {
+      ScaffoldMessenger.of(context).showSnackBar(
+          SnackBar(content: Text('No hay pedidos para exportar.')));
+    }
   }
 
   Future<void> imprimirResumenPedidos(List<Pedido> pedidos) async {
-    if (fechaSeleccionada == null) {
-      print("No se ha seleccionado una fecha.");
+    if (fechaInicialSeleccionada == null) {
+      print("No se ha seleccionado una fecha inicial.");
       return;
     }
-    await VentaTicket.imprimirResumenPedidos(pedidos, fechaSeleccionada!);
+    if (fechaFinalSeleccionada == null) {
+      print("No se ha seleccionado una fecha final.");
+      return;
+    }
+    await VentaTicket.imprimirResumenPedidos(
+        pedidos, fechaInicialSeleccionada!);
+  }
+
+  String _formatDateTime(String? dateTimeString) {
+    if (dateTimeString == null) return "Sin fecha";
+
+    DateTime parsedDate = DateTime.parse(dateTimeString);
+
+    var formatter = DateFormat('dd-MM-yyyy HH:mm:ss');
+    return formatter.format(parsedDate.toLocal());
+  }
+
+  double? _propinaPedido(int? idPedido) {
+    double? propina = 0.0;
+
+    for (var p in listaPropinas) {
+      if (p.idPedido == idPedido) {
+        propina = p.cantidad;
+      }
+    }
+
+    return propina;
   }
 }