Parcourir la source

Version nueva

OscarGil03 il y a 8 mois
Parent
commit
7f6d87310c
40 fichiers modifiés avec 4028 ajouts et 1486 suppressions
  1. 2 2
      android/app/src/main/AndroidManifest.xml
  2. BIN
      android/app/src/main/res/mipmap-hdpi/launcher_icon.png
  3. BIN
      android/app/src/main/res/mipmap-mdpi/launcher_icon.png
  4. BIN
      android/app/src/main/res/mipmap-xhdpi/launcher_icon.png
  5. BIN
      android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png
  6. BIN
      android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png
  7. BIN
      assets/JoshiLogo.ico
  8. 12 0
      lib/models/categoria_producto_model.dart
  9. 41 0
      lib/models/corte_caja_model.dart
  10. 31 0
      lib/models/deposito_model.dart
  11. 31 0
      lib/models/gasto_model.dart
  12. 6 3
      lib/models/item_carrito_model.dart
  13. 3 0
      lib/models/models.dart
  14. 4 0
      lib/models/pedido_model.dart
  15. 8 12
      lib/models/pedido_producto_model.dart
  16. 13 37
      lib/models/pedido_producto_toping_model.dart
  17. 2 5
      lib/models/producto_model.dart
  18. 27 0
      lib/models/producto_topping_model.dart
  19. 28 12
      lib/services/productos_service.dart
  20. 360 52
      lib/services/repo_service.dart
  21. 1 1
      lib/themes/themes.dart
  22. 107 61
      lib/viewmodels/categoria_producto_view_model.dart
  23. 118 6
      lib/viewmodels/pedido_view_model.dart
  24. 138 32
      lib/viewmodels/producto_view_model.dart
  25. 138 88
      lib/views/categoria_producto/categoria_producto_form.dart
  26. 415 199
      lib/views/categoria_producto/categoria_producto_screen.dart
  27. 12 13
      lib/views/pedido/pedido_csv.dart
  28. 152 59
      lib/views/pedido/pedido_detalle_screen.dart
  29. 290 135
      lib/views/pedido/pedido_form.dart
  30. 328 103
      lib/views/pedido/pedido_screen.dart
  31. 149 192
      lib/views/pedido/pedido_ticket.dart
  32. 419 198
      lib/views/producto/producto_form.dart
  33. 17 0
      lib/views/producto/producto_imagen.dart
  34. 431 229
      lib/views/producto/producto_screen.dart
  35. 248 0
      lib/views/venta/venta_screen.dart
  36. 134 0
      lib/views/venta/venta_ticket.dart
  37. 41 16
      lib/widgets/app_drawer.dart
  38. 42 21
      lib/widgets/widgets_components.dart
  39. 249 9
      pubspec.lock
  40. 31 1
      pubspec.yaml

+ 2 - 2
android/app/src/main/AndroidManifest.xml

@@ -5,9 +5,9 @@
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
     <application
-        android:label="yoshi_papas_app"
+        android:label="joshi_papas_app"
         android:name="${applicationName}"
-        android:icon="@mipmap/ic_launcher">
+        android:icon="@mipmap/launcher_icon">
         <activity
             android:name=".MainActivity"
             android:exported="true"

BIN
android/app/src/main/res/mipmap-hdpi/launcher_icon.png


BIN
android/app/src/main/res/mipmap-mdpi/launcher_icon.png


BIN
android/app/src/main/res/mipmap-xhdpi/launcher_icon.png


BIN
android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png


BIN
android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png


BIN
assets/JoshiLogo.ico


+ 12 - 0
lib/models/categoria_producto_model.dart

@@ -3,10 +3,16 @@ import '../services/services.dart';
 
 class CategoriaProducto extends Basico {
   String? nombre;
+  String? descripcion;
+  int? esToping;
+  int? maximo;
 
   CategoriaProducto({
     super.id,
     this.nombre,
+    this.descripcion,
+    this.esToping,
+    this.maximo,
   });
 
   @override
@@ -14,12 +20,18 @@ class CategoriaProducto extends Basico {
     return {
       'id': id,
       'nombre': nombre,
+      'descripcion': descripcion,
+      'esToping': esToping,
+      'maximo': maximo,
     }..addAll(super.toJson());
   }
 
   CategoriaProducto.fromJson(Map<String, dynamic> json) {
     super.parseJson(json);
     nombre = Basico.parseString(json['nombre']);
+    descripcion = Basico.parseString(json['descripcion']);
+    esToping = Basico.parseInt(json['esToping']);
+    maximo = Basico.parseInt(json['maximo']);
   }
 
   Future<void> guardar() async {

+ 41 - 0
lib/models/corte_caja_model.dart

@@ -0,0 +1,41 @@
+import 'basico_model.dart';
+import '../services/services.dart';
+
+class CorteCaja extends Basico {
+  double? fondo;
+  double? fondo_ds;
+  double? ventas;
+  String? fecha;
+
+  CorteCaja({
+    super.id,
+    this.fondo,
+    this.fondo_ds,
+    this.ventas,
+    this.fecha,
+  });
+
+  @override
+  Map<String, dynamic> toJson() {
+    return {
+      'id': id,
+      'fondo': fondo,
+      'fondo_ds': fondo_ds,
+      'ventas': ventas,
+      'fecha': fecha,
+    }..addAll(super.toJson());
+  }
+
+  CorteCaja.fromJson(Map<String, dynamic> json) {
+    super.parseJson(json);
+    fondo = Basico.parseDouble(json['fondo']);
+    fondo_ds = Basico.parseDouble(json['fondo_ds']);
+    fondo_ds = Basico.parseDouble(json['fondo_ds']);
+    ventas = Basico.parseDouble(json['ventas']);
+    fecha = Basico.parseString(json['fecha']);
+  }
+
+  Future<void> guardar() async {
+    idLocal = await RepoService().guardar(this);
+  }
+}

+ 31 - 0
lib/models/deposito_model.dart

@@ -0,0 +1,31 @@
+import 'basico_model.dart';
+import '../services/services.dart';
+
+class Deposito extends Basico {
+  double? monto;
+  String? descripcion;
+  int? idCorteCaja;
+
+  Deposito({super.id, this.monto, this.descripcion, this.idCorteCaja});
+
+  @override
+  Map<String, dynamic> toJson() {
+    return {
+      'id': id,
+      'monto': monto,
+      'descripcion': descripcion,
+      'idCorteCaja': idCorteCaja,
+    }..addAll(super.toJson());
+  }
+
+  Deposito.fromJson(Map<String, dynamic> json) {
+    super.parseJson(json);
+    monto = Basico.parseDouble(json['monto']);
+    descripcion = Basico.parseString(json['descripcion']);
+    idCorteCaja = Basico.parseInt(json['idCorteCaja']);
+  }
+
+  Future<void> guardar() async {
+    idLocal = await RepoService().guardar(this);
+  }
+}

+ 31 - 0
lib/models/gasto_model.dart

@@ -0,0 +1,31 @@
+import 'basico_model.dart';
+import '../services/services.dart';
+
+class Gasto extends Basico {
+  double? monto;
+  String? descripcion;
+  int? idCorteCaja;
+
+  Gasto({super.id, this.monto, this.descripcion, this.idCorteCaja});
+
+  @override
+  Map<String, dynamic> toJson() {
+    return {
+      'id': id,
+      'monto': monto,
+      'descripcion': descripcion,
+      'idCorteCaja': idCorteCaja,
+    }..addAll(super.toJson());
+  }
+
+  Gasto.fromJson(Map<String, dynamic> json) {
+    super.parseJson(json);
+    monto = Basico.parseDouble(json['monto']);
+    descripcion = Basico.parseString(json['descripcion']);
+    idCorteCaja = Basico.parseInt(json['idCorteCaja']);
+  }
+
+  Future<void> guardar() async {
+    idLocal = await RepoService().guardar(this);
+  }
+}

+ 6 - 3
lib/models/item_carrito_model.dart

@@ -4,11 +4,14 @@ import 'producto_model.dart';
 class ItemCarrito {
   Producto producto;
   int cantidad;
-  List<Toping> toppings;
+  Map<int, Set<int>> selectedToppings;
+  Map<int, List<Producto>> selectableToppings;
 
   ItemCarrito({
     required this.producto,
     this.cantidad = 1,
-    this.toppings = const [],
-  });
+    Map<int, Set<int>>? selectedToppings,
+    Map<int, List<Producto>>? selectableToppings,
+  })  : selectedToppings = selectedToppings ?? {},
+        selectableToppings = selectableToppings ?? {};
 }

+ 3 - 0
lib/models/models.dart

@@ -13,3 +13,6 @@ export '../models/producto_model.dart';
 export '../models/toping_categoria_model.dart';
 export '../models/toping_model.dart';
 export '../models/item_carrito_model.dart';
+export '../models/gasto_model.dart';
+export '../models/deposito_model.dart';
+export '../models/corte_caja_model.dart';

+ 4 - 0
lib/models/pedido_model.dart

@@ -8,6 +8,7 @@ class Pedido extends Basico {
   String? comentarios;
   String? peticion;
   String? nombreCliente;
+  double? totalPedido;
   int? idCliente;
   int? idMesa;
   String? terminado;
@@ -24,6 +25,7 @@ class Pedido extends Basico {
     this.comentarios,
     this.peticion,
     this.nombreCliente,
+    this.totalPedido,
     this.idCliente,
     this.idMesa,
     this.terminado,
@@ -43,6 +45,7 @@ class Pedido extends Basico {
       'comentarios': comentarios,
       'peticion': peticion,
       'nombreCliente': nombreCliente,
+      'totalPedido': totalPedido,
       'idCliente': idCliente,
       'idMesa': idMesa,
       'terminado': terminado,
@@ -62,6 +65,7 @@ class Pedido extends Basico {
     comentarios = Basico.parseString(json['comentarios']);
     peticion = Basico.parseString(json['peticion']);
     nombreCliente = Basico.parseString(json['nombreCliente']);
+    totalPedido = Basico.parseDouble(json['totalPedido']);
     idCliente = Basico.parseInt(json['idCliente']);
     idMesa = Basico.parseInt(json['idMesa']);
     terminado = Basico.parseString(json['terminado']);

+ 8 - 12
lib/models/pedido_producto_model.dart

@@ -1,7 +1,4 @@
-import 'package:yoshi_papas_app/models/producto_model.dart';
-import 'package:yoshi_papas_app/models/toping_model.dart';
-
-import 'basico_model.dart';
+import 'models.dart';
 
 class PedidoProducto extends Basico {
   int? idPedido;
@@ -12,7 +9,7 @@ class PedidoProducto extends Basico {
   int? cantidad;
   int? terminar;
   String? comentario;
-  //List<Map<String, dynamic>>? topings;
+  List<PedidoProductoTopping> toppings = [];
 
   PedidoProducto({
     super.id,
@@ -24,7 +21,7 @@ class PedidoProducto extends Basico {
     this.cantidad,
     this.terminar,
     this.comentario,
-    //this.topings = const [],
+    this.toppings = const [],
   });
 
   @override
@@ -38,7 +35,6 @@ class PedidoProducto extends Basico {
       'cantidad': cantidad,
       'terminar': terminar,
       'comentario': comentario,
-      //'topings': topings,
     }..addAll(super.toJson());
   }
 
@@ -53,10 +49,10 @@ class PedidoProducto extends Basico {
     cantidad = Basico.parseInt(json['cantidad']);
     terminar = Basico.parseInt(json['terminar']);
     comentario = Basico.parseString(json['comentario']);
-    // topings = (json['topings'] as List<dynamic>?)
-    //         ?.map((toping) =>
-    //             {'id': toping['id'], 'cantidad': toping['cantidad']})
-    //         .toList() ??
-    //     [];
+    toppings = (json['toppings'] as List<dynamic>?)
+            ?.map((topingJson) => PedidoProductoTopping.fromJson(
+                topingJson as Map<String, dynamic>))
+            .toList() ??
+        [];
   }
 }

+ 13 - 37
lib/models/pedido_producto_toping_model.dart

@@ -1,54 +1,30 @@
 import 'package:yoshi_papas_app/models/producto_model.dart';
-
 import 'basico_model.dart';
 
-class PedidoProductoToping extends Basico {
-  int? idPedido;
-  int? idProducto;
-  int? idToping;
-  String? costoUnitario;
-  String? descuento;
-  int? cantidad;
-  int? terminar;
-  String? comentario;
-  String? cancelado;
+class PedidoProductoTopping extends Basico {
+  int? idPedidoProducto;
+  int? idTopping;
+  Producto? topping;
 
-  PedidoProductoToping({
+  PedidoProductoTopping({
     super.id,
-    this.idPedido,
-    this.idProducto,
-    this.costoUnitario,
-    this.descuento,
-    this.cantidad,
-    this.terminar,
-    this.comentario,
-    this.cancelado,
+    this.idPedidoProducto,
+    this.idTopping,
+    this.topping,
   });
 
   @override
   Map<String, dynamic> toJson() {
     return {
       'id': id,
-      'idPedido': idPedido,
-      'idProducto': idProducto,
-      'costoUnitario': costoUnitario,
-      'descuento': descuento,
-      'cantidad': cantidad,
-      'terminar': terminar,
-      'comentario': comentario,
-      'cancelado': cancelado,
+      'idPedidoProducto': idPedidoProducto,
+      'idTopping': idTopping,
     }..addAll(super.toJson());
   }
 
-  PedidoProductoToping.fromJson(Map<String, dynamic> json) {
+  PedidoProductoTopping.fromJson(Map<String, dynamic> json) {
     super.parseJson(json);
-    idPedido = Basico.parseInt(json['idPedido']);
-    idProducto = Basico.parseInt(json['idProducto']);
-    costoUnitario = Basico.parseString(json['costoUnitario']);
-    descuento = Basico.parseString(json['descuento']);
-    cantidad = Basico.parseInt(json['cantidad']);
-    terminar = Basico.parseInt(json['terminar']);
-    comentario = Basico.parseString(json['comentario']);
-    cancelado = Basico.parseString(json['cancelado']);
+    idPedidoProducto = Basico.parseInt(json['idPedidoProducto']);
+    idTopping = Basico.parseInt(json['idTopping']);
   }
 }

+ 2 - 5
lib/models/producto_model.dart

@@ -17,7 +17,7 @@ class Producto extends Basico {
   String? descuento;
   String? venceDescuento;
   int? toping;
-  List<Toping>? topings;
+  List<Producto>? topings;
 
   Producto({
     super.id,
@@ -52,7 +52,6 @@ class Producto extends Basico {
       'descuento': descuento,
       'venceDescuento': venceDescuento,
       'toping': toping,
-      'toppings': topings?.map((toping) => toping.toJson()).toList(),
     }..addAll(super.toJson());
   }
 
@@ -71,8 +70,6 @@ class Producto extends Basico {
       'descuento': descuento,
       'venceDescuento': venceDescuento,
       'toping': toping,
-      // Asegúrate de convertir también los toppings si es necesario
-      'toppings': topings?.map((t) => t.toJson()).toList(),
     };
   }
 
@@ -99,7 +96,7 @@ class Producto extends Basico {
     }
     if (json['toppings'] != null) {
       topings = (json['toppings'] as List)
-          .map((topingJson) => Toping.fromJson(topingJson))
+          .map((id) => Producto(id: id as int))
           .toList();
     }
   }

+ 27 - 0
lib/models/producto_topping_model.dart

@@ -0,0 +1,27 @@
+import 'basico_model.dart';
+
+class ProductoTopping extends Basico {
+  int? idProducto;
+  int? idTopping;
+
+  ProductoTopping({
+    super.id,
+    this.idProducto,
+    this.idTopping,
+  });
+
+  @override
+  Map<String, dynamic> toJson() {
+    return {
+      'id': id,
+      'idProducto': idProducto,
+      'idTopping': idTopping,
+    }..addAll(super.toJson());
+  }
+
+  ProductoTopping.fromJson(Map<String, dynamic> json) {
+    super.parseJson(json);
+    idProducto = Basico.parseInt(json['idProducto']);
+    idTopping = Basico.parseInt(json['idTopping']);
+  }
+}

+ 28 - 12
lib/services/productos_service.dart

@@ -12,36 +12,52 @@ class ProductosService {
     final productoCount = Sqflite.firstIntValue(
       await dbClient.rawQuery('SELECT COUNT(*) FROM Producto'),
     );
+    print('Categoria Count: $categoriaCount, Producto Count: $productoCount');
     return categoriaCount == 0 && productoCount == 0;
   }
 
   Future<void> fillCategoriaBD() async {
     final List<CategoriaProducto> categorias = [
-      CategoriaProducto(id: 1, nombre: 'HAMBURGUESA DE POLLO'),
-      CategoriaProducto(id: 2, nombre: 'ESPECIALIDADES'),
-      CategoriaProducto(id: 3, nombre: 'POSTRES'),
-      CategoriaProducto(id: 4, nombre: 'BEBIDAS'),
+      CategoriaProducto(id: 1, nombre: 'COMIDAS'),
+      CategoriaProducto(id: 2, nombre: 'BEBIDAS'),
     ];
 
+    RepoService<CategoriaProducto> repoService =
+        RepoService<CategoriaProducto>();
     for (var categoria in categorias) {
-      await categoria.guardar();
+      await repoService.guardarLocal(categoria);
     }
   }
 
   Future<void> fillProductoBD() async {
     List<Producto> productos = [
-      //Snacks
+      Producto(id: 1, idCategoria: 1, nombre: 'CONO DE PAPAS', precio: '100'),
       Producto(
-          id: 1, idCategoria: 1, nombre: 'HAMBURGUESA DE POLLO', precio: '110'),
+          id: 2,
+          idCategoria: 1,
+          nombre: 'CONO DE BONELESS (250 GR)',
+          precio: '150'),
       Producto(
-          id: 2, idCategoria: 2, nombre: 'CONO FIT MEDIANO', precio: '120'),
-      Producto(id: 3, idCategoria: 3, nombre: 'MUFFIN', precio: '35'),
-      Producto(id: 4, idCategoria: 4, nombre: 'COCA COLA 600ML', precio: '25'),
-      Producto(id: 5, idCategoria: 4, nombre: 'AGUA', precio: '20'),
+          id: 3,
+          idCategoria: 1,
+          nombre: 'CONO DE PAPAS + BONELESS (CURLY, GAJO O FRANCESA)',
+          precio: '180'),
+      Producto(id: 4, idCategoria: 1, nombre: 'PIZZARINO', precio: '50'),
+      Producto(
+          id: 5,
+          idCategoria: 1,
+          nombre: 'CASCO DE BASEBALL CON PAPAS FRANCESAS',
+          precio: '120'),
+      Producto(
+          id: 6,
+          idCategoria: 1,
+          nombre: 'EXTRA 2 DEDOS DE QUESOS Y 2 AROS DE CEBOLLA',
+          precio: '50'),
     ];
 
+    RepoService<Producto> repoService = RepoService<Producto>();
     for (var producto in productos) {
-      await producto.guardar();
+      await repoService.guardarLocal(producto);
     }
   }
 }

+ 360 - 52
lib/services/repo_service.dart

@@ -1,12 +1,13 @@
 import 'dart:convert';
+import 'package:intl/intl.dart';
 import 'package:path/path.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:sqflite/sqflite.dart';
 import '../models/models.dart';
 
 class RepoService<T> {
-  static int dbVersion = 1;
-  static String dbName = 'joshipos023.db';
+  static int dbVersion = 4;
+  static String dbName = 'joshipos026.db';
   static const String id = Basico.identificadorWeb;
   static const String idLocal = Basico.identificadorLocal;
   static Database? _db;
@@ -15,6 +16,9 @@ class RepoService<T> {
     'Producto': Producto(),
     'Pedido': Pedido(productos: []),
     'PedidoProducto': PedidoProducto(),
+    'Gasto': Gasto(),
+    'Deposito': Deposito(),
+    'CorteCaja': CorteCaja(),
   };
 
   Future<Database?> get db async {
@@ -35,7 +39,6 @@ class RepoService<T> {
     );
   }
 
-  /// Crea la base de datos
   _onCreate(Database db, int version) async {
     contexto.forEach((String nombre, dynamic modelo) async {
       Map<String, dynamic> model = json.decode(json.encode(modelo.toJson()));
@@ -43,7 +46,6 @@ class RepoService<T> {
           "CREATE TABLE ${modelo.runtimeType.toString()} (id INTEGER PRIMARY KEY AUTOINCREMENT";
       model.forEach((String key, dynamic value) {
         if (key != "id") {
-          // No incluir la columna de identificador local de nuevo
           String tipo = value.runtimeType.toString();
           String sqlType = "";
           if (equals(tipo, 'int')) {
@@ -56,56 +58,281 @@ class RepoService<T> {
             sqlType = "TEXT";
           }
 
-          // Añadir cada campo al SQL, asegurándose de no configurar AUTOINCREMENT en folio
           sql += ", $key $sqlType";
         }
       });
       sql += ")";
       await db.execute(sql);
+
+      await db.execute('''
+        CREATE TABLE PedidoProductoTopping (
+          id INTEGER PRIMARY KEY AUTOINCREMENT,
+          idPedidoProducto INTEGER,
+          idTopping INTEGER,
+          FOREIGN KEY (idPedidoProducto) REFERENCES PedidoProducto(id),
+          FOREIGN KEY (idTopping) REFERENCES Producto(id)
+        )
+        ''');
     });
+
+    await db.execute('''
+      CREATE TABLE ProductoTopping (
+        id INTEGER PRIMARY KEY AUTOINCREMENT,
+        idProducto INTEGER,
+        idTopping INTEGER,
+        FOREIGN KEY (idProducto) REFERENCES Producto(id),
+        FOREIGN KEY (idTopping) REFERENCES Producto(id)
+      )
+    ''');
   }
 
-  /// Actualiza la version de la base de datos
-  _onUpgrade(Database db, int oldVersion, int newVersion) async {
-    while (oldVersion <= newVersion) {
+  void _onUpgrade(Database db, int oldVersion, int newVersion) async {
+    while (oldVersion < newVersion) {
       switch (oldVersion) {
-        case 100:
-          /* await db.execute("DROP TABLE IF EXISTS Evidencia");
-         await db.execute('CREATE TABLE TABLAA (idLocal INTEGER PRIMARY KEY)');
-          List<String> sueltos = [
-            "ALTER TABLE Evidencia ADD id INTEGER",
-          ];
-          for(int i = 0; i < sueltos.length; i++) {
-            try {
-              String sql = sueltos.elementAt(i);
-              await db.execute(sql);
-            } catch (e) {}
-          } */
+        case 1:
+          await db.execute(
+              "ALTER TABLE CategoriaProducto ADD COLUMN esToping INTEGER DEFAULT 0");
+
+          await db.execute(
+              "ALTER TABLE CategoriaProducto ADD COLUMN descripcion TEXT DEFAULT ''");
+
+          await db.execute(
+              "ALTER TABLE CategoriaProducto ADD COLUMN maximo INTEGER");
+
+          await db.insert('CategoriaProducto', {
+            'nombre': 'BASE PRODUCTO',
+            'descripcion': 'Base del producto',
+            'esToping': 1,
+            'maximo': 1,
+          });
+
+          await db.insert('CategoriaProducto', {
+            'nombre': 'SALSAS PRODUCTO',
+            'descripcion': 'Elige tus salsas (Máx. 2)',
+            'esToping': 1,
+            'maximo': 2,
+          });
+
+          await db.insert('CategoriaProducto', {
+            'nombre': 'ADEREZO PRODUCTO',
+            'descripcion': 'Elige tu aderezo (Máx. 2)',
+            'esToping': 1,
+            'maximo': 2,
+          });
+
+          await db.insert('CategoriaProducto', {
+            'nombre': 'TOPPING PRODUCTO',
+            'descripcion': 'Elige tus toppings (Máx. 2)',
+            'esToping': 1,
+            'maximo': 2,
+          });
+
+          await db.insert('Producto', {
+            'nombre': 'Papa Gajo',
+            'precio': 0,
+          });
+
+          await db.insert('Producto', {
+            'nombre': 'Papa Regilla',
+            'precio': 0,
+          });
+
+          await db.insert('Producto', {
+            'nombre': 'Papa Curly',
+            'precio': 0,
+          });
+
+          await db.insert('Producto', {
+            'nombre': 'Papa Smile',
+            'precio': 0,
+          });
+
+          await db.insert('Producto', {
+            'nombre': 'Papa Francesa',
+            'precio': 0,
+          });
+
+          await db.insert('Producto', {
+            'nombre': 'BBQ',
+            'precio': 0,
+          });
+
+          await db.insert('Producto', {
+            'nombre': 'HOTBBQ',
+            'precio': 0,
+          });
+
+          await db.insert('Producto', {
+            'nombre': 'BUFFALO',
+            'precio': 0,
+          });
+
+          await db.insert('Producto', {
+            'nombre': 'TERIYAKI',
+            'precio': 0,
+          });
+
+          await db.insert('Producto', {
+            'nombre': 'PARMESAN GARLIC',
+            'precio': 0,
+          });
+
+          await db.insert('Producto', {
+            'nombre': 'QUESO AMARILLO',
+            'precio': 0,
+          });
+
+          await db.insert('Producto', {
+            'nombre': 'RANCH',
+            'precio': 0,
+          });
+
+          await db.insert('Producto', {
+            'nombre': 'CHIPOTLE',
+            'precio': 0,
+          });
+
+          await db.insert('Producto', {
+            'nombre': 'ADEREZO JALAPEÑO',
+            'precio': 0,
+          });
+
+          await db.insert('Producto', {
+            'nombre': 'KETCHUP',
+            'precio': 0,
+          });
+
+          await db.insert('Producto', {
+            'nombre': 'JALAPEÑO',
+            'precio': 0,
+          });
+
+          await db.insert('Producto', {
+            'nombre': 'QUESO BLANCO',
+            'precio': 0,
+          });
+
+          await db.insert('Producto', {
+            'nombre': 'TAKIS',
+            'precio': 0,
+          });
+
+          await db.insert('Producto', {
+            'nombre': 'RUFFLES',
+            'precio': 0,
+          });
+
+          await db.insert('Producto', {
+            'nombre': 'QUESO PARMESANO',
+            'precio': 0,
+          });
+
+          await db.insert('Producto', {
+            'nombre': 'ELOTE',
+            'precio': 0,
+          });
+
+          break;
+
+        case 2:
+          await db.execute('''
+            CREATE TABLE ProductoTopping (
+              id INTEGER PRIMARY KEY AUTOINCREMENT,
+              idProducto INTEGER,
+              idTopping INTEGER,
+              FOREIGN KEY (idProducto) REFERENCES Producto(id),
+              FOREIGN KEY (idTopping) REFERENCES Producto(id)
+            )
+          ''');
+          break;
+
+        case 3:
+          await db.execute('''
+            CREATE TABLE PedidoProductoTopping (
+              id INTEGER PRIMARY KEY AUTOINCREMENT,
+              idPedidoProducto INTEGER,
+              idTopping INTEGER,
+              FOREIGN KEY (idPedidoProducto) REFERENCES PedidoProducto(id),
+              FOREIGN KEY (idTopping) REFERENCES Producto(id)
+            )
+          ''');
           break;
       }
       oldVersion++;
     }
   }
 
-  /// Guarda un modelo de datos nuevo o modificado en el repositorio
   Future<int> guardar(T model) async {
+    try {
+      var dbClient = await db;
+      String nombreTabla = model.runtimeType.toString();
+      String modelo = json.encode(model, toEncodable: toEncodable);
+      Map<String, dynamic> modelMap = json.decode(modelo);
+
+      int id = 0;
+      if (modelMap['id'] != null && modelMap['id'] != 0) {
+        await dbClient!.update(
+          nombreTabla,
+          modelMap,
+          where: 'id = ?',
+          whereArgs: [modelMap['id']],
+        );
+        id = modelMap['id'];
+      } else {
+        modelMap.remove('id');
+        id = await dbClient!.insert(nombreTabla, modelMap);
+      }
+
+      if (model is Producto) {
+        await _guardarToppings(dbClient, id, model.topings);
+      }
+
+      return id;
+    } catch (e) {
+      print('Error al guardar en $T: $e');
+      return 0;
+    }
+  }
+
+  Future<void> _guardarToppings(
+      Database db, int idProducto, List<Producto>? topings) async {
+    await db.delete('ProductoTopping',
+        where: 'idProducto = ?', whereArgs: [idProducto]);
+
+    if (topings != null) {
+      for (var topping in topings) {
+        await db.insert('ProductoTopping', {
+          'idProducto': idProducto,
+          'idTopping': topping.id,
+        });
+      }
+    }
+  }
+
+  Future<int> guardarLocal(T model) async {
     var dbClient = await db;
     String nombreTabla = model.runtimeType.toString();
     String modelo = json.encode(model, toEncodable: toEncodable);
     Map<String, dynamic> modelMap = json.decode(modelo);
 
-    // Elimina 'id' si es 0 o null para evitar conflictos con AUTOINCREMENT
+    if (nombreTabla == "PedidoProductoTopping") {
+      modelMap.remove('idLocal');
+      modelMap.remove('eliminado');
+    }
+
     if (modelMap['id'] == null || modelMap['id'] == 0) {
       modelMap.remove('id');
     }
 
     int resultado;
-    if (modelMap[Basico.identificadorLocal] > 0) {
+    int? identificadorLocal = modelMap[Basico.identificadorLocal];
+
+    if (identificadorLocal != null && identificadorLocal > 0) {
       resultado = await dbClient!.update(
         nombreTabla,
         modelMap,
         where: "$idLocal = ?",
-        whereArgs: [modelMap[Basico.identificadorLocal]],
+        whereArgs: [identificadorLocal],
       );
     } else {
       resultado = await dbClient!.insert(nombreTabla, modelMap);
@@ -113,49 +340,50 @@ class RepoService<T> {
           await dbClient.rawQuery("SELECT last_insert_rowid() as id");
       if (rawQuery.isNotEmpty) {
         resultado = int.parse(rawQuery.first["id"].toString());
-        modelMap[Basico.identificadorLocal] =
-            resultado; // Actualiza el ID del modelo
+        modelMap[Basico.identificadorLocal] = resultado;
       }
     }
 
     if (model is Pedido) {
-      // Asegúrate de actualizar el ID en el objeto Pedido después de guardar
       model.id = resultado;
     }
 
+    if (model is Producto) {
+      await _guardarToppings(dbClient!, model.id!, model.topings);
+    }
+
     return resultado;
   }
 
   dynamic toEncodable(dynamic item) {
-    Map<String, dynamic> map = {};
-
-    item.toJson().forEach((key, value) {
-      if (value is DateTime) {
-        map[key] = value.toIso8601String();
-      } else {
-        map[key] = value;
-      }
-    });
-
-    return map;
+    if (item is Pedido) {
+      return item.toJson();
+    } else if (item is PedidoProducto) {
+      return item.toJson();
+    } else if (item is PedidoProductoTopping) {
+      return item.toJson();
+    } else if (item is Producto) {
+      return item.toJson();
+    } else if (item is CategoriaProducto) {
+      return item.toJson();
+    }
+    throw UnsupportedError(
+        'Type not supported for serialization: ${item.runtimeType}');
   }
 
-  Future<List<T>> obtenerTodos() async {
-    var dbClient = await db;
-    String tableName = T
-        .toString(); // Obtiene el nombre del modelo, que asumimos coincide con el nombre de la tabla
-    List<Map<String, dynamic>> result = await dbClient!.query(tableName);
+  Future<List<T>> obtenerTodos({String orderBy = 'id DESC'}) async {
+    var db = await this.db;
+    String tableName = T.toString();
+    var result = await db!.query(tableName, orderBy: orderBy);
     return result.map((map) => fromMap<T>(map)).toList();
   }
 
-// Necesitarás una función que convierta un Map a un objeto de tipo T
   T fromMap<T>(Map<String, dynamic> map) {
     switch (T) {
       case Pedido:
         return Pedido.fromJson(map) as T;
       case Producto:
         return Producto.fromJson(map) as T;
-      // Añade más casos si tienes más tipos
       default:
         throw Exception('Tipo no soportado');
     }
@@ -163,10 +391,8 @@ class RepoService<T> {
 
   Future<Pedido?> obtenerPorId(int id) async {
     Database? dbClient = await db;
-    List<Map> maps = await dbClient!.query('Pedido',
-        where:
-            'id = ?', // Asegúrate de que el nombre de la columna sea 'id' y no 'idLocal'
-        whereArgs: [id]);
+    List<Map> maps =
+        await dbClient!.query('Pedido', where: 'id = ?', whereArgs: [id]);
     if (maps.isNotEmpty) {
       return Pedido.fromJson(Map<String, dynamic>.from(maps.first));
     }
@@ -182,6 +408,19 @@ class RepoService<T> {
         .toList();
   }
 
+  Future<int> contarPedidos() async {
+    Database? dbClient = await db;
+    var result = await dbClient!.rawQuery('SELECT COUNT(*) FROM Pedido');
+    return Sqflite.firstIntValue(result) ?? 0;
+  }
+
+  Future<List<Pedido>> obtenerPedidosPaginados(int limit, int offset) async {
+    Database? dbClient = await db;
+    List<Map<String, dynamic>> result = await dbClient!
+        .query('Pedido', limit: limit, offset: offset, orderBy: 'id DESC');
+    return result.map((map) => Pedido.fromJson(map)).toList();
+  }
+
   Future<Producto?> obtenerProductoPorId(int idProducto) async {
     Database? dbClient = await db;
     List<Map> maps = await dbClient!
@@ -192,13 +431,82 @@ class RepoService<T> {
     return null;
   }
 
+  Future<List<int>> obtenerToppingsPorProducto(int idProducto) async {
+    var dbClient = await db;
+    var result = await dbClient!.query(
+      'ProductoTopping',
+      where: 'idProducto = ?',
+      whereArgs: [idProducto],
+    );
+    return result.map((map) => map['idTopping'] as int).toList();
+  }
+
+  Future<List<PedidoProductoTopping>> obtenerToppingsPorPedidoProducto(
+      int idPedidoProducto) async {
+    var dbClient = await db;
+    List<Map> maps = await dbClient!.query('PedidoProductoTopping',
+        where: 'idPedidoProducto = ?', whereArgs: [idPedidoProducto]);
+
+    if (maps.isNotEmpty) {
+      return maps
+          .map((map) =>
+              PedidoProductoTopping.fromJson(Map<String, dynamic>.from(map)))
+          .toList();
+    }
+    return [];
+  }
+
   Future<int> obtenerProximoFolio() async {
     var dbClient = await db;
-    var result =
-        await dbClient!.rawQuery('SELECT MAX(folio) as last_folio FROM Pedido');
+    var result = await dbClient!.rawQuery(
+        'SELECT MAX(CAST(folio AS INTEGER)) as last_folio FROM Pedido');
     if (result.isNotEmpty && result.first["last_folio"] != null) {
       return int.tryParse(result.first["last_folio"].toString())! + 1;
     }
-    return 1; // Retorna 1 si no hay registros anteriores
+    return 1;
+  }
+
+  Future<List<T>> buscarPorFolio(String folio) async {
+    var dbClient = await db;
+    List<Map<String, dynamic>> maps = await dbClient!.query(
+      'Pedido',
+      where: 'folio LIKE ?',
+      whereArgs: ['%$folio%'],
+    );
+    return maps.map((map) => fromMap<T>(map)).toList();
+  }
+
+  Future<List<Pedido>> buscarPorFecha(
+      DateTime startDate, DateTime endDate) async {
+    var dbClient = await db;
+    String startDateString = DateFormat('dd-MM-yyyy').format(startDate);
+    String endDateString = DateFormat('dd-MM-yyyy 23:59:59').format(endDate);
+
+    List<Map<String, dynamic>> maps = await dbClient!.query(
+      'Pedido',
+      where: 'peticion BETWEEN ? AND ?',
+      whereArgs: [startDateString, endDateString],
+    );
+    return maps.map((map) => Pedido.fromJson(map)).toList();
+  }
+
+  Future<List<CorteCaja>> buscarPorFechaCorte(
+      DateTime startDate, DateTime endDate) async {
+    var dbClient = await db;
+    String startDateString = DateFormat('dd-MM-yyyy').format(startDate);
+    String endDateString = DateFormat('dd-MM-yyyy 23:59:59').format(endDate);
+
+    List<Map<String, dynamic>> maps = await dbClient!.query(
+      'CorteCaja',
+      where: 'fecha BETWEEN ? AND ?',
+      whereArgs: [startDateString, endDateString],
+    );
+    return maps.map((map) => CorteCaja.fromJson(map)).toList();
+  }
+
+  Future<void> eliminar<T>(int id) async {
+    var dbClient = await db;
+    String tableName = T.toString();
+    await dbClient!.delete(tableName, where: 'id = ?', whereArgs: [id]);
   }
 }

+ 1 - 1
lib/themes/themes.dart

@@ -4,7 +4,7 @@ import 'package:flutter/services.dart';
 class AppTheme {
   static Color primary = Color.fromRGBO(242, 75, 89, 1.000);
   static Color secondary = Colors.black;
-  static Color tertiary = const Color(0xFF060000);
+  static Color tertiary = const Color(0xFF242424);
   static Color quaternary = const Color(0xFFF1F1F3);
   static ThemeData lightTheme = ThemeData.light().copyWith(
     useMaterial3: true,

+ 107 - 61
lib/viewmodels/categoria_producto_view_model.dart

@@ -5,21 +5,90 @@ import '../services/services.dart';
 import '../models/models.dart';
 
 class CategoriaProductoViewModel extends ChangeNotifier {
+  String _busqueda = "";
+  String get busqueda => _busqueda;
+
   List<CategoriaProducto> _categoriaProductos = [];
+  bool _isLoading = false;
   CategoriaProducto? _selectedCategoriaProducto;
-  bool _isLoading = true;
+  Map<int, String> _categoriaMap = {};
 
   List<CategoriaProducto> get categoriaProductos => _categoriaProductos;
   CategoriaProducto? get selectedCategoriaProducto =>
       _selectedCategoriaProducto;
-
   bool get isLoading => _isLoading;
 
-  //Metodo para obtener lista de CategoriaProducto
-  Future<void> fetchLocalAll() async {
+  Map<int, String> get categoriaMap => _categoriaMap;
+
+  int _currentPage = 1;
+  int _totalProducts = 0;
+  int _limit = 10;
+
+  int get currentPage => _currentPage;
+  int get totalProducts => _totalProducts;
+  int get totalPages => (_totalProducts / _limit).ceil();
+
+  setBusqueda(String value) {
+    _busqueda = value;
+    notifyListeners();
+  }
+
+  void selectCategoriaProducto(CategoriaProducto categoriaProducto) {
+    _selectedCategoriaProducto = categoriaProducto;
+    notifyListeners();
+  }
+
+  Future<void> fetchLocalAll({int page = 1}) async {
+    _currentPage = page;
+    var db = await RepoService().db;
+
+    int? count = Sqflite.firstIntValue(
+        await db!.rawQuery('SELECT COUNT(*) FROM CategoriaProducto'));
+    _totalProducts = count ?? 0;
+
+    int offset = (_limit * (page - 1));
+
+    var query = await db.query('CategoriaProducto',
+        orderBy: 'id asc', limit: _limit, offset: offset);
+    _categoriaProductos =
+        query.map((element) => CategoriaProducto.fromJson(element)).toList();
+    notifyListeners();
+  }
+
+  Future<void> fetchLocalCategoria() async {
+    var db = await RepoService().db;
+    var query = await db!.query('CategoriaProducto', orderBy: 'idLocal asc');
+    List<CategoriaProducto> aux = [];
+    _categoriaMap = {};
+    for (var element in query) {
+      CategoriaProducto categoria = CategoriaProducto.fromJson(element);
+      aux.add(categoria);
+      _categoriaMap[categoria.id!] = categoria.nombre!;
+    }
+    _categoriaProductos = aux;
+    notifyListeners();
+  }
+
+  Future<List<CategoriaProducto>> getCategoriaProducto({String q = ''}) async {
+    var db = await RepoService().db;
+    List<Map> results = await db!.query('CategoriaProducto',
+        where: 'nombre LIKE ?', whereArgs: ['%$q%'], orderBy: 'nombre ASC');
+    return results
+        .map(
+            (map) => CategoriaProducto.fromJson(Map<String, dynamic>.from(map)))
+        .toList();
+  }
+
+  Future<void> addCategoriaProducto(CategoriaProducto categoriaProducto) async {
+    await RepoService().guardar(categoriaProducto);
+    fetchLocalAll();
+  }
+
+  Future<void> fetchLocalByName({required String nombre}) async {
     var db = await RepoService().db;
     var query = await db!.query(
       'CategoriaProducto',
+      where: 'nombre LIKE "%$nombre%"',
       orderBy: 'idLocal asc',
     );
     List<CategoriaProducto> aux = [];
@@ -31,61 +100,38 @@ class CategoriaProductoViewModel extends ChangeNotifier {
     notifyListeners();
   }
 
-  // int pagina = 1;
-  // int totalPaginas = 1;
-  // int limite = 10;
-
-  // void setLoading(bool loading) {
-  //   _isLoading = loading;
-  //   notifyListeners();
-  // }
-
-  // setBusqueda(String value) {
-  //   _busqueda = value;
-  //   notifyListeners();
-  // }
-
-  // CategoriaProducto selectCategoriaProducto(
-  //     CategoriaProducto categoriaProducto) {
-  //   _selectedCategoriaProducto = categoriaProducto;
-  //   return categoriaProducto;
-  // }
-
-  // cambiarPagina(int nuevaPagina, {bool segmentar = false}) {
-  //   pagina = nuevaPagina;
-  //   //fetchCategoriaProductos();
-  // }
-
-  // Future<void> addCategoriaProducto({
-  //   required String nombre,
-  // }) async {
-  //   await CategoriaProductoService().postCategoriaProducto(
-  //     nombre: nombre,
-  //   );
-  //   // await fetchCategoriaProductos();
-  //   notifyListeners();
-  // }
-
-  // Future<void> editCategoriaProducto({
-  //   required int id,
-  //   required String nombre,
-  // }) async {
-  //   await CategoriaProductoService().editCategoriaProducto(
-  //     id: id,
-  //     nombre: nombre,
-  //   );
-  //   //await fetchCategoriaProductos();
-  //   notifyListeners();
-  // }
-
-  // setIsLoading(bool loading) {
-  //   _isLoading = loading;
-  //   notifyListeners();
-  // }
-
-  // Future<void> deleteCategoriaProducto(int id) async {
-  //   await CategoriaProductoService().deleteCategoriaProducto(id: id);
-  //   //await fetchCategoriaProductos();
-  //   notifyListeners();
-  // }
+  Future<void> updateCategoriaProducto(
+      CategoriaProducto categoriaProducto) async {
+    setIsLoading(true);
+    try {
+      int result = await RepoService().guardar(categoriaProducto);
+      print("Update result: $result");
+      fetchLocalAll();
+    } catch (e) {
+      print('Error updating product: $e');
+    }
+    setIsLoading(false);
+  }
+
+  Future<void> deleteCategoriaProducto(int id) async {
+    await RepoService().eliminar<CategoriaProducto>(id);
+    fetchLocalAll();
+  }
+
+  void setIsLoading(bool loading) {
+    _isLoading = loading;
+    notifyListeners();
+  }
+
+  void nextPage() {
+    if (_currentPage < totalPages) {
+      fetchLocalAll(page: _currentPage + 1);
+    }
+  }
+
+  void previousPage() {
+    if (_currentPage > 1) {
+      fetchLocalAll(page: _currentPage - 1);
+    }
+  }
 }

+ 118 - 6
lib/viewmodels/pedido_view_model.dart

@@ -1,5 +1,6 @@
 import 'package:flutter/material.dart';
 import 'package:intl/intl.dart';
+import 'package:sqflite/sqflite.dart';
 import 'package:yoshi_papas_app/services/repo_service.dart';
 
 import '../data/api_response.dart';
@@ -14,6 +15,14 @@ class PedidoViewModel extends ChangeNotifier {
   Pedido? _selectedPedido;
   bool _isLoading = false;
 
+  int _currentPage = 1;
+  int _totalPedidos = 0;
+  int _limit = 10;
+
+  int get currentPage => _currentPage;
+  int get totalPedidos => _totalPedidos;
+  int get totalPages => (_totalPedidos / _limit).ceil();
+
   List<Pedido> get pedidos => _pedidos;
   Pedido? get selectedPedido => _selectedPedido;
   bool get isLoading => _isLoading;
@@ -29,11 +38,14 @@ class PedidoViewModel extends ChangeNotifier {
     int nextFolio = await repoPedido.obtenerProximoFolio();
     pedido.folio = nextFolio;
 
-    int idPedido = await repoPedido.guardar(pedido);
+    int idPedido = await repoPedido.guardarLocal(pedido);
     if (idPedido > 0) {
       pedido.id = idPedido;
       RepoService<PedidoProducto> repoPedidoProducto =
           RepoService<PedidoProducto>();
+      RepoService<PedidoProductoTopping> repoPedidoProductoTopping =
+          RepoService<PedidoProductoTopping>();
+
       for (var producto in pedido.productos) {
         PedidoProducto pedidoProducto = PedidoProducto(
           idPedido: idPedido,
@@ -42,7 +54,16 @@ class PedidoViewModel extends ChangeNotifier {
           costoUnitario: producto.costoUnitario,
           comentario: producto.comentario,
         );
-        await repoPedidoProducto.guardar(pedidoProducto);
+        int idPedidoProducto =
+            await repoPedidoProducto.guardarLocal(pedidoProducto);
+
+        for (var topping in producto.toppings) {
+          PedidoProductoTopping pedidoProductoTopping = PedidoProductoTopping(
+            idPedidoProducto: idPedidoProducto,
+            idTopping: topping.idTopping,
+          );
+          await repoPedidoProductoTopping.guardarLocal(pedidoProductoTopping);
+        }
       }
       notifyListeners();
       return true;
@@ -51,11 +72,48 @@ class PedidoViewModel extends ChangeNotifier {
     }
   }
 
-  Future<void> fetchLocalPedidos() async {
+  Future<void> fetchLocalPedidos({int page = 1}) async {
+    _isLoading = true;
+    _currentPage = page;
+    notifyListeners();
+
+    RepoService<Pedido> repoPedido = RepoService<Pedido>();
+    _totalPedidos = await repoPedido.contarPedidos();
+    int offset = (_limit * (page - 1));
+    List<Pedido> paginatedPedidos =
+        await repoPedido.obtenerPedidosPaginados(_limit, offset);
+
+    _pedidos = paginatedPedidos;
+    _isLoading = false;
+    notifyListeners();
+  }
+
+  void nextPage() {
+    if (_currentPage < totalPages) {
+      fetchLocalPedidosForScreen(page: _currentPage + 1);
+    }
+  }
+
+  void previousPage() {
+    if (_currentPage > 1) {
+      fetchLocalPedidosForScreen(page: _currentPage - 1);
+    }
+  }
+
+  Future<void> fetchLocalPedidosForScreen({int page = 1}) async {
     setIsLoading(true);
     RepoService<Pedido> repoPedido = RepoService<Pedido>();
+    _currentPage = page;
+    var db = await RepoService().db;
+
+    int? count = Sqflite.firstIntValue(
+        await db!.rawQuery('SELECT COUNT(*) FROM Pedido'));
+    _totalPedidos = count ?? 0;
 
-    List<Pedido> localPedidos = await repoPedido.obtenerTodos();
+    int offset = (_limit * (page - 1));
+
+    List<Pedido> localPedidos =
+        await repoPedido.obtenerPedidosPaginados(_limit, offset);
     _pedidos = localPedidos;
 
     setIsLoading(false);
@@ -76,9 +134,23 @@ class PedidoViewModel extends ChangeNotifier {
         Producto? prodInfo =
             await repoProductoInfo.obtenerProductoPorId(producto.idProducto!);
         if (prodInfo != null) {
-          producto.producto =
-              prodInfo; // Asegúrate de que tu clase PedidoProducto pueda sostener este objeto
+          producto.producto = prodInfo;
+        }
+
+        RepoService<PedidoProductoTopping> repoTopping =
+            RepoService<PedidoProductoTopping>();
+        List<PedidoProductoTopping> toppings =
+            await repoTopping.obtenerToppingsPorPedidoProducto(producto.id!);
+
+        for (var topping in toppings) {
+          Producto? toppingInfo =
+              await repoProductoInfo.obtenerProductoPorId(topping.idTopping!);
+          if (toppingInfo != null) {
+            topping.topping = toppingInfo;
+          }
         }
+
+        producto.toppings = toppings;
       }
 
       pedido.productos = productos;
@@ -86,4 +158,44 @@ class PedidoViewModel extends ChangeNotifier {
 
     return pedido;
   }
+
+  Future<void> buscarPedidosPorFolio(String folio) async {
+    setIsLoading(true);
+    RepoService<Pedido> repoPedido = RepoService<Pedido>();
+    List<Pedido> localPedidos = await repoPedido.buscarPorFolio(folio);
+    _pedidos = localPedidos;
+
+    setIsLoading(false);
+    notifyListeners();
+  }
+
+  Future<void> buscarPedidosPorFecha(
+      DateTime startDate, DateTime endDate) async {
+    setIsLoading(true);
+    RepoService<Pedido> repoPedido = RepoService<Pedido>();
+
+    List<Pedido> localPedidos =
+        await repoPedido.buscarPorFecha(startDate, endDate);
+    _pedidos = localPedidos;
+
+    setIsLoading(false);
+    notifyListeners();
+  }
+
+  Future<List<Pedido>> buscarPorFecha(
+      DateTime startDate, DateTime endDate) async {
+    setIsLoading(true);
+    RepoService<Pedido> repoPedido = RepoService<Pedido>();
+    List<Pedido> pedidos = await repoPedido.buscarPorFecha(startDate, endDate);
+    setIsLoading(false);
+    notifyListeners();
+    return pedidos;
+  }
+
+  Future<void> cancelarPedido(int idPedido) async {
+    var db = await RepoService().db;
+    await db?.update('Pedido', {'estatus': 'CANCELADO'},
+        where: 'id = ?', whereArgs: [idPedido]);
+    fetchLocalPedidosForScreen();
+  }
 }

+ 138 - 32
lib/viewmodels/producto_view_model.dart

@@ -1,65 +1,66 @@
 import 'package:flutter/material.dart';
+import 'package:sqflite/sqflite.dart';
 
 import '../data/api_response.dart';
 import '../services/base_service.dart';
 import '../models/models.dart';
 import '../services/services.dart';
+import '../services/repo_service.dart';
 
 class ProductoViewModel<T> extends ChangeNotifier {
   String _busqueda = "";
   String get busqueda => _busqueda;
 
   List<Producto> _productos = [];
-  Producto? _selectedModelo;
+  List<CategoriaProducto> _toppingCategories = [];
   bool _isLoading = false;
   int? _selectedCategoriaId = 0;
+  Producto? _selectedProducto;
 
   List<Producto> get productos => _productos;
-  Producto? get selectedProducto => _selectedModelo;
+  Producto? get selectedProducto => _selectedProducto;
   bool get isLoading => _isLoading;
 
-  int pagina = 1;
-  int totalPaginas = 1;
-  int limite = 10;
+  int _currentPage = 1;
+  int _totalProducts = 0;
+  int _limit = 10;
+
+  int get currentPage => _currentPage;
+  int get totalProducts => _totalProducts;
+  int get totalPages => (_totalProducts / _limit).ceil();
 
   setBusqueda(String value) {
     _busqueda = value;
     notifyListeners();
   }
 
-  Future<void> fetchLocalAll() async {
-    _selectedCategoriaId = 0;
-    var db = await RepoService().db;
-    var query = await db!.query(
-      'Producto',
-      orderBy: 'idLocal asc',
-    );
-    List<Producto> aux = [];
-    for (var element in query) {
-      Producto producto = Producto.fromJson(element);
-      aux.add(producto);
-    }
-    _productos = aux;
+  void selectProducto(Producto producto) {
+    _selectedProducto = producto;
     notifyListeners();
   }
 
-  Future<void> fetchLocalByID({required int idCategoria}) async {
-    if (idCategoria == _selectedCategoriaId) {
-      _selectedCategoriaId = 0;
-      fetchLocalAll();
-      return;
-    }
+  Future<void> fetchLocalAll({int page = 1}) async {
+    _currentPage = page;
+    var db = await RepoService().db;
+    int? count = Sqflite.firstIntValue(
+        await db!.rawQuery('SELECT COUNT(*) FROM Producto'));
+    _totalProducts = count ?? 0;
+
+    int offset = (_limit * (page - 1));
 
-    //Se utiliza para actualizar la UI del botón
-    _selectedCategoriaId = idCategoria;
+    var query = await db.query('Producto',
+        orderBy: 'idLocal asc', limit: _limit, offset: offset);
+    _productos = query.map((element) => Producto.fromJson(element)).toList();
+
+    notifyListeners();
+  }
 
+  Future<void> fetchLocalByID({required int idCategoria}) async {
     var db = await RepoService().db;
-    var query = await db!.query(
-      'Producto',
-      where: 'idCategoria ==?',
-      whereArgs: ['$idCategoria'],
-      orderBy: 'idLocal asc',
-    );
+    var query = await db!.query('Producto',
+        where: 'idCategoria = ?',
+        whereArgs: [idCategoria],
+        orderBy: 'idLocal asc');
     List<Producto> aux = [];
     for (var element in query) {
       Producto producto = Producto.fromJson(element);
@@ -69,6 +70,16 @@ class ProductoViewModel<T> extends ChangeNotifier {
     notifyListeners();
   }
 
+  Future<void> fetchAllByCategory(int idCategoria) async {
+    var db = await RepoService().db;
+    var query = await db!.query('Producto',
+        where: 'idCategoria = ?',
+        whereArgs: [idCategoria],
+        orderBy: 'idLocal asc');
+    _productos = query.map((e) => Producto.fromJson(e)).toList();
+    notifyListeners();
+  }
+
   Future<void> fetchLocalByName({required String nombre}) async {
     var db = await RepoService().db;
     var query = await db!.query(
@@ -85,8 +96,103 @@ class ProductoViewModel<T> extends ChangeNotifier {
     notifyListeners();
   }
 
+  Future<int> addProducto(Producto producto) async {
+    setIsLoading(true);
+    try {
+      int id = await RepoService().guardar(producto);
+      fetchLocalAll();
+      return id;
+    } catch (e) {
+      print('Error al agregar producto: $e');
+      return -1;
+    } finally {
+      setIsLoading(false);
+    }
+  }
+
+  Future<void> updateProducto(Producto producto) async {
+    setIsLoading(true);
+    try {
+      int result = await RepoService().guardar(producto);
+      print("Resultado: $result");
+      fetchLocalAll();
+    } catch (e) {
+      print('Error al actualizar: $e');
+    }
+    setIsLoading(false);
+  }
+
+  Future<void> deleteProducto(int id) async {
+    await RepoService().eliminar<Producto>(id);
+    fetchLocalAll();
+  }
+
+  Future<bool> updateProductImagePath(int productId, String imagePath) async {
+    setIsLoading(true);
+    try {
+      Producto? producto = await RepoService().obtenerProductoPorId(productId);
+      producto!.imagen = imagePath;
+      await RepoService().guardar(producto);
+      notifyListeners();
+      return true;
+    } catch (e) {
+      print('Error al actualizar la imagen del producto: $e');
+      return false;
+    } finally {
+      setIsLoading(false);
+    }
+  }
+
+  Future<List<CategoriaProducto>> fetchToppingCategories() async {
+    var db = await RepoService().db;
+    var query = await db!.query('CategoriaProducto',
+        where: 'esToping = ?', whereArgs: [1], orderBy: 'id asc');
+    _toppingCategories =
+        query.map((element) => CategoriaProducto.fromJson(element)).toList();
+    return _toppingCategories;
+  }
+
+  Future<List<Producto>> fetchProductsByCategory(int categoryId) async {
+    var db = await RepoService().db;
+    var query = await db!.query('Producto',
+        where: 'idCategoria = ?', whereArgs: [categoryId], orderBy: 'id asc');
+    return query.map((e) => Producto.fromJson(e)).toList();
+  }
+
+  Future<List<int>> obtenerToppingsPorProducto(int idProducto) async {
+    var dbClient = await RepoService().db;
+    var result = await dbClient!.query(
+      'ProductoTopping',
+      where: 'idProducto = ?',
+      whereArgs: [idProducto],
+    );
+    return result.map((map) => map['idTopping'] as int).toList();
+  }
+
+  Future<Producto?> obtenerProductoPorId(int idProducto) async {
+    Database? dbClient = await RepoService().db;
+    List<Map> maps = await dbClient!
+        .query('Producto', where: 'id = ?', whereArgs: [idProducto]);
+    if (maps.isNotEmpty) {
+      return Producto.fromJson(Map<String, dynamic>.from(maps.first));
+    }
+    return null;
+  }
+
   void setIsLoading(bool loading) {
     _isLoading = loading;
     notifyListeners();
   }
+
+  void nextPage() {
+    if (_currentPage < totalPages) {
+      fetchLocalAll(page: _currentPage + 1);
+    }
+  }
+
+  void previousPage() {
+    if (_currentPage > 1) {
+      fetchLocalAll(page: _currentPage - 1);
+    }
+  }
 }

+ 138 - 88
lib/views/categoria_producto/categoria_producto_form.dart

@@ -1,97 +1,147 @@
-// // ignore_for_file: use_build_context_synchronously
+import 'package:yoshi_papas_app/themes/themes.dart';
+import 'package:flutter/material.dart';
+import 'package:yoshi_papas_app/widgets/widgets.dart';
+import 'package:provider/provider.dart';
 
-// import 'package:flutter/material.dart';
-// import 'package:yoshi_papas_app/widgets/widgets.dart';
-// import 'package:provider/provider.dart';
+import '../../models/models.dart';
+import '../../viewmodels/viewmodels.dart';
 
-// import '../../models/models.dart';
-// import '../../viewmodels/viewmodels.dart';
+class CategoriaProductoForm extends StatefulWidget {
+  final CategoriaProducto categoriaProducto;
 
-// class CategoriaProductoForm extends StatefulWidget {
-//   const CategoriaProductoForm({Key? key}) : super(key: key);
+  const CategoriaProductoForm({Key? key, required this.categoriaProducto})
+      : super(key: key);
 
-//   @override
-//   State<CategoriaProductoForm> createState() => Formulario();
-// }
+  @override
+  State<CategoriaProductoForm> createState() => Formulario();
+}
 
-// class Formulario extends State<CategoriaProductoForm> {
-//   final _nombre = TextEditingController();
-//   final _busquedaNombre = TextEditingController();
+class Formulario extends State<CategoriaProductoForm> {
+  final _nombre = TextEditingController();
+  final _descripcion = TextEditingController();
+  final _maximo = TextEditingController();
+  late int esToping;
 
-//   @override
-//   void initState() {
-//     super.initState();
-//     Future(() async {
-//       Provider.of<CategoriaProductoViewModel>(context, listen: false)
-//           .setIsLoading(true);
-//       final mvm =
-//           Provider.of<CategoriaProductoViewModel>(context, listen: false);
-//       CategoriaProducto? modelo = mvm.selectedCategoriaProducto;
-//       if (modelo != null && modelo.id > 0) {
-//         setState(() {
-//           _nombre.text = modelo.nombre.toString();
-//         });
-//       }
-//       Provider.of<CategoriaProductoViewModel>(context, listen: false)
-//           .setIsLoading(false);
-//     });
-//   }
+  @override
+  void initState() {
+    super.initState();
+    _nombre.text = widget.categoriaProducto.nombre ?? "";
+    _descripcion.text = widget.categoriaProducto.descripcion ?? "";
+    _maximo.text = widget.categoriaProducto.maximo?.toString() ?? "";
+    esToping = widget.categoriaProducto.esToping ?? 0;
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      _loadData();
+    });
+  }
 
-//   @override
-//   void dispose() {
-//     super.dispose();
-//     _nombre.dispose();
-//   }
+  void _loadData() async {
+    await Provider.of<CategoriaProductoViewModel>(context, listen: false)
+        .fetchLocalAll();
+  }
 
-//   @override
-//   Widget build(BuildContext context) {
-//     final mvm = Provider.of<CategoriaProductoViewModel>(context);
-//     if (mvm.isLoading) return const Cargando();
-//     final segmento = mvm.selectedCategoriaProducto;
+  @override
+  Widget build(BuildContext context) {
+    final CategoriaProductoViewModel categoriaProductoVM =
+        Provider.of<CategoriaProductoViewModel>(context);
 
-//     return Scaffold(
-//       appBar: encabezado(titulo: "Agregar categoría producto"),
-//       body: SingleChildScrollView(
-//         padding: const EdgeInsets.all(8),
-//         child: Column(
-//           children: [
-//             tarjeta(
-//               Padding(
-//                 padding: const EdgeInsets.all(8),
-//                 child: Column(
-//                   children: [
-//                     Row(
-//                       children: [
-//                         Expanded(
-//                           child: AppTextField(
-//                             maxLength: 100,
-//                             etiqueta: 'Nombre',
-//                             controller: _nombre,
-//                             hintText: 'Nombre de categoría producto',
-//                           ),
-//                         ),
-//                       ],
-//                     ),
-//                   ],
-//                 ),
-//               ),
-//             ),
-//             const SizedBox(height: 15),
-//             boton("Guardar", () async {
-//               Provider.of<CategoriaProductoViewModel>(context, listen: false)
-//                   .setIsLoading(true);
-//               await mvm.addCategoriaProducto(
-//                 nombre: _nombre.text,
-//               );
-//               Provider.of<CategoriaProductoViewModel>(context, listen: false)
-//                   .setIsLoading(false);
-//               if (context.mounted) {
-//                 Navigator.pop(context);
-//               }
-//             }),
-//           ],
-//         ),
-//       ),
-//     );
-//   }
-// }
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(
+          'Nueva Categoría Producto',
+          style: TextStyle(color: AppTheme.secondary),
+        ),
+        iconTheme: IconThemeData(color: AppTheme.secondary),
+      ),
+      body: SingleChildScrollView(
+        padding: EdgeInsets.all(8),
+        child: Column(
+          children: [
+            tarjeta(
+              Padding(
+                padding: const EdgeInsets.all(8),
+                child: Column(
+                  children: [
+                    AppTextField(
+                      maxLength: 100,
+                      etiqueta: 'Nombre',
+                      controller: _nombre,
+                      hintText: 'Nombre de la categoría',
+                    ),
+                    AppTextField(
+                      maxLength: 100,
+                      etiqueta: 'Descripción',
+                      controller: _descripcion,
+                      hintText: 'Descripción de la categoría',
+                    ),
+                    Align(
+                      alignment: Alignment.centerLeft,
+                      child: Column(children: [
+                        Text(
+                          'Es Toping',
+                          style: TextStyle(
+                            fontSize: 18,
+                            fontWeight: FontWeight.bold,
+                          ),
+                        ),
+                        Switch(
+                          value: esToping == 1,
+                          onChanged: (value) {
+                            setState(() {
+                              esToping = value ? 1 : 0;
+                            });
+                          },
+                          activeColor: AppTheme.primary,
+                        ),
+                      ]),
+                    ),
+                    SizedBox(height: 10),
+                    if (esToping == 1)
+                      AppTextField(
+                        maxLength: 2,
+                        etiqueta: 'Máximo',
+                        controller: _maximo,
+                        hintText: 'Máximo permitido en un pedido (opcional)',
+                        keyboardType: TextInputType.number,
+                      ),
+                  ],
+                ),
+              ),
+            ),
+            SizedBox(height: 15),
+            boton("Guardar", () async {
+              Provider.of<CategoriaProductoViewModel>(context, listen: false)
+                  .setIsLoading(true);
+
+              int? maximoValue =
+                  _maximo.text.isNotEmpty ? int.tryParse(_maximo.text) : null;
+
+              await categoriaProductoVM.updateCategoriaProducto(
+                CategoriaProducto(
+                  id: widget.categoriaProducto.id,
+                  nombre: _nombre.text,
+                  descripcion: _descripcion.text,
+                  esToping: esToping,
+                  maximo: maximoValue,
+                ),
+              );
+
+              Provider.of<CategoriaProductoViewModel>(context, listen: false)
+                  .setIsLoading(false);
+              if (context.mounted) {
+                Navigator.pop(context);
+              }
+            })
+          ],
+        ),
+      ),
+    );
+  }
+
+  @override
+  void dispose() {
+    _nombre.dispose();
+    _descripcion.dispose();
+    _maximo.dispose();
+    super.dispose();
+  }
+}

+ 415 - 199
lib/views/categoria_producto/categoria_producto_screen.dart

@@ -1,207 +1,423 @@
-// // ignore_for_file: use_build_context_synchronously
+import 'package:yoshi_papas_app/themes/themes.dart';
+import 'package:yoshi_papas_app/viewmodels/categoria_producto_view_model.dart';
+import 'package:yoshi_papas_app/widgets/app_textfield.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import '../../models/models.dart';
+import 'categoria_producto_form.dart';
+import '../../widgets/widgets_components.dart' as clase;
 
-// import 'package:flutter/material.dart';
-// import 'package:provider/provider.dart';
-// import '../../models/models.dart';
-// import '../../themes/themes.dart';
-// import '../../viewmodels/viewmodels.dart';
-// import '../../widgets/app_textfield.dart';
-// import '../../widgets/pagination_buttons.dart';
-// import '../../widgets/widgets_components.dart';
-// import 'categoria_producto_form.dart';
+class CategoriaProductoScreen extends StatefulWidget {
+  @override
+  _CategoriaProductoScreenState createState() =>
+      _CategoriaProductoScreenState();
+}
 
-// class CategoriaProductoScreen extends StatefulWidget {
-//   const CategoriaProductoScreen({Key? key}) : super(key: key);
+class _CategoriaProductoScreenState extends State<CategoriaProductoScreen> {
+  final _busqueda = TextEditingController(text: '');
+  ScrollController horizontalScrollController = ScrollController();
 
-//   @override
-//   State<CategoriaProductoScreen> createState() => Formulario();
-// }
+  @override
+  void initState() {
+    super.initState();
+    final categoriaModel =
+        Provider.of<CategoriaProductoViewModel>(context, listen: false);
+    categoriaModel.fetchLocalAll();
+  }
 
-// class Formulario extends State<CategoriaProductoScreen> {
-//   final _busqueda = TextEditingController(text: '');
+  void go(CategoriaProducto categoriaProducto) {
+    Navigator.push(
+      context,
+      MaterialPageRoute(
+        builder: (context) =>
+            CategoriaProductoForm(categoriaProducto: categoriaProducto),
+      ),
+    ).then((_) =>
+        Provider.of<CategoriaProductoViewModel>(context, listen: false)
+            .fetchLocalAll());
+  }
 
-//   @override
-//   void initState() {
-//     super.initState();
-//     Future(() async {
-//       await Provider.of<CategoriaProductoViewModel>(context, listen: false)
-//           .fetchCategoriaProductos();
-//     });
-//   }
+  void clearSearchAndReset() {
+    setState(() {
+      _busqueda.clear();
+      Provider.of<CategoriaProductoViewModel>(context, listen: false)
+          .fetchLocalAll();
+    });
+  }
 
-//   go(CategoriaProducto item) async {
-//     Provider.of<CategoriaProductoViewModel>(context, listen: false)
-//         .selectCategoriaProducto(item);
-//     Navigator.push(
-//       context,
-//       MaterialPageRoute(
-//         builder: (context) => const CategoriaProductoForm(),
-//       ),
-//     ).then((value) async {
-//       await Provider.of<CategoriaProductoViewModel>(context, listen: false)
-//           .fetchCategoriaProductos();
-//     });
-//   }
+  @override
+  Widget build(BuildContext context) {
+    final model = Provider.of<CategoriaProductoViewModel>(context);
+    final categoriasModel = Provider.of<CategoriaProductoViewModel>(context);
+    double screenWidth = MediaQuery.of(context).size.width;
+    final isMobile = screenWidth < 1250;
+    final double? columnSpacing = isMobile ? null : 0;
+    TextStyle estilo = const TextStyle(fontWeight: FontWeight.bold);
+    List<DataRow> registros = [];
+    for (CategoriaProducto item in model.categoriaProductos) {
+      registros.add(DataRow(cells: [
+        DataCell(
+            Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
+          PopupMenuButton(
+            itemBuilder: (context) => [
+              PopupMenuItem(
+                child: const Text('Editar'),
+                onTap: () => go(item),
+              ),
+              PopupMenuItem(
+                child: const Text('Eliminar'),
+                onTap: () async {
+                  // Retrasa la ejecución para permitir que se cierre el menú popup.
+                  await Future.delayed(Duration(milliseconds: 100));
+                  bool confirmado = await showDialog<bool>(
+                        context: context,
+                        builder: (context) {
+                          return AlertDialog(
+                            title: const Text("Eliminar",
+                                style: TextStyle(
+                                    fontWeight: FontWeight.w500, fontSize: 22)),
+                            content: const Text(
+                                '¿Estás seguro de que deseas eliminar esta categoría?',
+                                style: TextStyle(fontSize: 18)),
+                            actions: [
+                              Row(
+                                mainAxisAlignment:
+                                    MainAxisAlignment.spaceBetween,
+                                children: [
+                                  TextButton(
+                                    onPressed: () =>
+                                        Navigator.of(context).pop(false),
+                                    child: const Text('No',
+                                        style: TextStyle(fontSize: 18)),
+                                    style: ButtonStyle(
+                                        padding: MaterialStatePropertyAll(
+                                            EdgeInsets.fromLTRB(
+                                                20, 10, 20, 10)),
+                                        backgroundColor:
+                                            MaterialStatePropertyAll(
+                                                AppTheme.primary),
+                                        foregroundColor:
+                                            MaterialStatePropertyAll(
+                                                AppTheme.secondary)),
+                                  ),
+                                  TextButton(
+                                    onPressed: () =>
+                                        Navigator.of(context).pop(true),
+                                    child: const Text('Sí',
+                                        style: TextStyle(fontSize: 18)),
+                                    style: ButtonStyle(
+                                        padding: MaterialStatePropertyAll(
+                                            EdgeInsets.fromLTRB(
+                                                20, 10, 20, 10)),
+                                        backgroundColor:
+                                            MaterialStatePropertyAll(
+                                                AppTheme.tertiary),
+                                        foregroundColor:
+                                            MaterialStatePropertyAll(
+                                                AppTheme.quaternary)),
+                                  ),
+                                ],
+                              )
+                            ],
+                          );
+                        },
+                      ) ??
+                      false;
 
-//   @override
-//   Widget build(BuildContext context) {
-//     final mvm = Provider.of<CategoriaProductoViewModel>(context);
-//     TextStyle estilo = const TextStyle(fontWeight: FontWeight.bold);
-//     int vuelta = 0;
-//     List<DataRow> categoriaProductos = [];
-//     if (mvm.categoriaProductos.isNotEmpty) {
-//       for (CategoriaProducto item in mvm.categoriaProductos) {
-//         var _tipo = vuelta % 2;
-//         vuelta++;
-//         categoriaProductos.add(DataRow(selected: _tipo > 0, cells: [
-//           DataCell(
-//             Text(item.nombre.toString()),
-//             onTap: () => go(item),
-//           ),
-//           DataCell(
-//               Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
-//             PopupMenuButton(
-//               surfaceTintColor: AppTheme.primary,
-//               itemBuilder: (context) => [
-//                 PopupMenuItem(
-//                   child: const Text('Editar'),
-//                   onTap: () => go(item),
-//                 ),
-//                 PopupMenuItem(
-//                   child: const Text(
-//                     'Eliminar',
-//                   ),
-//                   onTap: () async {
-//                     return showDialog(
-//                       context: context,
-//                       builder: (context) {
-//                         return AlertDialog(
-//                           title: const Text("Eliminar registro"),
-//                           content: const Text('¿Desea eliminar el registro?'),
-//                           actions: [
-//                             Row(children: [
-//                               Expanded(
-//                                   child: TextButton(
-//                                 onPressed: () {
-//                                   Navigator.pop(context);
-//                                 },
-//                                 child: const Text('Cancelar'),
-//                               )),
-//                               Expanded(
-//                                   child: TextButton(
-//                                 onPressed: () async {
-//                                   Navigator.pop(context);
-//                                 },
-//                                 child: const Text('Continuar'),
-//                               ))
-//                             ])
-//                           ],
-//                         );
-//                       },
-//                     );
-//                   },
-//                 )
-//               ],
-//               icon: const Icon(Icons.more_vert),
-//               shape: RoundedRectangleBorder(
-//                   borderRadius: BorderRadius.circular(15)),
-//             )
-//           ]))
-//         ]));
-//       }
-//     }
+                  if (confirmado) {
+                    await Provider.of<CategoriaProductoViewModel>(context,
+                            listen: false)
+                        .deleteCategoriaProducto(item.id);
+                    Provider.of<CategoriaProductoViewModel>(context,
+                            listen: false)
+                        .fetchLocalAll();
+                  }
+                },
+              )
+            ],
+            icon: const Icon(Icons.more_vert),
+          ),
+        ])),
+        DataCell(
+          Text(item.id.toString()),
+          onTap: () {
+            Provider.of<CategoriaProductoViewModel>(context, listen: false)
+                .selectCategoriaProducto(item);
+            go(item);
+          },
+        ),
+        DataCell(
+          Text(item.nombre.toString()),
+          onTap: () {
+            Provider.of<CategoriaProductoViewModel>(context, listen: false)
+                .selectCategoriaProducto(item);
+            go(item);
+          },
+        ),
+      ]));
+    }
 
-//     return Scaffold(
-//       appBar: AppBar(
-//         title: const Text('Categoría Productos'),
-//       ),
-//       floatingActionButton: FloatingActionButton(
-//         onPressed: () {
-//           mvm.selectCategoriaProducto(CategoriaProducto());
-//           Navigator.push(
-//             context,
-//             MaterialPageRoute(
-//               builder: (context) => const CategoriaProductoForm(),
-//             ),
-//           ).then((value) async {
-//             await Provider.of<CategoriaProductoViewModel>(context,
-//                     listen: false)
-//                 .fetchCategoriaProductos();
-//           });
-//         },
-//         child: const Icon(Icons.add),
-//       ),
-//       body: Column(
-//         children: [
-//           Expanded(
-//             child: ListView(
-//               padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
-//               children: [
-//                 const SizedBox(height: 8),
-//                 tarjeta(Padding(
-//                     padding: const EdgeInsets.all(10),
-//                     child: Row(
-//                       children: [
-//                         Expanded(
-//                           flex: 8,
-//                           child: AppTextField(
-//                             prefixIcon: const Icon(Icons.search),
-//                             maxLength: 100,
-//                             etiqueta: 'Búsqueda por nombre...',
-//                             controller: _busqueda,
-//                             hintText: 'Búsqueda por nombre...',
-//                           ),
-//                         ),
-//                         const SizedBox(width: 5),
-//                         Expanded(
-//                           flex: 2,
-//                           child: botonElevated(
-//                             accion: () async {
-//                               _busqueda.text = _busqueda.text.trim();
-//                               await Provider.of<CategoriaProductoViewModel>(
-//                                       context,
-//                                       listen: false)
-//                                   .setIsLoading(true);
-//                               await Provider.of<CategoriaProductoViewModel>(
-//                                       context,
-//                                       listen: false)
-//                                   .setBusqueda(_busqueda.text);
-//                               await Provider.of<CategoriaProductoViewModel>(
-//                                       context,
-//                                       listen: false)
-//                                   .fetchCategoriaProductos();
-//                               await Provider.of<CategoriaProductoViewModel>(
-//                                       context,
-//                                       listen: false)
-//                                   .setBusqueda("");
-//                               await Provider.of<CategoriaProductoViewModel>(
-//                                       context,
-//                                       listen: false)
-//                                   .setIsLoading(false);
-//                             },
-//                           ),
-//                         ),
-//                       ],
-//                     ))),
-//                 const SizedBox(height: 8),
-//                 tarjeta(DataTable(
-//                     sortAscending: true,
-//                     sortColumnIndex: 1,
-//                     columns: [
-//                       DataColumn(label: Text("NOMBRE", style: estilo)),
-//                       DataColumn(label: Text("", style: estilo)),
-//                     ],
-//                     rows: categoriaProductos)),
-//                 PaginationButtons(
-//                   currentPage: mvm.pagina,
-//                   totalPages: mvm.totalPaginas,
-//                   onPageChanged: (i) => mvm.cambiarPagina(i),
-//                 )
-//               ],
-//             ),
-//           ),
-//         ],
-//       ),
-//     );
-//   }
-// }
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(
+          'Categoria Producto',
+          style: TextStyle(color: AppTheme.secondary),
+        ),
+        iconTheme: IconThemeData(color: AppTheme.secondary),
+      ),
+      body: Column(
+        children: [
+          Expanded(
+            child: ListView(
+              padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
+              children: [
+                const SizedBox(height: 8),
+                clase.tarjeta(
+                  Padding(
+                    padding: const EdgeInsets.all(8.0),
+                    child: LayoutBuilder(
+                      builder: (context, constraints) {
+                        if (screenWidth > 1000) {
+                          return Row(
+                            children: [
+                              Expanded(
+                                flex: 7,
+                                child: busquedaTextField(),
+                              ),
+                              SizedBox(width: 5),
+                              botonBuscar()
+                            ],
+                          );
+                        } else {
+                          return Column(
+                            children: [
+                              Row(
+                                children: [busquedaTextField()],
+                              ),
+                              SizedBox(height: 15),
+                              Row(
+                                children: [botonBuscar()],
+                              ),
+                            ],
+                          );
+                        }
+                      },
+                    ),
+                  ),
+                ),
+                const SizedBox(height: 8),
+                model.isLoading
+                    ? const Center(child: CircularProgressIndicator())
+                    : Container(),
+                clase.tarjeta(
+                  Column(
+                    children: [
+                      LayoutBuilder(builder: (context, constraints) {
+                        return SingleChildScrollView(
+                          scrollDirection: Axis.vertical,
+                          child: Scrollbar(
+                            controller: horizontalScrollController,
+                            interactive: true,
+                            thumbVisibility: true,
+                            thickness: 10.0,
+                            child: SingleChildScrollView(
+                              controller: horizontalScrollController,
+                              scrollDirection: Axis.horizontal,
+                              child: ConstrainedBox(
+                                constraints: BoxConstraints(
+                                    minWidth: isMobile
+                                        ? constraints.maxWidth
+                                        : screenWidth),
+                                child: DataTable(
+                                  columnSpacing: columnSpacing,
+                                  sortAscending: true,
+                                  sortColumnIndex: 1,
+                                  columns: [
+                                    DataColumn(label: Text(" ", style: estilo)),
+                                    DataColumn(
+                                        label: Text("ID", style: estilo)),
+                                    DataColumn(
+                                        label: Text("NOMBRE", style: estilo)),
+                                  ],
+                                  rows: registros,
+                                ),
+                              ),
+                            ),
+                          ),
+                        );
+                      }),
+                    ],
+                  ),
+                ),
+                const SizedBox(
+                  height: 15,
+                ),
+                if (!model.isLoading) ...[
+                  Row(
+                    mainAxisAlignment: MainAxisAlignment.center,
+                    children: [
+                      TextButton(
+                        onPressed:
+                            model.currentPage > 1 ? model.previousPage : null,
+                        child: Text('Anterior'),
+                        style: ButtonStyle(
+                          backgroundColor:
+                              MaterialStateProperty.resolveWith<Color?>(
+                            (Set<MaterialState> states) {
+                              if (states.contains(MaterialState.disabled)) {
+                                return Colors.grey;
+                              }
+                              return AppTheme.tertiary;
+                            },
+                          ),
+                          foregroundColor:
+                              MaterialStateProperty.resolveWith<Color?>(
+                            (Set<MaterialState> states) {
+                              if (states.contains(MaterialState.disabled)) {
+                                return Colors.black;
+                              }
+                              return Colors.white;
+                            },
+                          ),
+                        ),
+                      ),
+                      SizedBox(width: 15),
+                      Text(
+                          'Página ${model.currentPage} de ${model.totalPages}'),
+                      SizedBox(width: 15),
+                      TextButton(
+                        onPressed: model.currentPage < model.totalPages
+                            ? model.nextPage
+                            : null,
+                        child: Text('Siguiente'),
+                        style: ButtonStyle(
+                          backgroundColor:
+                              MaterialStateProperty.resolveWith<Color?>(
+                            (Set<MaterialState> states) {
+                              if (states.contains(MaterialState.disabled)) {
+                                return Colors.grey;
+                              }
+                              return AppTheme.tertiary;
+                            },
+                          ),
+                          foregroundColor:
+                              MaterialStateProperty.resolveWith<Color?>(
+                            (Set<MaterialState> states) {
+                              if (states.contains(MaterialState.disabled)) {
+                                return Colors.black;
+                              }
+                              return Colors.white;
+                            },
+                          ),
+                        ),
+                      ),
+                    ],
+                  ),
+                ],
+                const SizedBox(
+                  height: 15,
+                ),
+              ],
+            ),
+          ),
+        ],
+      ),
+      floatingActionButton: FloatingActionButton.extended(
+        onPressed: () async {
+          CategoriaProducto nuevoProducto = CategoriaProducto();
+          Navigator.push(
+            context,
+            MaterialPageRoute(
+              builder: (context) =>
+                  CategoriaProductoForm(categoriaProducto: nuevoProducto),
+            ),
+          ).then((_) =>
+              Provider.of<CategoriaProductoViewModel>(context, listen: false)
+                  .fetchLocalAll());
+        },
+        icon: Icon(Icons.add, size: 30, color: AppTheme.quaternary),
+        label: Text(
+          "Agregar Categoria Producto",
+          style: TextStyle(fontSize: 18, color: AppTheme.quaternary),
+        ),
+        shape: RoundedRectangleBorder(
+          borderRadius: BorderRadius.circular(8),
+        ),
+        materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
+        backgroundColor: AppTheme.tertiary,
+      ),
+    );
+  }
+
+  Widget busquedaTextField() {
+    return Row(
+      children: [
+        Expanded(
+          flex: 3,
+          child: AppTextField(
+            prefixIcon: const Icon(Icons.search),
+            etiqueta: 'Búsqueda por nombre...',
+            controller: _busqueda,
+            hintText: 'Búsqueda por nombre...',
+          ),
+        ),
+        const SizedBox(width: 5),
+      ],
+    );
+  }
+
+  Widget botonBuscar() {
+    return Expanded(
+        flex: 2,
+        child: Row(
+          children: [
+            Expanded(
+              flex: 2,
+              child: Padding(
+                padding: const EdgeInsets.only(top: 30),
+                child: ElevatedButton(
+                  onPressed: clearSearchAndReset,
+                  style: ElevatedButton.styleFrom(
+                    shape: RoundedRectangleBorder(
+                      borderRadius: BorderRadius.circular(20.0),
+                    ),
+                    primary: AppTheme.tertiary,
+                    padding: const EdgeInsets.symmetric(vertical: 25),
+                  ),
+                  child: Text('Limpiar',
+                      style: TextStyle(color: AppTheme.quaternary)),
+                ),
+              ),
+            ),
+            const SizedBox(width: 8),
+            Expanded(
+              flex: 2,
+              child: Padding(
+                padding: const EdgeInsets.only(top: 30),
+                child: ElevatedButton(
+                  onPressed: () async {
+                    if (_busqueda.text.isNotEmpty) {
+                      await Provider.of<CategoriaProductoViewModel>(context,
+                              listen: false)
+                          .fetchLocalByName(nombre: _busqueda.text.trim());
+                    } else {
+                      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
+                          content: Text('Introduce un nombre para buscar.')));
+                    }
+                  },
+                  style: ElevatedButton.styleFrom(
+                    shape: RoundedRectangleBorder(
+                      borderRadius: BorderRadius.circular(20.0),
+                    ),
+                    primary: AppTheme.tertiary,
+                    padding: const EdgeInsets.symmetric(vertical: 25),
+                  ),
+                  child: Text('Buscar',
+                      style: TextStyle(color: AppTheme.quaternary)),
+                ),
+              ),
+            ),
+          ],
+        ));
+  }
+}

+ 12 - 13
lib/views/pedido/pedido_csv.dart

@@ -1,12 +1,11 @@
 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; // Importa path como p para evitar conflictos con otros nombres
+import 'package:path/path.dart' as p;
+import 'package:yoshi_papas_app/models/pedido_model.dart';
 
-import '../../models/models.dart';
-
-Future<void> exportarPedidosACSV(List<Pedido> pedidos) async {
+Future<void> exportarPedidosACSV(List<Pedido> pedidos, String fileName) async {
   List<List<dynamic>> rows = [
     [
       "Folio",
@@ -14,31 +13,31 @@ Future<void> exportarPedidosACSV(List<Pedido> pedidos) async {
       "Producto",
       "Cantidad",
       "Precio Unitario",
-      "Comentarios"
+      "Estado",
+      "Fecha"
     ]
   ];
 
   for (var pedido in pedidos) {
     for (var producto in pedido.productos) {
       List<dynamic> row = [
-        pedido.folio,
+        pedido.id,
         pedido.nombreCliente,
         producto.producto?.nombre ?? 'No especificado',
         producto.cantidad,
         producto.producto?.precio ?? '0.0',
-        pedido.comentarios ?? ''
+        pedido.estatus,
+        pedido.peticion ?? ''
       ];
       rows.add(row);
     }
   }
 
   String csv = const ListToCsvConverter().convert(rows);
-  final directory =
-      await getApplicationDocumentsDirectory(); // Obtiene el directorio de documentos
-  final path = p.join(
-      directory.path, 'pedidos.csv'); // Crea la ruta completa para el archivo
+  final directory = await getApplicationDocumentsDirectory();
+  final path = p.join(directory.path, fileName);
   final file = File(path);
 
-  await file.writeAsString(csv); // Escribe el CSV en el archivo
+  await file.writeAsString(csv);
   print('Archivo CSV guardado en $path');
 }

+ 152 - 59
lib/views/pedido/pedido_detalle_screen.dart

@@ -1,72 +1,74 @@
-import 'dart:io';
-
-import 'package:flutter/material.dart';
-import 'package:pdf/pdf.dart';
-import 'package:printing/printing.dart';
-import 'package:provider/provider.dart';
+import 'package:intl/intl.dart';
 import 'package:yoshi_papas_app/themes/themes.dart';
 import 'package:yoshi_papas_app/views/pedido/pedido_ticket.dart';
+import 'package:flutter/material.dart';
 import '../../models/models.dart';
 
 class PedidoDetalleScreen extends StatelessWidget {
   final Pedido pedido;
 
-  const PedidoDetalleScreen({Key? key, required this.pedido}) : super(key: key);
-
-  String obtenerEstadoPedido(String? estado) {
-    switch (estado) {
-      case '1':
-        return "NUEVO";
-      case '2':
-        return "EN PROCESO";
-      case '3':
-        return "TERMINADO";
-      case '4':
-        return "CANCELADO";
-      default:
-        return "ESTADO DESCONOCIDO"; // En caso de que se reciba un valor no esperado
-    }
+  String formatCurrency(double amount) {
+    final format = NumberFormat("#,##0.00", "es_MX");
+    return format.format(amount);
   }
 
+  const PedidoDetalleScreen({Key? key, required this.pedido}) : super(key: key);
+
   @override
   Widget build(BuildContext context) {
-    print("Productos en el pedido: ${pedido.productos.length}");
+    double total = pedido.productos.fold(0, (previousValue, element) {
+      double productTotal = element.cantidad! *
+          (double.tryParse(element.producto?.precio ?? '') ?? 0.0);
+
+      double toppingsTotal = element.toppings.fold(0, (toppingTotal, topping) {
+        return toppingTotal +
+            (double.tryParse(topping.topping?.precio ?? '') ?? 0.0) *
+                element.cantidad!;
+      });
+
+      return previousValue + productTotal + toppingsTotal;
+    });
+
     return Scaffold(
       appBar: AppBar(
-        title: Text('Detalle del Pedido ${pedido.folio}'),
+        title: Text(
+          'Detalle del Pedido ${pedido.folio}',
+          style: TextStyle(fontWeight: FontWeight.w500),
+        ),
       ),
       body: SingleChildScrollView(
-          padding: const EdgeInsets.all(
-              12.0), // Agrega padding al contenedor general
+          padding: const EdgeInsets.all(12.0),
           child: Column(
             children: [
               Card(
-                  elevation: 4, // Sombra de la tarjeta
-                  color: Colors.white, // Fondo blanco de la tarjeta
+                  elevation: 4,
+                  color: Colors.white,
                   child: Column(
                     children: [
                       ListTile(
                         title: Text(
                           'Cliente: ${pedido.nombreCliente}',
                           style: TextStyle(
-                              fontWeight:
-                                  FontWeight.bold), // Negritas para el título
+                              fontWeight: FontWeight.bold, fontSize: 22),
+                        ),
+                        subtitle: Text(
+                          'Comentarios: ${pedido.comentarios}',
+                          style: TextStyle(
+                              fontSize: 20, fontWeight: FontWeight.w500),
                         ),
-                        subtitle: Text('Comentarios: ${pedido.comentarios}'),
                       ),
-                      // ListTile(
-                      //   title: Text(
-                      //     'Estado del Pedido: ${obtenerEstadoPedido(pedido.estatus)}',
-                      //     style: TextStyle(
-                      //         fontSize: 16,
-                      //         fontWeight: FontWeight.bold,
-                      //         color: Colors
-                      //             .deepOrange), // Estilo para el estado del pedido
-                      //   ),
-                      // )
+                      ListTile(
+                        title: Text(
+                          'Estado del Pedido: ${pedido.estatus}',
+                          style: TextStyle(
+                            fontSize: 22,
+                            fontWeight: FontWeight.bold,
+                          ),
+                        ),
+                      )
                     ],
                   )),
-              SizedBox(height: 10), // Espacio entre componentes
+              SizedBox(height: 10),
               Card(
                 elevation: 4,
                 color: Colors.white,
@@ -77,39 +79,130 @@ class PedidoDetalleScreen extends StatelessWidget {
                     children: [
                       Text('Productos',
                           style: TextStyle(
-                              fontSize: 18, fontWeight: FontWeight.bold)),
+                              fontSize: 22, fontWeight: FontWeight.bold)),
+                      const SizedBox(height: 15),
                       ListView.builder(
                         shrinkWrap: true,
                         physics: NeverScrollableScrollPhysics(),
                         itemCount: pedido.productos.length,
                         itemBuilder: (context, index) {
-                          final pedidoProducto = pedido.productos[index];
-                          return ListTile(
-                            title: Text(
-                              pedidoProducto.producto?.nombre ??
-                                  "Producto no especificado",
-                              style: TextStyle(
-                                  fontWeight: FontWeight
-                                      .bold), // Negritas para el nombre del producto
+                          final producto = pedido.productos[index];
+                          return Padding(
+                            padding: const EdgeInsets.symmetric(vertical: 4.0),
+                            child: Column(
+                              crossAxisAlignment: CrossAxisAlignment.start,
+                              children: [
+                                Row(
+                                  children: [
+                                    Expanded(
+                                      flex: 6,
+                                      child: Text(
+                                        producto.producto?.nombre ??
+                                            "Producto no especificado",
+                                        style: TextStyle(
+                                            fontWeight: FontWeight.bold,
+                                            fontSize: 17),
+                                        overflow: TextOverflow.ellipsis,
+                                      ),
+                                    ),
+                                    Expanded(
+                                      flex: 1,
+                                      child: Text(
+                                        'x${producto.cantidad}',
+                                        style: TextStyle(
+                                            fontWeight: FontWeight.w500,
+                                            fontSize: 17),
+                                        textAlign: TextAlign.center,
+                                      ),
+                                    ),
+                                    Expanded(
+                                      flex: 2,
+                                      child: Text(
+                                        '\$${formatCurrency(double.tryParse(producto.producto?.precio ?? '0.0') ?? 0.0)}',
+                                        style: TextStyle(
+                                            fontWeight: FontWeight.w500,
+                                            fontSize: 17),
+                                        textAlign: TextAlign.right,
+                                      ),
+                                    ),
+                                  ],
+                                ),
+                                if (producto.toppings.isNotEmpty)
+                                  Padding(
+                                    padding: const EdgeInsets.only(top: 4.0),
+                                    child: Column(
+                                      crossAxisAlignment:
+                                          CrossAxisAlignment.start,
+                                      children:
+                                          producto.toppings.map((topping) {
+                                        return Padding(
+                                          padding: const EdgeInsets.symmetric(
+                                              vertical: 2.0),
+                                          child: Row(
+                                            children: [
+                                              Text(
+                                                '- ${topping.topping?.nombre ?? "Topping no especificado"}',
+                                                style: TextStyle(
+                                                    fontSize: 15,
+                                                    color: Colors.grey[600]),
+                                              ),
+                                              Spacer(),
+                                              Text(
+                                                '\$${formatCurrency(double.tryParse(topping.topping?.precio ?? '0.0') ?? 0.0)}',
+                                                style: TextStyle(
+                                                    fontSize: 15,
+                                                    color: Colors.grey[600]),
+                                              ),
+                                            ],
+                                          ),
+                                        );
+                                      }).toList(),
+                                    ),
+                                  ),
+                              ],
                             ),
-                            subtitle:
-                                Text('Cantidad: ${pedidoProducto.cantidad}'),
                           );
                         },
                       ),
-                      SizedBox(height: 20), // Espacio antes del botón
-                      ElevatedButton(
-                        onPressed: () => printTickets(pedido),
-                        child: Text('Imprimir Ticket'),
-                        style: ElevatedButton.styleFrom(
-                          primary: AppTheme.primary,
-                          onPrimary: Colors.white,
+                      Divider(),
+                      Padding(
+                        padding: const EdgeInsets.symmetric(vertical: 8.0),
+                        child: Row(
+                          mainAxisAlignment: MainAxisAlignment.end,
+                          children: [
+                            Text('Total: \$${formatCurrency(total)}',
+                                style: TextStyle(
+                                    fontSize: 16, fontWeight: FontWeight.bold)),
+                          ],
                         ),
                       ),
                     ],
                   ),
                 ),
               ),
+              const SizedBox(height: 20),
+              Align(
+                alignment: Alignment.centerLeft,
+                child: ElevatedButton.icon(
+                  icon: Icon(
+                    Icons.receipt_long_outlined,
+                    color: AppTheme.quaternary,
+                    size: 30,
+                  ),
+                  onPressed: () => imprimirTicketsJuntos(pedido),
+                  label: Text(
+                    'Imprimir Ticket',
+                    style: TextStyle(
+                        fontWeight: FontWeight.w500,
+                        fontSize: 18,
+                        color: AppTheme.quaternary),
+                  ),
+                  style: ElevatedButton.styleFrom(
+                    padding: EdgeInsets.fromLTRB(50, 20, 50, 20),
+                    primary: AppTheme.tertiary,
+                  ),
+                ),
+              )
             ],
           )),
     );

+ 290 - 135
lib/views/pedido/pedido_form.dart

@@ -1,21 +1,14 @@
 import 'dart:async';
-
+import 'dart:io';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:intl/intl.dart';
-import 'package:pdf/pdf.dart';
-import 'package:printing/printing.dart';
 import 'package:provider/provider.dart';
-import 'package:sqflite/sqflite.dart';
-import 'package:yoshi_papas_app/models/pedido_producto_model.dart';
 import 'package:yoshi_papas_app/views/pedido/pedido_ticket.dart';
-import 'package:yoshi_papas_app/widgets/widgets_components.dart';
-import 'package:yoshi_papas_app/models/item_carrito_model.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 {
@@ -38,20 +31,37 @@ class _PedidoFormState extends State<PedidoForm> {
   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();
-    cargarProductosIniciales();
+    cargarCategoriasIniciales().then((_) {
+      if (categorias.isNotEmpty) {
+        categoriaSeleccionada = categorias.first;
+        cargarProductosPorCategoria(categoriaSeleccionada!.id);
+      }
+    });
   }
 
   _onSearchChanged(String value) {
@@ -69,21 +79,23 @@ class _PedidoFormState extends State<PedidoForm> {
         context: context,
         builder: (BuildContext context) {
           return AlertDialog(
-            title: const Text('Pedido vacío'),
+            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.'),
+                '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: <Widget>[
               TextButton(
                 style: TextButton.styleFrom(
-                    foregroundColor: Colors.black,
-                    backgroundColor: AppTheme.primary // Color del texto
-                    ),
+                    padding: EdgeInsets.fromLTRB(30, 20, 30, 20),
+                    foregroundColor: AppTheme.quaternary,
+                    backgroundColor: AppTheme.tertiary),
                 onPressed: () {
-                  Navigator.of(context).pop(); // Cerrar el diálogo
+                  Navigator.of(context).pop();
                 },
-                child: const Text('Aceptar'),
+                child: const Text('Aceptar', style: TextStyle(fontSize: 18)),
               ),
             ],
           );
@@ -91,11 +103,9 @@ class _PedidoFormState extends State<PedidoForm> {
       );
       return;
     } else {
-      // Suponiendo que `pedidoActual` es tu pedido actual que ya tiene un ID asignado
       int? pedidoId = pedidoActual?.id;
       if (pedidoId != null) {
-        Navigator.of(context)
-            .pop(); // Regresar a la pantalla anterior o cerrar diálogo
+        Navigator.of(context).pop();
       }
     }
     await _promptForCustomerName();
@@ -104,28 +114,26 @@ class _PedidoFormState extends State<PedidoForm> {
   Future<void> _promptForCustomerName() async {
     TextEditingController nombreController = TextEditingController();
     TextEditingController comentarioController = TextEditingController();
-    String errorMessage = ''; // Variable para almacenar el mensaje de error
-
+    String errorMessage = '';
     bool? shouldSave = await showDialog<bool>(
       context: context,
       builder: (BuildContext context) {
         return StatefulBuilder(
-          // Usar StatefulBuilder para actualizar el contenido del diálogo
           builder: (context, setState) {
             return AlertDialog(
-              title: const Text('Finalizar Pedido'),
+              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: [
-                  TextField(
+                  AppTextField(
                     controller: nombreController,
-                    decoration: InputDecoration(
-                      hintText: "Nombre del Cliente",
-                      errorText: errorMessage.isEmpty
-                          ? null
-                          : errorMessage, // Mostrar el mensaje de error aquí
-                    ),
-                    autofocus: true,
+                    etiqueta: 'Nombre',
+                    hintText: "Nombre del Cliente",
+                    errorText: errorMessage.isEmpty ? null : errorMessage,
                     onChanged: (value) {
                       if (value.trim().isEmpty) {
                         setState(() => errorMessage =
@@ -135,33 +143,55 @@ class _PedidoFormState extends State<PedidoForm> {
                       }
                     },
                   ),
-                  TextField(
+                  const SizedBox(height: 10),
+                  AppTextField(
                     controller: comentarioController,
-                    decoration: const InputDecoration(
-                        hintText: "Comentarios (opcional)"),
+                    etiqueta: 'Comentarios (opcional)',
+                    hintText: 'Comentarios',
                     maxLines: 3,
                   ),
                 ],
               ),
               actions: <Widget>[
-                TextButton(
-                  child: const Text('Cancelar'),
-                  onPressed: () {
-                    Navigator.of(context).pop(false); // Return false on cancel
-                  },
-                ),
-                TextButton(
-                  child: const Text('Guardar'),
-                  onPressed: () {
-                    if (nombreController.text.trim().isEmpty) {
-                      // Actualizar el mensaje de error si el campo está vacío
-                      setState(() => errorMessage =
-                          "El nombre del cliente es obligatorio.");
-                      return; // No cerrar el diálogo
-                    }
-                    Navigator.of(context).pop(true); // Return true on save
-                  },
-                ),
+                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)),
+                    ),
+                  ],
+                )
               ],
             );
           },
@@ -170,45 +200,53 @@ class _PedidoFormState extends State<PedidoForm> {
     );
 
     if (shouldSave ?? false) {
-      // Preparar y guardar el pedido sólo si el usuario no canceló el diálogo
       prepararPedidoActual(nombreController.text, comentarioController.text);
     }
   }
 
   void prepararPedidoActual(String nombreCliente, String comentarios) async {
     DateTime now = DateTime.now();
-    String formattedDate = DateFormat('yyyy-MM-dd kk:mm:ss').format(now);
+    String formattedDate = DateFormat('dd-MM-yyyy kk:mm:ss').format(now);
+
+    double totalPedido = calcularTotalPedido();
 
-    // Crear una instancia de Pedido
     Pedido nuevoPedido = Pedido(
       peticion: formattedDate,
       nombreCliente: nombreCliente,
       comentarios: comentarios,
       estatus: "NUEVO",
+      totalPedido: totalPedido,
     );
 
-    // Preparar la lista de PedidoProducto a partir del carrito
     List<PedidoProducto> listaPedidoProducto = carrito.map((item) {
+      List<PedidoProductoTopping> 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, // Esto debe tener todos los detalles del producto.
+        producto: item.producto,
         costoUnitario: item.producto.precio,
         cantidad: item.cantidad,
         comentario: comentarios,
+        toppings: selectedToppings,
       );
     }).toList();
 
-    // Asignar la lista de productos al pedido
     nuevoPedido.productos = listaPedidoProducto;
 
-    // Usar el ViewModel para guardar el pedido en la base de datos local
     bool result = await Provider.of<PedidoViewModel>(context, listen: false)
         .guardarPedidoLocal(pedido: nuevoPedido);
 
     if (result) {
-      printTickets(nuevoPedido);
-      Navigator.of(context).pop(); // Volver a la pantalla anterior.
+      imprimirTicketsJuntos(nuevoPedido);
+      Navigator.of(context).pop();
     } else {
       print("Error al guardar el pedido");
     }
@@ -219,17 +257,13 @@ class _PedidoFormState extends State<PedidoForm> {
       _busqueda.text = '';
       _estadoBusqueda = false;
     });
-    // Carga nuevamente las categorías y productos iniciales.
     await cargarCategoriasIniciales();
-    // Opcionalmente, puedes llamar a una función específica para cargar productos iniciales si tienes una.
   }
 
   Future<void> cargarProductosIniciales() async {
     setState(() => _isLoading = true);
-    // Llama al método para obtener todos los productos desde el ViewModel local
     await Provider.of<ProductoViewModel>(context, listen: false)
         .fetchLocalAll();
-    // Obtiene la lista de productos del ViewModel
     productos =
         Provider.of<ProductoViewModel>(context, listen: false).productos;
     setState(() => _isLoading = false);
@@ -261,32 +295,53 @@ class _PedidoFormState extends State<PedidoForm> {
   void cargarProductosPorCategoria(int categoriaId) async {
     setState(() => _isLoading = true);
     await Provider.of<ProductoViewModel>(context, listen: false)
-        .fetchLocalByID(idCategoria: categoriaId);
+        .fetchAllByCategory(categoriaId);
     productos =
         Provider.of<ProductoViewModel>(context, listen: false).productos;
-    // Restablece la posición de desplazamiento
-    _gridViewController.jumpTo(_gridViewController.position.minScrollExtent);
     setState(() => _isLoading = false);
-    if (_gridViewController.hasClients) {
-      _gridViewController.jumpTo(0.0);
-    }
   }
 
-  void agregarAlCarrito(Producto producto) {
-    setState(() {
-      var existente =
-          carrito.firstWhereOrNull((item) => item.producto.id == producto.id);
-      if (existente != null) {
+  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 {
-        carrito.add(ItemCarrito(producto: producto, cantidad: 1));
+      });
+    } else {
+      Map<int, List<Producto>> toppingsSeleccionables =
+          await obtenerToppingsSeleccionables(producto);
+
+      setState(() {
+        carrito.add(ItemCarrito(
+          producto: producto,
+          cantidad: 1,
+          selectableToppings: toppingsSeleccionables,
+        ));
+      });
+    }
+  }
+
+  Future<Map<int, List<Producto>>> obtenerToppingsSeleccionables(
+      Producto producto) async {
+    Map<int, List<Producto>> 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(() {
-      // Comienza con setState por la misma razón
       var indice =
           carrito.indexWhere((item) => item.producto.id == producto.id);
       if (indice != -1) {
@@ -299,18 +354,20 @@ class _PedidoFormState extends State<PedidoForm> {
     });
   }
 
-  void addToCart(Producto producto, {List<Toping>? toppings}) {
-    // Revisa si hay un producto en el carrito con los mismos toppings
+  void addToCart(Producto producto) {
     var existingIndex = carrito.indexWhere((item) =>
-        item.producto.id == producto.id && listEquals(item.toppings, toppings));
+        item.producto.id == producto.id &&
+        mapEquals(item.selectedToppings, {}));
 
     if (existingIndex != -1) {
-      carrito[existingIndex].cantidad++;
+      setState(() {
+        carrito[existingIndex].cantidad++;
+      });
     } else {
-      carrito.add(ItemCarrito(
-          producto: producto, cantidad: 1, toppings: toppings ?? []));
+      setState(() {
+        carrito.add(ItemCarrito(producto: producto, cantidad: 1));
+      });
     }
-    setState(() {});
   }
 
   void finalizeCustomization() {
@@ -326,8 +383,9 @@ class _PedidoFormState extends State<PedidoForm> {
   Widget build(BuildContext context) {
     return Scaffold(
       appBar: AppBar(
-        title: const Text("Crear Pedido"),
-      ),
+          title:
+              Text("Crear Pedido", style: TextStyle(color: AppTheme.secondary)),
+          iconTheme: IconThemeData(color: AppTheme.secondary)),
       body: Row(
         children: [
           Flexible(
@@ -342,8 +400,8 @@ class _PedidoFormState extends State<PedidoForm> {
   }
 
   Widget _buildTotalSection() {
-    double total =
-        calcularTotalPedido(); // Aquí llamarías a la función calcularTotalPedido
+    double total = calcularTotalPedido();
+    String formattedTotal = _numberFormat.format(total);
     return Padding(
       padding: const EdgeInsets.symmetric(horizontal: 8.0),
       child: Row(
@@ -351,7 +409,7 @@ class _PedidoFormState extends State<PedidoForm> {
         children: [
           const Text('Total',
               style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
-          Text("\$${total.toStringAsFixed(2)}",
+          Text("\$$formattedTotal",
               style:
                   const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
         ],
@@ -359,23 +417,6 @@ class _PedidoFormState extends State<PedidoForm> {
     );
   }
 
-  List<Widget> _buildToppingList(Map<String, dynamic>? customizations) {
-    List<Widget> list = [];
-    customizations?.forEach((category, toppingsAsString) {
-      if (toppingsAsString is String && toppingsAsString.isNotEmpty) {
-        // Divide la string por comas para obtener los nombres individuales de los toppings
-        List<String> toppingNames = toppingsAsString.split(', ');
-        for (var toppingName in toppingNames) {
-          list.add(ListTile(
-            title: Text(toppingName),
-            subtitle: Text(category), // Muestra la categoría como subtítulo
-          ));
-        }
-      }
-    });
-    return list;
-  }
-
   Widget _buildCartSection() {
     return Card(
       margin: const EdgeInsets.all(8.0),
@@ -400,9 +441,6 @@ class _PedidoFormState extends State<PedidoForm> {
               itemCount: carrito.length,
               itemBuilder: (context, index) {
                 final item = carrito[index];
-                // Concatena los nombres de los toppings en una sola línea
-                String toppingsList =
-                    item.toppings.map((topping) => topping.nombre).join(', ');
 
                 return Column(
                   children: [
@@ -422,7 +460,7 @@ class _PedidoFormState extends State<PedidoForm> {
                                   eliminarProductoDelCarrito(index)),
                           IconButton(
                               icon: const Icon(Icons.remove),
-                              onPressed: () => quitarDelCarrito(item.producto)),
+                              onPressed: () => quitarProductoDelCarrito(item)),
                           const SizedBox(width: 5),
                           Text('${item.cantidad}',
                               style: const TextStyle(
@@ -430,17 +468,104 @@ class _PedidoFormState extends State<PedidoForm> {
                           const SizedBox(width: 5),
                           IconButton(
                               icon: const Icon(Icons.add),
-                              onPressed: () => agregarAlCarrito(item.producto)),
+                              onPressed: () => incrementarProducto(item)),
                         ],
                       ),
                     ),
-                    Padding(
-                      padding: const EdgeInsets.only(left: 16.0, top: 4.0),
-                      child: Text(toppingsList, // Usa la lista concatenada aquí
-                          style: const TextStyle(
-                              fontWeight: FontWeight.w500, fontSize: 14.0)),
-                    ),
-                    Divider(), // Opcional: Un divisor visual entre los elementos.
+                    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<bool> isSelectedNotifier =
+                                    ValueNotifier<bool>(
+                                  item.selectedToppings[categoryId]
+                                          ?.contains(topping.id) ??
+                                      false,
+                                );
+
+                                return ValueListenableBuilder<bool>(
+                                  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] ??=
+                                              <int>{};
+                                          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(),
                   ],
                 );
               },
@@ -454,11 +579,12 @@ class _PedidoFormState extends State<PedidoForm> {
             child: ElevatedButton(
               onPressed: _finalizeOrder,
               style: ElevatedButton.styleFrom(
-                  primary: AppTheme.primary,
-                  onPrimary: AppTheme.secondary,
-                  textStyle: const TextStyle(fontSize: 22),
-                  fixedSize: const Size(250, 50)),
-              child: const Text('Finalizar Pedido'),
+                primary: AppTheme.tertiary,
+                textStyle: const TextStyle(fontSize: 22),
+                fixedSize: const Size(250, 50),
+              ),
+              child: Text('Finalizar Pedido',
+                  style: TextStyle(color: AppTheme.quaternary)),
             ),
           ),
         ],
@@ -472,6 +598,22 @@ class _PedidoFormState extends State<PedidoForm> {
     });
   }
 
+  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: [
@@ -501,7 +643,15 @@ class _PedidoFormState extends State<PedidoForm> {
                     child: Column(
                       mainAxisAlignment: MainAxisAlignment.center,
                       children: [
-                        const Icon(Icons.fastfood, size: 80),
+                        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),
@@ -536,13 +686,16 @@ class _PedidoFormState extends State<PedidoForm> {
   }
 
   Widget _buildCategoryButtons() {
+    List<CategoriaProducto> categoriasFiltradas =
+        categorias.where((categoria) => categoria.esToping == 0).toList();
+
     return Container(
-      height: 50, // Define una altura fija para los botones
+      height: 50,
       child: ListView.builder(
         scrollDirection: Axis.horizontal,
-        itemCount: categorias.length,
+        itemCount: categoriasFiltradas.length,
         itemBuilder: (context, index) {
-          final categoria = categorias[index];
+          final categoria = categoriasFiltradas[index];
           bool isSelected = categoriaSeleccionada?.id == categoria.id;
           return Padding(
             padding: const EdgeInsets.symmetric(horizontal: 4.0),
@@ -554,8 +707,10 @@ class _PedidoFormState extends State<PedidoForm> {
                 });
               },
               style: ElevatedButton.styleFrom(
-                primary: isSelected ? AppTheme.primary : Colors.grey,
-                onPrimary: Colors.white,
+                primary: isSelected ? AppTheme.tertiary : Colors.grey,
+                foregroundColor:
+                    isSelected ? AppTheme.quaternary : AppTheme.secondary,
+                onPrimary: AppTheme.secondary,
               ),
               child: Text(categoria.nombre!),
             ),
@@ -577,7 +732,7 @@ class _PedidoFormState extends State<PedidoForm> {
             borderRadius: BorderRadius.circular(12.0),
           ),
         ),
-        onChanged: _onSearchChanged, // Usar el método de búsqueda aquí
+        onChanged: _onSearchChanged,
       ),
     );
   }

+ 328 - 103
lib/views/pedido/pedido_screen.dart

@@ -1,4 +1,6 @@
 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:yoshi_papas_app/themes/themes.dart';
 import 'package:yoshi_papas_app/views/pedido/pedido_csv.dart';
@@ -6,9 +8,8 @@ import 'package:yoshi_papas_app/views/pedido/pedido_detalle_screen.dart';
 import '../../models/models.dart';
 import '../../viewmodels/viewmodels.dart';
 import '../../widgets/app_textfield.dart';
-import '../../widgets/pagination_buttons.dart';
-import '../../widgets/widgets_components.dart';
-import 'pedido_form.dart'; // Asumiendo que tienes un formulario para los pedidos similar al de los productos.
+import '../../widgets/widgets_components.dart' as clase;
+import 'pedido_form.dart';
 
 class PedidoScreen extends StatefulWidget {
   const PedidoScreen({Key? key}) : super(key: key);
@@ -19,28 +20,61 @@ class PedidoScreen extends StatefulWidget {
 
 class _PedidoScreenState extends State<PedidoScreen> {
   final _busqueda = TextEditingController(text: '');
+  DateTime? fechaInicio;
+  DateTime? fechaFin;
   ScrollController horizontalScrollController = ScrollController();
 
   @override
   void initState() {
     super.initState();
     WidgetsBinding.instance.addPostFrameCallback((_) {
-      Provider.of<PedidoViewModel>(context, listen: false).fetchLocalPedidos();
+      Provider.of<PedidoViewModel>(context, listen: false)
+          .fetchLocalPedidosForScreen();
     });
   }
 
   void exportCSV() async {
-    // Obtiene los pedidos actuales del estado de PedidoViewModel
-    List<Pedido> pedidos =
-        Provider.of<PedidoViewModel>(context, listen: false).pedidos;
-    // Llamada a la función de exportación que debes definir en otro lugar y que crea el CSV
-    exportarPedidosACSV(pedidos);
-    ScaffoldMessenger.of(context).showSnackBar(
-        SnackBar(content: Text('Exportación de CSV completada!')));
+    final pedidosViewModel =
+        Provider.of<PedidoViewModel>(context, listen: false);
+    List<Pedido> pedidosConProductos = [];
+
+    for (Pedido pedido in pedidosViewModel.pedidos) {
+      Pedido? pedidoConProductos =
+          await pedidosViewModel.fetchPedidoConProductos(pedido.id);
+      if (pedidoConProductos != null) {
+        pedidosConProductos.add(pedidoConProductos);
+      }
+    }
+
+    if (pedidosConProductos.isNotEmpty) {
+      String fileName = 'Pedidos_JoshiPapas_POS';
+      if (fechaInicio != null && fechaFin != null) {
+        String startDateStr = DateFormat('dd-MM-yyyy').format(fechaInicio!);
+        String endDateStr = DateFormat('dd-MM-yyyy').format(fechaFin!);
+        fileName += '_${startDateStr}_al_${endDateStr}';
+      }
+      fileName += '.csv';
+
+      await exportarPedidosACSV(pedidosConProductos, 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.')));
+    }
+  }
+
+  void clearSearchAndReset() {
+    setState(() {
+      _busqueda.clear();
+      fechaInicio = null;
+      fechaFin = null;
+      Provider.of<PedidoViewModel>(context, listen: false)
+          .fetchLocalPedidosForScreen();
+    });
   }
 
   void go(Pedido item) async {
-    // Obtener el pedido completo con productos antes de navegar
     Pedido? pedidoCompleto =
         await Provider.of<PedidoViewModel>(context, listen: false)
             .fetchPedidoConProductos(item.id);
@@ -81,20 +115,51 @@ class _PedidoScreenState extends State<PedidoScreen> {
                         context: context,
                         builder: (context) {
                           return AlertDialog(
-                            title: const Text("Cancelar Pedido"),
+                            title: const Text("Cancelar Pedido",
+                                style: TextStyle(
+                                    fontWeight: FontWeight.w500, fontSize: 22)),
                             content: const Text(
-                                '¿Estás seguro de que deseas cancelar este pedido?'),
+                                '¿Estás seguro de que deseas cancelar este pedido?',
+                                style: TextStyle(fontSize: 18)),
                             actions: [
-                              TextButton(
-                                onPressed: () =>
-                                    Navigator.of(context).pop(false),
-                                child: const Text('No'),
-                              ),
-                              TextButton(
-                                onPressed: () =>
-                                    Navigator.of(context).pop(false),
-                                child: const Text('Sí'),
-                              ),
+                              Row(
+                                mainAxisAlignment:
+                                    MainAxisAlignment.spaceBetween,
+                                children: [
+                                  TextButton(
+                                    onPressed: () =>
+                                        Navigator.of(context).pop(false),
+                                    child: const Text('No',
+                                        style: TextStyle(fontSize: 18)),
+                                    style: ButtonStyle(
+                                        padding: MaterialStatePropertyAll(
+                                            EdgeInsets.fromLTRB(
+                                                20, 10, 20, 10)),
+                                        backgroundColor:
+                                            MaterialStatePropertyAll(
+                                                AppTheme.primary),
+                                        foregroundColor:
+                                            MaterialStatePropertyAll(
+                                                AppTheme.secondary)),
+                                  ),
+                                  TextButton(
+                                    onPressed: () =>
+                                        Navigator.of(context).pop(true),
+                                    child: const Text('Sí',
+                                        style: TextStyle(fontSize: 18)),
+                                    style: ButtonStyle(
+                                        padding: MaterialStatePropertyAll(
+                                            EdgeInsets.fromLTRB(
+                                                20, 10, 20, 10)),
+                                        backgroundColor:
+                                            MaterialStatePropertyAll(
+                                                AppTheme.tertiary),
+                                        foregroundColor:
+                                            MaterialStatePropertyAll(
+                                                AppTheme.quaternary)),
+                                  ),
+                                ],
+                              )
                             ],
                           );
                         },
@@ -102,55 +167,54 @@ class _PedidoScreenState extends State<PedidoScreen> {
                       false;
 
                   if (confirmado) {
-                    // bool result = await Provider.of<PedidoViewModel>(context,
-                    //         listen: false)
-                    //     .cancelarPedido(item.id!);
-                    // if (result) {
-                    //   ScaffoldMessenger.of(context).showSnackBar(
-                    //     SnackBar(
-                    //         content: Text("Pedido cancelado correctamente")),
-                    //   );
-                    // } else {
-                    //   ScaffoldMessenger.of(context).showSnackBar(
-                    //     SnackBar(content: Text("Error al cancelar el pedido")),
-                    //   );
-                    // }
+                    await Provider.of<PedidoViewModel>(context, listen: false)
+                        .cancelarPedido(item.id);
+                    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
+                        content: Text("Pedido cancelado correctamente")));
                   }
                 },
               )
             ],
             icon: const Icon(Icons.more_vert),
-          ),
+          )
         ])),
         DataCell(
-          Text(item.id.toString()), // Ajusta de acuerdo a tu modelo de Pedido.
+          Text(item.folio.toString()),
           onTap: () => go(item),
         ),
         DataCell(
-          Text(item.nombreCliente ??
-              "Sin nombre"), // Ajusta de acuerdo a tu modelo de Pedido.
+          Text(item.nombreCliente ?? "Sin nombre"),
           onTap: () => go(item),
         ),
         DataCell(
-          Text(item.comentarios ??
-              "Sin comentarios"), // Ajusta de acuerdo a tu modelo de Pedido.
+          Text(item.comentarios ?? "Sin comentarios"),
+          onTap: () => go(item),
+        ),
+        DataCell(
+          Text(item.estatus ?? "Sin Estatus"),
+          onTap: () => go(item),
+        ),
+        DataCell(
+          Text(item.peticion ?? "Sin fecha"),
           onTap: () => go(item),
         ),
-        // Continúa con las demás celdas que necesites mostrar
       ]));
     }
 
     return Scaffold(
       appBar: AppBar(
-        title: const Text('Pedidos'),
-        actions: <Widget>[
-          IconButton(
-            icon: const Icon(Icons.save_alt),
-            onPressed: exportCSV,
-            tooltip: 'Exportar a CSV',
+          title: Text(
+            'Pedidos',
+            style: TextStyle(color: AppTheme.secondary),
           ),
-        ],
-      ),
+          actions: <Widget>[
+            IconButton(
+              icon: const Icon(Icons.save_alt),
+              onPressed: exportCSV,
+              tooltip: 'Exportar a CSV',
+            ),
+          ],
+          iconTheme: IconThemeData(color: AppTheme.secondary)),
       floatingActionButton: FloatingActionButton.extended(
         onPressed: () async {
           await Navigator.push(
@@ -159,21 +223,18 @@ class _PedidoScreenState extends State<PedidoScreen> {
               builder: (context) => PedidoForm(),
             ),
           ).then((_) => Provider.of<PedidoViewModel>(context, listen: false)
-              .fetchLocalPedidos());
+              .fetchLocalPedidosForScreen());
         },
-        icon: const Icon(Icons.add, size: 30), // Incrementa el tamaño del ícono
-        label: const Text(
+        icon: Icon(Icons.add, size: 30, color: AppTheme.quaternary),
+        label: Text(
           "Agregar Pedido",
-          style: TextStyle(
-            fontSize: 20, // Incrementa el tamaño del texto
-          ),
+          style: TextStyle(fontSize: 20, color: AppTheme.quaternary),
         ),
         shape: RoundedRectangleBorder(
           borderRadius: BorderRadius.circular(8),
         ),
         materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
-        backgroundColor: AppTheme.primary,
-        foregroundColor: AppTheme.tertiary,
+        backgroundColor: AppTheme.tertiary,
       ),
       body: Column(
         children: [
@@ -182,7 +243,7 @@ class _PedidoScreenState extends State<PedidoScreen> {
               padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
               children: [
                 const SizedBox(height: 8),
-                tarjeta(
+                clase.tarjeta(
                   Padding(
                     padding: const EdgeInsets.all(8.0),
                     child: LayoutBuilder(
@@ -191,27 +252,22 @@ class _PedidoScreenState extends State<PedidoScreen> {
                           return Row(
                             children: [
                               Expanded(
-                                  flex: 10,
-                                  child: Row(
-                                    crossAxisAlignment:
-                                        CrossAxisAlignment.start,
-                                    children: [
-                                      BusquedaTextField(),
-                                    ],
-                                  )),
+                                flex: 7,
+                                child: _buildDateRangePicker(),
+                              ),
                               SizedBox(width: 5),
-                              BotonBuscar()
+                              botonBuscar()
                             ],
                           );
                         } else {
                           return Column(
                             children: [
                               Row(
-                                children: [BusquedaTextField()],
+                                children: [_buildDateRangePicker()],
                               ),
                               SizedBox(height: 15),
                               Row(
-                                children: [BotonBuscar()],
+                                children: [botonBuscar()],
                               ),
                             ],
                           );
@@ -224,7 +280,7 @@ class _PedidoScreenState extends State<PedidoScreen> {
                 pvm.isLoading
                     ? const Center(child: CircularProgressIndicator())
                     : Container(),
-                tarjeta(
+                clase.tarjeta(
                   Column(
                     children: [
                       LayoutBuilder(builder: (context, constraints) {
@@ -234,8 +290,7 @@ class _PedidoScreenState extends State<PedidoScreen> {
                             controller: horizontalScrollController,
                             interactive: true,
                             thumbVisibility: true,
-                            thickness:
-                                10.0, // Esto es opcional para cambiar el grosor
+                            thickness: 10.0,
                             child: SingleChildScrollView(
                               controller: horizontalScrollController,
                               scrollDirection: Axis.horizontal,
@@ -257,6 +312,10 @@ class _PedidoScreenState extends State<PedidoScreen> {
                                     DataColumn(
                                         label:
                                             Text("COMENTARIOS", style: estilo)),
+                                    DataColumn(
+                                        label: Text("ESTATUS", style: estilo)),
+                                    DataColumn(
+                                        label: Text("FECHA", style: estilo)),
                                   ],
                                   rows: registros,
                                 ),
@@ -268,11 +327,68 @@ class _PedidoScreenState extends State<PedidoScreen> {
                     ],
                   ),
                 ),
-                // PaginationButtons(
-                //   currentPage: pvm.pagina,
-                //   totalPages: pvm.totalPaginas,
-                //   onPageChanged: (i) => pvm.cambiarPagina(i),
-                // )
+                const SizedBox(height: 15),
+                if (!pvm.isLoading)
+                  Row(
+                    mainAxisAlignment: MainAxisAlignment.center,
+                    children: [
+                      TextButton(
+                        onPressed:
+                            pvm.currentPage > 1 ? pvm.previousPage : null,
+                        child: Text('Anterior'),
+                        style: ButtonStyle(
+                          backgroundColor:
+                              MaterialStateProperty.resolveWith<Color?>(
+                            (Set<MaterialState> states) {
+                              if (states.contains(MaterialState.disabled)) {
+                                return Colors.grey;
+                              }
+                              return AppTheme.tertiary;
+                            },
+                          ),
+                          foregroundColor:
+                              MaterialStateProperty.resolveWith<Color?>(
+                            (Set<MaterialState> states) {
+                              if (states.contains(MaterialState.disabled)) {
+                                return Colors.black;
+                              }
+                              return Colors.white;
+                            },
+                          ),
+                        ),
+                      ),
+                      SizedBox(width: 15),
+                      Text('Página ${pvm.currentPage} de ${pvm.totalPages}'),
+                      SizedBox(width: 15),
+                      TextButton(
+                        onPressed: pvm.currentPage < pvm.totalPages
+                            ? pvm.nextPage
+                            : null,
+                        child: Text('Siguiente'),
+                        style: ButtonStyle(
+                          backgroundColor:
+                              MaterialStateProperty.resolveWith<Color?>(
+                            (Set<MaterialState> states) {
+                              if (states.contains(MaterialState.disabled)) {
+                                return Colors.grey;
+                              }
+                              return AppTheme.tertiary;
+                            },
+                          ),
+                          foregroundColor:
+                              MaterialStateProperty.resolveWith<Color?>(
+                            (Set<MaterialState> states) {
+                              if (states.contains(MaterialState.disabled)) {
+                                return Colors.black;
+                              }
+                              return Colors.white;
+                            },
+                          ),
+                        ),
+                      ),
+                    ],
+                  ),
+                const SizedBox(height: 15),
               ],
             ),
           ),
@@ -281,32 +397,141 @@ class _PedidoScreenState extends State<PedidoScreen> {
     );
   }
 
-  Widget BusquedaTextField() {
-    return Expanded(
-      flex: 4,
-      child: AppTextField(
-        prefixIcon: const Icon(Icons.search),
-        etiqueta: 'Búsqueda por folio...',
-        controller: _busqueda,
-        hintText: 'Búsqueda por folio...',
-      ),
+  Widget _buildDateRangePicker() {
+    return Row(
+      children: [
+        Expanded(
+          flex: 3,
+          child: AppTextField(
+            prefixIcon: const Icon(Icons.search),
+            etiqueta: 'Búsqueda por folio...',
+            controller: _busqueda,
+            hintText: 'Búsqueda por folio...',
+          ),
+        ),
+        const SizedBox(width: 5),
+        Expanded(
+            flex: 2,
+            child: clase.tarjeta(
+                ListTile(
+                    title: Text(
+                      "Fecha Inicial",
+                      style: TextStyle(
+                          color: AppTheme.quaternary,
+                          fontWeight: FontWeight.bold),
+                    ),
+                    subtitle: Text(
+                      fechaInicio == null
+                          ? ""
+                          : DateFormat("dd/MM/yyyy").format(fechaInicio!),
+                      style: TextStyle(
+                          color: AppTheme.quaternary,
+                          fontWeight: FontWeight.bold),
+                    ),
+                    trailing:
+                        Icon(Icons.calendar_month, color: AppTheme.quaternary),
+                    onTap: () async {
+                      DateTime? d = await clase.showDatetimePicker(
+                          context, fechaInicio,
+                          tipo: OmniDateTimePickerType.date, solofecha: true);
+                      if (d == null) return;
+                      setState(() {
+                        fechaInicio = d;
+                      });
+                    }),
+                color: AppTheme.tertiary)),
+        const SizedBox(width: 5),
+        Expanded(
+            flex: 2,
+            child: clase.tarjeta(
+                ListTile(
+                    title: Text(
+                      "Fecha Final",
+                      style: TextStyle(
+                          color: AppTheme.quaternary,
+                          fontWeight: FontWeight.bold),
+                    ),
+                    subtitle: Text(
+                      fechaFin == null
+                          ? ""
+                          : DateFormat("dd/MM/yyyy").format(fechaFin!),
+                      style: TextStyle(
+                          color: AppTheme.quaternary,
+                          fontWeight: FontWeight.bold),
+                    ),
+                    trailing:
+                        Icon(Icons.calendar_month, color: AppTheme.quaternary),
+                    onTap: () async {
+                      DateTime? d = await clase.showDatetimePicker(
+                          context, fechaInicio,
+                          inicia: fechaInicio,
+                          tipo: OmniDateTimePickerType.date,
+                          solofecha: true);
+                      if (d == null) return;
+                      setState(() {
+                        fechaFin = d;
+                      });
+                    }),
+                color: AppTheme.tertiary)),
+      ],
     );
   }
 
-  Widget BotonBuscar() {
+  Widget botonBuscar() {
     return Expanded(
-      flex: 2,
-      child: botonElevated(
-        accion: () async {
-          // PedidoViewModel mvm =
-          //     Provider.of<PedidoViewModel>(context, listen: false);
-          // await mvm.setIsLoading(true);
-          // await mvm.setBusqueda(_busqueda.text);
-          // await mvm.fetchPedidos(q: _busqueda.text);
-          // await mvm.setBusqueda("");
-          // await mvm.setIsLoading(false);
-        },
-      ),
-    );
+        flex: 2,
+        child: Row(
+          children: [
+            Expanded(
+              flex: 2,
+              child: Padding(
+                padding: const EdgeInsets.only(top: 0),
+                child: ElevatedButton(
+                  onPressed: clearSearchAndReset,
+                  style: ElevatedButton.styleFrom(
+                    shape: RoundedRectangleBorder(
+                      borderRadius: BorderRadius.circular(20.0),
+                    ),
+                    primary: AppTheme.tertiary,
+                    padding: const EdgeInsets.symmetric(vertical: 25),
+                  ),
+                  child: Text('Limpiar',
+                      style: TextStyle(color: AppTheme.quaternary)),
+                ),
+              ),
+            ),
+            const SizedBox(width: 8),
+            Expanded(
+              flex: 2,
+              child: Padding(
+                padding: const EdgeInsets.only(top: 0),
+                child: ElevatedButton(
+                  onPressed: () async {
+                    if (_busqueda.text.isNotEmpty) {
+                      await Provider.of<PedidoViewModel>(context, listen: false)
+                          .buscarPedidosPorFolio(_busqueda.text.trim());
+                    } else if (fechaInicio != null && fechaFin != null) {
+                      await Provider.of<PedidoViewModel>(context, listen: false)
+                          .buscarPedidosPorFecha(fechaInicio!, fechaFin!);
+                    } else {
+                      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
+                          content: Text(
+                              'Introduce un folio o selecciona un rango de fechas para buscar.')));
+                    }
+                  },
+                  style: ElevatedButton.styleFrom(
+                    shape: RoundedRectangleBorder(
+                      borderRadius: BorderRadius.circular(20.0),
+                    ),
+                    primary: AppTheme.tertiary,
+                    padding: const EdgeInsets.symmetric(vertical: 25),
+                  ),
+                  child: Text('Buscar',
+                      style: TextStyle(color: AppTheme.quaternary)),
+                ),
+              ),
+            ),
+          ],
+        ));
   }
 }

+ 149 - 192
lib/views/pedido/pedido_ticket.dart

@@ -1,189 +1,133 @@
 import 'dart:typed_data';
+import 'package:flutter/material.dart';
+import 'package:intl/intl.dart';
 import 'package:pdf/pdf.dart';
 import 'package:pdf/widgets.dart' as pw;
 import '../../models/models.dart';
 import 'package:printing/printing.dart';
 import 'package:flutter/services.dart' show rootBundle;
 
-// Future<Uint8List> generateTicket(Pedido pedido) async {
-//   final pdf = pw.Document();
-
-//   final image = pw.MemoryImage(
-//     (await rootBundle.load('assets/JoshiLogo-BN.png')).buffer.asUint8List(),
-//   );
-
-//   pdf.addPage(
-//     pw.Page(
-//       pageFormat: PdfPageFormat.roll57,
-//       build: (pw.Context context) {
-//         return pw.Column(
-//           crossAxisAlignment: pw.CrossAxisAlignment.center,
-//           children: [
-//             pw.Center(child: pw.Image(image, width: 50, height: 50)),
-//             pw.SizedBox(height: 10),
-//             pw.Text('Joshi Papas Tu Sabor tu Estilo',
-//                 style:
-//                     pw.TextStyle(fontSize: 12, fontWeight: pw.FontWeight.bold)),
-//             pw.SizedBox(height: 10),
-//             pw.Text('RFC: XXXX0000', style: pw.TextStyle(fontSize: 9)),
-//             pw.Text('Dirección: Calle Falsa 123',
-//                 style: pw.TextStyle(fontSize: 9)),
-//             pw.Text('Ciudad: Ciudad Ejemplo', style: pw.TextStyle(fontSize: 9)),
-//             pw.Text('Régimen: General de Ley',
-//                 style: pw.TextStyle(fontSize: 9)),
-//             pw.SizedBox(height: 10),
-//             pw.Text('Folio: ${pedido.folio}',
-//                 style:
-//                     pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 10)),
-//             pw.SizedBox(height: 10),
-//             ...pedido.productos.map(
-//               (producto) => pw.Row(
-//                 mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
-//                 children: [
-//                   pw.Expanded(
-//                     flex: 2,
-//                     child: pw.Text(
-//                         producto.producto?.nombre ?? "Producto no especificado",
-//                         style: pw.TextStyle(fontSize: 7)),
-//                   ),
-//                   pw.Expanded(
-//                     flex: 1,
-//                     child: pw.Text('x${producto.cantidad}',
-//                         style: pw.TextStyle(fontSize: 9)),
-//                   ),
-//                   pw.Expanded(
-//                     flex: 1,
-//                     child: pw.Text('\$${producto.producto?.precio}',
-//                         style: pw.TextStyle(fontSize: 9)),
-//                   ),
-//                 ],
-//               ),
-//             ),
-//             pw.Divider(),
-//             pw.Row(
-//               mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
-//               children: [
-//                 pw.Text('Total:',
-//                     style: pw.TextStyle(
-//                         fontWeight: pw.FontWeight.bold, fontSize: 9)),
-//                 pw.Padding(
-//                   padding: pw.EdgeInsets.only(right: 20),
-//                   child: pw.Text(
-//                       '\$${pedido.productos.fold<double>(0.0, (sum, p) => sum + (double.parse(p.producto?.precio ?? '0') * (p.cantidad ?? 1)))}',
-//                       style: pw.TextStyle(
-//                           fontWeight: pw.FontWeight.bold, fontSize: 9)),
-//                 ),
-//               ],
-//             ),
-//             pw.SizedBox(height: 5),
-//             pw.Text('¡GRACIAS POR SU COMPRA!',
-//                 style:
-//                     pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold)),
-//             pw.SizedBox(height: 20),
-//             pw.Divider(),
-//             pw.SizedBox(height: 10),
-//             // Existing Ticket Content
-//             pw.Text('Folio: ${pedido.folio}',
-//                 style:
-//                     pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 9)),
-//             pw.SizedBox(height: 10),
-//             pw.Text('Cliente: ${pedido.nombreCliente}',
-//                 style:
-//                     pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 9)),
-//             pw.SizedBox(height: 10),
-//             ...pedido.productos.map(
-//               (producto) => pw.Row(
-//                 mainAxisAlignment: pw.MainAxisAlignment.start,
-//                 children: [
-//                   pw.Expanded(
-//                     flex: 3,
-//                     child: pw.Text(
-//                         producto.producto?.nombre ?? "Producto no especificado",
-//                         style: pw.TextStyle(fontSize: 9)),
-//                   ),
-//                   pw.Expanded(
-//                     flex: 1,
-//                     child: pw.Text('x${producto.cantidad}',
-//                         style: pw.TextStyle(fontSize: 9)),
-//                   ),
-//                 ],
-//               ),
-//             ),
-//             pw.SizedBox(height: 80),
-//           ],
-//         );
-//       },
-//     ),
-//   );
-
-//   return Uint8List.fromList(await pdf.save());
-// }
-
-Future<Uint8List> primerTicket(Pedido pedido) async {
+Future<void> imprimirTicketsJuntos(Pedido pedido) async {
   final pdf = pw.Document();
-
   final image = pw.MemoryImage(
     (await rootBundle.load('assets/JoshiLogo-BN.png')).buffer.asUint8List(),
   );
 
-  pdf.addPage(pw.Page(
+  pdf.addPage(
+    generarPaginaPrimerTicket(pedido, image),
+  );
+
+  pdf.addPage(
+    generarPaginaSegundoTicket(pedido),
+  );
+
+  await printPdf(Uint8List.fromList(await pdf.save()));
+}
+
+pw.Page generarPaginaPrimerTicket(Pedido pedido, pw.MemoryImage image) {
+  final numberFormat = NumberFormat('#,##0.00', 'es_MX');
+
+  return pw.Page(
       pageFormat: PdfPageFormat.roll57,
       build: (pw.Context context) {
+        double total = 0;
+
+        final productList = pedido.productos
+            .map(
+              (producto) {
+                final productPrice =
+                    double.parse(producto.producto?.precio ?? '0');
+                final productTotal = productPrice * (producto.cantidad ?? 1);
+
+                final toppingsList = producto.toppings.where((topping) {
+                  final toppingPrice =
+                      double.parse(topping.topping?.precio ?? '0');
+                  return toppingPrice > 0;
+                }).map((topping) {
+                  final toppingPrice =
+                      double.parse(topping.topping?.precio ?? '0');
+                  total += toppingPrice * (producto.cantidad ?? 1);
+
+                  return pw.Row(
+                    children: [
+                      pw.Text(
+                          '- ${topping.topping?.nombre ?? "Topping no especificado"}',
+                          style: const pw.TextStyle(fontSize: 7)),
+                      pw.Spacer(),
+                      pw.Text('\$${numberFormat.format(toppingPrice)}',
+                          style: const pw.TextStyle(fontSize: 7)),
+                    ],
+                  );
+                }).toList();
+
+                total += productTotal;
+
+                return [
+                  pw.Row(
+                    mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
+                    children: [
+                      pw.Expanded(
+                        flex: 2,
+                        child: pw.Text(
+                            producto.producto?.nombre ??
+                                "Producto no especificado",
+                            style: const pw.TextStyle(fontSize: 7)),
+                      ),
+                      pw.Expanded(
+                        flex: 1,
+                        child: pw.Text('x${producto.cantidad}',
+                            style: const pw.TextStyle(fontSize: 7)),
+                      ),
+                      pw.Padding(
+                        padding: pw.EdgeInsets.only(right: 2),
+                        child: pw.Expanded(
+                          flex: 1,
+                          child: pw.Text(
+                              '\$${numberFormat.format(productPrice)}',
+                              style: const pw.TextStyle(fontSize: 8)),
+                        ),
+                      )
+                    ],
+                  ),
+                  ...toppingsList,
+                ];
+              },
+            )
+            .expand((e) => e)
+            .toList();
+
         return pw.Column(
             crossAxisAlignment: pw.CrossAxisAlignment.center,
             children: [
               pw.Padding(
-                  padding: const pw.EdgeInsets.only(right: 20),
+                  padding: const pw.EdgeInsets.only(right: 15),
                   child:
                       pw.Center(child: pw.Image(image, width: 50, height: 50))),
               pw.SizedBox(height: 10),
               pw.Padding(
                   padding: const pw.EdgeInsets.only(right: 15),
                   child: pw.Column(children: [
-                    pw.Text('Joshi Papas Tu Sabor tu Estilo',
-                        style: pw.TextStyle(
-                            fontSize: 12, fontWeight: pw.FontWeight.bold)),
+                    pw.Padding(
+                        padding: pw.EdgeInsets.only(left: 10),
+                        child: pw.Text('Joshi Papas Tu Sabor tu Estilo',
+                            style: pw.TextStyle(
+                                fontSize: 12, fontWeight: pw.FontWeight.bold))),
                     pw.SizedBox(height: 10),
                     pw.Text('Fecha: ${pedido.peticion}',
                         style: const pw.TextStyle(fontSize: 9)),
-                    pw.Text('RFC: XXXX0000',
-                        style: const pw.TextStyle(fontSize: 9)),
-                    pw.Text('Dirección: Calle Falsa 123',
-                        style: const pw.TextStyle(fontSize: 9)),
-                    pw.Text('Ciudad: Ciudad Ejemplo',
+                    pw.Text('JoshiPapas',
                         style: const pw.TextStyle(fontSize: 9)),
-                    pw.Text('Régimen: General de Ley',
+                    pw.Text('Chihuahua',
                         style: const pw.TextStyle(fontSize: 9)),
                   ])),
               pw.SizedBox(height: 10),
-              pw.Text('Folio: ${pedido.folio}',
+              pw.Text('Pedido: ${pedido.folio}',
                   style: pw.TextStyle(
                       fontWeight: pw.FontWeight.bold, fontSize: 10)),
               pw.SizedBox(height: 10),
-              ...pedido.productos.map(
-                (producto) => pw.Row(
-                  mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
-                  children: [
-                    pw.Expanded(
-                      flex: 2,
-                      child: pw.Text(
-                          producto.producto?.nombre ??
-                              "Producto no especificado",
-                          style: const pw.TextStyle(fontSize: 7)),
-                    ),
-                    pw.Expanded(
-                      flex: 1,
-                      child: pw.Text('x${producto.cantidad}',
-                          style: const pw.TextStyle(fontSize: 9)),
-                    ),
-                    pw.Expanded(
-                      flex: 1,
-                      child: pw.Text('\$${producto.producto?.precio}',
-                          style: const pw.TextStyle(fontSize: 9)),
-                    ),
-                  ],
-                ),
-              ),
+              pw.Padding(
+                  padding: const pw.EdgeInsets.only(right: 20),
+                  child: pw.Column(children: productList)),
               pw.Divider(),
               pw.Row(
                 mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
@@ -192,9 +136,8 @@ Future<Uint8List> primerTicket(Pedido pedido) async {
                       style: pw.TextStyle(
                           fontWeight: pw.FontWeight.bold, fontSize: 9)),
                   pw.Padding(
-                    padding: const pw.EdgeInsets.only(right: 20),
-                    child: pw.Text(
-                        '\$${pedido.productos.fold<double>(0.0, (sum, p) => sum + (double.parse(p.producto?.precio ?? '0') * (p.cantidad ?? 1)))}',
+                    padding: const pw.EdgeInsets.only(right: 30),
+                    child: pw.Text('\$${numberFormat.format(total)}',
                         style: pw.TextStyle(
                             fontWeight: pw.FontWeight.bold, fontSize: 9)),
                   ),
@@ -206,28 +149,27 @@ Future<Uint8List> primerTicket(Pedido pedido) async {
                   child: pw.Text('¡GRACIAS POR SU COMPRA!',
                       style: pw.TextStyle(
                           fontSize: 8, fontWeight: pw.FontWeight.bold))),
-              pw.SizedBox(height: 20),
               pw.Divider(),
+              pw.SizedBox(height: 20),
+              pw.Text('.', style: pw.TextStyle(fontSize: 1)),
             ]);
-      }));
-  return Uint8List.fromList(await pdf.save());
+      });
 }
 
-Future<Uint8List> segundoTicket(Pedido pedido) async {
-  final pdf = pw.Document();
-
-  pdf.addPage(pw.Page(
+pw.Page generarPaginaSegundoTicket(Pedido pedido) {
+  return pw.Page(
       pageFormat: PdfPageFormat.roll57,
       build: (pw.Context context) {
         List<pw.Widget> content = [
-          pw.SizedBox(height: 10),
+          pw.SizedBox(height: 20),
+          pw.Text('.', style: pw.TextStyle(fontSize: 1)),
           pw.Padding(
               padding: const pw.EdgeInsets.only(right: 15),
               child: pw.Text('Fecha: ${pedido.peticion}',
                   style: pw.TextStyle(
                       fontSize: 9, fontWeight: pw.FontWeight.bold))),
           pw.SizedBox(height: 5),
-          pw.Text('Folio: ${pedido.folio}',
+          pw.Text('Pedido: ${pedido.folio}',
               style: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 9)),
           pw.SizedBox(height: 10),
           pw.Text('Cliente: ${pedido.nombreCliente}',
@@ -235,22 +177,45 @@ Future<Uint8List> segundoTicket(Pedido pedido) async {
           pw.SizedBox(height: 10),
         ];
 
-        content.addAll(pedido.productos.map((producto) => pw.Row(
-              mainAxisAlignment: pw.MainAxisAlignment.start,
-              children: [
-                pw.Expanded(
-                  flex: 3,
-                  child: pw.Text(
-                      producto.producto?.nombre ?? "Producto no especificado",
-                      style: const pw.TextStyle(fontSize: 9)),
-                ),
-                pw.Expanded(
-                  flex: 1,
-                  child: pw.Text('x${producto.cantidad}',
-                      style: const pw.TextStyle(fontSize: 9)),
+        content.addAll(pedido.productos
+            .map((producto) {
+              final productPrice =
+                  double.parse(producto.producto?.precio ?? '0');
+              final productTotal = productPrice * (producto.cantidad ?? 1);
+
+              final toppingsList = producto.toppings.map((topping) {
+                return pw.Row(
+                  children: [
+                    pw.Text(
+                        '-${topping.topping?.nombre ?? "Topping no especificado"}',
+                        style: const pw.TextStyle(fontSize: 7)),
+                  ],
+                );
+              }).toList();
+
+              return [
+                pw.Row(
+                  mainAxisAlignment: pw.MainAxisAlignment.start,
+                  children: [
+                    pw.Expanded(
+                      flex: 3,
+                      child: pw.Text(
+                          producto.producto?.nombre ??
+                              "Producto no especificado",
+                          style: const pw.TextStyle(fontSize: 9)),
+                    ),
+                    pw.Expanded(
+                      flex: 1,
+                      child: pw.Text('x${producto.cantidad}',
+                          style: const pw.TextStyle(fontSize: 9)),
+                    ),
+                  ],
                 ),
-              ],
-            )));
+                ...toppingsList,
+              ];
+            })
+            .expand((e) => e)
+            .toList());
 
         if (pedido.comentarios != null && pedido.comentarios!.isNotEmpty) {
           content.add(pw.SizedBox(height: 10));
@@ -262,17 +227,16 @@ Future<Uint8List> segundoTicket(Pedido pedido) async {
             child: pw.Text(pedido.comentarios!,
                 style: const pw.TextStyle(fontSize: 9)),
           ));
+          content.add(pw.Text('.', style: pw.TextStyle(fontSize: 1)));
+          content.add(pw.SizedBox(height: 20));
+          content.add(pw.Text('.', style: pw.TextStyle(fontSize: 1)));
         }
 
-        content.add(
-            pw.SizedBox(height: 20)); // Some spacing before finalizing the page
-
+        content.add(pw.Text('.', style: pw.TextStyle(fontSize: 1)));
         return pw.Column(
             crossAxisAlignment: pw.CrossAxisAlignment.center,
             children: content);
-      }));
-
-  return Uint8List.fromList(await pdf.save());
+      });
 }
 
 Future<void> printPdf(Uint8List pdfBytes) async {
@@ -280,10 +244,3 @@ Future<void> printPdf(Uint8List pdfBytes) async {
     onLayout: (PdfPageFormat format) => pdfBytes,
   );
 }
-
-void printTickets(Pedido pedido) async {
-  Uint8List firstTicket = await primerTicket(pedido);
-  await printPdf(firstTicket);
-  Uint8List secondTicket = await segundoTicket(pedido);
-  await printPdf(secondTicket);
-}

+ 419 - 198
lib/views/producto/producto_form.dart

@@ -1,198 +1,419 @@
-// // ignore_for_file: use_build_context_synchronously
-
-// import 'package:flutter/material.dart';
-// import 'package:yoshi_papas_app/widgets/widgets.dart';
-// import 'package:provider/provider.dart';
-
-// import '../../models/models.dart';
-// import '../../viewmodels/viewmodels.dart';
-
-// class ProductoForm extends StatefulWidget {
-//   const ProductoForm({Key? key}) : super(key: key);
-
-//   @override
-//   State<ProductoForm> createState() => Formulario();
-// }
-
-// class Formulario extends State<ProductoForm> {
-//   final _nombre = TextEditingController();
-//   final _descripcion = TextEditingController();
-//   final _precio = TextEditingController();
-//   final _existencia = TextEditingController();
-//   final _descuento = TextEditingController();
-//   final _busquedaCategoria = TextEditingController();
-//   final _venceDescuento = TextEditingController();
-//   CategoriaProducto? categoriaProducto;
-
-//   @override
-//   void initState() {
-//     super.initState();
-//     Future(() async {
-//       Provider.of<ProductoViewModel>(context, listen: false).setIsLoading(true);
-//       final mvm = Provider.of<ProductoViewModel>(context, listen: false);
-//       await Provider.of<CategoriaProductoViewModel>(context, listen: false)
-//           .fetchCategoriaProductos();
-//       List<CategoriaProducto> catProductos =
-//           Provider.of<CategoriaProductoViewModel>(context, listen: false)
-//               .categoriaProductos;
-//       Producto? modelo = mvm.selectedProducto;
-//       if (modelo != null && modelo.id > 0) {
-//         setState(() {
-//           _nombre.text = modelo.nombre.toString();
-//           _descripcion.text = modelo.descripcion.toString();
-//           _precio.text = modelo.precio.toString();
-//           _existencia.text = modelo.existencia.toString();
-//           _descuento.text = modelo.descuento.toString();
-//           if (modelo.idCategoria != null &&
-//               modelo.idCategoria! != "" &&
-//               modelo.categoria != null) {
-//             categoriaProducto = modelo.categoria;
-//           }
-//           if (categoriaProducto == null && catProductos.isNotEmpty) {
-//             categoriaProducto = catProductos.first;
-//           }
-//         });
-//       }
-//       Provider.of<ProductoViewModel>(context, listen: false)
-//           .setIsLoading(false);
-//     });
-//   }
-
-//   @override
-//   void dispose() {
-//     super.dispose();
-//     _descripcion.dispose();
-//     _nombre.dispose();
-//   }
-
-//   @override
-//   Widget build(BuildContext context) {
-//     final mvm = Provider.of<ProductoViewModel>(context);
-//     final evm = Provider.of<CategoriaProductoViewModel>(context);
-//     if (mvm.isLoading) return const Cargando();
-//     final producto = mvm.selectedProducto;
-//     List<CategoriaProducto> categoriaProductos = [];
-//     if (evm.categoriaProductos.isNotEmpty) {
-//       categoriaProductos = evm.categoriaProductos;
-//     }
-
-//     return Scaffold(
-//       appBar: encabezado(titulo: "Producto"),
-//       body: SingleChildScrollView(
-//         padding: const EdgeInsets.all(8),
-//         child: Column(
-//           children: [
-//             tarjeta(
-//               Padding(
-//                 padding: const EdgeInsets.all(8),
-//                 child: Column(
-//                   children: [
-//                     categoriaProductos.isNotEmpty
-//                         ? AppDropdownSearch(
-//                             etiqueta: 'Categoría',
-//                             controller: _busquedaCategoria,
-//                             selectedItem: categoriaProducto,
-//                             onPressedClear: () {
-//                               categoriaProducto = null;
-//                             },
-//                             itemAsString: (item) => '${item.nombre}',
-//                             asyncItems: (text) async {
-//                               final xregistros = await evm.getCategoriaProducto(
-//                                   q: text, segmentar: true);
-//                               return xregistros;
-//                             },
-//                             onChanged: (value) {
-//                               categoriaProducto = value;
-//                             },
-//                             validator: (value) {
-//                               if (value == null) {
-//                                 return 'Seleccione';
-//                               }
-//                               return null;
-//                             },
-//                           )
-//                         : Container(),
-//                     Row(
-//                       children: [
-//                         Expanded(
-//                           child: AppTextField(
-//                             maxLength: 100,
-//                             etiqueta: 'Nombre',
-//                             controller: _nombre,
-//                             hintText: 'Nombre producto',
-//                           ),
-//                         ),
-//                       ],
-//                     ),
-//                     Row(
-//                       children: [
-//                         Expanded(
-//                           child: AppTextField(
-//                             maxLength: 100,
-//                             etiqueta: 'Precio',
-//                             controller: _precio,
-//                             hintText: 'Precio del producto',
-//                           ),
-//                         ),
-//                       ],
-//                     ),
-//                     Row(
-//                       children: [
-//                         Expanded(
-//                           child: AppTextField(
-//                             maxLength: 100,
-//                             etiqueta: 'Existencia',
-//                             controller: _existencia,
-//                             hintText: 'Existencia del producto',
-//                           ),
-//                         ),
-//                       ],
-//                     ),
-//                     Row(
-//                       children: [
-//                         Expanded(
-//                           child: AppTextField(
-//                             maxLength: 1000,
-//                             maxLines: 3,
-//                             etiqueta: 'Descripción',
-//                             controller: _descripcion,
-//                             hintText: 'descripción del producto',
-//                           ),
-//                         ),
-//                       ],
-//                     ),
-//                   ],
-//                 ),
-//               ),
-//             ),
-//             const SizedBox(height: 15),
-//             boton("Guardar", () async {
-//               if (categoriaProducto == null ||
-//                   categoriaProducto!.id == null ||
-//                   categoriaProducto!.id == "") {
-//                 await alerta(context, etiqueta: "Seleccionar categoría");
-//                 return;
-//               }
-//               Provider.of<ProductoViewModel>(context, listen: false)
-//                   .setIsLoading(true);
-//               await mvm.guardarModelo(
-//                   modelo: producto!,
-//                   nombre: _nombre.text,
-//                   descripcion: _descripcion.text,
-//                   precio: _precio.text,
-//                   descuento: _descuento.text,
-//                   existencia: int.tryParse(_existencia.text) ?? 0,
-//                   venceDescuento: _venceDescuento.text,
-//                   idCategoria: categoriaProducto?.id ?? 0);
-//               Provider.of<ProductoViewModel>(context, listen: false)
-//                   .setIsLoading(false);
-//               if (context.mounted) {
-//                 Navigator.pop(context);
-//               }
-//             }),
-//           ],
-//         ),
-//       ),
-//     );
-//   }
-// }
+import 'dart:io';
+
+import 'package:yoshi_papas_app/themes/themes.dart';
+import 'package:flutter/material.dart';
+import 'package:yoshi_papas_app/views/producto/producto_imagen.dart';
+import 'package:yoshi_papas_app/widgets/widgets.dart';
+import 'package:provider/provider.dart';
+import 'package:file_picker/file_picker.dart';
+import '../../models/models.dart';
+import '../../viewmodels/viewmodels.dart';
+
+class ProductoForm extends StatefulWidget {
+  final Producto producto;
+
+  const ProductoForm({Key? key, required this.producto}) : super(key: key);
+
+  @override
+  State<ProductoForm> createState() => Formulario();
+}
+
+class Formulario extends State<ProductoForm> {
+  final _nombre = TextEditingController();
+  final _descripcion = TextEditingController();
+  final _precio = TextEditingController();
+  final _existencia = TextEditingController();
+  final _descuento = TextEditingController();
+  final _busquedaCategoria = TextEditingController();
+  final _venceDescuento = TextEditingController();
+  File? _selectedFile;
+  String? _selectedFilePath;
+  CategoriaProducto? categoriaProducto;
+
+  Map<int, List<int>> selectedToppingsByCategory = {};
+  Map<int, bool> expandedCategoryStates = {};
+
+  List<CategoriaProducto> toppingCategories = [];
+  Map<int, List<Producto>> productsByCategory = {};
+
+  @override
+  void initState() {
+    super.initState();
+    _nombre.text = widget.producto.nombre ?? "";
+    _descripcion.text = widget.producto.descripcion ?? "";
+    _precio.text = widget.producto.precio?.toString() ?? "";
+    _existencia.text = widget.producto.existencia?.toString() ?? "";
+    _descuento.text = widget.producto.descuento?.toString() ?? "";
+    _selectedFilePath = widget.producto.imagen;
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      _loadData();
+    });
+
+    Future.microtask(() async {
+      await Provider.of<CategoriaProductoViewModel>(context, listen: false)
+          .fetchLocalAll();
+      List<CategoriaProducto> categories =
+          Provider.of<CategoriaProductoViewModel>(context, listen: false)
+              .categoriaProductos;
+      if (widget.producto.idCategoria != null) {
+        categoriaProducto = categories.firstWhere(
+            (c) => c.id == widget.producto.idCategoria,
+            orElse: () =>
+                CategoriaProducto(id: 0, nombre: 'Categoría Default'));
+      } else if (categories.isNotEmpty) {
+        categoriaProducto = categories.first;
+      }
+
+      // Usa ProductoViewModel para obtener los toppings guardados
+      final productoVM = Provider.of<ProductoViewModel>(context, listen: false);
+      await cargarToppingsGuardados(widget.producto.id, productoVM);
+
+      await loadToppingData();
+
+      setState(() {});
+    });
+  }
+
+  Future<void> cargarToppingsGuardados(
+      int? idProducto, ProductoViewModel productoVM) async {
+    if (idProducto != null && idProducto > 0) {
+      List<int> toppingIds =
+          await productoVM.obtenerToppingsPorProducto(idProducto);
+
+      // Asignar toppings a las categorías
+      for (var toppingId in toppingIds) {
+        Producto? topping = await productoVM.obtenerProductoPorId(toppingId);
+        if (topping != null && topping.idCategoria != null) {
+          selectedToppingsByCategory[topping.idCategoria!] ??= [];
+          selectedToppingsByCategory[topping.idCategoria!]!.add(toppingId);
+        }
+      }
+    }
+  }
+
+  void _loadData() async {
+    await Provider.of<CategoriaProductoViewModel>(context, listen: false)
+        .fetchLocalAll();
+  }
+
+  Future<void> _selectImage() async {
+    final result = await FilePicker.platform.pickFiles(
+      type: FileType.image,
+      dialogTitle: 'Seleccionar imagen para el producto',
+    );
+    if (result != null && result.files.single.path != null) {
+      File selectedFile = File(result.files.single.path!);
+      setState(() {
+        _selectedFile = selectedFile;
+      });
+    }
+  }
+
+  Future<void> loadToppingData() async {
+    final productoVM = Provider.of<ProductoViewModel>(context, listen: false);
+
+    toppingCategories = await productoVM.fetchToppingCategories();
+
+    for (var category in toppingCategories) {
+      productsByCategory[category.id!] =
+          await productoVM.fetchProductsByCategory(category.id!);
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final ProductoViewModel productoVM =
+        Provider.of<ProductoViewModel>(context);
+
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(
+          'Nuevo Producto',
+          style: TextStyle(color: AppTheme.secondary),
+        ),
+        iconTheme: IconThemeData(color: AppTheme.secondary),
+      ),
+      body: SingleChildScrollView(
+        padding: EdgeInsets.all(8),
+        child: Column(
+          children: [
+            tarjeta(
+              Padding(
+                padding: const EdgeInsets.all(8),
+                child: Column(
+                  children: [
+                    AppDropdownSearch(
+                      etiqueta: 'Categoría',
+                      controller: _busquedaCategoria,
+                      selectedItem: categoriaProducto,
+                      onPressedClear: () {
+                        setState(() {
+                          categoriaProducto = null;
+                        });
+                      },
+                      itemAsString: (item) => item.nombre,
+                      asyncItems: (text) async {
+                        return await Provider.of<CategoriaProductoViewModel>(
+                                context,
+                                listen: false)
+                            .getCategoriaProducto(q: text);
+                      },
+                      onChanged: (newValue) {
+                        setState(() {
+                          categoriaProducto = newValue as CategoriaProducto?;
+                        });
+                      },
+                      validator: (value) =>
+                          value == null ? 'Seleccione una categoría' : null,
+                    ),
+                    AppTextField(
+                      maxLength: 100,
+                      etiqueta: 'Nombre',
+                      controller: _nombre,
+                      hintText: 'Nombre del producto',
+                    ),
+                    AppTextField(
+                      maxLength: 100,
+                      etiqueta: 'Precio',
+                      controller: _precio,
+                      hintText: 'Precio del producto',
+                      keyboardType: TextInputType.number,
+                    ),
+                    const SizedBox(height: 5),
+                    if (categoriaProducto != null &&
+                        categoriaProducto!.esToping == 0)
+                      Column(
+                        children: [
+                          Align(
+                              alignment: Alignment.centerLeft,
+                              child: Text(
+                                'Toppings a escoger en el pedido:',
+                                style: TextStyle(
+                                    fontSize: 18, fontWeight: FontWeight.bold),
+                              )),
+                          buildToppingExpansionTiles(),
+                        ],
+                      ),
+                    const SizedBox(height: 5),
+                    AppTextField(
+                      maxLength: 1000,
+                      maxLines: 3,
+                      etiqueta: 'Descripción',
+                      controller: _descripcion,
+                      hintText: 'Descripción del producto',
+                    ),
+                    Align(
+                      alignment: Alignment.centerLeft,
+                      child: ElevatedButton(
+                        onPressed: _selectImage,
+                        style: ElevatedButton.styleFrom(
+                          shape: RoundedRectangleBorder(
+                              borderRadius:
+                                  BorderRadius.all(Radius.circular(20))),
+                          primary: AppTheme.tertiary,
+                          onPrimary: AppTheme.quaternary,
+                        ),
+                        child: Column(
+                          mainAxisSize: MainAxisSize.min,
+                          children: <Widget>[
+                            Icon(Icons.add, size: 120),
+                            Text('Agregar Imagen',
+                                style: TextStyle(fontSize: 16)),
+                            SizedBox(height: 10),
+                          ],
+                        ),
+                      ),
+                    ),
+                    if (_selectedFilePath != null &&
+                        File(_selectedFilePath!).existsSync())
+                      Column(
+                        crossAxisAlignment: CrossAxisAlignment.start,
+                        children: [
+                          Align(
+                            alignment: Alignment.centerLeft,
+                            child: Padding(
+                              padding:
+                                  const EdgeInsets.only(top: 10.0, bottom: 4.0),
+                              child: Text(
+                                'Imagen:',
+                                style: TextStyle(
+                                    fontSize: 18,
+                                    fontWeight: FontWeight.bold,
+                                    color: AppTheme.secondary),
+                              ),
+                            ),
+                          ),
+                          Image.file(
+                            File(_selectedFilePath!),
+                            height: 250,
+                            fit: BoxFit.cover,
+                            key: ValueKey(_selectedFilePath),
+                          ),
+                        ],
+                      ),
+                  ],
+                ),
+              ),
+            ),
+            SizedBox(height: 15),
+            boton("Guardar", () async {
+              await saveOrUpdateProduct();
+            })
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget buildToppingExpansionTiles() {
+    return Column(
+      children: toppingCategories.map((category) {
+        expandedCategoryStates[category.id!] ??= false;
+
+        List<Producto>? products = productsByCategory[category.id!] ?? [];
+
+        bool allSelected =
+            selectedToppingsByCategory[category.id]?.length == products.length;
+
+        return ExpansionTile(
+          title: Row(
+            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            children: [
+              Text(category.nombre!),
+              Checkbox(
+                activeColor: AppTheme.primary,
+                value: allSelected,
+                onChanged: (value) {
+                  setState(() {
+                    if (value == true) {
+                      selectedToppingsByCategory[category.id!] =
+                          products.map((p) => p.id!).toList();
+                    } else {
+                      selectedToppingsByCategory[category.id!] = [];
+                    }
+                  });
+                },
+              ),
+            ],
+          ),
+          children: products.map((product) {
+            bool isSelected =
+                selectedToppingsByCategory[category.id]?.contains(product.id) ??
+                    false;
+            return CheckboxListTile(
+              activeColor: AppTheme.primary,
+              title: Text(product.nombre!),
+              value: isSelected,
+              onChanged: (value) {
+                setState(() {
+                  if (value == true) {
+                    if (selectedToppingsByCategory[category.id!] == null) {
+                      selectedToppingsByCategory[category.id!] = [];
+                    }
+                    selectedToppingsByCategory[category.id!]!.add(product.id!);
+                  } else {
+                    selectedToppingsByCategory[category.id!]!
+                        .remove(product.id);
+                  }
+                });
+              },
+            );
+          }).toList(),
+          initiallyExpanded: expandedCategoryStates[category.id]!,
+          onExpansionChanged: (expanded) {
+            setState(() {
+              expandedCategoryStates[category.id!] = expanded;
+            });
+          },
+        );
+      }).toList(),
+    );
+  }
+
+  @override
+  void dispose() {
+    _nombre.dispose();
+    _descripcion.dispose();
+    _precio.dispose();
+    _existencia.dispose();
+    _descuento.dispose();
+    _busquedaCategoria.dispose();
+    _venceDescuento.dispose();
+    super.dispose();
+  }
+
+  Future<void> saveOrUpdateProduct() async {
+    if (categoriaProducto == null || categoriaProducto?.id == null) {
+      await alerta(context, etiqueta: "Seleccionar categoría");
+      return;
+    }
+
+    // Recopilar los toppings seleccionados
+    List<Producto> selectedToppings = [];
+    selectedToppingsByCategory.forEach((categoryId, productIds) {
+      selectedToppings
+          .addAll(productIds.map((productId) => Producto(id: productId)));
+    });
+
+    Producto productToUpdate = Producto(
+      id: widget.producto.id,
+      nombre: _nombre.text,
+      descripcion: _descripcion.text,
+      precio: _precio.text,
+      idCategoria: categoriaProducto!.id,
+      imagen: _selectedFilePath,
+      topings: selectedToppings.isNotEmpty ? selectedToppings : null,
+    );
+
+    if (productToUpdate.id != null && productToUpdate.id != 0) {
+      await Provider.of<ProductoViewModel>(context, listen: false)
+          .updateProducto(productToUpdate);
+
+      if (_selectedFile != null) {
+        String? imagePath =
+            await pickAndStoreImage(_selectedFile!, productToUpdate.id);
+        if (imagePath != null) {
+          setState(() {
+            _selectedFilePath = imagePath;
+            productToUpdate.imagen = imagePath;
+          });
+          await Provider.of<ProductoViewModel>(context, listen: false)
+              .updateProductImagePath(productToUpdate.id, imagePath);
+        }
+      }
+    } else {
+      Producto nuevoProducto = Producto(
+        nombre: _nombre.text,
+        descripcion: _descripcion.text,
+        precio: _precio.text,
+        idCategoria: categoriaProducto!.id,
+        imagen: _selectedFilePath,
+        topings: selectedToppings.isNotEmpty ? selectedToppings : null,
+      );
+
+      int newProductId =
+          await Provider.of<ProductoViewModel>(context, listen: false)
+              .addProducto(nuevoProducto);
+
+      if (newProductId != -1 && _selectedFile != null) {
+        String? imagePath =
+            await pickAndStoreImage(_selectedFile!, newProductId);
+        if (imagePath != null) {
+          setState(() {
+            _selectedFilePath = imagePath;
+          });
+          await Provider.of<ProductoViewModel>(context, listen: false)
+              .updateProductImagePath(newProductId, imagePath);
+        }
+      }
+    }
+
+    print('Datos del producto antes de guardar: ${productToUpdate.toJson()}');
+    print(
+        'Toppings seleccionados: ${selectedToppings.map((t) => t.id).toList()}');
+
+    if (context.mounted) {
+      Navigator.pop(context);
+    }
+  }
+}

+ 17 - 0
lib/views/producto/producto_imagen.dart

@@ -0,0 +1,17 @@
+import 'dart:io';
+import 'package:path/path.dart' as path;
+import 'package:path_provider/path_provider.dart';
+import 'package:file_picker/file_picker.dart';
+
+Future<String?> pickAndStoreImage(File file, int productId) async {
+  Directory appDocDir = await getApplicationDocumentsDirectory();
+  String productsDirectoryPath = path.join(appDocDir.path, 'productos');
+  await Directory(productsDirectoryPath).create(recursive: true);
+  String fileExtension = path.extension(file.path);
+  String newFileName = "$productId$fileExtension";
+  String finalPath = path.join(productsDirectoryPath, newFileName);
+
+  await file.copy(finalPath);
+
+  return finalPath;
+}

+ 431 - 229
lib/views/producto/producto_screen.dart

@@ -1,239 +1,441 @@
-// // ignore_for_file: use_build_context_synchronously
+import 'package:yoshi_papas_app/themes/themes.dart';
+import 'package:yoshi_papas_app/viewmodels/categoria_producto_view_model.dart';
+import 'package:yoshi_papas_app/widgets/app_textfield.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import '../../models/models.dart';
+import '../../viewmodels/producto_view_model.dart';
+import 'producto_form.dart';
+import '../../widgets/widgets_components.dart' as clase;
 
-// import 'package:flutter/material.dart';
-// import 'package:provider/provider.dart';
-// import '../../models/models.dart';
-// import '../../themes/themes.dart';
-// import '../../viewmodels/viewmodels.dart';
-// import '../../widgets/app_textfield.dart';
-// import '../../widgets/pagination_buttons.dart';
-// import '../../widgets/widgets_components.dart';
-// import 'producto_form.dart';
+class ProductoScreen extends StatefulWidget {
+  @override
+  _ProductoScreenState createState() => _ProductoScreenState();
+}
 
-// class ProductoScreen extends StatefulWidget {
-//   const ProductoScreen({Key? key}) : super(key: key);
+class _ProductoScreenState extends State<ProductoScreen> {
+  final _busqueda = TextEditingController(text: '');
+  ScrollController horizontalScrollController = ScrollController();
 
-//   @override
-//   State<ProductoScreen> createState() => Formulario();
-// }
+  @override
+  void initState() {
+    super.initState();
+    final productoModel =
+        Provider.of<ProductoViewModel>(context, listen: false);
+    final categoriaModel =
+        Provider.of<CategoriaProductoViewModel>(context, listen: false);
+    productoModel.fetchLocalAll();
+    categoriaModel.fetchLocalCategoria();
+  }
 
-// class Formulario extends State<ProductoScreen> {
-//   final _busqueda = TextEditingController(text: '');
+  void go(Producto producto) {
+    Navigator.push(
+      context,
+      MaterialPageRoute(
+        builder: (context) => ProductoForm(producto: producto),
+      ),
+    ).then((_) =>
+        Provider.of<ProductoViewModel>(context, listen: false).fetchLocalAll());
+  }
 
-//   @override
-//   void initState() {
-//     super.initState();
-//     Future(() async {
-//       await Provider.of<ProductoViewModel>(context, listen: false)
-//           .fetchRegistros();
-//       await Provider.of<CategoriaProductoViewModel>(context, listen: false)
-//           .fetchCategoriaProductos(segmentar: true);
-//     });
-//   }
+  void clearSearchAndReset() {
+    setState(() {
+      _busqueda.clear();
+      Provider.of<ProductoViewModel>(context, listen: false).fetchLocalAll();
+      Provider.of<CategoriaProductoViewModel>(context, listen: false)
+          .fetchLocalAll();
+    });
+  }
 
-//   go(Producto item) async {
-//     Provider.of<ProductoViewModel>(context, listen: false).selectModelo(item);
-//     Navigator.push(
-//       context,
-//       MaterialPageRoute(
-//         builder: (context) => const ProductoForm(),
-//       ),
-//     ).then((value) async {
-//       await Provider.of<ProductoViewModel>(context, listen: false)
-//           .fetchRegistros();
-//     });
-//   }
+  @override
+  Widget build(BuildContext context) {
+    final model = Provider.of<ProductoViewModel>(context);
+    final categoriasModel = Provider.of<CategoriaProductoViewModel>(context);
+    double screenWidth = MediaQuery.of(context).size.width;
+    final isMobile = screenWidth < 1250;
+    final double? columnSpacing = isMobile ? null : 0;
+    TextStyle estilo = const TextStyle(fontWeight: FontWeight.bold);
+    List<DataRow> registros = [];
+    for (Producto item in model.productos) {
+      registros.add(DataRow(cells: [
+        DataCell(
+            Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
+          PopupMenuButton(
+            itemBuilder: (context) => [
+              PopupMenuItem(
+                child: const Text('Editar'),
+                onTap: () => go(item),
+              ),
+              PopupMenuItem(
+                child: const Text('Eliminar'),
+                onTap: () async {
+                  bool confirmado = await showDialog<bool>(
+                        context: context,
+                        builder: (context) {
+                          return AlertDialog(
+                            title: const Text("Eliminar",
+                                style: TextStyle(
+                                    fontWeight: FontWeight.w500, fontSize: 22)),
+                            content: const Text(
+                                '¿Estás seguro de que deseas eliminar este producto?',
+                                style: TextStyle(fontSize: 18)),
+                            actions: [
+                              Row(
+                                mainAxisAlignment:
+                                    MainAxisAlignment.spaceBetween,
+                                children: [
+                                  TextButton(
+                                    onPressed: () =>
+                                        Navigator.of(context).pop(false),
+                                    child: const Text('No',
+                                        style: TextStyle(fontSize: 18)),
+                                    style: ButtonStyle(
+                                        padding: MaterialStatePropertyAll(
+                                            EdgeInsets.fromLTRB(
+                                                20, 10, 20, 10)),
+                                        backgroundColor:
+                                            MaterialStatePropertyAll(
+                                                AppTheme.primary),
+                                        foregroundColor:
+                                            MaterialStatePropertyAll(
+                                                AppTheme.secondary)),
+                                  ),
+                                  TextButton(
+                                    onPressed: () =>
+                                        Navigator.of(context).pop(true),
+                                    child: const Text('Sí',
+                                        style: TextStyle(fontSize: 18)),
+                                    style: ButtonStyle(
+                                        padding: MaterialStatePropertyAll(
+                                            EdgeInsets.fromLTRB(
+                                                20, 10, 20, 10)),
+                                        backgroundColor:
+                                            MaterialStatePropertyAll(
+                                                AppTheme.tertiary),
+                                        foregroundColor:
+                                            MaterialStatePropertyAll(
+                                                AppTheme.quaternary)),
+                                  ),
+                                ],
+                              )
+                            ],
+                          );
+                        },
+                      ) ??
+                      false;
 
-//   @override
-//   Widget build(BuildContext context) {
-//     final evm = Provider.of<CategoriaProductoViewModel>(context);
-//     List<CategoriaProducto> categoriaProductos = [];
-//     if (evm.categoriaProductos.isNotEmpty) {
-//       categoriaProductos = evm.categoriaProductos;
-//     }
+                  if (confirmado) {
+                    await Provider.of<ProductoViewModel>(context, listen: false)
+                        .deleteProducto(item.id);
+                    Provider.of<ProductoViewModel>(context, listen: false)
+                        .fetchLocalAll();
+                  }
+                },
+              )
+            ],
+            icon: const Icon(Icons.more_vert),
+          ),
+        ])),
+        DataCell(
+          Text(item.id.toString()),
+          onTap: () {
+            Provider.of<ProductoViewModel>(context, listen: false)
+                .selectProducto(item);
+            go(item);
+          },
+        ),
+        DataCell(
+          Text(item.nombre.toString()),
+          onTap: () {
+            Provider.of<ProductoViewModel>(context, listen: false)
+                .selectProducto(item);
+            go(item);
+          },
+        ),
+        DataCell(
+          Text(categoriasModel.categoriaMap[item.idCategoria] ??
+              "Sin categoría"),
+          onTap: () {
+            Provider.of<ProductoViewModel>(context, listen: false)
+                .selectProducto(item);
+            go(item);
+          },
+        ),
+        DataCell(
+          Text(item.precio ?? "Sin Precio"),
+          onTap: () {
+            Provider.of<ProductoViewModel>(context, listen: false)
+                .selectProducto(item);
+            go(item);
+          },
+        ),
+      ]));
+    }
 
-//     final mvm = Provider.of<ProductoViewModel>(context);
-//     TextStyle estilo = const TextStyle(fontWeight: FontWeight.bold);
-//     int vuelta = 0;
-//     List<DataRow> registros = [];
-//     if (mvm.registros.isNotEmpty) {
-//       for (Producto item in mvm.registros) {
-//         String? categoriaProducto = "";
-//         if (item.idCategoria != null) {
-//           categoriaProducto = item.categoria!.nombre;
-//         }
+    return Scaffold(
+      appBar: AppBar(
+        title: Text(
+          'Productos',
+          style: TextStyle(color: AppTheme.secondary),
+        ),
+        iconTheme: IconThemeData(color: AppTheme.secondary),
+      ),
+      body: Column(
+        children: [
+          Expanded(
+            child: ListView(
+              padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
+              children: [
+                const SizedBox(height: 8),
+                clase.tarjeta(
+                  Padding(
+                    padding: const EdgeInsets.all(8.0),
+                    child: LayoutBuilder(
+                      builder: (context, constraints) {
+                        if (screenWidth > 1000) {
+                          return Row(
+                            children: [
+                              Expanded(
+                                flex: 7,
+                                child: busquedaTextField(),
+                              ),
+                              SizedBox(width: 5),
+                              botonBuscar()
+                            ],
+                          );
+                        } else {
+                          return Column(
+                            children: [
+                              Row(
+                                children: [busquedaTextField()],
+                              ),
+                              SizedBox(height: 15),
+                              Row(
+                                children: [botonBuscar()],
+                              ),
+                            ],
+                          );
+                        }
+                      },
+                    ),
+                  ),
+                ),
+                const SizedBox(height: 8),
+                model.isLoading
+                    ? const Center(child: CircularProgressIndicator())
+                    : Container(),
+                clase.tarjeta(
+                  Column(
+                    children: [
+                      LayoutBuilder(builder: (context, constraints) {
+                        return SingleChildScrollView(
+                          scrollDirection: Axis.vertical,
+                          child: Scrollbar(
+                            controller: horizontalScrollController,
+                            interactive: true,
+                            thumbVisibility: true,
+                            thickness: 10.0,
+                            child: SingleChildScrollView(
+                              controller: horizontalScrollController,
+                              scrollDirection: Axis.horizontal,
+                              child: ConstrainedBox(
+                                constraints: BoxConstraints(
+                                    minWidth: isMobile
+                                        ? constraints.maxWidth
+                                        : screenWidth),
+                                child: DataTable(
+                                  columnSpacing: columnSpacing,
+                                  sortAscending: true,
+                                  sortColumnIndex: 1,
+                                  columns: [
+                                    DataColumn(label: Text(" ", style: estilo)),
+                                    DataColumn(
+                                        label: Text("ID", style: estilo)),
+                                    DataColumn(
+                                        label: Text("NOMBRE", style: estilo)),
+                                    DataColumn(
+                                        label:
+                                            Text("CATEGORIA", style: estilo)),
+                                    DataColumn(
+                                        label: Text("PRECIO", style: estilo)),
+                                  ],
+                                  rows: registros,
+                                ),
+                              ),
+                            ),
+                          ),
+                        );
+                      }),
+                    ],
+                  ),
+                ),
+                const SizedBox(
+                  height: 15,
+                ),
+                if (!model.isLoading) ...[
+                  Row(
+                    mainAxisAlignment: MainAxisAlignment.center,
+                    children: [
+                      TextButton(
+                        onPressed:
+                            model.currentPage > 1 ? model.previousPage : null,
+                        child: Text('Anterior'),
+                        style: ButtonStyle(
+                          backgroundColor:
+                              MaterialStateProperty.resolveWith<Color?>(
+                            (Set<MaterialState> states) {
+                              if (states.contains(MaterialState.disabled)) {
+                                return Colors.grey;
+                              }
+                              return AppTheme.tertiary;
+                            },
+                          ),
+                          foregroundColor:
+                              MaterialStateProperty.resolveWith<Color?>(
+                            (Set<MaterialState> states) {
+                              if (states.contains(MaterialState.disabled)) {
+                                return Colors.black;
+                              }
+                              return Colors.white;
+                            },
+                          ),
+                        ),
+                      ),
+                      SizedBox(width: 15),
+                      Text(
+                          'Página ${model.currentPage} de ${model.totalPages}'),
+                      SizedBox(width: 15),
+                      TextButton(
+                        onPressed: model.currentPage < model.totalPages
+                            ? model.nextPage
+                            : null,
+                        child: Text('Siguiente'),
+                        style: ButtonStyle(
+                          backgroundColor:
+                              MaterialStateProperty.resolveWith<Color?>(
+                            (Set<MaterialState> states) {
+                              if (states.contains(MaterialState.disabled)) {
+                                return Colors.grey;
+                              }
+                              return AppTheme.tertiary;
+                            },
+                          ),
+                          foregroundColor:
+                              MaterialStateProperty.resolveWith<Color?>(
+                            (Set<MaterialState> states) {
+                              if (states.contains(MaterialState.disabled)) {
+                                return Colors.black;
+                              }
+                              return Colors.white;
+                            },
+                          ),
+                        ),
+                      ),
+                    ],
+                  ),
+                ],
+                const SizedBox(
+                  height: 15,
+                ),
+              ],
+            ),
+          ),
+        ],
+      ),
+      floatingActionButton: FloatingActionButton.extended(
+        onPressed: () async {
+          Producto nuevoProducto = Producto();
+          Navigator.push(
+            context,
+            MaterialPageRoute(
+              builder: (context) => ProductoForm(producto: nuevoProducto),
+            ),
+          ).then((_) => Provider.of<ProductoViewModel>(context, listen: false)
+              .fetchLocalAll());
+        },
+        icon: Icon(Icons.add, size: 30, color: AppTheme.quaternary),
+        label: Text(
+          "Agregar Producto",
+          style: TextStyle(fontSize: 18, color: AppTheme.quaternary),
+        ),
+        shape: RoundedRectangleBorder(
+          borderRadius: BorderRadius.circular(8),
+        ),
+        materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
+        backgroundColor: AppTheme.tertiary,
+      ),
+    );
+  }
 
-//         var _tipo = vuelta % 2;
-//         vuelta++;
-//         registros.add(DataRow(selected: _tipo > 0, cells: [
-//           DataCell(
-//             Text(item.nombre.toString()),
-//             onTap: () => go(item),
-//           ),
-//           DataCell(
-//             Text(item.descripcion.toString()),
-//             onTap: () => go(item),
-//           ),
-//           DataCell(
-//             Text(categoriaProducto.toString()),
-//             onTap: () => go(item),
-//           ),
-//           DataCell(
-//             Text(item.precio.toString()),
-//             onTap: () => go(item),
-//           ),
-//           DataCell(
-//             Text(item.venta.toString()),
-//             onTap: () => go(item),
-//           ),
-//           DataCell(
-//             Text(item.existencia.toString()),
-//             onTap: () => go(item),
-//           ),
-//           DataCell(
-//               Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
-//             PopupMenuButton(
-//               surfaceTintColor: AppTheme.primary,
-//               itemBuilder: (context) => [
-//                 PopupMenuItem(
-//                   child: const Text('Editar'),
-//                   onTap: () => go(item),
-//                 ),
-//                 PopupMenuItem(
-//                   child: const Text(
-//                     'Eliminar',
-//                   ),
-//                   onTap: () async {
-//                     return showDialog(
-//                       context: context,
-//                       builder: (context) {
-//                         return AlertDialog(
-//                           title: const Text("Eliminar registro"),
-//                           content: const Text('¿Desea eliminar el registro?'),
-//                           actions: [
-//                             Row(children: [
-//                               Expanded(
-//                                   child: TextButton(
-//                                 onPressed: () {
-//                                   Navigator.pop(context);
-//                                 },
-//                                 child: const Text('Cancelar'),
-//                               )),
-//                               Expanded(
-//                                   child: TextButton(
-//                                 onPressed: () async {
-//                                   Navigator.pop(context);
-//                                 },
-//                                 child: const Text('Continuar'),
-//                               ))
-//                             ])
-//                           ],
-//                         );
-//                       },
-//                     );
-//                   },
-//                 )
-//               ],
-//               icon: const Icon(Icons.more_vert),
-//               shape: RoundedRectangleBorder(
-//                   borderRadius: BorderRadius.circular(15)),
-//             )
-//           ]))
-//         ]));
-//       }
-//     }
+  Widget busquedaTextField() {
+    return Row(
+      children: [
+        Expanded(
+          flex: 3,
+          child: AppTextField(
+            prefixIcon: const Icon(Icons.search),
+            etiqueta: 'Búsqueda por nombre...',
+            controller: _busqueda,
+            hintText: 'Búsqueda por nombre...',
+          ),
+        ),
+        const SizedBox(width: 5),
+      ],
+    );
+  }
 
-//     return Scaffold(
-//       appBar: AppBar(
-//         title: const Text('Productos'),
-//       ),
-//       floatingActionButton: FloatingActionButton(
-//         onPressed: () {
-//           mvm.selectModelo(Producto());
-//           Navigator.push(
-//             context,
-//             MaterialPageRoute(
-//               builder: (context) => const ProductoForm(),
-//             ),
-//           ).then((value) async {
-//             await Provider.of<ProductoViewModel>(context, listen: false)
-//                 .fetchRegistros();
-//           });
-//         },
-//         child: const Icon(Icons.add),
-//       ),
-//       body: Column(
-//         children: [
-//           Expanded(
-//             child: ListView(
-//               padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
-//               children: [
-//                 const SizedBox(height: 8),
-//                 tarjeta(Padding(
-//                     padding: const EdgeInsets.all(10),
-//                     child: Row(
-//                       children: [
-//                         Expanded(
-//                           flex: 8,
-//                           child: AppTextField(
-//                             prefixIcon: const Icon(Icons.search),
-//                             maxLength: 100,
-//                             etiqueta: 'Búsqueda por nombre...',
-//                             controller: _busqueda,
-//                             hintText: 'Búsqueda por nombre...',
-//                           ),
-//                         ),
-//                         const SizedBox(width: 5),
-//                         Expanded(
-//                           flex: 2,
-//                           child: botonElevated(
-//                             accion: () async {
-//                               _busqueda.text = _busqueda.text.trim();
-//                               await Provider.of<ProductoViewModel>(context,
-//                                       listen: false)
-//                                   .setIsLoading(true);
-//                               await Provider.of<ProductoViewModel>(context,
-//                                       listen: false)
-//                                   .setBusqueda(_busqueda.text);
-//                               await Provider.of<ProductoViewModel>(context,
-//                                       listen: false)
-//                                   .fetchRegistros();
-//                               await Provider.of<ProductoViewModel>(context,
-//                                       listen: false)
-//                                   .setBusqueda("");
-//                               await Provider.of<ProductoViewModel>(context,
-//                                       listen: false)
-//                                   .setIsLoading(false);
-//                             },
-//                           ),
-//                         ),
-//                       ],
-//                     ))),
-//                 const SizedBox(height: 8),
-//                 tarjeta(DataTable(
-//                     sortAscending: true,
-//                     sortColumnIndex: 1,
-//                     columns: [
-//                       DataColumn(label: Text("NOMBRE", style: estilo)),
-//                       DataColumn(label: Text("DESCRIPCIÓN", style: estilo)),
-//                       DataColumn(
-//                           label: Text("CATEGORÍA PRODUCTOS", style: estilo)),
-//                       DataColumn(label: Text("PRECIO", style: estilo)),
-//                       DataColumn(label: Text("VENTAS", style: estilo)),
-//                       DataColumn(label: Text("EN EXISTENCIA", style: estilo)),
-//                       DataColumn(label: Text("", style: estilo)),
-//                     ],
-//                     rows: registros)),
-//                 PaginationButtons(
-//                   currentPage: mvm.pagina,
-//                   totalPages: mvm.totalPaginas,
-//                   onPageChanged: (i) => mvm.cambiarPagina(i),
-//                 )
-//               ],
-//             ),
-//           ),
-//         ],
-//       ),
-//     );
-//   }
-// }
+  Widget botonBuscar() {
+    return Expanded(
+        flex: 2,
+        child: Row(
+          children: [
+            Expanded(
+              flex: 2,
+              child: Padding(
+                padding: const EdgeInsets.only(top: 30),
+                child: ElevatedButton(
+                  onPressed: clearSearchAndReset,
+                  style: ElevatedButton.styleFrom(
+                    shape: RoundedRectangleBorder(
+                      borderRadius: BorderRadius.circular(20.0),
+                    ),
+                    primary: AppTheme.tertiary,
+                    padding: const EdgeInsets.symmetric(vertical: 25),
+                  ),
+                  child: Text('Limpiar',
+                      style: TextStyle(color: AppTheme.quaternary)),
+                ),
+              ),
+            ),
+            const SizedBox(width: 8),
+            Expanded(
+              flex: 2,
+              child: Padding(
+                padding: const EdgeInsets.only(top: 30),
+                child: ElevatedButton(
+                  onPressed: () async {
+                    if (_busqueda.text.isNotEmpty) {
+                      await Provider.of<ProductoViewModel>(context,
+                              listen: false)
+                          .fetchLocalByName(nombre: _busqueda.text.trim());
+                    } else {
+                      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
+                          content: Text('Introduce un nombre para buscar.')));
+                    }
+                  },
+                  style: ElevatedButton.styleFrom(
+                    shape: RoundedRectangleBorder(
+                      borderRadius: BorderRadius.circular(20.0),
+                    ),
+                    primary: AppTheme.tertiary,
+                    padding: const EdgeInsets.symmetric(vertical: 25),
+                  ),
+                  child: Text('Buscar',
+                      style: TextStyle(color: AppTheme.quaternary)),
+                ),
+              ),
+            ),
+          ],
+        ));
+  }
+}

+ 248 - 0
lib/views/venta/venta_screen.dart

@@ -0,0 +1,248 @@
+import 'package:yoshi_papas_app/themes/themes.dart';
+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 '../../widgets/widgets_components.dart';
+import '../../models/models.dart';
+import '../../viewmodels/viewmodels.dart';
+import 'venta_ticket.dart';
+
+class VentaScreen extends StatefulWidget {
+  @override
+  _VentaScreenState createState() => _VentaScreenState();
+}
+
+class _VentaScreenState extends State<VentaScreen> {
+  DateTime? fechaSeleccionada;
+  List<Pedido> pedidosNoCancelados = [];
+  List<Pedido> pedidosCancelados = [];
+  final _busqueda = TextEditingController(text: '');
+  double totalDelDia = 0.0;
+  double totalCancelados = 0.0;
+
+  String formatCurrency(double amount) {
+    final format = NumberFormat("#,##0.00", "es_MX");
+    return format.format(amount);
+  }
+
+  void clearSearchAndReset() {
+    setState(() {
+      _busqueda.clear();
+      fechaSeleccionada = null;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+          title: Text(
+            "Resumen de Pedidos por Día",
+            style: TextStyle(color: AppTheme.secondary),
+          ),
+          iconTheme: IconThemeData(color: AppTheme.secondary)),
+      body: Padding(
+        padding: const EdgeInsets.all(16.0),
+        child: Column(
+          children: [
+            Row(
+              children: [
+                Expanded(
+                    flex: 3,
+                    child: Padding(
+                      padding: const EdgeInsets.symmetric(horizontal: 0.0),
+                      child: tarjeta(
+                        ListTile(
+                            title: Text(
+                              "Fecha",
+                              style: TextStyle(
+                                  color: AppTheme.quaternary,
+                                  fontWeight: FontWeight.bold),
+                            ),
+                            subtitle: Text(
+                              fechaSeleccionada == null
+                                  ? ""
+                                  : DateFormat("dd/MM/yyyy")
+                                      .format(fechaSeleccionada!),
+                              style: TextStyle(
+                                  color: AppTheme.quaternary,
+                                  fontWeight: FontWeight.bold),
+                            ),
+                            trailing: Icon(Icons.calendar_month,
+                                color: AppTheme.quaternary),
+                            onTap: () async {
+                              DateTime? d = await showDatetimePicker(
+                                  context, fechaSeleccionada,
+                                  inicia: fechaSeleccionada,
+                                  tipo: OmniDateTimePickerType.date,
+                                  solofecha: true);
+                              if (d == null) return;
+                              setState(() {
+                                fechaSeleccionada = d;
+                              });
+                              cargarPedidos(fechaSeleccionada!);
+                            }),
+                        color: AppTheme.tertiary,
+                      ),
+                    )),
+                const SizedBox(
+                  width: 500,
+                ),
+                Expanded(
+                  flex: 2,
+                  child: Padding(
+                    padding: const EdgeInsets.only(top: 0),
+                    child: ElevatedButton(
+                      onPressed: clearSearchAndReset,
+                      style: ElevatedButton.styleFrom(
+                        shape: RoundedRectangleBorder(
+                          borderRadius: BorderRadius.circular(20.0),
+                        ),
+                        primary: AppTheme.tertiary,
+                        padding: const EdgeInsets.symmetric(vertical: 25),
+                      ),
+                      child: Text('Limpiar',
+                          style: TextStyle(
+                              color: AppTheme.quaternary, fontSize: 18)),
+                    ),
+                  ),
+                ),
+              ],
+            ),
+            Expanded(
+                child: tarjeta(
+              Padding(
+                padding: const EdgeInsets.all(8.0),
+                child: ListView.builder(
+                  itemCount:
+                      pedidosNoCancelados.length + pedidosCancelados.length,
+                  itemBuilder: (context, index) {
+                    if (index < pedidosNoCancelados.length) {
+                      final pedido = pedidosNoCancelados[index];
+                      return Row(
+                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                        children: [
+                          Text(
+                            "Folio: ${pedido.folio}",
+                            style: TextStyle(
+                                fontSize: 20, fontWeight: FontWeight.w500),
+                          ),
+                          Text(
+                              "Total: \$${formatCurrency(pedido.totalPedido ?? 0)}",
+                              style: TextStyle(
+                                  fontSize: 20, fontWeight: FontWeight.w500)),
+                        ],
+                      );
+                    } else {
+                      final pedido =
+                          pedidosCancelados[index - pedidosNoCancelados.length];
+                      return Row(
+                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                        children: [
+                          Text(
+                            "Folio: ${pedido.folio} (Cancelado)",
+                            style: TextStyle(
+                                fontSize: 20,
+                                fontWeight: FontWeight.w500,
+                                color: Colors.red),
+                          ),
+                          Text(
+                              "Total: \$${formatCurrency(pedido.totalPedido ?? 0)}",
+                              style: TextStyle(
+                                  fontSize: 20,
+                                  fontWeight: FontWeight.w500,
+                                  color: Colors.red)),
+                        ],
+                      );
+                    }
+                  },
+                ),
+              ),
+            )),
+            Row(
+              mainAxisAlignment: MainAxisAlignment.spaceBetween,
+              children: [
+                ElevatedButton.icon(
+                  icon: Icon(
+                    Icons.receipt_long_outlined,
+                    color: AppTheme.quaternary,
+                    size: 30,
+                  ),
+                  onPressed: pedidosNoCancelados.isNotEmpty ||
+                          pedidosCancelados.isNotEmpty
+                      ? () => imprimirResumenPedidos(
+                          pedidosNoCancelados + pedidosCancelados)
+                      : null,
+                  label: Text(
+                    "Imprimir Resumen",
+                    style: TextStyle(color: AppTheme.quaternary, fontSize: 18),
+                  ),
+                  style: ButtonStyle(
+                      backgroundColor:
+                          MaterialStatePropertyAll(AppTheme.tertiary),
+                      padding: MaterialStatePropertyAll(
+                          EdgeInsets.fromLTRB(30, 20, 30, 20))),
+                ),
+                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),
+                      ),
+                    ),
+                    if (totalCancelados > 0)
+                      Padding(
+                        padding: const EdgeInsets.all(16.0),
+                        child: Text(
+                          "Total cancelados: \$${formatCurrency(totalCancelados)}",
+                          style: TextStyle(
+                              fontSize: 20,
+                              fontWeight: FontWeight.bold,
+                              color: Colors.red),
+                        ),
+                      ),
+                  ],
+                ),
+              ],
+            )
+          ],
+        ),
+      ),
+    );
+  }
+
+  void cargarPedidos(DateTime fecha) async {
+    final inicioDelDia = DateTime(fecha.year, fecha.month, fecha.day);
+    final finDelDia = DateTime(fecha.year, fecha.month, fecha.day, 23, 59, 59);
+    final pedidos = await Provider.of<PedidoViewModel>(context, listen: false)
+        .buscarPorFecha(inicioDelDia, finDelDia);
+
+    final pedidosNoCancelados =
+        pedidos.where((p) => p.estatus != "CANCELADO").toList();
+    final pedidosCancelados =
+        pedidos.where((p) => p.estatus == "CANCELADO").toList();
+
+    totalDelDia = pedidosNoCancelados.fold(
+        0.0, (sum, current) => sum + (current.totalPedido ?? 0.0));
+    totalCancelados = pedidosCancelados.fold(
+        0.0, (sum, current) => sum + (current.totalPedido ?? 0.0));
+
+    setState(() {
+      this.pedidosNoCancelados = pedidosNoCancelados;
+      this.pedidosCancelados = pedidosCancelados;
+    });
+  }
+
+  Future<void> imprimirResumenPedidos(List<Pedido> pedidos) async {
+    if (fechaSeleccionada == null) {
+      print("No se ha seleccionado una fecha.");
+      return;
+    }
+    await VentaTicket.imprimirResumenPedidos(pedidos, fechaSeleccionada!);
+  }
+}

+ 134 - 0
lib/views/venta/venta_ticket.dart

@@ -0,0 +1,134 @@
+import 'package:intl/intl.dart';
+import 'package:pdf/pdf.dart';
+import 'package:pdf/widgets.dart' as pw;
+import 'package:printing/printing.dart';
+import '../../models/models.dart';
+import 'package:spelling_number/spelling_number.dart';
+
+String toTitleCase(String text) {
+  if (text.isEmpty) return text;
+  return text.split(' ').map((word) {
+    if (word.isNotEmpty) {
+      return word[0].toUpperCase() + word.substring(1).toLowerCase();
+    }
+    return '';
+  }).join(' ');
+}
+
+class VentaTicket {
+  static Future<void> imprimirResumenPedidos(
+      List<Pedido> pedidosDelDia, DateTime fecha) async {
+    final pdf = pw.Document();
+    String formattedDate = DateFormat('dd-MM-yyyy').format(fecha);
+
+    final pedidosNoCancelados =
+        pedidosDelDia.where((p) => p.estatus != "CANCELADO").toList();
+    final pedidosCancelados =
+        pedidosDelDia.where((p) => p.estatus == "CANCELADO").toList();
+
+    double totalNoCancelados =
+        pedidosNoCancelados.fold(0, (sum, p) => sum + (p.totalPedido ?? 0));
+    double totalCancelados =
+        pedidosCancelados.fold(0, (sum, p) => sum + (p.totalPedido ?? 0));
+
+    final spelling = SpellingNumber(lang: 'es');
+    String totalEnLetras = toTitleCase(spelling.convert(totalNoCancelados));
+
+    final numberFormat = NumberFormat('#,##0.00', 'en_US');
+    String formattedTotalNoCancelados = numberFormat.format(totalNoCancelados);
+    String formattedTotalCancelados = numberFormat.format(totalCancelados);
+
+    int centavos =
+        ((totalNoCancelados - totalNoCancelados.floor()) * 100).round();
+    String centavosEnLetras = centavos.toString().padLeft(2, '0') + "/100 M.N.";
+
+    print("Total en letras: $totalEnLetras $centavosEnLetras");
+    print("Total formateado: $formattedTotalNoCancelados");
+
+    pdf.addPage(pw.Page(
+        pageFormat: PdfPageFormat.roll57,
+        build: (pw.Context context) {
+          return pw.Column(
+            crossAxisAlignment: pw.CrossAxisAlignment.start,
+            children: [
+              pw.Padding(
+                padding: pw.EdgeInsets.only(bottom: 10),
+                child: pw.Text('Pedidos del día: $formattedDate',
+                    style: pw.TextStyle(
+                        fontWeight: pw.FontWeight.bold, fontSize: 12)),
+              ),
+              pw.Padding(
+                padding: const pw.EdgeInsets.only(right: 15),
+                child: pw.Text("Pedidos Completados:",
+                    style: pw.TextStyle(
+                        fontWeight: pw.FontWeight.bold, fontSize: 10)),
+              ),
+              ...pedidosNoCancelados.map((pedido) {
+                return pw.Padding(
+                  padding: const pw.EdgeInsets.only(right: 15),
+                  child: pw.Row(
+                    mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
+                    children: [
+                      pw.Text("Folio: ${pedido.folio}",
+                          style: pw.TextStyle(fontSize: 10)),
+                      pw.Text("\$${pedido.totalPedido?.toStringAsFixed(2)}",
+                          style: pw.TextStyle(fontSize: 10)),
+                    ],
+                  ),
+                );
+              }).toList(),
+              pw.Divider(),
+              pw.Padding(
+                padding: const pw.EdgeInsets.only(right: 15),
+                child: pw.Column(
+                  crossAxisAlignment: pw.CrossAxisAlignment.start,
+                  children: [
+                    pw.Text("Total: \$${formattedTotalNoCancelados}",
+                        style: pw.TextStyle(
+                            fontWeight: pw.FontWeight.bold, fontSize: 12)),
+                    pw.Text("Son: $totalEnLetras Pesos $centavosEnLetras",
+                        style: pw.TextStyle(fontSize: 10))
+                  ],
+                ),
+              ),
+              pw.SizedBox(height: 10),
+              if (pedidosCancelados.isNotEmpty) ...[
+                pw.Padding(
+                  padding: const pw.EdgeInsets.only(right: 15),
+                  child: pw.Text("Pedidos Cancelados:",
+                      style: pw.TextStyle(
+                          fontWeight: pw.FontWeight.bold, fontSize: 10)),
+                ),
+                ...pedidosCancelados.map((pedido) {
+                  return pw.Padding(
+                    padding: const pw.EdgeInsets.only(right: 15),
+                    child: pw.Row(
+                      mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
+                      children: [
+                        pw.Text("Folio: ${pedido.folio} (Cancelado)",
+                            style: pw.TextStyle(fontSize: 10)),
+                        pw.Text("\$${pedido.totalPedido?.toStringAsFixed(2)}",
+                            style: pw.TextStyle(fontSize: 10)),
+                      ],
+                    ),
+                  );
+                }).toList(),
+                pw.Divider(),
+                pw.Padding(
+                  padding: const pw.EdgeInsets.only(right: 15),
+                  child: pw.Text(
+                      "Total Cancelados: \$${formattedTotalCancelados}",
+                      style: pw.TextStyle(
+                          fontWeight: pw.FontWeight.bold, fontSize: 12)),
+                ),
+              ],
+              pw.SizedBox(height: 40),
+              pw.Text('.', style: pw.TextStyle(fontSize: 1)),
+            ],
+          );
+        }));
+
+    await Printing.layoutPdf(
+        onLayout: (PdfPageFormat format) async => pdf.save());
+  }
+}

+ 41 - 16
lib/widgets/app_drawer.dart

@@ -9,6 +9,7 @@ import 'package:yoshi_papas_app/views/pedido/pedido_screen.dart';
 import 'package:yoshi_papas_app/views/producto/producto_screen.dart';
 import 'package:yoshi_papas_app/views/toping/toping_screen.dart';
 import 'package:yoshi_papas_app/views/toping_categoria/toping_categoria_screen.dart';
+import 'package:yoshi_papas_app/views/venta/venta_screen.dart';
 import '../models/usuario_model.dart';
 import 'package:provider/provider.dart';
 import '../themes/themes.dart';
@@ -104,18 +105,6 @@ class AppDrawer extends StatelessWidget {
           //HEADER
           Expanded(
               child: ListView(children: [
-            // ListTile(
-            //   leading: circulo(const Icon(Icons.lunch_dining)),
-            //   title: const Text('Inicio'),
-            //   onTap: () => {
-            //     Navigator.pop(context),
-            //     Navigator.of(context).push(
-            //       MaterialPageRoute(
-            //         builder: (context) => const HomeScreen(),
-            //       ),
-            //     ),
-            //   },
-            // ),
             ListTile(
               leading: circulo(const Icon(Icons.restaurant_menu)),
               title: const Text('Pedidos'),
@@ -129,12 +118,48 @@ class AppDrawer extends StatelessWidget {
               },
             ),
             ListTile(
-              leading: const Icon(Icons.logout),
-              title: const Text('Cerrar sesión'),
-              onTap: () {
-                _showExitConfirmationDialog(context);
+              leading: circulo(const Icon(Icons.menu_book_rounded)),
+              title: const Text('Productos'),
+              onTap: () => {
+                Navigator.pop(context),
+                Navigator.of(context).push(
+                  MaterialPageRoute(
+                    builder: (context) => ProductoScreen(),
+                  ),
+                ),
+              },
+            ),
+            ListTile(
+              leading: circulo(const Icon(Icons.format_list_bulleted_rounded)),
+              title: const Text('Categoria Producto'),
+              onTap: () => {
+                Navigator.pop(context),
+                Navigator.of(context).push(
+                  MaterialPageRoute(
+                    builder: (context) => CategoriaProductoScreen(),
+                  ),
+                ),
+              },
+            ),
+            ListTile(
+              leading: circulo(const Icon(Icons.receipt_long_outlined)),
+              title: const Text('Pedidos Por Día'),
+              onTap: () => {
+                Navigator.pop(context),
+                Navigator.of(context).push(
+                  MaterialPageRoute(
+                    builder: (context) => VentaScreen(),
+                  ),
+                ),
               },
             ),
+            // ListTile(
+            //   leading: const Icon(Icons.logout),
+            //   title: const Text('Cerrar sesión'),
+            //   onTap: () {
+            //     _showExitConfirmationDialog(context);
+            //   },
+            // ),
           ]))
         ],
       ),

+ 42 - 21
lib/widgets/widgets_components.dart

@@ -127,14 +127,14 @@ boton(String etiqueta, Function()? accion,
               borderRadius: BorderRadius.circular(10),
             ),
           ),
-          backgroundColor: MaterialStatePropertyAll(AppTheme.primary),
+          backgroundColor: MaterialStatePropertyAll(AppTheme.tertiary),
         ),
         onPressed: accion,
         child: Row(
           mainAxisAlignment: MainAxisAlignment.center,
           children: [
             Text(etiqueta,
-                style: TextStyle(fontSize: 18, color: AppTheme.secondary)),
+                style: TextStyle(fontSize: 18, color: AppTheme.quaternary)),
           ],
         ),
       ),
@@ -179,38 +179,59 @@ Future<DateTime?> showDatetimePicker(BuildContext context, DateTime? fecha,
   if (termina == null) {
     termina = DateTime(2030);
   }
-  final f = await d.showDatePicker(
+  final f = await showDatePicker(
     context: context,
-    initialDate: fecha != null ? DateTime.now() : fecha,
-    firstDate: inicia == null ? DateTime(2023) : inicia, // DateTime(2023),
+    initialDate: fecha ?? DateTime.now(),
+    firstDate: inicia ?? DateTime(2023),
     lastDate: termina,
-    cancelText: "CANCELAR", confirmText: "ACEPTAR",
+    cancelText: "CANCELAR",
+    confirmText: "ACEPTAR",
+    builder: (context, child) {
+      return Theme(
+        data: Theme.of(context).copyWith(
+          colorScheme: ColorScheme.light(
+            primary: AppTheme.primary,
+            onSurface: AppTheme.secondary,
+          ),
+          textButtonTheme: TextButtonThemeData(
+            style: TextButton.styleFrom(
+              primary: AppTheme.secondary,
+            ),
+          ),
+        ),
+        child: child!,
+      );
+    },
   );
-  if (f == null) {
-    return fecha;
-  }
 
-  if (solofecha) {
+  if (f == null || solofecha) {
     return f;
   }
+
   final tiempo = await showTimePicker(
-    useRootNavigator: false,
+    context: context,
+    initialTime: TimeOfDay.fromDateTime(f),
     helpText: "CAPTURAR HORA",
     confirmText: "ACEPTAR",
     cancelText: "CANCELAR",
     hourLabelText: "HORA",
     minuteLabelText: "MINUTO",
     initialEntryMode: TimePickerEntryMode.input,
-    context: context,
-    initialTime: TimeOfDay.fromDateTime(f),
-    builder: (context, childs) {
-      return MediaQuery(
-          data: MediaQuery.of(context).copyWith(
-              alwaysUse24HourFormat: true,
-              padding: const EdgeInsets.all(1),
-              size: const Size.square(1),
-              textScaler: const TextScaler.linear(.8)),
-          child: childs!);
+    builder: (context, child) {
+      return Theme(
+        data: Theme.of(context).copyWith(
+          colorScheme: ColorScheme.light(
+            primary: AppTheme.primary,
+            onSurface: AppTheme.secondary,
+          ),
+          textButtonTheme: TextButtonThemeData(
+            style: TextButton.styleFrom(
+              primary: AppTheme.secondary,
+            ),
+          ),
+        ),
+        child: child!,
+      );
     },
   );
 

+ 249 - 9
pubspec.lock

@@ -1,6 +1,22 @@
 # Generated by pub
 # See https://dart.dev/tools/pub/glossary#lockfile
 packages:
+  archive:
+    dependency: transitive
+    description:
+      name: archive
+      sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.4.10"
+  args:
+    dependency: transitive
+    description:
+      name: args
+      sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.5.0"
   assets_audio_player:
     dependency: "direct main"
     description:
@@ -25,6 +41,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.11.0"
+  barcode:
+    dependency: transitive
+    description:
+      name: barcode
+      sha256: "1fe4a55344505850517ce72d4a3a7b9ccf51b0dc1631ee7e552f6eacc4947f96"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.7"
+  bidi:
+    dependency: transitive
+    description:
+      name: bidi
+      sha256: "1a7d0c696324b2089f72e7671fd1f1f64fef44c980f3cebc84e803967c597b63"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.10"
   boolean_selector:
     dependency: transitive
     description:
@@ -89,6 +121,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.3.1"
+  checked_yaml:
+    dependency: transitive
+    description:
+      name: checked_yaml
+      sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.3"
+  cli_util:
+    dependency: transitive
+    description:
+      name: cli_util
+      sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.4.1"
   clock:
     dependency: transitive
     description:
@@ -98,13 +146,21 @@ packages:
     source: hosted
     version: "1.1.1"
   collection:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: collection
       sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
       url: "https://pub.dev"
     source: hosted
     version: "1.18.0"
+  convert:
+    dependency: transitive
+    description:
+      name: convert
+      sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.1"
   cross_file:
     dependency: transitive
     description:
@@ -129,6 +185,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.0.0"
+  csv:
+    dependency: "direct main"
+    description:
+      name: csv
+      sha256: c6aa2679b2a18cb57652920f674488d89712efaf4d3fdf2e537215b35fc19d6c
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.0.0"
   cupertino_icons:
     dependency: "direct main"
     description:
@@ -153,6 +217,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "5.0.6"
+  equatable:
+    dependency: transitive
+    description:
+      name: equatable
+      sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.5"
+  excel:
+    dependency: "direct main"
+    description:
+      name: excel
+      sha256: c299b5ccf14bb7f24fe64ca1a15e4a3796ab563575087ce3f7e07fc2e3429db5
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.0.2"
   fake_async:
     dependency: transitive
     description:
@@ -177,19 +257,35 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "7.0.0"
+  file_picker:
+    dependency: "direct main"
+    description:
+      name: file_picker
+      sha256: d1d0ac3966b36dc3e66eeefb40280c17feb87fa2099c6e22e6a1fc959327bd03
+      url: "https://pub.dev"
+    source: hosted
+    version: "8.0.0+1"
+  file_picker_writable:
+    dependency: "direct main"
+    description:
+      name: file_picker_writable
+      sha256: bb7ad0ec8b7f0cc1df668538114319a5bcd6be506dce60a54b87ad64fbdbb6ab
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.0+1"
   flutter:
     dependency: "direct main"
     description: flutter
     source: sdk
     version: "0.0.0"
-  flutter_lints:
+  flutter_launcher_icons:
     dependency: "direct dev"
     description:
-      name: flutter_lints
-      sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
+      name: flutter_launcher_icons
+      sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.3"
+    version: "0.13.1"
   flutter_plugin_android_lifecycle:
     dependency: transitive
     description:
@@ -256,6 +352,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "4.0.2"
+  image:
+    dependency: "direct main"
+    description:
+      name: image
+      sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.1.7"
   intl:
     dependency: "direct main"
     description:
@@ -272,14 +376,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.6.7"
-  lints:
+  json_annotation:
     dependency: transitive
     description:
-      name: lints
-      sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
+      name: json_annotation
+      sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.1"
+    version: "4.9.0"
   logger:
     dependency: transitive
     description:
@@ -288,6 +392,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.4.0"
+  logging:
+    dependency: transitive
+    description:
+      name: logging
+      sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.0"
   matcher:
     dependency: transitive
     description:
@@ -328,6 +440,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.0.9"
+  open_file:
+    dependency: "direct main"
+    description:
+      name: open_file
+      sha256: a5a32d44acb7c899987d0999e1e3cbb0a0f1adebbf41ac813ec6d2d8faa0af20
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.3.2"
   path:
     dependency: transitive
     description:
@@ -336,6 +456,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.8.3"
+  path_parsing:
+    dependency: transitive
+    description:
+      name: path_parsing
+      sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.1"
   path_provider:
     dependency: "direct main"
     description:
@@ -384,6 +512,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.2.1"
+  pdf:
+    dependency: "direct main"
+    description:
+      name: pdf
+      sha256: "243f05342fc0bdf140eba5b069398985cdbdd3dbb1d776cf43d5ea29cc570ba6"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.10.8"
+  pdf_widget_wrapper:
+    dependency: transitive
+    description:
+      name: pdf_widget_wrapper
+      sha256: e9d31fd7782ce28ae346b127ea7d1cd748d799bddee379f31191693610e23749
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.1"
   permission_handler:
     dependency: "direct main"
     description:
@@ -432,6 +576,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.2.1"
+  petitparser:
+    dependency: transitive
+    description:
+      name: petitparser
+      sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.0.2"
   platform:
     dependency: transitive
     description:
@@ -448,6 +600,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.8"
+  pointycastle:
+    dependency: transitive
+    description:
+      name: pointycastle
+      sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.7.4"
+  printing:
+    dependency: "direct main"
+    description:
+      name: printing
+      sha256: "1c99cab90ebcc1fff65831d264627d5b529359d563e53f33ab9b8117f2d280bc"
+      url: "https://pub.dev"
+    source: hosted
+    version: "5.12.0"
   provider:
     dependency: "direct main"
     description:
@@ -456,6 +624,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "6.1.2"
+  qr:
+    dependency: transitive
+    description:
+      name: qr
+      sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.1"
+  quiver:
+    dependency: transitive
+    description:
+      name: quiver
+      sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.2.1"
   recase:
     dependency: transitive
     description:
@@ -541,6 +725,46 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.10.0"
+  spelling_number:
+    dependency: "direct main"
+    description:
+      name: spelling_number
+      sha256: "86076b2726abd51d7e63fcbd5193672d0b177adbf9f49df128d38c51be57adee"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.0.4"
+  sqflite:
+    dependency: "direct main"
+    description:
+      name: sqflite
+      sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.2"
+  sqflite_common:
+    dependency: transitive
+    description:
+      name: sqflite_common
+      sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.5.3"
+  sqflite_common_ffi:
+    dependency: "direct main"
+    description:
+      name: sqflite_common_ffi
+      sha256: "754927d82de369a6b9e760fb60640aa81da650f35ffd468d5a992814d6022908"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.2+1"
+  sqlite3:
+    dependency: transitive
+    description:
+      name: sqlite3
+      sha256: "072128763f1547e3e9b4735ce846bfd226d68019ccda54db4cd427b12dfdedc9"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.0"
   stack_trace:
     dependency: transitive
     description:
@@ -733,6 +957,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.0.4"
+  xml:
+    dependency: transitive
+    description:
+      name: xml
+      sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.5.0"
+  yaml:
+    dependency: transitive
+    description:
+      name: yaml
+      sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.2"
 sdks:
   dart: ">=3.2.3 <4.0.0"
   flutter: ">=3.16.0"

+ 31 - 1
pubspec.yaml

@@ -50,17 +50,46 @@ dependencies:
   url_launcher: ^6.2.5
   timezone: ^0.9.2
   provider: ^6.1.2
+  image: ^4.1.7
+  file_picker: ^8.0.0+1
+  collection: ^1.18.0
+  sqflite: ^2.3.2
+  pdf: ^3.10.8
+  printing: ^5.12.0
+  sqflite_common_ffi: ^2.3.2+1
+  open_file: ^3.3.2
+  excel: ^4.0.2
+  csv: ^6.0.0
+  spelling_number: ^0.0.4
+  file_picker_writable: ^2.1.0+1
 
 dev_dependencies:
   flutter_test:
     sdk: flutter
+  flutter_launcher_icons: "^0.13.1"
+
+flutter_launcher_icons:
+    android: "launcher_icon"
+    ios: true
+    image_path: "assets/JoshiLogo.png"
+    min_sdk_android: 21 # android min sdk min:16, default 21
+    web:
+      generate: true
+      image_path: "assets/JoshiLogo.png"
+    windows:
+      generate: true
+      image_path: "assets/JoshiLogo.png"
+      icon_size: 256
+    macos:
+      generate: true
+      image_path: "assets/JoshiLogo.png"
 
   # The "flutter_lints" package below contains a set of recommended lints to
   # encourage good coding practices. The lint set provided by the package is
   # activated in the `analysis_options.yaml` file located at the root of your
   # package. See that file for information about deactivating specific lint
   # rules and activating additional ones.
-  flutter_lints: ^2.0.0
+flutter_lints: ^2.0.0
 
 # For information on the generic Dart part of this file, see the
 # following page: https://dart.dev/tools/pub/pubspec
@@ -77,6 +106,7 @@ flutter:
   assets:
     - assets/JoshiLogoHorizontal.png
     - assets/JoshiLogo.png
+    - assets/JoshiLogo-BN.png
 
   # An image asset can refer to one or more resolution-specific "variants", see
   # https://flutter.dev/assets-and-images/#resolution-aware