Selaa lähdekoodia

Actualizacion sincronizacion usuarios, sucursales, permisos y modulo corte caja

OscarGil03 7 kuukautta sitten
vanhempi
commit
458d6e33c1
39 muutettua tiedostoa jossa 4531 lisäystä ja 966 poistoa
  1. 6 1
      lib/data/session/session_storage.dart
  2. 3 1
      lib/main.dart
  3. 93 19
      lib/models/corte_caja_model.dart
  4. 58 9
      lib/models/deposito_model.dart
  5. 58 9
      lib/models/gasto_model.dart
  6. 4 0
      lib/models/models.dart
  7. 14 1
      lib/models/pedido_producto_toping_model.dart
  8. 76 0
      lib/models/permiso_model.dart
  9. 28 14
      lib/models/producto_model.dart
  10. 17 0
      lib/models/producto_topping_model.dart
  11. 80 0
      lib/models/retiro_model.dart
  12. 79 0
      lib/models/sucursal_model.dart
  13. 142 18
      lib/models/usuario_model.dart
  14. 9 0
      lib/services/base_service.dart
  15. 739 65
      lib/services/repo_service.dart
  16. 1 1
      lib/viewmodels/categoria_producto_view_model.dart
  17. 350 34
      lib/viewmodels/corte_caja_view_model.dart
  18. 51 44
      lib/viewmodels/login_view_model.dart
  19. 12 9
      lib/viewmodels/pedido_view_model.dart
  20. 99 0
      lib/viewmodels/permiso_view_model.dart
  21. 86 9
      lib/viewmodels/producto_view_model.dart
  22. 77 0
      lib/viewmodels/sucursal_view_model.dart
  23. 41 133
      lib/viewmodels/usuarios_view_model.dart
  24. 2 0
      lib/viewmodels/viewmodels.dart
  25. 211 0
      lib/views/corte_caja/corte_caja_finalizado_screen.dart
  26. 507 0
      lib/views/corte_caja/corte_caja_form copy.dart
  27. 938 321
      lib/views/corte_caja/corte_caja_form.dart
  28. 58 64
      lib/views/corte_caja/corte_caja_screen.dart
  29. 65 0
      lib/views/corte_caja/corte_caja_ticket.dart
  30. 6 0
      lib/views/home/home_screen.dart
  31. 16 8
      lib/views/pedido/pedido_screen.dart
  32. 4 2
      lib/views/producto/producto_form.dart
  33. 32 4
      lib/views/producto/producto_imagen.dart
  34. 52 0
      lib/views/producto/producto_screen.dart
  35. 118 0
      lib/views/sucursal/sucursal_screen.dart
  36. 52 46
      lib/views/venta/venta_screen.dart
  37. 226 122
      lib/widgets/app_drawer.dart
  38. 66 29
      lib/widgets/app_textfield.dart
  39. 55 3
      lib/widgets/widgets_components.dart

+ 6 - 1
lib/data/session/session_storage.dart

@@ -14,7 +14,7 @@ class SessionStorage {
     final preferences = await _getPreferences();
     await preferences.setInt('id', id);
   }
-  
+
   Future<void> saveCorreo(String value) async {
     final preferences = await _getPreferences();
     await preferences.setString('correo', value);
@@ -59,4 +59,9 @@ class SessionStorage {
     final preferences = await _getPreferences();
     await preferences.remove('token');
   }
+
+  Future<void> clearId() async {
+    final preferences = await _getPreferences();
+    await preferences.remove('id');
+  }
 }

+ 3 - 1
lib/main.dart

@@ -39,7 +39,7 @@ void main() async {
   ]).then((_) {
     runApp(MultiProvider(providers: [
       ChangeNotifierProvider(create: (_) => LoginViewModel()),
-      ChangeNotifierProvider(create: (_) => UsuariosViewModel()),
+      ChangeNotifierProvider(create: (_) => UsuarioViewModel()),
       ChangeNotifierProvider(create: (_) => ProfileViewModel()),
       ChangeNotifierProvider(create: (_) => CategoriaProductoViewModel()),
       ChangeNotifierProvider(create: (_) => ProductoViewModel()),
@@ -48,6 +48,8 @@ void main() async {
       ChangeNotifierProvider(create: (_) => CorteCajaViewModel()),
       ChangeNotifierProvider(create: (_) => DescuentoViewModel()),
       ChangeNotifierProvider(create: (_) => VariableViewModel()),
+      ChangeNotifierProvider(create: (_) => SucursalViewModel()),
+      ChangeNotifierProvider(create: (_) => PermisoViewModel()),
       // Agrega aquí cualquier otro provider que necesites
     ], child: const MyApp()));
   });

+ 93 - 19
lib/models/corte_caja_model.dart

@@ -1,41 +1,115 @@
 import 'basico_model.dart';
 import '../services/services.dart';
 
-class CorteCaja extends Basico {
+class CorteCaja {
+  String? id;
+  DateTime? fechaApertura;
+  DateTime? fechaCorte;
+  int? idUsuario;
   double? fondo;
-  double? fondo_ds;
-  double? ventas;
-  String? fecha;
+  int? idSucursal;
+  double? fondoDiaSig;
+  double? ventaPuntos;
+  double? ventaEfe;
+  double? ventaTrans;
+  double? ventaTarj;
+  double? gasto;
+  double? retiro;
+  double? deposito;
+  double? corteFinal;
+  DateTime? creado;
+  DateTime? eliminado;
+  DateTime? modificado;
 
   CorteCaja({
-    super.id,
+    this.id,
+    this.fechaApertura,
+    this.fechaCorte,
+    this.idUsuario,
+    this.idSucursal,
     this.fondo,
-    this.fondo_ds,
-    this.ventas,
-    this.fecha,
+    this.fondoDiaSig,
+    this.ventaPuntos,
+    this.ventaEfe,
+    this.ventaTrans,
+    this.ventaTarj,
+    this.gasto,
+    this.retiro,
+    this.deposito,
+    this.corteFinal,
+    this.creado,
+    this.modificado,
+    this.eliminado,
   });
 
-  @override
   Map<String, dynamic> toJson() {
     return {
       'id': id,
+      'fechaApertura': fechaApertura?.toIso8601String(),
+      'fechaCorte': fechaCorte?.toIso8601String(),
+      'idUsuario': idUsuario ?? 0,
+      'idSucursal': idSucursal ?? 0,
+      'fondo': fondo ?? 0.0,
+      'fondoDiaSig': fondoDiaSig ?? 0.0,
+      'ventaPuntos': ventaPuntos ?? 0.0,
+      'ventaEfe': ventaEfe ?? 0.0,
+      'ventaTrans': ventaTrans ?? 0.0,
+      'ventaTarj': ventaTarj ?? 0.0,
+      'gasto': gasto ?? 0.0,
+      'retiro': retiro ?? 0.0,
+      'deposito': deposito ?? 0.0,
+      'corteFinal': corteFinal ?? 0.0,
+      'creado': creado?.toIso8601String(),
+      'modificado': modificado?.toIso8601String(),
+      'eliminado': eliminado?.toIso8601String(),
+    };
+  }
+
+  Map<String, dynamic> toApi() {
+    return {
+      'id': id,
+      'fechaApertura': fechaApertura,
+      'fechaCorte': fechaCorte,
+      'idUsuario': idUsuario,
+      'idSucursal': idSucursal,
       'fondo': fondo,
-      'fondo_ds': fondo_ds,
-      'ventas': ventas,
-      'fecha': fecha,
-    }..addAll(super.toJson());
+      'fondoDiaSig': fondoDiaSig,
+      'ventaPuntos': ventaPuntos,
+      'ventaEfe': ventaEfe,
+      'ventaTrans': ventaTrans,
+      'ventaTarj': ventaTarj,
+      'gasto': gasto,
+      'retiro': retiro,
+      'deposito': deposito,
+      'corteFinal': corteFinal,
+      'creado': creado,
+      'modificado': modificado,
+      'eliminado': eliminado,
+    };
   }
 
   CorteCaja.fromJson(Map<String, dynamic> json) {
-    super.parseJson(json);
+    id = Basico.parseString(json['id']);
+    fechaApertura = Basico.parseDate(json['fechaApertura']);
+    fechaCorte = Basico.parseDate(json['fechaCorte']);
+    idUsuario = Basico.parseInt(json['idUsuario']);
+    idSucursal = Basico.parseInt(json['idSucursal']);
     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']);
+    fondoDiaSig = Basico.parseDouble(json['fondoDiaSig']);
+    ventaPuntos = Basico.parseDouble(json['ventaPuntos']);
+    ventaEfe = Basico.parseDouble(json['ventaEfe']);
+    ventaTrans = Basico.parseDouble(json['ventaTrans']);
+    ventaTarj = Basico.parseDouble(json['ventaTarj']);
+    gasto = Basico.parseDouble(json['gasto']);
+    retiro = Basico.parseDouble(json['retiro']);
+    deposito = Basico.parseDouble(json['deposito']);
+    corteFinal = Basico.parseDouble(json['corteFinal']);
+    creado = Basico.parseDate(json['creado']);
+    eliminado = Basico.parseDate(json['eliminado']);
+    modificado = Basico.parseDate(json['modificado']);
   }
 
   Future<void> guardar() async {
-    idLocal = await RepoService().guardar(this);
+    await RepoService().guardar(this);
   }
 }

+ 58 - 9
lib/models/deposito_model.dart

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

+ 58 - 9
lib/models/gasto_model.dart

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

+ 4 - 0
lib/models/models.dart

@@ -14,3 +14,7 @@ export '../models/deposito_model.dart';
 export '../models/corte_caja_model.dart';
 export '../models/descuento_model.dart';
 export '../models/variable_model.dart';
+export '../models/sucursal_model.dart';
+export '../models/producto_topping_model.dart';
+export '../models/permiso_model.dart';
+export '../models/retiro_model.dart';

+ 14 - 1
lib/models/pedido_producto_toping_model.dart

@@ -1,15 +1,17 @@
+import '/models/producto_model.dart';
 import 'basico_model.dart';
-import 'producto_model.dart';
 
 class PedidoProductoTopping extends Basico {
   int? idPedidoProducto;
   int? idTopping;
+  int? idCategoria;
   Producto? topping;
 
   PedidoProductoTopping({
     super.id,
     this.idPedidoProducto,
     this.idTopping,
+    this.idCategoria,
     this.topping,
   });
 
@@ -19,12 +21,23 @@ class PedidoProductoTopping extends Basico {
       'id': id,
       'idPedidoProducto': idPedidoProducto,
       'idTopping': idTopping,
+      'idCategoria': idCategoria,
     }..addAll(super.toJson());
   }
 
+  Map<String, dynamic> toApi() {
+    return {
+      'idTopping': idTopping,
+      'idCategoria': idCategoria,
+      'nombreTopping': topping?.nombre ?? '',
+      'precio': topping?.precio ?? 0.0,
+    };
+  }
+
   PedidoProductoTopping.fromJson(Map<String, dynamic> json) {
     super.parseJson(json);
     idPedidoProducto = Basico.parseInt(json['idPedidoProducto']);
     idTopping = Basico.parseInt(json['idTopping']);
+    idCategoria = Basico.parseInt(json['idCategoria']);
   }
 }

+ 76 - 0
lib/models/permiso_model.dart

@@ -0,0 +1,76 @@
+import '/models/models.dart';
+
+import '../services/services.dart';
+import 'basico_model.dart';
+
+class Permiso {
+  String? id;
+  String? idModulo;
+  String? nombre;
+  String? descripcion;
+  DateTime? eliminado;
+  DateTime? creado;
+  DateTime? modificado;
+
+  Permiso(
+      {this.id,
+      this.idModulo,
+      this.nombre,
+      this.descripcion,
+      this.eliminado,
+      this.creado,
+      this.modificado});
+
+  @override
+  Map<String, dynamic> toJson() {
+    return {
+      'id': id,
+      'idModulo': idModulo ?? '',
+      'nombre': nombre ?? '',
+      'descripcion': descripcion ?? '',
+      'creado': creado?.toIso8601String(),
+      'modificado': modificado?.toIso8601String(),
+      'eliminado': eliminado?.toIso8601String(),
+    };
+  }
+
+  Map<String, dynamic> toMap() {
+    return {
+      'id': id,
+      'idModulo': idModulo,
+      'nombre': nombre,
+      'descripcion': descripcion,
+      'creado': creado != null ? creado!.toIso8601String() : null,
+      'modificado': modificado != null ? modificado!.toIso8601String() : null,
+      'eliminado': eliminado != null ? eliminado!.toIso8601String() : null,
+    };
+  }
+
+  Permiso.fromJson(Map<String, dynamic> json) {
+    id = Basico.parseString(json['id']);
+    idModulo = Basico.parseString(json['idModulo']);
+    nombre = Basico.parseString(json['nombre']);
+    descripcion = Basico.parseString(json['descripcion']);
+    creado = json['creado'] != null ? Basico.parseDate(json['creado']) : creado;
+    eliminado = json['eliminado'] != null
+        ? Basico.parseDate(json['eliminado'])
+        : eliminado;
+    modificado = json['modificado'] != null
+        ? Basico.parseDate(json['modificado'])
+        : modificado;
+  }
+
+  Permiso.fromApi(Map<String, dynamic> json) {
+    id = Basico.parseString(json['id']);
+    idModulo = Basico.parseString(json['idModulo']);
+    nombre = Basico.parseString(json['nombre']);
+    descripcion = Basico.parseString(json['descripcion']);
+    creado = Basico.parseDate(json['creado']);
+    modificado = Basico.parseDate(json['modificado']);
+    eliminado = Basico.parseDate(json['eliminado']);
+  }
+
+  Future<void> guardar() async {
+    await RepoService().guardar(this);
+  }
+}

+ 28 - 14
lib/models/producto_model.dart

@@ -1,6 +1,7 @@
+import '/models/models.dart';
+
 import '../services/services.dart';
 import 'basico_model.dart';
-import 'categoria_producto_model.dart';
 
 class Producto extends Basico {
   int? idCategoria;
@@ -16,6 +17,8 @@ class Producto extends Basico {
   String? descuento;
   int? toping;
   List<Producto>? topings;
+  int? activo;
+  List<Media>? media;
 
   Producto({
     super.id,
@@ -31,18 +34,20 @@ class Producto extends Basico {
     this.descuento,
     this.toping,
     this.topings,
+    this.activo,
+    this.media,
   });
 
   @override
   Map<String, dynamic> toJson() {
-    print("Convirtiendo Producto a JSON");
-    print("ID: $id, Categoria: $idCategoria, Nombre: $nombre");
-    print("Descripcion: $descripcion, imagen: $imagen, venta: $venta");
-    print("existencia: $existencia, precio: $precio, verMenu: $verMenu");
-    print("codigo: $codigo, descuento: $descuento, creado: $creado");
-    print("eliminado: $eliminado, modificado: $modificado");
+    // print("Convirtiendo Producto a JSON");
+    // print("ID: $id, Categoria: $idCategoria, Nombre: $nombre");
+    // print("Descripcion: $descripcion, imagen: $imagen, venta: $venta");
+    // print("existencia: $existencia, precio: $precio, verMenu: $verMenu");
+    // print("codigo: $codigo, descuento: $descuento, creado: $creado");
+    // print("eliminado: $eliminado, modificado: $modificado");
 
-    return {
+    Map<String, dynamic> data = {
       'id': id,
       'idCategoria': idCategoria ?? 0,
       'nombre': nombre ?? '',
@@ -55,10 +60,13 @@ class Producto extends Basico {
       'codigo': codigo ?? '',
       'descuento': descuento ?? '',
       'toping': toping ?? 0,
-      'creado': creado != null ? creado!.toIso8601String() : null,
-      'modificado': modificado != null ? modificado!.toIso8601String() : null,
-      'eliminado': eliminado != null ? eliminado!.toIso8601String() : null,
-    }..addAll(super.toJson());
+      'activo': activo ?? 0,
+      'creado': creado?.toIso8601String(),
+      'modificado': modificado?.toIso8601String(),
+      'eliminado': eliminado?.toIso8601String(),
+    };
+
+    return data..addAll(super.toJson());
   }
 
   Map<String, dynamic> toMap() {
@@ -75,6 +83,7 @@ class Producto extends Basico {
       'codigo': codigo,
       'descuento': descuento,
       'toping': toping,
+      'activo': activo,
       'creado': creado != null ? creado!.toIso8601String() : null,
       'modificado': modificado != null ? modificado!.toIso8601String() : null,
       'eliminado': eliminado != null ? eliminado!.toIso8601String() : null,
@@ -96,6 +105,7 @@ class Producto extends Basico {
     verMenu = Basico.parseInt(json['verMenu']);
     codigo = Basico.parseString(json['codigo']);
     descuento = Basico.parseString(json['descuento']);
+    activo = Basico.parseInt(json['activo']);
     if (json['toping'] is bool) {
       toping = json['toping'] ? 1 : 0;
     } else {
@@ -120,9 +130,13 @@ class Producto extends Basico {
     verMenu = Basico.parseInt(json['verEnMenu']);
     codigo = Basico.parseString(json['codigo']);
     descuento = Basico.parseString(json['descuento']);
-    creado = Basico.parseDate(json['fechaCreado']);
-    modificado = Basico.parseDate(json['fechaModificado']);
+    activo = Basico.parseInt(json['activo']);
+    creado = Basico.parseDate(json['creado']);
+    modificado = Basico.parseDate(json['modificado']);
     eliminado = Basico.parseDate(json['eliminado']);
+    if (json['media'] != null) {
+      media = (json['media'] as List).map((i) => Media.fromJson(i)).toList();
+    }
   }
 
   Future<void> guardar() async {

+ 17 - 0
lib/models/producto_topping_model.dart

@@ -3,11 +3,13 @@ import 'basico_model.dart';
 class ProductoTopping extends Basico {
   int? idProducto;
   int? idTopping;
+  int? idCategoria;
 
   ProductoTopping({
     super.id,
     this.idProducto,
     this.idTopping,
+    this.idCategoria,
   });
 
   @override
@@ -16,6 +18,10 @@ class ProductoTopping extends Basico {
       'id': id,
       'idProducto': idProducto,
       'idTopping': idTopping,
+      'idCategoria': idCategoria,
+      'creado': creado?.toIso8601String(),
+      'modificado': modificado?.toIso8601String(),
+      'eliminado': eliminado?.toIso8601String(),
     }..addAll(super.toJson());
   }
 
@@ -23,5 +29,16 @@ class ProductoTopping extends Basico {
     super.parseJson(json);
     idProducto = Basico.parseInt(json['idProducto']);
     idTopping = Basico.parseInt(json['idTopping']);
+    idCategoria = Basico.parseInt(json['idCategoria']);
+  }
+
+  ProductoTopping.fromApi(Map<String, dynamic> json) {
+    super.parseJson(json);
+    idProducto = Basico.parseInt(json['idProducto']);
+    idTopping = Basico.parseInt(json['idProductoTopping']);
+    idCategoria = Basico.parseInt(json['idCategoriaTopping']);
+    creado = Basico.parseDate(json['creado']);
+    modificado = Basico.parseDate(json['modificado']);
+    eliminado = Basico.parseDate(json['eliminado']);
   }
 }

+ 80 - 0
lib/models/retiro_model.dart

@@ -0,0 +1,80 @@
+import 'basico_model.dart';
+import '../services/services.dart';
+
+class Retiro {
+  String? id;
+  String? idCorteCaja;
+  int? idSucursal;
+  int? idUsuario;
+  DateTime? fechaRetiro;
+  double? monto;
+  String? persona;
+  String? descripcion;
+  DateTime? creado;
+  DateTime? modificado;
+  DateTime? eliminado;
+
+  Retiro({
+    this.id,
+    this.idCorteCaja,
+    this.idSucursal,
+    this.idUsuario,
+    this.fechaRetiro,
+    this.monto,
+    this.persona,
+    this.descripcion,
+    this.creado,
+    this.modificado,
+    this.eliminado,
+  });
+
+  Map<String, dynamic> toJson() {
+    return {
+      'id': id,
+      'idCorteCaja': idCorteCaja ?? 0,
+      'idSucursal': idSucursal ?? 0,
+      'idUsuario': idUsuario ?? 0,
+      'fechaRetiro': fechaRetiro?.toIso8601String(),
+      'monto': monto ?? 0.0,
+      'persona': persona ?? '',
+      'descripcion': descripcion ?? '',
+      'creado': creado?.toIso8601String(),
+      'modificado': modificado?.toIso8601String(),
+      'eliminado': eliminado?.toIso8601String(),
+    };
+  }
+
+  Map<String, dynamic> toApi() {
+    return {
+      'id': id,
+      'idCorteCaja': idCorteCaja,
+      'idSucursal': idSucursal,
+      'idUsuario': idUsuario,
+      'fechaRetiro': fechaRetiro,
+      'monto': monto,
+      'persona': persona,
+      'descripcion': descripcion,
+      'creado': creado,
+      'modificado': modificado,
+      'eliminado': eliminado,
+    };
+  }
+
+  Retiro.fromJson(Map<String, dynamic> json) {
+    id = Basico.parseString(json['id']);
+    idCorteCaja = Basico.parseString(json['idCorteCaja']);
+    idSucursal = Basico.parseInt(json['idSucursal']);
+    idUsuario = Basico.parseInt(json['idUsuario']);
+    fechaRetiro = Basico.parseDate(json['fechaRetiro']);
+    monto = Basico.parseDouble(json['monto']);
+    persona = Basico.parseString(json['persona']);
+    descripcion = Basico.parseString(json['descripcion']);
+    creado = Basico.parseDate(json['creado']);
+    modificado = Basico.parseDate(json['modificado']);
+    eliminado = Basico.parseDate(json['eliminado']);
+  }
+
+  Future<void> guardar() async {
+    await RepoService().guardar(this);
+  }
+}

+ 79 - 0
lib/models/sucursal_model.dart

@@ -0,0 +1,79 @@
+import 'basico_model.dart';
+import '../services/services.dart';
+
+class Sucursal extends Basico {
+  String? nombre;
+  String? descripcion;
+  String? direccion;
+  String? ciudad;
+  String? clave;
+  int? activo;
+  int? seleccionado;
+
+  Sucursal({
+    super.id,
+    this.nombre,
+    this.descripcion,
+    this.direccion,
+    this.ciudad,
+    this.clave,
+    this.activo,
+    this.seleccionado,
+  });
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+    return other is Sucursal && other.id == id;
+  }
+
+  @override
+  Map<String, dynamic> toJson() {
+    print("Convirtiendo Sucursal a JSON");
+    print(
+        "ID: $id, Nombre: $nombre, Descripción: $descripcion, Dirección: $direccion, Ciudad: $ciudad, Activo: $activo, Clave: $clave");
+    print("Creado: $creado, Modificado: $modificado, Eliminado: $eliminado");
+
+    return {
+      'id': id,
+      'nombre': nombre ?? '',
+      'descripcion': descripcion ?? '',
+      'direccion': direccion ?? '',
+      'ciudad': ciudad ?? '',
+      'clave': clave ?? '',
+      'activo': activo ?? 0,
+      'seleccionado': seleccionado ?? 0,
+      'creado': creado?.toIso8601String(),
+      'modificado': modificado?.toIso8601String(),
+      'eliminado': eliminado?.toIso8601String(),
+    }..addAll(super.toJson());
+  }
+
+  Sucursal.fromJson(Map<String, dynamic> json) {
+    super.parseJson(json);
+    nombre = Basico.parseString(json['nombre']);
+    descripcion = Basico.parseString(json['descripcion']);
+    direccion = Basico.parseString(json['direccion']);
+    ciudad = Basico.parseString(json['ciudad']);
+    activo = Basico.parseInt(json['activo']);
+    seleccionado = Basico.parseInt(json['seleccionado']);
+    clave = Basico.parseString(json['clave']);
+  }
+
+  Sucursal.fromApi(Map<String, dynamic> json) {
+    super.parseJson(json);
+    nombre = Basico.parseString(json['nombre']);
+    descripcion = Basico.parseString(json['descripcion']);
+    direccion = Basico.parseString(json['direccion']);
+    ciudad = Basico.parseString(json['ciudad']);
+    activo = json['activo'] == 1 ? 1 : 0;
+    clave = Basico.parseString(json['clave']);
+    creado = Basico.parseDate(json['creado']);
+    modificado = Basico.parseDate(json['modificado']);
+    eliminado = Basico.parseDate(json['eliminado']);
+  }
+
+  Future<void> guardar() async {
+    idLocal = await RepoService().guardar(this);
+  }
+}

+ 142 - 18
lib/models/usuario_model.dart

@@ -1,47 +1,171 @@
+import '../models/models.dart';
+import '../services/repo_service.dart';
 import 'basico_model.dart';
 
 class Usuario extends Basico {
+  String? nombre;
+  String? apellidos;
   String? correo;
+  String? celular;
+  String? celularPersonal;
+  int? rol;
+  bool? genero;
+  int? estatus;
+  String? imagen;
+  String? rfc;
+  String? razonSocial;
+  String? calle;
+  String? numeroExterior;
+  String? colonia;
+  String? codigoPostal;
+  int? idCiudad;
+  int? idEstado;
+  int? idSucursal;
+  String? turno;
   String? clave;
-  String? nombre;
-  String? estatus;
-  String? telefono;
-  String? empresa;
-  String? rol;
+  List<String>? permisos;
+  static const VER_CATEGORIAS = 'VER_CATEGORIAS';
+  static const VER_DESC_APP = 'VER_DESC_APP';
+  static const VER_SUCURSALES = 'VER_SUCURSALES';
+  static const VER_ADMIN = 'VER_ADMIN';
+  static const CANCELAR_PEDIDO = 'CANCELAR_PEDIDO';
+  static const VER_REPORTE = 'VER_REPORTE';
+  static const FORZAR_SINCRONIZACION = 'FORZAR_SINCRONIZACION';
 
   Usuario({
     super.id,
-    this.correo,
-    this.clave,
     this.nombre,
-    this.estatus,
-    this.telefono,
-    this.empresa,
+    this.apellidos,
+    this.correo,
+    this.celular,
+    this.celularPersonal,
     this.rol,
+    this.genero,
+    this.estatus,
+    this.imagen,
+    this.rfc,
+    this.razonSocial,
+    this.calle,
+    this.numeroExterior,
+    this.colonia,
+    this.codigoPostal,
+    this.idCiudad,
+    this.idEstado,
+    this.idSucursal,
+    this.turno,
+    this.clave,
+    this.permisos,
   });
 
   @override
   Map<String, dynamic> toJson() {
     return {
       'id': id,
+      'nombre': nombre,
+      'apellidos': apellidos,
       'correo': correo,
+      'celular': celular,
+      'celularPersonal': celularPersonal,
+      'rol': rol,
+      'genero': genero != null ? (genero! ? 1 : 0) : null,
+      'estatus': estatus,
+      'imagen': imagen,
+      'rfc': rfc,
+      'razonSocial': razonSocial,
+      'calle': calle,
+      'numeroExterior': numeroExterior,
+      'colonia': colonia,
+      'codigoPostal': codigoPostal,
+      'idCiudad': idCiudad,
+      'idEstado': idEstado,
+      'idSucursal': idSucursal,
+      'turno': turno,
       'clave': clave,
+      'creado': creado?.toIso8601String(),
+      'modificado': modificado?.toIso8601String(),
+      'eliminado': eliminado?.toIso8601String(),
+    }..addAll(super.toJson());
+  }
+
+  Map<String, dynamic> toMap() {
+    return {
+      'id': id,
       'nombre': nombre,
-      'estatus': estatus,
-      'telefono': telefono,
-      'empresa': empresa,
+      'apellidos': apellidos,
+      'correo': correo,
+      'celular': celular,
+      'celularPersonal': celularPersonal,
       'rol': rol,
-    }..addAll(super.toJson());
+      'genero': genero,
+      'estatus': estatus,
+      'imagen': imagen,
+      'rfc': rfc,
+      'razonSocial': razonSocial,
+      'calle': calle,
+      'numeroExterior': numeroExterior,
+      'colonia': colonia,
+      'codigoPostal': codigoPostal,
+      'idCiudad': idCiudad,
+      'idEstado': idEstado,
+      'idSucursal': idSucursal,
+      'turno': turno,
+      'clave': clave,
+      'creado': creado != null ? creado!.toIso8601String() : null,
+      'modificado': modificado != null ? modificado!.toIso8601String() : null,
+      'eliminado': eliminado != null ? eliminado!.toIso8601String() : null,
+    };
   }
 
   Usuario.fromJson(Map<String, dynamic> json) {
     super.parseJson(json);
+    nombre = Basico.parseString(json['nombre']);
+    apellidos = Basico.parseString(json['apellidos']);
     correo = Basico.parseString(json['correo']);
+    celular = Basico.parseString(json['celular']);
+    celularPersonal = Basico.parseString(json['celularPersonal']);
+    rol = Basico.parseInt(json['rol']);
+    genero = Basico.parseBolean(json['genero']);
+    estatus = Basico.parseInt(json['estatus']);
+    imagen = Basico.parseString(json['imagen']);
+    rfc = Basico.parseString(json['rfc']);
+    razonSocial = Basico.parseString(json['razonSocial']);
+    calle = Basico.parseString(json['calle']);
+    numeroExterior = Basico.parseString(json['numeroExterior']);
+    colonia = Basico.parseString(json['colonia']);
+    codigoPostal = Basico.parseString(json['codigoPostal']);
+    idCiudad = Basico.parseInt(json['idCiudad']);
+    idEstado = Basico.parseInt(json['idEstado']);
+    idSucursal = Basico.parseInt(json['idSucursal']);
+    turno = Basico.parseString(json['turno']);
     clave = Basico.parseString(json['clave']);
+  }
+
+  Usuario.fromApi(Map<String, dynamic> json) {
+    super.parseJson(json);
     nombre = Basico.parseString(json['nombre']);
-    estatus = Basico.parseString(json['estatus']);
-    telefono = Basico.parseString(json['telefono']);
-    empresa = Basico.parseString(json['empresa']);
-    rol = Basico.parseString(json['rol']);
+    apellidos = Basico.parseString(json['apellidos']);
+    correo = Basico.parseString(json['correo']);
+    celular = Basico.parseString(json['celular']);
+    celularPersonal = Basico.parseString(json['celularPersonal']);
+    rol = Basico.parseInt(json['rol']);
+    genero = Basico.parseBolean(json['genero']);
+    estatus = Basico.parseInt(json['estatus']);
+    imagen = Basico.parseString(json['imagen']);
+    rfc = Basico.parseString(json['rfc']);
+    razonSocial = Basico.parseString(json['razonSocial']);
+    calle = Basico.parseString(json['calle']);
+    numeroExterior = Basico.parseString(json['numeroExterior']);
+    colonia = Basico.parseString(json['colonia']);
+    codigoPostal = Basico.parseString(json['codigoPostal']);
+    idCiudad = Basico.parseInt(json['idCiudad']);
+    idEstado = Basico.parseInt(json['idEstado']);
+    idSucursal = Basico.parseInt(json['idSucursal']);
+    turno = Basico.parseString(json['turno']);
+    clave = Basico.parseString(json['clave']);
+    permisos = (json['permisos'] as List).map((e) => e.toString()).toList();
+  }
+
+  Future<void> guardar() async {
+    idLocal = await RepoService().guardar(this);
   }
 }

+ 9 - 0
lib/services/base_service.dart

@@ -52,4 +52,13 @@ class BaseService {
     var head = {...?headers, ...defaultHeaders};
     return await http.delete(uri, body: json.encode(body), headers: head);
   }
+
+  String prefijoVersion() {
+    if (base_url.contains('api')) {
+      return 'vP';
+    } else if (base_url.contains('test')) {
+      return 'vT';
+    }
+    return 'v';
+  }
 }

+ 739 - 65
lib/services/repo_service.dart

@@ -3,10 +3,13 @@ import 'package:intl/intl.dart';
 import 'package:path/path.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:sqflite/sqflite.dart';
+import '../data/api_response.dart';
 import '../models/models.dart';
+import '../views/producto/producto_imagen.dart';
+import 'base_service.dart';
 
 class RepoService<T> {
-  static int dbVersion = 14;
+  static int dbVersion = 23;
   static String dbName = 'conalepPos7.db';
   static const String id = Basico.identificadorWeb;
   static const String idLocal = Basico.identificadorLocal;
@@ -117,6 +120,33 @@ class RepoService<T> {
         eliminado TEXT
       )
     ''');
+
+    await db.insert('Variable', {
+      'nombre': 'Imprimir Ticket Cocina',
+      'clave': 'ticket_cocina',
+      'descripcion':
+          'Variable para imprimir ticket de cocina automaticamente al momento de generar un pedido',
+      'activo': 1,
+      'idLocal': -1,
+    });
+
+    await db.insert('Variable', {
+      'nombre': 'Imprimir Ticket Venta',
+      'clave': 'ticket_venta',
+      'descripcion':
+          'Variable para imprimir ticket de venta automaticamente al momento de generar un pedido',
+      'activo': 0,
+      'idLocal': -1,
+    });
+
+    await db.insert('Variable', {
+      'nombre': 'Imprimir ticket PC/Tablet',
+      'clave': 'ticket_pc',
+      'descripcion':
+          'Al estar activo imprime ticket para pc, el estar desactivado imprime ticket para tablet',
+      'activo': 1,
+      'idLocal': -1,
+    });
   }
 
   /// Actualiza la version de la base de datos
@@ -125,13 +155,13 @@ class RepoService<T> {
       switch (oldVersion) {
         case 1:
           await db.execute(
-              "ALTER TABLE CategoriaProducto ADD COLUMN IF NOT EXISTS esToping INTEGER DEFAULT 0");
+              "ALTER TABLE CategoriaProducto ADD COLUMN esToping INTEGER DEFAULT 0");
 
           await db.execute(
-              "ALTER TABLE CategoriaProducto ADD COLUMN IF NOT EXISTS descripcion TEXT DEFAULT ''");
+              "ALTER TABLE CategoriaProducto ADD COLUMN descripcion TEXT DEFAULT ''");
 
           await db.execute(
-              "ALTER TABLE CategoriaProducto ADD COLUMN IF NOT EXISTS maximo INTEGER");
+              "ALTER TABLE CategoriaProducto ADD COLUMN maximo INTEGER");
 
           break;
 
@@ -184,16 +214,16 @@ class RepoService<T> {
 
         case 6:
           await db.execute('''
-          ALTER TABLE Pedido ADD COLUMN IF NOT EXISTS tipoPago TEXT DEFAULT '';
+          ALTER TABLE Pedido ADD COLUMN tipoPago TEXT DEFAULT '';
         ''');
           await db.execute('''
-          ALTER TABLE Pedido ADD COLUMN IF NOT EXISTS cantEfectivo REAL DEFAULT 0;
+          ALTER TABLE Pedido ADD COLUMN cantEfectivo REAL DEFAULT 0;
         ''');
           await db.execute('''
-          ALTER TABLE Pedido ADD COLUMN IF NOT EXISTS cantTarjeta REAL DEFAULT 0;
+          ALTER TABLE Pedido ADD COLUMN cantTarjeta REAL DEFAULT 0;
         ''');
           await db.execute('''
-          ALTER TABLE Pedido ADD COLUMN IF NOT EXISTS cantTransferencia REAL DEFAULT 0;
+          ALTER TABLE Pedido ADD COLUMN cantTransferencia REAL DEFAULT 0;
         ''');
           break;
 
@@ -213,7 +243,7 @@ class RepoService<T> {
 
         case 8:
           await db.execute('''
-          ALTER TABLE Producto ADD COLUMN IF NOT EXISTS toping INTEGER DEFAULT 0;
+          ALTER TABLE Producto ADD COLUMN toping INTEGER DEFAULT 0;
         ''');
           break;
 
@@ -310,6 +340,249 @@ class RepoService<T> {
           ''');
 
           break;
+
+        case 14:
+          await db.execute('''
+            ALTER TABLE Producto ADD COLUMN activo INTEGER;
+          ''');
+
+          break;
+
+        case 16:
+          await db.execute('''
+            ALTER TABLE ProductoTopping ADD COLUMN idCategoria INTEGER;
+          ''');
+          await db.execute('''
+            ALTER TABLE ProductoTopping ADD COLUMN creado text;
+          ''');
+          await db.execute('''
+            ALTER TABLE ProductoTopping ADD COLUMN modificado text;
+          ''');
+          await db.execute('''
+            ALTER TABLE ProductoTopping ADD COLUMN eliminado text;
+          ''');
+
+          break;
+
+        case 17:
+          await db.execute('''
+          CREATE TABLE Sucursal (
+            id INTEGER PRIMARY KEY AUTOINCREMENT,
+            nombre TEXT,
+            descripcion TEXT,
+            direccion TEXT,
+            ciudad TEXT,
+            activo INTEGER,
+            clave TEXT,
+            eliminado TEXT,
+            creado TEXT,
+            modificado TEXT,
+            idLocal INTEGER,
+            seleccionado INTEGER
+          )
+        ''');
+
+          break;
+
+        case 18:
+          await db.execute('''
+          CREATE TABLE Usuario (
+            id INTEGER PRIMARY KEY AUTOINCREMENT,
+            nombre TEXT,
+            apellidos TEXT,
+            correo TEXT,
+            celular TEXT,
+            celularPersonal TEXT,
+            rol INTEGER,
+            genero BOOLEAN,
+            estatus INTEGER,
+            imagen TEXT,
+            rfc TEXT,
+            razonSocial TEXT,
+            calle TEXT,
+            numeroExterior TEXT,
+            colonia TEXT,
+            codigoPostal TEXT,
+            idCiudad INTEGER,
+            idEstado INTEGER,
+            idSucursal INTEGER,
+            turno TEXT,
+            eliminado TEXT,
+            creado TEXT,
+            modificado TEXT,
+            idLocal INTEGER
+          )
+        ''');
+
+          await db.execute('''
+          CREATE TABLE Permiso (
+            id TEXT PRIMARY KEY,
+            idModulo TEXT,
+            nombre TEXT,
+            descripcion TEXT,
+            eliminado TEXT,
+            creado TEXT,
+            modificado TEXT
+          )
+        ''');
+
+          await db.execute('''
+          CREATE TABLE UsuarioPermiso (
+            id INTEGER PRIMARY KEY AUTOINCREMENT,
+            idUsuario INTEGER,
+            idPermiso TEXT,
+            asignado TEXT,
+            modificado TEXT,
+            eliminado TEXT,
+            idLocal INTEGER
+          )
+        ''');
+
+          await db.execute('''
+            ALTER TABLE ProductoTopping ADD COLUMN idLocal INTEGER;
+          ''');
+
+          await db.execute('''
+            ALTER TABLE PedidoProductoTopping ADD COLUMN idLocal INTEGER;
+          ''');
+
+          await db.execute('''
+            ALTER TABLE PedidoProductoTopping ADD COLUMN idCategoria INTEGER;
+          ''');
+
+          break;
+
+        case 19:
+          await db.execute('''
+            ALTER TABLE CategoriaProducto ADD COLUMN minimo INTEGER;
+          ''');
+
+          break;
+
+        case 20:
+          await db.execute('''
+            ALTER TABLE Usuario ADD COLUMN clave TEXT;
+          ''');
+
+          await db.insert('Variable', {
+            'nombre': 'Imprimir ticket PC/Tablet',
+            'clave': 'ticket_pc',
+            'descripcion':
+                'Al estar activo imprime ticket para pc, el estar desactivado imprime ticket para tablet',
+            'activo': 1,
+            'idLocal': -1,
+          });
+
+          break;
+
+        case 21:
+          await db.execute('DROP TABLE CorteCaja');
+          await db.execute('DROP TABLE Deposito');
+          await db.execute('DROP TABLE Gasto');
+
+          await db.execute('''
+          CREATE TABLE CorteCaja (
+            id TEXT PRIMARY KEY,
+            fechaApertura TEXT,
+            fechaCorte TEXT,
+            idUsuario INTEGER,
+            idSucursal INTEGER,
+            fondo REAL,
+            fondoDiaSig REAL,
+            ventaPuntos REAL,
+            ventaEfe REAL,
+            ventaTrans REAL,
+            ventaTarj REAL,
+            gasto REAL,
+            retiro REAL,
+            corteFinal REAL,
+            creado TEXT,
+            modificado TEXT,
+            eliminado TEXT
+          )
+        ''');
+
+          await db.execute('''
+          CREATE TABLE Deposito (
+            id TEXT PRIMARY KEY,
+            idCorteCaja TEXT,
+            idSucursal INTEGER,
+            idUsuario INTEGER,
+            fechaDeposito TEXT,
+            monto REAL,
+            persona TEXT,
+            descripcion TEXT,
+            creado TEXT,
+            modificado TEXT,
+            eliminado TEXT
+          )
+        ''');
+
+          await db.execute('''
+          CREATE TABLE Retiro (
+            id TEXT PRIMARY KEY,
+            idCorteCaja TEXT,
+            idSucursal INTEGER,
+            idUsuario INTEGER,
+            fechaRetiro TEXT,
+            monto REAL,
+            persona TEXT,
+            descripcion TEXT,
+            creado TEXT,
+            modificado TEXT,
+            eliminado TEXT
+          )
+        ''');
+
+          await db.execute('''
+          CREATE TABLE Gasto (
+            id TEXT PRIMARY KEY,
+            idCorteCaja TEXT,
+            idSucursal INTEGER,
+            idUsuario INTEGER,
+            fechaGasto TEXT,
+            monto REAL,
+            persona TEXT,
+            descripcion TEXT,
+            creado TEXT,
+            modificado TEXT,
+            eliminado TEXT
+          )
+        ''');
+
+          await db.execute('''
+          update  Pedido set sincronizado = null where estatus = 'CANCELADO'
+        ''');
+          break;
+
+        case 22:
+          await db.execute('''
+            ALTER TABLE CorteCaja ADD COLUMN deposito REAL;
+          ''');
+          await db.execute('''
+          ALTER TABLE CorteCaja ADD COLUMN idWeb TEXT;
+        ''');
+          await db.execute('''
+          ALTER TABLE CorteCaja ADD COLUMN sincronizado TEXT;
+        ''');
+          await db.execute('''
+          ALTER TABLE Gasto ADD COLUMN idWeb TEXT;
+        ''');
+          await db.execute('''
+          ALTER TABLE Gasto ADD COLUMN sincronizado TEXT;
+        ''');
+          await db.execute('''
+          ALTER TABLE Retiro ADD COLUMN idWeb TEXT;
+        ''');
+          await db.execute('''
+          ALTER TABLE Retiro ADD COLUMN sincronizado TEXT;
+        ''');
+          await db.execute('''
+          ALTER TABLE Deposito ADD COLUMN idWeb TEXT;
+        ''');
+          await db.execute('''
+          ALTER TABLE Deposito ADD COLUMN sincronizado TEXT;
+        ''');
       }
       oldVersion++;
     }
@@ -317,47 +590,85 @@ class RepoService<T> {
 
   Future<int> guardar(T model) async {
     try {
-      print("Guardando modelo en la base de datos: ${model.runtimeType}");
-
+      // Convert the model to JSON for the database
       String modelo = json.encode(model, toEncodable: toEncodable);
-      print("Modelo convertido a JSON: $modelo");
-
       Map<String, dynamic> modelMap = json.decode(modelo);
       var dbClient = await db;
       String nombreTabla = model.runtimeType.toString();
 
-      // Verificar si el modelo tiene ID asignado
-      int? id = modelMap['id'];
-
-      // Eliminar el campo 'id' si es 0 o null para permitir autoincremento
-      if (id == null || id == 0) {
-        modelMap.remove('id');
-      }
+      // Check if the model has a String ID or an int ID
+      dynamic id = modelMap['id'];
+      bool isStringId = id is String;
+      bool isIntId = id is int;
 
-      List<Map> existing = id != null && id > 0
-          ? await dbClient!.query(nombreTabla, where: 'id = ?', whereArgs: [id])
-          : [];
+      // If id is of type String (e.g., Permiso)
+      if (isStringId) {
+        if (id == null || (id as String).isEmpty) {
+          throw Exception('El ID del modelo no puede ser nulo o vacío');
+        }
 
-      if (existing.isNotEmpty) {
-        print("Actualizando registro existente con ID: $id");
-        await dbClient!.update(
+        // Check for existing record with the String ID
+        List<Map> existing = await dbClient!.query(
           nombreTabla,
-          modelMap,
           where: 'id = ?',
           whereArgs: [id],
         );
+
+        if (existing.isNotEmpty) {
+          await dbClient.update(
+            nombreTabla,
+            modelMap,
+            where: 'id = ?',
+            whereArgs: [id],
+          );
+        } else {
+          print(
+              "Insertando nuevo registro en la tabla $nombreTabla con ID: $id");
+          await dbClient.insert(nombreTabla, modelMap);
+        }
+        return 1;
+      } else if (isIntId) {
+        // If id is of type int (e.g., other models)
+        if (id == null || id == 0) {
+          modelMap
+              .remove('id'); // Remove id if it's null or 0 for auto-increment
+        }
+
+        List<Map> existing = (id != null && id > 0)
+            ? await dbClient!
+                .query(nombreTabla, where: 'id = ?', whereArgs: [id])
+            : [];
+
+        if (existing.isNotEmpty) {
+          print("Actualizando registro existente con ID: $id");
+          await dbClient!.update(
+            nombreTabla,
+            modelMap,
+            where: 'id = ?',
+            whereArgs: [id],
+          );
+        } else {
+          print("Insertando nuevo registro en la tabla $nombreTabla");
+          id = await dbClient!.insert(nombreTabla, modelMap);
+        }
+        return id as int;
       } else {
-        print("Insertando nuevo registro en la tabla $nombreTabla");
-        id = await dbClient!.insert(nombreTabla, modelMap);
+        throw Exception('Tipo de ID no soportado');
       }
-
-      return id!;
     } catch (e) {
       print('Error al guardar en dynamic: $e');
       return 0;
     }
   }
 
+  void asignarFechasLocalmente(Basico model) {
+    DateTime ahora = DateTime.now();
+    if (model.creado == null) {
+      model.creado = ahora.toUtc();
+    }
+    model.modificado = ahora.toUtc();
+  }
+
   Future<void> _guardarToppings(
       Database db, int idProducto, List<Producto>? topings) async {
     await db.delete('ProductoTopping',
@@ -420,23 +731,7 @@ class RepoService<T> {
   }
 
   dynamic toEncodable(dynamic item) {
-    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) {
-      print("Convirtiendo Producto a JSON para guardar");
-      return item.toJson();
-    } else if (item is CategoriaProducto) {
-      print("Convirtiendo CategoriaProducto a JSON para guardar");
-      return item.toJson();
-    } else if (item is Variable) {
-      return item.toJson();
-    }
-    throw UnsupportedError(
-        'Type not supported for serialization: ${item.runtimeType}');
+    return item.toJson();
   }
 
   Future<List<T>> obtenerTodos({String orderBy = 'id DESC'}) async {
@@ -453,6 +748,10 @@ class RepoService<T> {
         return Pedido.fromJson(map) as T;
       case Producto:
         return Producto.fromJson(map) as T;
+      case Usuario:
+        return Usuario.fromJson(map) as T;
+      case CorteCaja:
+        return CorteCaja.fromJson(map) as T;
       default:
         throw Exception('Tipo no soportado');
     }
@@ -477,21 +776,34 @@ class RepoService<T> {
         .toList();
   }
 
-  Future<List<Deposito>> obtenerDepositosPorIdCorteCaja(int idCorteCaja) async {
+  //CORTE CAJA
+
+  Future<List<Deposito>> obtenerDepositosPorIdCorteCaja(
+      String idCorteCaja) async {
     var dbClient = await db;
     List<Map<String, dynamic>> maps = await dbClient!.query(
       'Deposito',
-      where: 'idCorteCaja = ?',
+      where: 'idCorteCaja = ? AND eliminado IS NULL',
       whereArgs: [idCorteCaja],
     );
     return maps.map((map) => Deposito.fromJson(map)).toList();
   }
 
-  Future<List<Gasto>> obtenerGastosPorIdCorteCaja(int idCorteCaja) async {
+  Future<List<Retiro>> obtenerRetirosPorIdCorteCaja(String idCorteCaja) async {
+    var dbClient = await db;
+    List<Map<String, dynamic>> maps = await dbClient!.query(
+      'Retiro',
+      where: 'idCorteCaja = ? AND eliminado IS NULL',
+      whereArgs: [idCorteCaja],
+    );
+    return maps.map((map) => Retiro.fromJson(map)).toList();
+  }
+
+  Future<List<Gasto>> obtenerGastosPorIdCorteCaja(String idCorteCaja) async {
     var dbClient = await db;
     List<Map<String, dynamic>> maps = await dbClient!.query(
       'Gasto',
-      where: 'idCorteCaja = ?',
+      where: 'idCorteCaja = ? AND eliminado IS NULL',
       whereArgs: [idCorteCaja],
     );
     return maps.map((map) => Gasto.fromJson(map)).toList();
@@ -503,6 +815,21 @@ class RepoService<T> {
     return Sqflite.firstIntValue(result) ?? 0;
   }
 
+  Future<double?> obtenerFondoDiaSigDelUltimoCorte() async {
+    var dbClient = await db;
+    List<Map<String, dynamic>> result = await dbClient!.query(
+      'CorteCaja',
+      orderBy: 'fechaApertura DESC',
+      limit: 1,
+    );
+
+    if (result.isNotEmpty) {
+      return result.first['fondoDiaSig'] as double?;
+    }
+
+    return null;
+  }
+
   Future<List<Pedido>> obtenerPedidosPaginados(int limit, int offset) async {
     Database? dbClient = await db;
     List<Map<String, dynamic>> result = await dbClient!
@@ -661,8 +988,8 @@ class RepoService<T> {
     return result.map((map) => Pedido.fromJson(map)).toList();
   }
 
-  Future<void> sincronizarCategorias(
-      List<CategoriaProducto> categoriasApi) async {
+  Future<void> sincronizarCategorias(List<CategoriaProducto> categoriasApi,
+      {bool forzar = false}) async {
     var db = await RepoService().db;
 
     var categoriasLocalesQuery = await db!.query('CategoriaProducto');
@@ -676,10 +1003,12 @@ class RepoService<T> {
         orElse: () => CategoriaProducto(),
       );
 
-      if (categoriaLocal.id != 0 &&
-          categoriaApi.modificado != null &&
-          categoriaLocal.modificado != null &&
-          categoriaApi.modificado!.isAfter(categoriaLocal.modificado!)) {
+      if (forzar ||
+          categoriaLocal.id != 0 &&
+              categoriaApi.modificado != null &&
+              (categoriaLocal.modificado == null ||
+                  categoriaApi.modificado!
+                      .isAfter(categoriaLocal.modificado!))) {
         await RepoService().guardar(categoriaApi);
       } else if (categoriaLocal.id == 0) {
         await RepoService().guardar(categoriaApi);
@@ -687,27 +1016,372 @@ class RepoService<T> {
     }
   }
 
-  Future<void> sincronizarProductos(List<Producto> productosApi) async {
+  Future<void> sincronizarProductos(List<Producto> productosApi,
+      {bool forzar = false}) async {
     var db = await RepoService().db;
 
+    // // Print del JSON recibido
+    // print(
+    //     "Productos API recibidos: ${productosApi.map((e) => e.toJson()).toList()}");
+
     var productosLocalesQuery = await db!.query('Producto');
     List<Producto> productosLocales =
         productosLocalesQuery.map((e) => Producto.fromJson(e)).toList();
 
     for (var productoApi in productosApi) {
+      // Validar que el ID del producto no sea nulo
+      if (productoApi.id == null) {
+        print("Producto con ID nulo, se omite: ${productoApi.nombre}");
+        continue; // Ignorar productos sin ID
+      }
+
+      // Buscar el producto localmente
       var productoLocal = productosLocales.firstWhere(
         (producto) => producto.id == productoApi.id,
-        orElse: () => Producto(),
+        orElse: () =>
+            Producto(), // Si no existe el producto, devolver uno nuevo con id 0
       );
 
-      if (productoLocal.id != 0 &&
-          productoApi.modificado != null &&
-          productoLocal.modificado != null &&
-          productoApi.modificado!.isAfter(productoLocal.modificado!)) {
+      if (productoLocal.id == 0) {
+        print("Insertando nuevo producto: ${productoApi.nombre}");
         await RepoService().guardar(productoApi);
-      } else if (productoLocal.id == 0) {
+      } else if (forzar ||
+          productoApi.modificado != null &&
+              (productoLocal.modificado == null ||
+                  productoApi.modificado!.isAfter(productoLocal.modificado!))) {
+        print("Actualizando producto: ${productoApi.nombre}");
         await RepoService().guardar(productoApi);
+      } else {
+        // Producto sin cambios
+        // print(
+        //     "Producto sin cambios o datos insuficientes: ${productoApi.nombre}");
+      }
+    }
+  }
+
+  Future<void> sincronizarProductoTopping(
+      List<ProductoTopping> toppingsApi) async {
+    var db = await RepoService().db;
+
+    await db!.delete('ProductoTopping');
+
+    for (var toppingApi in toppingsApi) {
+      await RepoService().guardar(toppingApi);
+    }
+
+    print("Todos los registros de ProductoTopping han sido sincronizados.");
+  }
+
+  Future<void> sincronizarSucursales(List<Sucursal> sucursalesApi,
+      {bool forzar = false}) async {
+    var db = await RepoService().db;
+
+    int? idSucursalSeleccionada = await obtenerIdSucursalSeleccionada();
+
+    var sucursalesLocalesQuery = await db!.query('Sucursal');
+    List<Sucursal> sucursalesLocales =
+        sucursalesLocalesQuery.map((e) => Sucursal.fromJson(e)).toList();
+
+    for (var sucursalApi in sucursalesApi) {
+      var sucursalLocal = sucursalesLocales.firstWhere(
+        (sucursal) => sucursal.id == sucursalApi.id,
+        orElse: () => Sucursal(),
+      );
+
+      sucursalApi.seleccionado =
+          (sucursalApi.id == idSucursalSeleccionada) ? 1 : 0;
+      if (forzar ||
+          sucursalLocal.id != 0 &&
+              sucursalApi.modificado != null &&
+              (sucursalLocal.modificado == null ||
+                  sucursalApi.modificado!.isAfter(sucursalLocal.modificado!))) {
+        await RepoService().guardar(sucursalApi);
+      } else if (sucursalLocal.id == 0) {
+        await RepoService().guardar(sucursalApi);
+      }
+    }
+  }
+
+  Future<void> sincronizarPermisos(List<Permiso> permisosApi,
+      {bool forzar = false}) async {
+    var db = await RepoService().db;
+    var permisosLocalesQuery = await db!.query('Permiso');
+    List<Permiso> permisosLocales =
+        permisosLocalesQuery.map((e) => Permiso.fromJson(e)).toList();
+
+    for (var permisoApi in permisosApi) {
+      var permisoLocal = permisosLocales.firstWhere(
+        (permiso) => permiso.id == permisoApi.id,
+        orElse: () => Permiso(),
+      );
+
+      if (forzar ||
+          permisoLocal.id != null &&
+              permisoApi.modificado != null &&
+              (permisoLocal.modificado == null ||
+                  permisoApi.modificado!.isAfter(permisoLocal.modificado!))) {
+        print('Actualizando permiso con ID: ${permisoApi.id}');
+        await RepoService().guardar(permisoApi);
+      } else if (permisoLocal.id == null) {
+        print('Insertando nuevo permiso con ID: ${permisoApi.id}');
+        await RepoService().guardar(permisoApi);
+      } else {
+        //print('Permiso sin cambios: ${permisoApi.id}');
+      }
+    }
+  }
+
+  Future<void> sincronizarUsuarios(List<Usuario> usuariosApi,
+      {bool forzar = false}) async {
+    var db = await RepoService().db;
+
+    var usuariosLocalesQuery = await db!.query('Usuario');
+    List<Usuario> usuariosLocales =
+        usuariosLocalesQuery.map((e) => Usuario.fromJson(e)).toList();
+
+    for (var usuarioApi in usuariosApi) {
+      var usuarioLocal = usuariosLocales.firstWhere(
+        (usuario) => usuario.id == usuarioApi.id,
+        orElse: () => Usuario(),
+      );
+
+      // Comprobar si realmente se necesita actualizar el usuario basado en la fecha de modificado
+      if (forzar ||
+          usuarioLocal.id != 0 &&
+              usuarioApi.modificado != null &&
+              (usuarioLocal.modificado == null ||
+                  usuarioApi.modificado!.isAfter(usuarioLocal.modificado!))) {
+        print('Actualizando usuario con ID: ${usuarioApi.id}');
+        await RepoService().guardar(usuarioApi);
+
+        // Comparar permisos antes de actualizarlos
+        await _actualizarPermisosUsuario(
+            db, usuarioApi.id!, usuarioApi.permisos!);
+      } else if (usuarioLocal.id == 0) {
+        print('Insertando nuevo usuario con ID: ${usuarioApi.id}');
+        await RepoService().guardar(usuarioApi);
+
+        // Insertar los permisos correspondientes
+        await _guardarPermisosUsuario(db, usuarioApi.id!, usuarioApi.permisos!);
+      } else {
+        //print('Usuario sin cambios: ${usuarioApi.id}');
       }
     }
   }
+
+  Future<void> _actualizarPermisosUsuario(
+      Database db, int idUsuario, List<String> permisosApi) async {
+    // Obtener los permisos actuales del usuario
+    var permisosLocalesQuery = await db.query(
+      'UsuarioPermiso',
+      where: 'idUsuario = ?',
+      whereArgs: [idUsuario],
+    );
+    List<String> permisosLocales = permisosLocalesQuery
+        .map((permiso) => permiso['idPermiso'] as String)
+        .toList();
+
+    // Comparar los permisos del API con los locales
+    bool sonIguales = _listasIguales(permisosLocales, permisosApi);
+
+    if (!sonIguales) {
+      // Si los permisos no son iguales, actualizarlos
+      print('Actualizando permisos del usuario con ID: $idUsuario');
+      await _guardarPermisosUsuario(db, idUsuario, permisosApi);
+    } else {
+      print('Permisos del usuario con ID: $idUsuario no han cambiado.');
+    }
+  }
+
+  Future<void> _guardarPermisosUsuario(
+      Database db, int idUsuario, List<String> permisos) async {
+    // Eliminar los permisos actuales solo si hay cambios
+    await db.delete('UsuarioPermiso',
+        where: 'idUsuario = ?', whereArgs: [idUsuario]);
+
+    // Insertar los nuevos permisos
+    for (var idPermiso in permisos) {
+      await db.insert('UsuarioPermiso', {
+        'idUsuario': idUsuario,
+        'idPermiso': idPermiso,
+      });
+    }
+  }
+
+  bool _listasIguales(List<String> lista1, List<String> lista2) {
+    if (lista1.length != lista2.length) return false;
+    lista1.sort();
+    lista2.sort();
+    for (int i = 0; i < lista1.length; i++) {
+      if (lista1[i] != lista2[i]) return false;
+    }
+    return true;
+  }
+
+  Future<String?> obtenerClaveSucursalSeleccionada() async {
+    var db = await this.db;
+    List<Map<String, dynamic>> queryResult = await db!.query(
+      'Sucursal',
+      where: 'seleccionado = ? AND eliminado IS NULL',
+      whereArgs: [1],
+      limit: 1,
+    );
+
+    if (queryResult.isNotEmpty) {
+      Sucursal sucursalSeleccionada = Sucursal.fromJson(queryResult.first);
+      return sucursalSeleccionada.clave;
+    }
+
+    return null;
+  }
+
+  Future<int?> obtenerIdSucursalSeleccionada() async {
+    var db = await this.db;
+    List<Map<String, dynamic>> queryResult = await db!.query(
+      'Sucursal',
+      where: 'seleccionado = ? AND eliminado IS NULL',
+      whereArgs: [1],
+      limit: 1,
+    );
+
+    if (queryResult.isNotEmpty) {
+      return Sucursal.fromJson(queryResult.first).id;
+    }
+
+    return null;
+  }
+
+  Future<void> forzarSincronizacion() async {
+    String? claveSucursal = await obtenerClaveSucursalSeleccionada();
+
+    try {
+      // Sincronizar categorías
+      await sincronizarCategorias(
+          await fetchCategoriasApi(claveSucursal: claveSucursal),
+          forzar: true);
+
+      // Sincronizar productos
+      await sincronizarProductos(
+          await fetchProductosApi(claveSucursal: claveSucursal),
+          forzar: true);
+
+      // Sincronizar toppings de producto
+      await sincronizarProductoTopping(
+          await fetchProductoToppingApi(claveSucursal: claveSucursal));
+
+      // Sincronizar sucursales
+      await sincronizarSucursales(await fetchSucursalesApi(), forzar: true);
+
+      // Sincronizar permisos
+      await sincronizarPermisos(await fetchPermisosApi(), forzar: true);
+
+      // Sincronizar usuarios
+      await sincronizarUsuarios(await fetchUsuariosApi(), forzar: true);
+
+      print('Sincronización forzosa completada.');
+    } catch (e) {
+      print('Error en la sincronización forzosa: $e');
+    }
+  }
+
+  Future<List<CategoriaProducto>> fetchCategoriasApi(
+      {String? claveSucursal}) async {
+    Map<String, String> parametros = {
+      "claveSucursal": claveSucursal!,
+      "limite": "-1"
+    };
+    final response = ApiResponse(
+        await BaseService().get('/pos/categoria', queryParameters: parametros));
+    return response.isOk && response.resultados != null
+        ? response.resultados!
+            .map((json) => CategoriaProducto.fromApi(json))
+            .toList()
+        : [];
+  }
+
+  Future<List<Producto>> fetchProductosApi({String? claveSucursal}) async {
+    Map<String, String> parametros = {
+      "limite": "-1",
+      "claveSucursal": claveSucursal!,
+      "expand": "media"
+    };
+
+    final response = ApiResponse(
+        await BaseService().get('/pos/producto', queryParameters: parametros));
+
+    if (response.isOk && response.resultados != null) {
+      List<Producto> productosApi =
+          response.resultados!.map((json) => Producto.fromApi(json)).toList();
+
+      // Descargar y almacenar cada imagen de producto
+      for (var productoApi in productosApi) {
+        if (productoApi.media != null && productoApi.media!.isNotEmpty) {
+          for (var media in productoApi.media!) {
+            // Descargar y guardar la imagen localmente
+            String? localImagePath = await downloadAndStoreImage(
+                media.ruta!, productoApi.id!, media.nombre!);
+
+            if (localImagePath != null) {
+              productoApi.imagen = localImagePath;
+            }
+          }
+        }
+      }
+      return productosApi;
+    }
+    return [];
+  }
+
+  Future<List<ProductoTopping>> fetchProductoToppingApi(
+      {String? claveSucursal}) async {
+    Map<String, String> parametros = {
+      "limite": "-1",
+      "claveSucursal": claveSucursal!,
+    };
+
+    final response = ApiResponse(await BaseService()
+        .get('/pos/producto-topping', queryParameters: parametros));
+    return response.isOk && response.resultados != null
+        ? response.resultados!
+            .map((json) => ProductoTopping.fromApi(json))
+            .toList()
+        : [];
+  }
+
+  Future<List<Sucursal>> fetchSucursalesApi() async {
+    Map<String, String> parametros = {
+      "limite": "-1",
+    };
+
+    final response = ApiResponse(
+        await BaseService().get('/pos/sucursal', queryParameters: parametros));
+    return response.isOk && response.resultados != null
+        ? response.resultados!.map((json) => Sucursal.fromApi(json)).toList()
+        : [];
+  }
+
+  Future<List<Permiso>> fetchPermisosApi() async {
+    Map<String, String> parametros = {
+      "limite": "-1",
+    };
+
+    final response = ApiResponse(
+        await BaseService().get('/pos/permiso', queryParameters: parametros));
+    return response.isOk && response.resultados != null
+        ? response.resultados!.map((json) => Permiso.fromJson(json)).toList()
+        : [];
+  }
+
+  Future<List<Usuario>> fetchUsuariosApi() async {
+    Map<String, String> parametros = {
+      "limite": "-1",
+      "expand": "permisos",
+    };
+
+    final response = ApiResponse(
+        await BaseService().get('/pos/usuario', queryParameters: parametros));
+    return response.isOk && response.resultados != null
+        ? response.resultados!.map((json) => Usuario.fromApi(json)).toList()
+        : [];
+  }
 }

+ 1 - 1
lib/viewmodels/categoria_producto_view_model.dart

@@ -22,7 +22,7 @@ class CategoriaProductoViewModel extends ChangeNotifier {
 
   int _currentPage = 1;
   int _totalProducts = 0;
-  int _limit = 10;
+  int _limit = 20;
 
   int get currentPage => _currentPage;
   int get totalProducts => _totalProducts;

+ 350 - 34
lib/viewmodels/corte_caja_view_model.dart

@@ -1,7 +1,10 @@
+import 'package:conalep_pos/widgets/widgets.dart';
 import 'package:flutter/material.dart';
 import 'package:intl/intl.dart';
+import 'package:uuid/uuid.dart';
 import '../models/models.dart';
 import '../services/repo_service.dart';
+import '../views/corte_caja/corte_caja_ticket.dart';
 
 class CorteCajaViewModel extends ChangeNotifier {
   List<CorteCaja> _cortes = [];
@@ -11,21 +14,49 @@ class CorteCajaViewModel extends ChangeNotifier {
   bool get isLoading => _isLoading;
   CorteCaja? _selectedCorte;
 
-  int _currentPage = 1;
-  int _totalCortes = 0;
-  int _limit = 10;
-
   double corteFinal = 0.0;
   double totalDepositos = 0.0;
+  double totalRetiros = 0.0;
   double totalGastos = 0.0;
   List<Deposito> depositos = [];
+  List<Retiro> retiros = [];
   List<Gasto> gastos = [];
+  double _ventaEfe = 0.0;
+  double _ventaTarj = 0.0;
+  double _ventaTransf = 0.0;
+  double _totalVenta = 0.0;
+
+  CorteCaja? get selectedCorte => _selectedCorte;
+
+  int _currentPage = 1;
+  int _totalCortes = 0;
+  int _limit = 10;
 
   int get currentPage => _currentPage;
-  int get totalCortes => _totalCortes;
+  int get totalPedidos => _totalCortes;
   int get totalPages => (_totalCortes / _limit).ceil();
 
-  CorteCaja? get selectedCorte => _selectedCorte;
+  double get ventaEfe => _ventaEfe;
+  double get ventaTarj => _ventaTarj;
+  double get ventaTransf => _ventaTransf;
+  double get totalVenta => _totalVenta;
+  double get salidasTotales => totalRetiros + totalGastos;
+
+  final TextEditingController _ventaEfeController = TextEditingController();
+  final TextEditingController _ventaTarjController = TextEditingController();
+  final TextEditingController _ventaTransfController = TextEditingController();
+  final TextEditingController _totalVentaController = TextEditingController();
+  final TextEditingController _fondoController = TextEditingController();
+  final TextEditingController _fondoDiaSigController = TextEditingController();
+  final TextEditingController _ventaPuntosController = TextEditingController();
+
+  TextEditingController get ventaEfeController => _ventaEfeController;
+  TextEditingController get ventaTarjController => _ventaTarjController;
+  TextEditingController get ventaTransfController => _ventaTransfController;
+  TextEditingController get totalVentaController => _totalVentaController;
+  TextEditingController get fondoController => _fondoController;
+  TextEditingController get fondoDiaSigController => _fondoDiaSigController;
+  TextEditingController get ventaPuntosController => ventaPuntosController;
 
   void setIsLoading(bool loading) {
     _isLoading = loading;
@@ -44,77 +75,362 @@ class CorteCajaViewModel extends ChangeNotifier {
     }
   }
 
-  Future<void> fetchCortes({int page = 1}) async {
-    setIsLoading(true);
+  bool isCorteAbierto() {
+    return _selectedCorte != null && _selectedCorte!.fechaCorte == null;
+  }
+
+  void actualizarVentaEfectivo(String value) {
+    _ventaEfe = double.tryParse(value.replaceAll(',', '')) ?? 0.0;
+    _ventaEfeController.text = formatoMiles(_ventaEfe);
+    _calcularTotalVenta();
+    notifyListeners();
+  }
+
+  void actualizarVentaTarjeta(String value) {
+    _ventaTarj = double.tryParse(value.replaceAll(',', '')) ?? 0.0;
+    _ventaTarjController.text = formatoMiles(_ventaTarj);
+    _calcularTotalVenta();
+    notifyListeners();
+  }
+
+  void actualizarVentaTransferencia(String value) {
+    _ventaTransf = double.tryParse(value.replaceAll(',', '')) ?? 0.0;
+    _ventaTransfController.text = formatoMiles(_ventaTransf);
+    _calcularTotalVenta();
+    notifyListeners();
+  }
+
+  void _calcularTotalVenta() {
+    _totalVenta = _ventaEfe + _ventaTarj + _ventaTransf;
+    _totalVentaController.text = formatoMiles(_totalVenta);
+  }
+
+  Future<String?> createCorteCaja() async {
     RepoService<CorteCaja> repoCorte = RepoService<CorteCaja>();
-    _currentPage = page;
-    _cortes = await repoCorte.obtenerTodos();
-    setIsLoading(false);
+
+    double? fondoDiaSigAnterior =
+        await repoCorte.obtenerFondoDiaSigDelUltimoCorte();
+
+    _selectedCorte = CorteCaja(
+      id: Uuid().v4(),
+      fechaApertura: DateTime.now().toUtc(),
+      creado: DateTime.now().toUtc(),
+      fondo: fondoDiaSigAnterior ?? 0.0,
+    );
+
+    await _selectedCorte!.guardar();
     notifyListeners();
+    return _selectedCorte!.id;
   }
 
-  Future<void> buscarPorFecha(DateTime startDate, DateTime endDate) async {
+  Future<void> eliminarDeposito(String id) async {
+    final deposito = depositos.firstWhere((d) => d.id == id);
+    deposito.eliminado = DateTime.now().toUtc();
+    await deposito.guardar();
+    calcularTotalDeposito();
+    notifyListeners();
+  }
+
+  Future<void> eliminarRetiro(String id) async {
+    final retiro = retiros.firstWhere((d) => d.id == id);
+    retiro.eliminado = DateTime.now().toUtc();
+    await retiro.guardar();
+    calcularTotalRetiro();
+    notifyListeners();
+  }
+
+  Future<void> eliminarGasto(String id) async {
+    final gasto = gastos.firstWhere((d) => d.id == id);
+    gasto.eliminado = DateTime.now().toUtc();
+    await gasto.guardar();
+    calcularTotalGasto();
+    notifyListeners();
+  }
+
+  Future<void> fetchCortes({int page = 1}) async {
     setIsLoading(true);
     RepoService<CorteCaja> repoCorte = RepoService<CorteCaja>();
-    _cortes = await repoCorte.buscarPorFechaCorte(startDate, endDate);
+    _currentPage = page;
+    _cortes = await repoCorte.obtenerTodos(orderBy: 'fechaApertura DESC');
     setIsLoading(false);
     notifyListeners();
   }
 
-  Future<void> fetchDepositosAndGastos(int corteCajaId) async {
+  Future<void> fetchDepositosAndRetiros(String corteCajaId) async {
     RepoService<Deposito> depositoRepo = RepoService<Deposito>();
+    RepoService<Retiro> retiroRepo = RepoService<Retiro>();
     RepoService<Gasto> gastoRepo = RepoService<Gasto>();
     depositos = await depositoRepo.obtenerDepositosPorIdCorteCaja(corteCajaId);
+    retiros = await retiroRepo.obtenerRetirosPorIdCorteCaja(corteCajaId);
     gastos = await gastoRepo.obtenerGastosPorIdCorteCaja(corteCajaId);
+
+    print('Depósitos obtenidos: ${depositos.length}');
+    print('Retiros obtenidos: ${retiros.length}');
+    print('Gastos obtenidos: ${gastos.length}');
+
     calcularTotalDeposito();
+    calcularTotalRetiro();
     calcularTotalGasto();
     notifyListeners();
   }
 
-  Future<void> addDeposito(
-      double monto, String descripcion, int corteCajaId) async {
+  void selectCorte(CorteCaja corte) {
+    _selectedCorte = corte;
+    notifyListeners();
+  }
+
+  Future<void> addDeposito(double monto, String descripcion, String? persona,
+      String corteCajaId) async {
     Deposito nuevoDeposito = Deposito(
-        monto: monto, descripcion: descripcion, idCorteCaja: corteCajaId);
+      id: Uuid().v4(),
+      idCorteCaja: corteCajaId,
+      fechaDeposito: DateTime.now().toUtc(),
+      creado: DateTime.now().toUtc(),
+      monto: monto,
+      descripcion: descripcion,
+      persona: persona,
+    );
     await nuevoDeposito.guardar();
     depositos.add(nuevoDeposito);
     calcularTotalDeposito();
+
+    final valores = obtenerValoresFondo();
+    calcularCorteFinal(
+        valores['fondo']!, valores['fondoDiaSig']!, valores['totalVenta']!);
+
     notifyListeners();
   }
 
-  Future<void> addGasto(
-      double monto, String descripcion, int corteCajaId) async {
-    Gasto nuevoGasto =
-        Gasto(monto: monto, descripcion: descripcion, idCorteCaja: corteCajaId);
+  Future<void> addRetiro(double monto, String descripcion, String? persona,
+      String corteCajaId) async {
+    Retiro nuevoRetiro = Retiro(
+      id: Uuid().v4(),
+      idCorteCaja: corteCajaId,
+      fechaRetiro: DateTime.now().toUtc(),
+      creado: DateTime.now().toUtc(),
+      monto: monto,
+      descripcion: descripcion,
+      persona: persona,
+    );
+    await nuevoRetiro.guardar();
+    retiros.add(nuevoRetiro);
+    calcularTotalRetiro();
+
+    final valores = obtenerValoresFondo();
+    calcularCorteFinal(
+        valores['fondo']!, valores['fondoDiaSig']!, valores['totalVenta']!);
+
+    notifyListeners();
+  }
+
+  Future<void> addGasto(double monto, String descripcion, String? persona,
+      String corteCajaId) async {
+    Gasto nuevoGasto = Gasto(
+      id: Uuid().v4(),
+      idCorteCaja: corteCajaId,
+      fechaGasto: DateTime.now().toUtc(),
+      creado: DateTime.now().toUtc(),
+      monto: monto,
+      descripcion: descripcion,
+      persona: persona,
+    );
     await nuevoGasto.guardar();
     gastos.add(nuevoGasto);
     calcularTotalGasto();
+
+    final valores = obtenerValoresFondo();
+    calcularCorteFinal(
+        valores['fondo']!, valores['fondoDiaSig']!, valores['totalVenta']!);
+
     notifyListeners();
   }
 
-  void calcularTotalDeposito() {
-    totalDepositos = depositos.fold(0, (sum, item) => sum + (item.monto ?? 0));
+  Future<void> cargarVentasDesdeFechaApertura(DateTime fechaApertura) async {
+    setIsLoading(true);
+    RepoService<Pedido> repoPedido = RepoService<Pedido>();
+
+    DateTime fechaFin = _selectedCorte?.fechaCorte ?? DateTime.now().toUtc();
+
+    List<Pedido> pedidos =
+        (await repoPedido.buscarPorFecha(fechaApertura, fechaFin))
+            .where((pedido) =>
+                pedido.estatus == 'NUEVO' || pedido.estatus == 'TERMINADO')
+            .toList();
+
+    // Calcula los totales
+    double totalEfectivo = 0;
+    double totalTarjeta = 0;
+    double totalTransferencia = 0;
+    double totalDia = 0.0;
+    double cambio = 0.0;
+    double efeSinCambio = 0.0;
+    double totalSinCambio = 0.0;
+
+    for (var pedido in pedidos) {
+      totalEfectivo += pedido.cantEfectivo ?? 0;
+      totalTarjeta += pedido.cantTarjeta ?? 0;
+      totalTransferencia += pedido.cantTransferencia ?? 0;
+      totalSinCambio += pedido.totalPedido ?? 0;
+    }
+
+    totalDia = totalEfectivo + totalTarjeta + totalTransferencia;
+    cambio = totalDia - totalSinCambio;
+    efeSinCambio = totalEfectivo - cambio;
+
+    _ventaEfeController.text = formatoMiles(efeSinCambio);
+    _ventaTarjController.text = formatoMiles(totalTarjeta);
+    _ventaTransfController.text = formatoMiles(totalTransferencia);
+    _totalVentaController.text =
+        formatoMiles(efeSinCambio + totalTarjeta + totalTransferencia);
+
+    print(
+        'Total efectivo: $totalEfectivo, Total tarjeta: $totalTarjeta, Total transferencia: $totalTransferencia');
+    print('Pedidos encontrados: ${pedidos.length}');
+
+    setIsLoading(false);
+    notifyListeners();
   }
 
-  void calcularTotalGasto() {
-    totalGastos = gastos.fold(0, (sum, item) => sum + (item.monto ?? 0));
+  bool hasOpenCorteCaja() {
+    return _cortes.any((corte) => corte.fechaCorte == null);
   }
 
-  void quitarDeposito(int index) {
-    depositos.removeAt(index);
-    calcularTotalDeposito();
+  Future<void> guardarCorte({bool esCorte = false}) async {
+    double fondo =
+        double.tryParse(_fondoController.text.replaceAll(',', '')) ?? 0;
+    double fondoDiaSig =
+        double.tryParse(_fondoDiaSigController.text.replaceAll(',', '')) ?? 0;
+    double ventaPuntos =
+        double.tryParse(_ventaPuntosController.text.replaceAll(',', '')) ?? 0;
+    double ventaEfe =
+        double.tryParse(_ventaEfeController.text.replaceAll(',', '')) ?? 0;
+    double ventaTrans =
+        double.tryParse(_ventaTransfController.text.replaceAll(',', '')) ?? 0;
+    double ventaTarj =
+        double.tryParse(_ventaTarjController.text.replaceAll(',', '')) ?? 0;
+    double gasto = totalGastos;
+    double retiro = totalRetiros;
+    double deposito = totalDepositos;
+
+    _selectedCorte?.fondo = fondo;
+    _selectedCorte?.fondoDiaSig = fondoDiaSig;
+    _selectedCorte?.ventaPuntos = ventaPuntos;
+    _selectedCorte?.ventaEfe = ventaEfe;
+    _selectedCorte?.ventaTrans = ventaTrans;
+    _selectedCorte?.ventaTarj = ventaTarj;
+    _selectedCorte?.gasto = gasto;
+    _selectedCorte?.retiro = retiro;
+    _selectedCorte?.deposito = deposito;
+
+    calcularCorteFinal(fondo, fondoDiaSig, ventaEfe + ventaTarj + ventaTrans);
+
+    _selectedCorte?.corteFinal = corteFinal;
+    _selectedCorte?.modificado = DateTime.now().toUtc();
+
+    if (esCorte) {
+      _selectedCorte?.fechaCorte = DateTime.now().toUtc();
+    }
+
+    await _selectedCorte?.guardar();
     notifyListeners();
   }
 
-  void quitarGasto(int index) {
-    gastos.removeAt(index);
-    calcularTotalGasto();
+  void calcularTotalDeposito() {
+    totalDepositos = depositos
+        .where((deposito) => deposito.eliminado == null)
+        .fold(0, (sum, item) => sum + (item.monto ?? 0));
+  }
+
+  void calcularTotalRetiro() {
+    totalRetiros = retiros
+        .where((retiro) => retiro.eliminado == null)
+        .fold(0, (sum, item) => sum + (item.monto ?? 0));
+  }
+
+  void calcularTotalGasto() {
+    totalGastos = gastos
+        .where((gasto) => gasto.eliminado == null)
+        .fold(0, (sum, item) => sum + (item.monto ?? 0));
+  }
+
+  Future<void> buscarPorFecha(DateTime startDate, DateTime endDate) async {
+    setIsLoading(true);
+    RepoService<CorteCaja> repoCorte = RepoService<CorteCaja>();
+    _cortes = await repoCorte.buscarPorFechaCorte(startDate, endDate);
+    setIsLoading(false);
     notifyListeners();
   }
 
   void calcularCorteFinal(
-      double fondo, double fondoSiguiente, double ventasMostrador) {
-    corteFinal = (fondo + totalDepositos + ventasMostrador) -
-        (fondoSiguiente + totalGastos);
+      double fondo, double fondoSiguiente, double ventaTotal) {
+    corteFinal = (fondo + totalDepositos + ventaTotal) -
+        (fondoSiguiente + totalRetiros + totalGastos);
     notifyListeners();
   }
+
+  Future<void> imprimirCorteCajaTicket({String? corteCajaId}) async {
+    if (corteCajaId != null) {
+      await fetchDepositosAndRetiros(corteCajaId);
+      _selectedCorte = _cortes.firstWhere((corte) => corte.id == corteCajaId);
+    }
+
+    if (_selectedCorte == null) {
+      print("CorteCaja no encontrado para impresión.");
+      return;
+    }
+
+    double fondo = _selectedCorte!.fondo ?? 0;
+    double fondoDiaSig = _selectedCorte!.fondoDiaSig ?? 0;
+    double? ventaEfe = _selectedCorte!.ventaEfe;
+    double? ventaTarj = _selectedCorte!.ventaTarj;
+    double? ventaTransf = _selectedCorte!.ventaTrans;
+    double deposito = _selectedCorte!.deposito!;
+    double retiro = _selectedCorte!.retiro!;
+    double gasto = _selectedCorte!.gasto!;
+    double corteFinal = _selectedCorte!.corteFinal!;
+    DateTime? fechaApertura = _selectedCorte!.fechaApertura;
+
+    await CorteCajaTicket.imprimirTicket(
+      fondo: fondo,
+      fondoDiaSig: fondoDiaSig,
+      ventaEfe: ventaEfe!,
+      ventaTarj: ventaTarj!,
+      ventaTrans: ventaTransf!,
+      deposito: deposito,
+      retiro: retiro,
+      gasto: gasto,
+      corteFinal: corteFinal,
+      fechaApertura: fechaApertura,
+    );
+  }
+
+  @override
+  void dispose() {
+    _ventaEfeController.dispose();
+    _ventaTarjController.dispose();
+    _ventaTransfController.dispose();
+    _totalVentaController.dispose();
+    _fondoController.dispose();
+    corteFinal = 0.0;
+    super.dispose();
+  }
+
+  Map<String, double> obtenerValoresFondo() {
+    double fondo =
+        double.tryParse(fondoController.text.replaceAll(',', '')) ?? 0;
+    double fondoDiaSig =
+        double.tryParse(fondoDiaSigController.text.replaceAll(',', '')) ?? 0;
+    double totalVenta =
+        (double.tryParse(ventaEfeController.text.replaceAll(',', '')) ?? 0) +
+            (double.tryParse(ventaTarjController.text.replaceAll(',', '')) ??
+                0) +
+            (double.tryParse(ventaTransfController.text.replaceAll(',', '')) ??
+                0);
+
+    return {
+      'fondo': fondo,
+      'fondoDiaSig': fondoDiaSig,
+      'totalVenta': totalVenta,
+    };
+  }
 }

+ 51 - 44
lib/viewmodels/login_view_model.dart

@@ -1,7 +1,9 @@
 import 'package:flutter/material.dart';
-import '../data/api_response.dart';
 import '../data/session/session_storage.dart';
+import '../models/models.dart';
 import '../services/login_service.dart';
+import '../services/services.dart';
+import 'package:bcrypt/bcrypt.dart';
 
 enum Status { uninitialized, authenticated, authenticating, unauthenticated }
 
@@ -11,65 +13,80 @@ class LoginViewModel extends ChangeNotifier {
   bool hasErrors = false;
   Map<String, dynamic>? _errores = {};
   bool _obscureText = true;
+  Usuario? _usuario;
   int? _idUsuario;
-  Map<String, dynamic>? get errores => _errores;
+  bool _isLoading = true;
+
+  bool get isLoading => _isLoading;
   bool get obscureText => _obscureText;
-  int? get idUsuario => _idUsuario;
-  //List<String> _permisos = [];
-  //List<String> get permisos => _permisos;
 
-  String _nombre = "";
-  String get nombre => _nombre;
+  // Getters
+  Usuario? get usuario => _usuario;
+  Map<String, dynamic>? get errores => _errores;
+
+  Future<void> login(String correo, String contrasena) async {
+    try {
+      print("Iniciando proceso de login para: $correo");
 
-  String _correo = "";
-  String get correo => _correo;
+      List<Usuario> usuarios = await RepoService<Usuario>().obtenerTodos();
+      Usuario? usuario = usuarios.firstWhere(
+        (usuario) => usuario.correo?.toLowerCase() == correo.toLowerCase(),
+        orElse: () => Usuario(),
+      );
 
-  String _error = "";
-  String get error => _error;
+      if (usuario.id != 0 && usuario.clave != null) {
+        bool esContrasenaValida = BCrypt.checkpw(contrasena, usuario.clave!);
 
-  Future login(String username, String password) async {
-    try {
-      ApiResponse apiResponse = await LoginService().logIn(username, password);
-      _errores = {};
-      if (apiResponse.isOk) {
-        _idUsuario = apiResponse.detalle?['id'];
-        String token = apiResponse.detalle?['token'];
-        if (token.isNotEmpty) {
-          SessionStorage().saveToken(apiResponse.detalle?['token']);
-          SessionStorage().saveId(apiResponse.detalle?['id']);
-          SessionStorage().saveCorreo(apiResponse.detalle!['correo']);
-          SessionStorage().saveNombre(apiResponse.detalle!['nombre']);
+        if (esContrasenaValida) {
           _status = Status.authenticated;
+          _errores = null;
+          _idUsuario = usuario.id;
+          await SessionStorage().saveId(_idUsuario!);
+          notifyListeners();
+        } else {
+          _errores = {'contrasena': 'Contraseña incorrecta'};
+          _status = Status.unauthenticated;
           notifyListeners();
         }
-      }
-      if (apiResponse.isError) {
-        hasErrors = true;
-        _errores = apiResponse.errores;
+      } else {
+        _errores = {'correo': 'El correo no existe en el sistema'};
         _status = Status.unauthenticated;
         notifyListeners();
       }
     } catch (e) {
+      print("Error durante el proceso de login: $e");
       _status = Status.unauthenticated;
       notifyListeners();
     }
   }
 
-  void checkSession() async {
-    var token = await SessionStorage().getToken();
-    var id = await SessionStorage().getId();
-    if (token != null && token.isNotEmpty) {
+  Future<void> checkSession() async {
+    _isLoading = true; // Inicia el estado de carga
+    notifyListeners();
+
+    int? idUsuarioGuardado = await SessionStorage().getId();
+    if (idUsuarioGuardado != null) {
+      print("Sesión encontrada para el ID de usuario: $idUsuarioGuardado");
+      _idUsuario = idUsuarioGuardado;
       _status = Status.authenticated;
-      _idUsuario = id;
     } else {
+      print("No se encontró ninguna sesión activa.");
       _status = Status.unauthenticated;
     }
+
+    _isLoading = false;
     notifyListeners();
   }
 
-  logOut() async {
-    await SessionStorage().clearToken();
+  void logOut() async {
+    print("Cerrando sesión...");
+    _usuario = null;
+    _idUsuario = null;
     _status = Status.unauthenticated;
+    _errores = {};
+
+    await SessionStorage().clearId();
+    print("ID de usuario eliminado de SharedPreferences.");
     notifyListeners();
   }
 
@@ -77,14 +94,4 @@ class LoginViewModel extends ChangeNotifier {
     _obscureText = !_obscureText;
     notifyListeners();
   }
-
-  setValores() async {
-    _nombre = (await SessionStorage().getNombre()).toString();
-    _correo = (await SessionStorage().getCorreo()).toString();
-    notifyListeners();
-  }
-
-  bool validarEmail(String email) =>
-      RegExp(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
-          .hasMatch(email.trim());
 }

+ 12 - 9
lib/viewmodels/pedido_view_model.dart

@@ -60,6 +60,7 @@ class PedidoViewModel extends ChangeNotifier {
           PedidoProductoTopping pedidoProductoTopping = PedidoProductoTopping(
             idPedidoProducto: idPedidoProducto,
             idTopping: topping.idTopping,
+            idCategoria: topping.idCategoria,
           );
           await repoPedidoProductoTopping.guardarLocal(pedidoProductoTopping);
 
@@ -225,8 +226,15 @@ class PedidoViewModel extends ChangeNotifier {
 
   Future<void> cancelarPedido(int idPedido) async {
     var db = await RepoService().db;
-    await db?.update('Pedido', {'estatus': 'CANCELADO'},
-        where: 'id = ?', whereArgs: [idPedido]);
+    await db?.update(
+      'Pedido',
+      {
+        'estatus': 'CANCELADO',
+        'sincronizado': null,
+      },
+      where: 'id = ?',
+      whereArgs: [idPedido],
+    );
     fetchLocalPedidosForScreen();
   }
 
@@ -297,7 +305,8 @@ class PedidoViewModel extends ChangeNotifier {
 }
 
 Future<Map<String, dynamic>> prepararPedidoParaApi(Pedido pedido) async {
-  String? claveSucursal = await obtenerClaveSucursal();
+  String? claveSucursal =
+      await RepoService().obtenerClaveSucursalSeleccionada();
 
   Map<String, dynamic> apiMap = pedido.toApi();
 
@@ -309,9 +318,3 @@ Future<Map<String, dynamic>> prepararPedidoParaApi(Pedido pedido) async {
 
   return apiMap;
 }
-
-Future<String?> obtenerClaveSucursal() async {
-  RepoService<Variable> repoVariable = RepoService<Variable>();
-  Variable? sucursalVariable = await repoVariable.obtenerPorNombre('Sucursal');
-  return sucursalVariable?.clave;
-}

+ 99 - 0
lib/viewmodels/permiso_view_model.dart

@@ -0,0 +1,99 @@
+import 'package:flutter/material.dart';
+import 'package:sqflite/sqflite.dart';
+
+import '../data/api_response.dart';
+import '../data/session/session_storage.dart';
+import '../models/models.dart';
+import '../services/services.dart';
+
+class PermisoViewModel extends ChangeNotifier {
+  List<Permiso> _permisos = [];
+  bool _isLoading = false;
+  Permiso? _selectedPermiso;
+
+  String _busqueda = "";
+  String get busqueda => _busqueda;
+
+  List<Permiso> get permisos => _permisos;
+  bool get isLoading => _isLoading;
+
+  int _currentPage = 1;
+  int _totalPermisos = 0;
+  int _limit = 10;
+
+  int get currentPage => _currentPage;
+  int get totalPermisos => _totalPermisos;
+  int get totalPages => (_totalPermisos / _limit).ceil();
+
+  List<String> _userPermisos = [];
+  List<String> get userPermisos => _userPermisos;
+
+  setBusqueda(String value) {
+    _busqueda = value;
+    notifyListeners();
+  }
+
+  void selectPermiso(Permiso permiso) {
+    _selectedPermiso = permiso;
+    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 Permiso'));
+    _totalPermisos = count ?? 0;
+
+    int offset = (_limit * (page - 1));
+
+    var query = await db.query('Permiso',
+        orderBy: 'id asc', limit: _limit, offset: offset);
+    _permisos = query.map((element) => Permiso.fromJson(element)).toList();
+    notifyListeners();
+  }
+
+  Future<void> sincronizarPermisos() async {
+    _isLoading = true;
+    notifyListeners();
+    try {
+      final response = ApiResponse(await BaseService().get('/pos/permiso'));
+      if (response.isOk && response.resultados != null) {
+        _permisos =
+            response.resultados!.map((json) => Permiso.fromJson(json)).toList();
+        await RepoService().sincronizarPermisos(_permisos);
+      }
+    } catch (e) {
+      print('Error fetching permisos: $e');
+    }
+    _isLoading = false;
+    notifyListeners();
+  }
+
+  Future<void> fetchUserPermisos() async {
+    _isLoading = true;
+    notifyListeners();
+
+    try {
+      int? userId = await SessionStorage().getId();
+      if (userId != null) {
+        var db = await RepoService().db;
+
+        var query = await db!.query(
+          'UsuarioPermiso',
+          where: 'idUsuario = ?',
+          whereArgs: [userId],
+        );
+
+        _userPermisos =
+            query.map((row) => row['idPermiso'].toString()).toList();
+      }
+    } catch (e) {
+      print('Error fetching user permisos: $e');
+    } finally {
+      _isLoading = false;
+      notifyListeners();
+    }
+  }
+}

+ 86 - 9
lib/viewmodels/producto_view_model.dart

@@ -6,6 +6,7 @@ import '../services/base_service.dart';
 import '../models/models.dart';
 import '../services/services.dart';
 import '../services/repo_service.dart';
+import '../views/producto/producto_imagen.dart';
 
 class ProductoViewModel<T> extends ChangeNotifier {
   String _busqueda = "";
@@ -50,7 +51,7 @@ class ProductoViewModel<T> extends ChangeNotifier {
 
     var query = await db.query('Producto',
         where: 'eliminado IS NULL',
-        orderBy: 'idLocal asc',
+        orderBy: 'id asc',
         limit: _limit,
         offset: offset);
     _productos = query.map((element) => Producto.fromJson(element)).toList();
@@ -179,8 +180,18 @@ class ProductoViewModel<T> extends ChangeNotifier {
   }
 
   Future<bool> sincronizarCategorias() async {
+    String? claveSucursal =
+        await RepoService().obtenerClaveSucursalSeleccionada();
+
     try {
-      final response = ApiResponse(await BaseService().get('/pos/categoria'));
+      Map<String, String> parametros = {
+        "claveSucursal": claveSucursal!,
+        "limite": "-1"
+      };
+      final response = ApiResponse(await BaseService()
+          .get('/pos/categoria', queryParameters: parametros));
+
+      //print(response.resultados);
 
       if (response.isOk && response.resultados != null) {
         List<CategoriaProducto> categoriasApi = response.resultados!
@@ -201,8 +212,15 @@ class ProductoViewModel<T> extends ChangeNotifier {
   }
 
   Future<bool> sincronizarProductos() async {
+    String? claveSucursal =
+        await RepoService().obtenerClaveSucursalSeleccionada();
+
     try {
-      Map<String, String> parametros = {"limite": "-1"};
+      Map<String, String> parametros = {
+        "limite": "-1",
+        "claveSucursal": claveSucursal!,
+        "expand": "media"
+      };
       final response = ApiResponse(await BaseService()
           .get('/pos/producto', queryParameters: parametros));
 
@@ -211,10 +229,32 @@ class ProductoViewModel<T> extends ChangeNotifier {
             response.resultados!.map((json) => Producto.fromApi(json)).toList();
 
         if (productosApi.isNotEmpty) {
+          print("Productos API obtenidos: ${productosApi.length}");
+
+          // Aquí mantengo tu lógica de descarga de imágenes.
+          for (var productoApi in productosApi) {
+            if (productoApi.media != null && productoApi.media!.isNotEmpty) {
+              for (var media in productoApi.media!) {
+                // Descargar y guardar la imagen localmente
+                String? localImagePath = await downloadAndStoreImage(
+                    media.ruta!, productoApi.id!, media.nombre!);
+
+                if (localImagePath != null) {
+                  productoApi.imagen = localImagePath;
+                }
+              }
+            }
+          }
+
+          // Delegamos la sincronización de los productos al RepoService
           await RepoService().sincronizarProductos(productosApi);
           notifyListeners();
           return true;
+        } else {
+          print("No se encontraron productos en la API.");
         }
+      } else {
+        print("Error en la respuesta de la API o resultados nulos.");
       }
       return false;
     } catch (e) {
@@ -223,25 +263,62 @@ class ProductoViewModel<T> extends ChangeNotifier {
     }
   }
 
+  Future<bool> sincronizarProductoTopping() async {
+    String? claveSucursal =
+        await RepoService().obtenerClaveSucursalSeleccionada();
+
+    try {
+      Map<String, String> parametros = {
+        "limite": "-1",
+        "claveSucursal": claveSucursal!,
+      };
+
+      final response = ApiResponse(await BaseService()
+          .get('/pos/producto-topping', queryParameters: parametros));
+
+      if (response.isOk && response.resultados != null) {
+        List<ProductoTopping> toppingApi = response.resultados!
+            .map((json) => ProductoTopping.fromApi(json))
+            .toList();
+
+        if (toppingApi.isNotEmpty) {
+          print("Producto Toppings API obtenidos: ${toppingApi.length}");
+
+          // Delegamos la sincronización de los toppings al RepoService
+          await RepoService().sincronizarProductoTopping(toppingApi);
+          notifyListeners();
+          return true;
+        } else {
+          print("No se encontraron toppings de producto en la API.");
+        }
+      } else {
+        print("Error en la respuesta de la API o resultados nulos.");
+      }
+      return false;
+    } catch (e) {
+      print('Error al sincronizar producto toppings: $e');
+      return false;
+    }
+  }
+
   Future<void> sincronizarProductosYCategorias() async {
-    print('Sincronizando productos');
     setIsLoading(true);
     try {
       bool categoriasSincronizadas = await sincronizarCategorias();
-      print('Categorias sincronizadas: $categoriasSincronizadas');
 
       if (categoriasSincronizadas) {
         bool productosSincronizados = await sincronizarProductos();
         if (productosSincronizados) {
-          await fetchLocalAll();
+          bool toppingsSincronizados = await sincronizarProductoTopping();
+          if (toppingsSincronizados) {
+            await fetchLocalAll();
+          }
         }
-        print('Productos sincronizados: $productosSincronizados');
       }
       notifyListeners();
     } catch (e, stackTrace) {
-      // Capturar el mensaje detallado del error
       throw Exception(
-          "Error al sincronizar productos y categorías: $e\n$stackTrace");
+          "Error al sincronizar productos, categorías y toppings: $e\n$stackTrace");
     } finally {
       setIsLoading(false);
     }

+ 77 - 0
lib/viewmodels/sucursal_view_model.dart

@@ -0,0 +1,77 @@
+import 'package:flutter/material.dart';
+
+import '../data/api_response.dart';
+import '../models/models.dart';
+import '../services/services.dart';
+
+class SucursalViewModel<T> extends ChangeNotifier {
+  List<Sucursal> _sucursales = [];
+  bool _isLoading = false;
+
+  List<Sucursal> get sucursales => _sucursales;
+  bool get isLoading => _isLoading;
+
+  Future<void> fetchLocalSucursales() async {
+    var db = await RepoService().db;
+    var query = await db!
+        .query('Sucursal', where: 'eliminado IS NULL', orderBy: 'id asc');
+    _sucursales = query.map((element) => Sucursal.fromJson(element)).toList();
+    notifyListeners();
+  }
+
+  Future<void> setSelectedSucursal(Sucursal sucursal) async {
+    var db = await RepoService().db;
+
+    await db!.update(
+      'Sucursal',
+      {'seleccionado': 0},
+    );
+
+    sucursal.seleccionado = 1;
+    await RepoService().guardar(sucursal);
+
+    await fetchLocalSucursales();
+  }
+
+  Future<bool> sincronizarSucursales() async {
+    try {
+      final response = ApiResponse(await BaseService().get('/pos/sucursal'));
+
+      if (response.isOk && response.resultados != null) {
+        List<Sucursal> sucursalesApi =
+            response.resultados!.map((json) => Sucursal.fromApi(json)).toList();
+
+        if (sucursalesApi.isNotEmpty) {
+          await RepoService().sincronizarSucursales(sucursalesApi);
+          await fetchLocalSucursales();
+          notifyListeners();
+          return true;
+        }
+      }
+      return false;
+    } catch (e) {
+      print('Error al sincronizar sucursales: $e');
+      return false;
+    }
+  }
+
+  Future<void> sincronizarSucursalesDesdeApi() async {
+    setIsLoading(true);
+    try {
+      bool sucursalesSincronizadas = await sincronizarSucursales();
+      if (sucursalesSincronizadas) {
+        await fetchLocalSucursales();
+      }
+      print('Sucursales sincronizadas: $sucursalesSincronizadas');
+    } catch (e, stackTrace) {
+      print("Error al sincronizar sucursales: $e\n$stackTrace");
+    } finally {
+      setIsLoading(false);
+    }
+  }
+
+  void setIsLoading(bool loading) {
+    _isLoading = loading;
+    notifyListeners();
+  }
+}

+ 41 - 133
lib/viewmodels/usuarios_view_model.dart

@@ -1,158 +1,66 @@
 import 'dart:convert';
 
 import 'package:flutter/material.dart';
+import 'package:sqflite/sqflite.dart';
 
+import '../data/api_response.dart';
 import '../models/models.dart';
 import '../services/base_service.dart';
+import '../services/repo_service.dart';
 
-class UsuariosViewModel extends ChangeNotifier {
-  String _busqueda = "";
-  String get busqueda => _busqueda;
-  List<Usuario> _registros = [];
-  List<Usuario> get registros => _registros;
-  Usuario? _selectedModelo;
-  Usuario? get selectedModelo => _selectedModelo;
+class UsuarioViewModel extends ChangeNotifier {
+  List<Usuario> _usuarios = [];
   bool _isLoading = false;
-  bool get isLoading => _isLoading;
-
-  Map<String, bool> _selectedPermisos = {};
-  Map<String, bool> get selectedPermisos => _selectedPermisos;
 
-  Future<void> fetchRegistros() async {
-    Map<String, String> parametros = {
-      "ordenar": "id-desc",
-      "expand": "empresas"
-    };
-    if (_busqueda.isNotEmpty) {
-      parametros = {
-        "ordenar": "id-desc",
-        "limite": "-1",
-        "q": _busqueda,
-        "expand": "empresas"
-      };
-    }
-    var r =
-        await BaseService().get("admin/usuario", queryParameters: parametros);
-    Map<String, dynamic> resJson = jsonDecode(r.body);
-    try {
-      List<Usuario> aux = [];
-      if (r.statusCode == 200) {
-        for (var x in resJson['resultado']) {
-          Usuario modelo = Usuario.fromJson(x);
-          aux.add(modelo);
-        }
-      }
-      _registros = aux;
-      notifyListeners();
-    } catch (e) {
-      print(e);
-    }
-  }
+  List<Usuario> get usuarios => _usuarios;
+  bool get isLoading => _isLoading;
 
-  Future<void> cargarPermisosUsuario(int usuarioId) async {
-    setIsLoading(true);
-    try {
-      var response = await BaseService().get("admin/usuario",
-          queryParameters: {'expand': 'permisos', 'id': '$usuarioId'});
-      Map<String, dynamic> resJson = jsonDecode(response.body);
+  int _currentPage = 1;
+  int _totalProducts = 0;
+  int _limit = 10;
 
-      List<dynamic> permisosUsuario = resJson['resultado'][0]['permisos'];
-      Map<String, bool> permisosActualizados = {};
+  int get currentPage => _currentPage;
+  int get totalProducts => _totalProducts;
+  int get totalPages => (_totalProducts / _limit).ceil();
 
-      for (var permiso in permisosUsuario) {
-        String permisoId = permiso.toString();
-        permisosActualizados[permisoId] = true;
-      }
+  Future<void> fetchLocalAll({int page = 1}) async {
+    _currentPage = page;
+    var db = await RepoService().db;
+    int? count = Sqflite.firstIntValue(
+        await db!.rawQuery('SELECT COUNT(*) FROM Usuario'));
+    _totalProducts = count ?? 0;
 
-      _selectedPermisos = permisosActualizados;
-      // print(_selectedPermisos);
-      notifyListeners(); // Avisar a los widgets que escuchan sobre el cambio
-    } catch (e) {
-      print("Error al cargar los permisos del usuario: $e");
-    } finally {
-      setIsLoading(false);
-    }
-  }
+    int offset = (_limit * (page - 1));
 
-  void setSelectedPermisos(Map<String, bool> permisos) {
-    _selectedPermisos = permisos;
-    notifyListeners();
-  }
+    var query = await db.query('Usuario',
+        where: 'eliminado IS NULL',
+        orderBy: 'id asc',
+        limit: _limit,
+        offset: offset);
+    _usuarios = query.map((element) => Usuario.fromJson(element)).toList();
 
-  Future<void> guardarModelo({
-    required Usuario modelo,
-    required String nombre,
-    required String correo,
-    required String telefono,
-  }) async {
-    //?TEMPORAL
-    modelo.nombre = nombre;
-    modelo.correo = correo;
-    modelo.telefono = telefono;
-    modelo.creado = DateTime.now();
     notifyListeners();
   }
 
-  Future<void> guardarUsuario({
-    required Usuario modelo,
-    required String nombre,
-    required String correo,
-    required List<String> permisosSeleccionados,
-  }) async {
-    setIsLoading(true);
+  Future<bool> sincronizarUsuarios() async {
     try {
-      var requestBody = {
-        'nombre': nombre,
-        'correo': correo,
-        'permisos': permisosSeleccionados,
-      };
-
-      if (modelo.id != null) {
-        requestBody['id'] = modelo.id;
-      }
-
-      var r =
-          await BaseService().post("admin/usuario/guardar", body: requestBody);
-      if (r.statusCode == 200) {
-        print('Usuario guardado exitosamente');
-      } else {
-        throw Exception('Error al guardar el usuario: ${r.statusCode}');
+      Map<String, String> parametros = {"expand": 'permisos'};
+      final response = ApiResponse(
+          await BaseService().get('/pos/usuario', queryParameters: parametros));
+
+      if (response.isOk && response.resultados != null) {
+        List<Usuario> usuariosApi =
+            response.resultados!.map((json) => Usuario.fromApi(json)).toList();
+        if (usuariosApi.isNotEmpty) {
+          await RepoService().sincronizarUsuarios(usuariosApi);
+          notifyListeners();
+          return true;
+        }
       }
+      return false;
     } catch (e) {
-      print('Error al guardar el usuario: $e');
-    } finally {
-      setIsLoading(false);
-    }
-  }
-
-  bool validarUsuario({
-    required String nombre,
-    required String correo,
-  }) {
-    if (nombre.isEmpty || correo.isEmpty) {
+      print('Error al sincronizar usuarios: $e');
       return false;
     }
-    return true;
-  }
-
-  setIsLoading(bool loading) {
-    _isLoading = loading;
-    notifyListeners();
-  }
-
-  selectUsuario(Usuario? usuario) {
-    _selectedModelo = usuario;
-    notifyListeners();
-  }
-
-  setBusqueda(String value) {
-    _busqueda = value;
-    notifyListeners();
-  }
-
-  Future usuarioEmpresa(int idUsuario, String idEmpresa) async {
-    var r = await BaseService().post("admin/usuario/usuario-empresa",
-        body: {"idUsuario": idUsuario, "idEmpresa": idEmpresa});
-    if (r.statusCode == 200) {}
   }
 }

+ 2 - 0
lib/viewmodels/viewmodels.dart

@@ -8,3 +8,5 @@ export '../viewmodels/pedido_view_model.dart';
 export '../viewmodels/corte_caja_view_model.dart';
 export '../viewmodels/descuento_view_model.dart';
 export '../viewmodels/variable_view_model.dart';
+export '../viewmodels/sucursal_view_model.dart';
+export '../viewmodels/permiso_view_model.dart';

+ 211 - 0
lib/views/corte_caja/corte_caja_finalizado_screen.dart

@@ -0,0 +1,211 @@
+import 'package:conalep_pos/widgets/widgets.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:intl/intl.dart';
+import '../../viewmodels/corte_caja_view_model.dart';
+import '../../themes/themes.dart';
+
+class CorteCajaFinalizadoScreen extends StatefulWidget {
+  final String? corteCajaId;
+
+  const CorteCajaFinalizadoScreen({this.corteCajaId, Key? key})
+      : super(key: key);
+
+  @override
+  _CorteCajaFinalizadoScreenState createState() =>
+      _CorteCajaFinalizadoScreenState();
+}
+
+class _CorteCajaFinalizadoScreenState extends State<CorteCajaFinalizadoScreen> {
+  Map<String, bool> _expandedSections = {};
+
+  @override
+  void initState() {
+    super.initState();
+    final viewModel = Provider.of<CorteCajaViewModel>(context, listen: false);
+    viewModel.fetchDepositosAndRetiros(widget.corteCajaId!);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final viewModel = Provider.of<CorteCajaViewModel>(context);
+    double totalEfe = viewModel.selectedCorte!.ventaEfe!;
+    double totalTarj = viewModel.selectedCorte!.ventaTarj!;
+    double totalTrans = viewModel.selectedCorte!.ventaTrans!;
+    double totalRetiro = viewModel.selectedCorte!.retiro!;
+    double totalGasto = viewModel.selectedCorte!.gasto!;
+    double totalSalidas = totalRetiro + totalGasto;
+    double totalVentas = totalEfe + totalTrans + totalTarj;
+
+    return Scaffold(
+      appBar: AppBar(
+        title: Text('Resumen Corte de Caja',
+            style: TextStyle(color: AppTheme.quaternary)),
+        iconTheme: IconThemeData(color: AppTheme.quaternary),
+      ),
+      body: SingleChildScrollView(
+        child: Padding(
+          padding: EdgeInsets.symmetric(horizontal: 50, vertical: 20),
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Text(
+                "Fecha: ${DateFormat('dd/MM/yyyy').format(viewModel.selectedCorte!.fechaApertura!)}",
+                style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
+              ),
+              SizedBox(height: 20),
+              tarjeta(Column(
+                children: [
+                  Table(
+                    defaultVerticalAlignment: TableCellVerticalAlignment.middle,
+                    columnWidths: {
+                      0: FlexColumnWidth(4),
+                      1: FlexColumnWidth(2)
+                    },
+                    children: [
+                      _buildTableRow("Fondo", viewModel.selectedCorte?.fondo),
+                      _buildTableRow("Fondo día siguiente",
+                          viewModel.selectedCorte?.fondoDiaSig),
+                      _buildTableRow("Ventas Efectivo", totalEfe),
+                      _buildTableRow("Ventas Tarjeta", totalTarj),
+                      _buildTableRow("Ventas Transferencia", totalTrans),
+                    ],
+                  ),
+                  _buildExpandableTable("Depósitos", viewModel.depositos,
+                      viewModel.selectedCorte?.deposito),
+                  _buildExpandableTable(
+                      "Retiros", viewModel.retiros, totalRetiro),
+                  _buildExpandableTable("Gastos", viewModel.gastos, totalGasto),
+                  Table(
+                    defaultVerticalAlignment: TableCellVerticalAlignment.middle,
+                    columnWidths: {
+                      0: FlexColumnWidth(4),
+                      1: FlexColumnWidth(2)
+                    },
+                    children: [
+                      _buildTableRow('', null),
+                      _buildTableRow('Venta total del día', totalVentas),
+                      _buildTableRow("Efectivo en caja",
+                          viewModel.selectedCorte?.corteFinal),
+                      _buildTableRow("Salidas totales del día", totalSalidas),
+                    ],
+                  ),
+                ],
+              )),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+
+  TableRow _buildTableRow(String label, double? value) {
+    return TableRow(
+      children: [
+        Container(
+          decoration: BoxDecoration(border: Border.all(color: Colors.grey)),
+          padding: const EdgeInsets.all(8.5),
+          child: Text(label,
+              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
+        ),
+        Container(
+          decoration: BoxDecoration(border: Border.all(color: Colors.grey)),
+          height: 45,
+          padding: const EdgeInsets.all(8.0),
+          child: Text(
+            value != null ? "\$${value.toStringAsFixed(2)}" : "",
+            style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
+          ),
+        ),
+      ],
+    );
+  }
+
+  Widget _buildExpandableTable(
+      String title, List<dynamic> items, double? total) {
+    final isExpanded = _expandedSections[title] ?? true;
+
+    return Column(
+      children: [
+        GestureDetector(
+          onTap: () {
+            setState(() {
+              _expandedSections[title] = !isExpanded;
+            });
+          },
+          child: Container(
+            child: Table(
+              defaultVerticalAlignment: TableCellVerticalAlignment.middle,
+              columnWidths: {0: FlexColumnWidth(4), 1: FlexColumnWidth(2)},
+              children: [
+                TableRow(
+                  children: [
+                    Container(
+                      padding: const EdgeInsets.all(8.5),
+                      decoration:
+                          BoxDecoration(border: Border.all(color: Colors.grey)),
+                      child: Text(
+                        title,
+                        style: TextStyle(
+                            fontWeight: FontWeight.bold, fontSize: 18),
+                      ),
+                    ),
+                    Container(
+                      padding: const EdgeInsets.all(8.5),
+                      decoration:
+                          BoxDecoration(border: Border.all(color: Colors.grey)),
+                      child: Row(
+                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                        children: [
+                          Text(
+                            total != null
+                                ? "\$${total.toStringAsFixed(2)}"
+                                : "",
+                            style: TextStyle(
+                                fontWeight: FontWeight.bold, fontSize: 18),
+                          ),
+                          Icon(isExpanded
+                              ? Icons.expand_less
+                              : Icons.expand_more),
+                        ],
+                      ),
+                    ),
+                  ],
+                ),
+              ],
+            ),
+          ),
+        ),
+        if (isExpanded)
+          Table(
+            defaultVerticalAlignment: TableCellVerticalAlignment.middle,
+            columnWidths: {0: FlexColumnWidth(4), 1: FlexColumnWidth(2)},
+            children: items.map((item) {
+              return TableRow(
+                children: [
+                  Container(
+                    padding: const EdgeInsets.all(8.5),
+                    decoration:
+                        BoxDecoration(border: Border.all(color: Colors.grey)),
+                    child: Text(" -${item.descripcion}",
+                        style: TextStyle(
+                            fontSize: 16, fontWeight: FontWeight.w600)),
+                  ),
+                  Container(
+                    padding: const EdgeInsets.all(8.5),
+                    decoration:
+                        BoxDecoration(border: Border.all(color: Colors.grey)),
+                    child: Text(
+                      "\$${item.monto?.toStringAsFixed(2)}",
+                      style:
+                          TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
+                    ),
+                  ),
+                ],
+              );
+            }).toList(),
+          ),
+      ],
+    );
+  }
+}

+ 507 - 0
lib/views/corte_caja/corte_caja_form copy.dart

@@ -0,0 +1,507 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:conalep_pos/themes/themes.dart';
+import 'package:intl/intl.dart';
+import '../../viewmodels/corte_caja_view_model.dart';
+import '../../widgets/widgets.dart';
+
+class CorteCajaForm extends StatefulWidget {
+  final String? corteCajaId;
+
+  const CorteCajaForm({this.corteCajaId, Key? key}) : super(key: key);
+
+  @override
+  _CorteCajaFormState createState() => _CorteCajaFormState();
+}
+
+class _CorteCajaFormState extends State<CorteCajaForm> {
+  final TextEditingController _fondoController = TextEditingController();
+  final TextEditingController _fondoDiaSigContr = TextEditingController();
+  final TextEditingController _ventaEfeContr = TextEditingController();
+  final TextEditingController _ventaTarjContr = TextEditingController();
+  final TextEditingController _ventaTransfContr = TextEditingController();
+  final TextEditingController _totalVentaContr = TextEditingController();
+  final TextEditingController _gastosContr = TextEditingController();
+  final TextEditingController _montoDepositoController =
+      TextEditingController();
+  final TextEditingController _montoRetiroController = TextEditingController();
+  final TextEditingController _descripcionDepositoController =
+      TextEditingController();
+  final TextEditingController _descripcionRetiroController =
+      TextEditingController();
+  final TextEditingController _personaDepositoContr = TextEditingController();
+  final TextEditingController _personaRetiroContr = TextEditingController();
+
+  bool showDepositoPanel = false;
+  bool showRetiroPanel = false;
+
+  @override
+  void initState() {
+    super.initState();
+    if (widget.corteCajaId != null) {
+      Provider.of<CorteCajaViewModel>(context, listen: false)
+          .fetchDepositosAndRetiros(widget.corteCajaId!);
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final viewModel = Provider.of<CorteCajaViewModel>(context);
+
+    return Scaffold(
+      appBar: AppBar(
+        title: Text('Corte de Caja Conalep',
+            style: TextStyle(color: AppTheme.quaternary)),
+        iconTheme: IconThemeData(color: AppTheme.quaternary),
+      ),
+      body: Column(children: [
+        Container(
+          padding: EdgeInsets.symmetric(horizontal: 50, vertical: 20),
+          width: MediaQuery.of(context).size.width,
+          child: Row(
+            crossAxisAlignment: CrossAxisAlignment.center,
+            mainAxisAlignment: MainAxisAlignment.center,
+            children: [
+              Expanded(
+                flex: 4,
+                child: _buildFormContent(context, viewModel),
+              ),
+              SizedBox(width: 80),
+              Expanded(
+                flex: 3,
+                child: Column(
+                  children: [
+                    if (showDepositoPanel) _buildDepositoPanel(viewModel),
+                    if (showRetiroPanel) _buildRetiroPanel(viewModel),
+                  ],
+                ),
+              ),
+            ],
+          ),
+        )
+      ]),
+    );
+  }
+
+  Widget _buildFormContent(BuildContext context, CorteCajaViewModel viewModel) {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Text(
+          "Fecha: ${DateFormat('dd/MM/yyyy').format(DateTime.now())}",
+          style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
+        ),
+        SizedBox(height: 20),
+        tarjeta(
+          Table(
+            defaultVerticalAlignment: TableCellVerticalAlignment.middle,
+            columnWidths: {0: FlexColumnWidth(4), 1: FlexColumnWidth(2)},
+            children: [
+              _buildTableRow("Fondo", _fondoController),
+              _buildDepositoRow(viewModel),
+              _buildTableRow("Fondo día siguiente", _fondoDiaSigContr),
+              _buildTableRow("Ventas efectivo", _ventaEfeContr),
+              _buildTableRow("Ventas tarjeta", _ventaTarjContr),
+              _buildTableRow("Ventas transferencia", _ventaTransfContr),
+              _buildTableRow("Total ventas", _totalVentaContr),
+              _buildRetiroRow(viewModel),
+              _buildTableRow("Gastos del día", _gastosContr),
+              _buildCorteFinalRow(viewModel),
+            ],
+          ),
+        ),
+        SizedBox(height: 20),
+        Center(
+          child: ElevatedButton(
+            onPressed: () {
+              viewModel.calcularCorteFinal(
+                double.tryParse(_fondoController.text) ?? 0,
+                double.tryParse(_fondoDiaSigContr.text) ?? 0,
+                double.tryParse(_totalVentaContr.text) ?? 0,
+              );
+            },
+            child:
+                Text('Finalizar', style: TextStyle(color: AppTheme.quaternary)),
+            style: ElevatedButton.styleFrom(
+              primary: AppTheme.primary,
+              padding: EdgeInsets.symmetric(horizontal: 50, vertical: 15),
+              shape: RoundedRectangleBorder(
+                borderRadius: BorderRadius.circular(10),
+              ),
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+
+  TableRow _buildTableRow(String label, TextEditingController controller) {
+    return TableRow(
+      children: [
+        Container(
+          decoration: BoxDecoration(
+            border: Border.all(color: Colors.grey),
+          ),
+          padding: const EdgeInsets.all(11.5),
+          child: Text(label, style: TextStyle(fontWeight: FontWeight.bold)),
+        ),
+        Container(
+          decoration: BoxDecoration(
+            border: Border.all(color: Colors.grey),
+          ),
+          height: 45,
+          padding: const EdgeInsets.all(8.0),
+          child: TextField(
+            controller: controller,
+            keyboardType: TextInputType.number,
+            decoration: InputDecoration(
+              hintText: "\$",
+              border: InputBorder.none,
+              contentPadding: EdgeInsets.symmetric(vertical: 8, horizontal: 12),
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+
+  TableRow _buildDepositoRow(CorteCajaViewModel viewModel) {
+    return TableRow(
+      children: [
+        Container(
+          decoration: BoxDecoration(
+            border: Border.all(color: Colors.grey),
+          ),
+          padding: const EdgeInsets.all(11.5),
+          child:
+              Text("Depósitos", style: TextStyle(fontWeight: FontWeight.bold)),
+        ),
+        Container(
+          decoration: BoxDecoration(
+            border: Border.all(color: Colors.grey),
+          ),
+          height: 45, // Ajuste de altura fija
+          child: Row(
+            children: [
+              Expanded(
+                child: Container(
+                  alignment: Alignment.center,
+                  padding: EdgeInsets.symmetric(horizontal: 12),
+                  child:
+                      Text("\$${viewModel.totalDepositos.toStringAsFixed(2)}"),
+                ),
+              ),
+              IconButton(
+                icon: Icon(Icons.add, color: AppTheme.primary),
+                onPressed: () {
+                  setState(() {
+                    showDepositoPanel = true;
+                    showRetiroPanel = false;
+                  });
+                },
+              ),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+
+  TableRow _buildRetiroRow(CorteCajaViewModel viewModel) {
+    return TableRow(
+      children: [
+        Container(
+          decoration: BoxDecoration(
+            border: Border.all(color: Colors.grey),
+          ),
+          padding: const EdgeInsets.all(11.5),
+          child: Text("Retiros", style: TextStyle(fontWeight: FontWeight.bold)),
+        ),
+        Container(
+          decoration: BoxDecoration(
+            border: Border.all(color: Colors.grey),
+          ),
+          height: 45,
+          child: Row(
+            children: [
+              Expanded(
+                child: Container(
+                  alignment: Alignment.center,
+                  padding: EdgeInsets.symmetric(horizontal: 12),
+                  child: Text("\$${viewModel.totalRetiros.toStringAsFixed(2)}"),
+                ),
+              ),
+              IconButton(
+                icon: Icon(Icons.add, color: AppTheme.primary),
+                onPressed: () {
+                  setState(() {
+                    showRetiroPanel = true;
+                    showDepositoPanel = false;
+                  });
+                },
+              ),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+
+  Widget _buildDepositoPanel(CorteCajaViewModel viewModel) {
+    return tarjeta(
+      Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Text(
+            "Depósitos",
+            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
+          ),
+          SizedBox(height: 10),
+          TextField(
+            controller: _montoDepositoController,
+            decoration: InputDecoration(
+              labelText: "Monto del depósito",
+              border: OutlineInputBorder(),
+            ),
+            keyboardType: TextInputType.number,
+          ),
+          SizedBox(height: 10),
+          TextField(
+            controller: _descripcionDepositoController,
+            decoration: InputDecoration(
+              labelText: "Descripción del depósito",
+              border: OutlineInputBorder(),
+            ),
+          ),
+          SizedBox(height: 10),
+          TextField(
+            controller: _personaDepositoContr,
+            decoration: InputDecoration(
+              labelText: "Persona",
+              border: OutlineInputBorder(),
+            ),
+          ),
+          SizedBox(height: 20),
+          // Lista de depósitos
+          Text("Lista de Depósitos",
+              style: TextStyle(fontWeight: FontWeight.bold)),
+          ListView.builder(
+            shrinkWrap: true,
+            itemCount: viewModel.depositos.length,
+            itemBuilder: (context, index) {
+              final deposito = viewModel.depositos[index];
+              if (deposito.eliminado == null) {
+                return ListTile(
+                  title: Text(
+                      "\$${deposito.monto?.toStringAsFixed(2)} - ${deposito.descripcion}"),
+                  trailing: IconButton(
+                    icon: Icon(Icons.remove_circle_outline, color: Colors.red),
+                    onPressed: () {
+                      viewModel.eliminarDeposito(deposito.id!);
+                    },
+                  ),
+                );
+              }
+              return SizedBox.shrink();
+            },
+          ),
+          Align(
+            alignment: Alignment.centerRight,
+            child: Text(
+                "Total: \$${viewModel.totalDepositos.toStringAsFixed(2)}",
+                style: TextStyle(fontWeight: FontWeight.bold)),
+          ),
+          SizedBox(height: 20),
+          Row(
+            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            children: [
+              ElevatedButton(
+                onPressed: () {
+                  double monto =
+                      double.tryParse(_montoDepositoController.text) ?? 0;
+                  String descripcion = _descripcionDepositoController.text;
+                  String persona = _personaDepositoContr.text;
+
+                  viewModel.addDeposito(
+                      monto, descripcion, persona, widget.corteCajaId!);
+                  _montoDepositoController.clear();
+                  _descripcionDepositoController.clear();
+                  _personaDepositoContr.clear();
+                },
+                child: Text(
+                  "Agregar",
+                  style: TextStyle(color: AppTheme.quaternary),
+                ),
+                style: ElevatedButton.styleFrom(
+                  backgroundColor: AppTheme.primary,
+                  padding: EdgeInsets.symmetric(horizontal: 50, vertical: 15),
+                  shape: RoundedRectangleBorder(
+                    borderRadius: BorderRadius.circular(10),
+                  ),
+                ),
+              ),
+              ElevatedButton(
+                onPressed: () {
+                  setState(() {
+                    showDepositoPanel = false;
+                  });
+                },
+                child: Text(
+                  "Finalizar",
+                  style: TextStyle(color: AppTheme.quaternary),
+                ),
+                style: ElevatedButton.styleFrom(
+                  backgroundColor: AppTheme.primary,
+                  padding: EdgeInsets.symmetric(horizontal: 50, vertical: 15),
+                  shape: RoundedRectangleBorder(
+                    borderRadius: BorderRadius.circular(10),
+                  ),
+                ),
+              ),
+            ],
+          ),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildRetiroPanel(CorteCajaViewModel viewModel) {
+    return tarjeta(
+      Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Text(
+            "Retiros",
+            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
+          ),
+          SizedBox(height: 10),
+          TextField(
+            controller: _montoRetiroController,
+            decoration: InputDecoration(
+              labelText: "Monto del retiro",
+              border: OutlineInputBorder(),
+            ),
+            keyboardType: TextInputType.number,
+          ),
+          SizedBox(height: 10),
+          TextField(
+            controller: _descripcionRetiroController,
+            decoration: InputDecoration(
+              labelText: "Descripción del depósito",
+              border: OutlineInputBorder(),
+            ),
+          ),
+          SizedBox(height: 10),
+          TextField(
+            controller: _personaRetiroContr,
+            decoration: InputDecoration(
+              labelText: "Persona",
+              border: OutlineInputBorder(),
+            ),
+          ),
+          SizedBox(height: 20),
+          // Lista de depósitos
+          Text("Lista de Retiros",
+              style: TextStyle(fontWeight: FontWeight.bold)),
+          ListView.builder(
+            shrinkWrap: true,
+            itemCount: viewModel.retiros.length,
+            itemBuilder: (context, index) {
+              final retiro = viewModel.retiros[index];
+              if (retiro.eliminado == null) {
+                return ListTile(
+                  title: Text(
+                      "\$${retiro.monto?.toStringAsFixed(2)} - ${retiro.descripcion}"),
+                  trailing: IconButton(
+                    icon: Icon(Icons.remove_circle_outline, color: Colors.red),
+                    onPressed: () {
+                      viewModel.eliminarRetiro(retiro.id!);
+                    },
+                  ),
+                );
+              }
+              return SizedBox.shrink();
+            },
+          ),
+          Align(
+            alignment: Alignment.centerRight,
+            child: Text("Total: \$${viewModel.totalRetiros.toStringAsFixed(2)}",
+                style: TextStyle(fontWeight: FontWeight.bold)),
+          ),
+          SizedBox(height: 20),
+          Row(
+            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            children: [
+              ElevatedButton(
+                onPressed: () {
+                  double monto =
+                      double.tryParse(_montoRetiroController.text) ?? 0;
+                  String descripcion = _descripcionRetiroController.text;
+                  String persona = _personaRetiroContr.text;
+
+                  viewModel.addRetiro(
+                      monto, descripcion, persona, widget.corteCajaId!);
+                  _montoRetiroController.clear();
+                  _descripcionRetiroController.clear();
+                },
+                child: Text(
+                  "Agregar",
+                  style: TextStyle(color: AppTheme.quaternary),
+                ),
+                style: ElevatedButton.styleFrom(
+                  backgroundColor: AppTheme.primary,
+                  padding: EdgeInsets.symmetric(horizontal: 50, vertical: 15),
+                  shape: RoundedRectangleBorder(
+                    borderRadius: BorderRadius.circular(10),
+                  ),
+                ),
+              ),
+              ElevatedButton(
+                onPressed: () {
+                  setState(() {
+                    showRetiroPanel = false;
+                  });
+                },
+                child: Text(
+                  "Finalizar",
+                  style: TextStyle(color: AppTheme.quaternary),
+                ),
+                style: ElevatedButton.styleFrom(
+                  backgroundColor: AppTheme.primary,
+                  padding: EdgeInsets.symmetric(horizontal: 50, vertical: 15),
+                  shape: RoundedRectangleBorder(
+                    borderRadius: BorderRadius.circular(10),
+                  ),
+                ),
+              ),
+            ],
+          ),
+        ],
+      ),
+    );
+  }
+
+  TableRow _buildCorteFinalRow(CorteCajaViewModel viewModel) {
+    return TableRow(
+      children: [
+        Container(
+          decoration: BoxDecoration(
+            border: Border.all(color: Colors.grey),
+          ),
+          padding: EdgeInsets.all(11.5),
+          child: Text("Corte Final",
+              style: TextStyle(fontWeight: FontWeight.bold)),
+        ),
+        Container(
+          decoration: BoxDecoration(
+            border: Border.all(color: Colors.grey),
+          ),
+          height: 45, // Ajuste de altura fija
+          padding: const EdgeInsets.all(8.0),
+          child: Text("\$${viewModel.corteFinal.toStringAsFixed(2)}",
+              style: TextStyle(fontWeight: FontWeight.bold)),
+        ),
+      ],
+    );
+  }
+}

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 938 - 321
lib/views/corte_caja/corte_caja_form.dart


+ 58 - 64
lib/views/corte_caja/corte_caja_screen.dart

@@ -1,4 +1,5 @@
 import 'package:conalep_pos/views/corte_caja/corte_caja_form.dart';
+import 'package:conalep_pos/widgets/widgets.dart';
 import 'package:flutter/material.dart';
 import 'package:intl/intl.dart';
 import 'package:omni_datetime_picker/omni_datetime_picker.dart';
@@ -8,6 +9,7 @@ import '../../models/models.dart';
 import '../../viewmodels/viewmodels.dart';
 import '../../widgets/app_textfield.dart';
 import '../../widgets/widgets_components.dart' as clase;
+import 'corte_caja_finalizado_screen.dart';
 
 class CorteCajaScreen extends StatefulWidget {
   const CorteCajaScreen({Key? key}) : super(key: key);
@@ -40,19 +42,17 @@ class _CorteCajaScreenState extends State<CorteCajaScreen> {
   }
 
   void go(CorteCaja item) async {
-    // CorteCaja? CorteCajaCompleto =
-    //     await Provider.of<CorteCajaViewModel>(context, listen: false)
-    //         .fetchCorteCajaConProductos(item.id);
-    // if (CorteCajaCompleto != null) {
-    //   // Navigator.push(
-    //   //   context,
-    //   //   MaterialPageRoute(
-    //   //     builder: (context) => CorteCajaDetalleScreen(CorteCaja: CorteCajaCompleto),
-    //   //   ),
-    //   // );
-    // } else {
-    //   print("Error al cargar el CorteCaja con productos");
-    // }
+    final viewModel = Provider.of<CorteCajaViewModel>(context, listen: false);
+    viewModel.selectCorte(item);
+
+    await Navigator.push(
+      context,
+      MaterialPageRoute(
+        builder: (context) => item.fechaCorte != null
+            ? CorteCajaFinalizadoScreen(corteCajaId: item.id)
+            : CorteCajaForm(corteCajaId: item.id),
+      ),
+    ).then((_) => viewModel.fetchCortes());
   }
 
   @override
@@ -63,6 +63,7 @@ class _CorteCajaScreenState extends State<CorteCajaScreen> {
     final double? columnSpacing = isMobile ? null : 0;
     TextStyle estilo = const TextStyle(fontWeight: FontWeight.bold);
     List<DataRow> registros = [];
+
     for (CorteCaja item in pvm.cortes) {
       registros.add(DataRow(cells: [
         DataCell(
@@ -73,60 +74,35 @@ class _CorteCajaScreenState extends State<CorteCajaScreen> {
                 child: const Text('Editar'),
                 onTap: () => go(item),
               ),
-              PopupMenuItem(
-                child: const Text('Cancelar CorteCaja'),
-                onTap: () async {
-                  bool confirmado = await showDialog<bool>(
-                        context: context,
-                        builder: (context) {
-                          return AlertDialog(
-                            title: const Text("Cancelar CorteCaja"),
-                            content: const Text(
-                                '¿Estás seguro de que deseas cancelar este CorteCaja?'),
-                            actions: [
-                              TextButton(
-                                onPressed: () =>
-                                    Navigator.of(context).pop(false),
-                                child: const Text('No'),
-                              ),
-                              TextButton(
-                                onPressed: () =>
-                                    Navigator.of(context).pop(false),
-                                child: const Text('Sí'),
-                              ),
-                            ],
-                          );
-                        },
-                      ) ??
-                      false;
-
-                  if (confirmado) {
-                    // bool result = await Provider.of<CorteCajaViewModel>(context,
-                    //         listen: false)
-                    //     .cancelarCorteCaja(item.id!);
-                    // if (result) {
-                    //   ScaffoldMessenger.of(context).showSnackBar(
-                    //     SnackBar(
-                    //         content: Text("CorteCaja cancelado correctamente")),
-                    //   );
-                    // } else {
-                    //   ScaffoldMessenger.of(context).showSnackBar(
-                    //     SnackBar(content: Text("Error al cancelar el CorteCaja")),
-                    //   );
-                    // }
-                  }
-                },
-              )
+              if (item.fechaCorte != null)
+                PopupMenuItem(
+                  child: const Text('Imprimir Ticket'),
+                  onTap: () async {
+                    final viewModel =
+                        Provider.of<CorteCajaViewModel>(context, listen: false);
+                    viewModel.selectCorte(item);
+                    await viewModel.imprimirCorteCajaTicket(
+                        corteCajaId: item.id);
+                  },
+                ),
             ],
             icon: const Icon(Icons.more_vert),
           ),
         ])),
         DataCell(
-          Text(item.id.toString()),
+          Text(item.fechaApertura != null
+              ? DateFormat("dd/MM/yyyy").format(item.fechaApertura!)
+              : "Sin fecha"),
           onTap: () => go(item),
         ),
         DataCell(
-          Text(item.fecha ?? "Sin nombre"),
+          Text(item.fechaCorte != null
+              ? DateFormat("dd/MM/yyyy").format(item.fechaCorte!)
+              : "Sin fecha"),
+          onTap: () => go(item),
+        ),
+        DataCell(
+          Text(formatoMiles(item.corteFinal)),
           onTap: () => go(item),
         ),
       ]));
@@ -141,13 +117,26 @@ class _CorteCajaScreenState extends State<CorteCajaScreen> {
           iconTheme: IconThemeData(color: AppTheme.quaternary)),
       floatingActionButton: FloatingActionButton.extended(
         onPressed: () async {
+          final viewModel =
+              Provider.of<CorteCajaViewModel>(context, listen: false);
+
+          if (viewModel.hasOpenCorteCaja()) {
+            alerta(context,
+                etiqueta:
+                    'No se puede crear un nuevo corte hasta que el anterior esté cerrado.');
+            return;
+          }
+
+          String? nuevoCorteId = await viewModel.createCorteCaja();
+
+          await viewModel.fetchCortes();
+
           await Navigator.push(
             context,
             MaterialPageRoute(
-              builder: (context) => CorteCajaForm(),
+              builder: (context) => CorteCajaForm(corteCajaId: nuevoCorteId),
             ),
-          ).then((_) => Provider.of<CorteCajaViewModel>(context, listen: false)
-              .fetchCortes());
+          ).then((_) => viewModel.fetchCortes());
         },
         icon: Icon(Icons.add, size: 30, color: AppTheme.quaternary),
         label: Text(
@@ -231,9 +220,14 @@ class _CorteCajaScreenState extends State<CorteCajaScreen> {
                                   columns: [
                                     DataColumn(label: Text(" ", style: estilo)),
                                     DataColumn(
-                                        label: Text("ID", style: estilo)),
+                                        label: Text("Fecha Apertura",
+                                            style: estilo)),
+                                    DataColumn(
+                                        label: Text("Fecha Cierre",
+                                            style: estilo)),
                                     DataColumn(
-                                        label: Text("FECHA", style: estilo)),
+                                        label: Text("Efectivo En Caja",
+                                            style: estilo)),
                                   ],
                                   rows: registros,
                                 ),

+ 65 - 0
lib/views/corte_caja/corte_caja_ticket.dart

@@ -0,0 +1,65 @@
+import 'package:intl/intl.dart';
+import 'package:pdf/pdf.dart';
+import 'package:pdf/widgets.dart' as pw;
+import 'package:printing/printing.dart';
+
+class CorteCajaTicket {
+  static Future<void> imprimirTicket({
+    required double fondo,
+    required double fondoDiaSig,
+    required double ventaEfe,
+    required double ventaTarj,
+    required double ventaTrans,
+    required double deposito,
+    required double retiro,
+    required double gasto,
+    required double corteFinal,
+    DateTime? fechaApertura,
+  }) async {
+    final pdf = pw.Document();
+    final numberFormat = NumberFormat('#,##0.00', 'en_US');
+
+    pdf.addPage(pw.Page(
+      pageFormat: PdfPageFormat.roll57,
+      build: (pw.Context context) {
+        return pw.Column(
+          crossAxisAlignment: pw.CrossAxisAlignment.start,
+          children: [
+            pw.Text(
+              "Fecha: ${DateFormat('dd/MM/yyyy').format(fechaApertura!)}",
+              style: pw.TextStyle(fontSize: 11, fontWeight: pw.FontWeight.bold),
+            ),
+            _crearLinea("Fondo Inicial:", numberFormat.format(fondo)),
+            _crearLinea("Fondo Siguiente:", numberFormat.format(fondoDiaSig)),
+            _crearLinea("Ventas Efe:", numberFormat.format(ventaEfe)),
+            _crearLinea("Ventas Tarj:", numberFormat.format(ventaTarj)),
+            _crearLinea("Venta Trans:", numberFormat.format(ventaTrans)),
+            _crearLinea("Depositos:", numberFormat.format(deposito)),
+            _crearLinea("Retiros:", numberFormat.format(retiro)),
+            _crearLinea("Gastos:", numberFormat.format(gasto)),
+            pw.Divider(),
+            _crearLinea("Venta Total del día:",
+                numberFormat.format(ventaEfe + ventaTarj + ventaTrans)),
+            _crearLinea("Efectivo en caja:", numberFormat.format(corteFinal)),
+            _crearLinea(
+                "Salidas totales día:", numberFormat.format(retiro + gasto)),
+          ],
+        );
+      },
+    ));
+
+    await Printing.layoutPdf(
+      onLayout: (PdfPageFormat format) async => pdf.save(),
+    );
+  }
+
+  static pw.Widget _crearLinea(String texto, String valor) {
+    return pw.Row(
+      mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
+      children: [
+        pw.Text(texto, style: pw.TextStyle(fontSize: 10)),
+        pw.Text("\$$valor", style: pw.TextStyle(fontSize: 10)),
+      ],
+    );
+  }
+}

+ 6 - 0
lib/views/home/home_screen.dart

@@ -27,6 +27,12 @@ class Formulario extends State<HomeScreen> {
     WidgetsBinding.instance.addPostFrameCallback((_) {
       Provider.of<ProductoViewModel>(context, listen: false)
           .sincronizarProductosYCategorias();
+
+      final permisoViewModel =
+          Provider.of<PermisoViewModel>(context, listen: false);
+      if (permisoViewModel.userPermisos.isEmpty) {
+        permisoViewModel.fetchUserPermisos();
+      }
     });
   }
 

+ 16 - 8
lib/views/pedido/pedido_screen.dart

@@ -411,14 +411,22 @@ class _PedidoScreenState extends State<PedidoScreen> {
             child: FloatingActionButton.extended(
               heroTag: 'addPedido',
               onPressed: () async {
-                await Navigator.push(
-                  context,
-                  MaterialPageRoute(
-                    builder: (context) => PedidoForm(),
-                  ),
-                ).then((_) =>
-                    Provider.of<PedidoViewModel>(context, listen: false)
-                        .fetchLocalPedidosForScreen());
+                final corteCajaViewModel =
+                    Provider.of<CorteCajaViewModel>(context, listen: false);
+                if (corteCajaViewModel.hasOpenCorteCaja()) {
+                  await Navigator.push(
+                    context,
+                    MaterialPageRoute(
+                      builder: (context) => PedidoForm(),
+                    ),
+                  ).then((_) =>
+                      Provider.of<PedidoViewModel>(context, listen: false)
+                          .fetchLocalPedidosForScreen());
+                } else {
+                  alerta(context,
+                      etiqueta:
+                          'Solo se pueden realizar pedidos cuando se encuentre la caja abierta.');
+                }
               },
               icon: Icon(Icons.add, size: 30, color: AppTheme.quaternary),
               label: Text(

+ 4 - 2
lib/views/producto/producto_form.dart

@@ -128,7 +128,9 @@ class Formulario extends State<ProductoForm> {
     return Scaffold(
       appBar: AppBar(
         title: Text(
-          'Nuevo Producto',
+          widget.producto.id > 0
+              ? 'Consulta Producto ID:${widget.producto.id}'
+              : 'Nuevo Producto',
           style: TextStyle(color: AppTheme.secondary),
         ),
         iconTheme: IconThemeData(color: AppTheme.secondary),
@@ -363,7 +365,7 @@ class Formulario extends State<ProductoForm> {
       id: widget.producto.id,
       nombre: _nombre.text,
       descripcion: _descripcion.text,
-      precio: precio, // Guardando como String para la base de datos
+      precio: precio,
       idCategoria: categoriaProducto!.id,
       imagen: _selectedFilePath,
       topings: selectedToppings.isNotEmpty ? selectedToppings : null,

+ 32 - 4
lib/views/producto/producto_imagen.dart

@@ -1,17 +1,45 @@
 import 'dart:io';
+import 'dart:typed_data';
+import 'package:http/http.dart' as http;
 import 'package:path/path.dart' as path;
 import 'package:path_provider/path_provider.dart';
 import 'package:file_picker/file_picker.dart';
 
+// Guarda la imagen que se añade desde el form
 Future<String?> pickAndStoreImage(File file, int productId) async {
   Directory appDocDir = await getApplicationDocumentsDirectory();
-  String productsDirectoryPath = path.join(appDocDir.path, 'productosC');
-  await Directory(productsDirectoryPath).create(recursive: true);
+  String baseDirPath =
+      path.join(appDocDir.path, 'ConalepPos', 'media', 'producto');
+  await Directory(baseDirPath).create(recursive: true);
   String fileExtension = path.extension(file.path);
-  String newFileName = "$productId$fileExtension";
-  String finalPath = path.join(productsDirectoryPath, newFileName);
+  String newFileName =
+      "$productId-${path.basenameWithoutExtension(file.path)}$fileExtension";
+  String finalPath = path.join(baseDirPath, newFileName);
 
   await file.copy(finalPath);
 
   return finalPath;
 }
+
+//Descarga imagen de la sincronizacion del producto
+Future<String?> downloadAndStoreImage(
+    String imageUrl, int productId, String imageName) async {
+  Directory appDocDir = await getApplicationDocumentsDirectory();
+  String baseDirPath =
+      path.join(appDocDir.path, 'ConalepPos', 'media', 'producto');
+  await Directory(baseDirPath).create(recursive: true);
+
+  String fileExtension = path.extension(imageUrl);
+  String newFileName = "$productId-$imageName$fileExtension";
+  String finalPath = path.join(baseDirPath, newFileName);
+
+  http.Response response = await http.get(Uri.parse(imageUrl));
+  if (response.statusCode == 200) {
+    Uint8List bytes = response.bodyBytes;
+    File file = File(finalPath);
+    await file.writeAsBytes(bytes);
+    return finalPath;
+  }
+
+  return null;
+}

+ 52 - 0
lib/views/producto/producto_screen.dart

@@ -46,6 +46,43 @@ class _ProductoScreenState extends State<ProductoScreen> {
     });
   }
 
+  Future<void> _sincronizarProductos(BuildContext context) async {
+    final productoViewModel =
+        Provider.of<ProductoViewModel>(context, listen: false);
+
+    try {
+      await productoViewModel.sincronizarProductosYCategorias();
+      _mostrarResultado(
+          context, 'La sincronización se completó exitosamente.', true);
+      await productoViewModel.fetchLocalAll();
+    } catch (e) {
+      _mostrarResultado(context, e.toString(), false);
+    }
+  }
+
+  void _mostrarResultado(BuildContext context, String mensaje, bool exito) {
+    showDialog(
+      context: context,
+      builder: (BuildContext context) {
+        return AlertDialog(
+          title: Text(
+              exito ? 'Sincronización exitosa' : 'Error de sincronización'),
+          content: SingleChildScrollView(
+            child: Text(mensaje),
+          ),
+          actions: [
+            TextButton(
+              child: Text('OK'),
+              onPressed: () {
+                Navigator.of(context).pop();
+              },
+            ),
+          ],
+        );
+      },
+    );
+  }
+
   @override
   Widget build(BuildContext context) {
     final model = Provider.of<ProductoViewModel>(context);
@@ -179,6 +216,21 @@ class _ProductoScreenState extends State<ProductoScreen> {
               TextStyle(color: AppTheme.secondary, fontWeight: FontWeight.w500),
         ),
         iconTheme: IconThemeData(color: AppTheme.secondary),
+        actions: [
+          TextButton.icon(
+            icon: Icon(Icons.sync, color: AppTheme.secondary),
+            label: Text(
+              "Sincronizar",
+              style: TextStyle(
+                  color: AppTheme.secondary,
+                  fontWeight: FontWeight.w500,
+                  fontSize: 18),
+            ),
+            onPressed: () async {
+              await _sincronizarProductos(context);
+            },
+          )
+        ],
       ),
       body: Column(
         children: [

+ 118 - 0
lib/views/sucursal/sucursal_screen.dart

@@ -0,0 +1,118 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import '../../viewmodels/viewmodels.dart';
+import '../../models/models.dart';
+import '../../widgets/widgets.dart';
+
+class SucursalesPage extends StatefulWidget {
+  @override
+  _SucursalesPageState createState() => _SucursalesPageState();
+}
+
+class _SucursalesPageState extends State<SucursalesPage> {
+  Sucursal? _selectedSucursal;
+
+  @override
+  void initState() {
+    super.initState();
+    Provider.of<SucursalViewModel>(context, listen: false)
+        .fetchLocalSucursales()
+        .then((_) {
+      final sucursales =
+          Provider.of<SucursalViewModel>(context, listen: false).sucursales;
+      setState(() {
+        _selectedSucursal = sucursales.firstWhere(
+          (sucursal) => sucursal.seleccionado == 1,
+          orElse: () => null as Sucursal,
+        );
+      });
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final sucursalViewModel = Provider.of<SucursalViewModel>(context);
+    final sucursales = sucursalViewModel.sucursales;
+
+    return Scaffold(
+      appBar: AppBar(
+        title: Text('Sucursales'),
+      ),
+      body: Padding(
+        padding: const EdgeInsets.all(16.0),
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.stretch,
+          children: [
+            AppDropdownModel<Sucursal>(
+              etiqueta: 'Seleccione una sucursal',
+              hint: 'Elija una sucursal',
+              selectedValue: _selectedSucursal,
+              onChanged: (Sucursal? newValue) {
+                setState(() {
+                  _selectedSucursal = newValue;
+                });
+              },
+              items: sucursales.map((Sucursal sucursal) {
+                return DropdownMenuItem<Sucursal>(
+                  value: sucursal,
+                  child: Text(
+                    sucursal.nombre ?? '',
+                    style: TextStyle(color: Colors.black),
+                  ),
+                );
+              }).toList(),
+            ),
+            SizedBox(height: 20),
+
+            if (_selectedSucursal != null)
+              tarjeta(
+                Padding(
+                  padding: const EdgeInsets.all(8.0),
+                  child: Column(
+                    crossAxisAlignment: CrossAxisAlignment.start,
+                    children: [
+                      Text(
+                        'Sucursal: ${_selectedSucursal!.nombre ?? ''}',
+                        style: TextStyle(
+                            fontSize: 16, fontWeight: FontWeight.bold),
+                      ),
+                      SizedBox(height: 5),
+                      Text('Dirección: ${_selectedSucursal!.direccion ?? ''}'),
+                      SizedBox(height: 5),
+                      Text('Ciudad: ${_selectedSucursal!.ciudad ?? ''}'),
+                      SizedBox(height: 5),
+                      Text('Clave: ${_selectedSucursal!.clave ?? ''}'),
+                      SizedBox(height: 5),
+                    ],
+                  ),
+                ),
+              ),
+
+            SizedBox(height: 20),
+
+            // Usar el widget de botón para guardar
+            boton("Guardar", () async {
+              await _guardarSucursalSeleccionada();
+              if (context.mounted) {
+                Navigator.pop(context);
+              }
+            }),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Future<void> _guardarSucursalSeleccionada() async {
+    if (_selectedSucursal != null) {
+      // Guardar la sucursal seleccionada utilizando el ViewModel
+      await Provider.of<SucursalViewModel>(context, listen: false)
+          .setSelectedSucursal(_selectedSucursal!);
+
+      setState(() {
+        // Volver a establecer la sucursal seleccionada en el Dropdown
+        _selectedSucursal = _selectedSucursal;
+      });
+    }
+  }
+}

+ 52 - 46
lib/views/venta/venta_screen.dart

@@ -27,6 +27,7 @@ class _VentaScreenState extends State<VentaScreen> {
   double totalTransferenciaDelDia = 0.0;
   double cambio = 0.0;
   double totalSinCambio = 0.0;
+  double efesinCambio = 0.0;
 
   String formatCurrency(double amount) {
     final format = NumberFormat("#,##0.00", "es_MX");
@@ -195,60 +196,52 @@ class _VentaScreenState extends State<VentaScreen> {
                   crossAxisAlignment: CrossAxisAlignment.end,
                   children: [
                     Padding(
-                      padding: const EdgeInsets.all(16.0),
+                      padding: const EdgeInsets.all(4.0),
                       child: Text(
                         "Total del día: \$${formatCurrency(totalDelDia)}",
                         style: TextStyle(
                             fontSize: 20, fontWeight: FontWeight.bold),
                       ),
                     ),
-                    Row(
-                      children: [
-                        if (totalTarjetaDelDia > 0)
-                          Padding(
-                            padding: const EdgeInsets.all(16.0),
-                            child: Text(
-                              "Tarjeta: \$${formatCurrency(totalTarjetaDelDia)}",
-                              style: TextStyle(
-                                  fontSize: 20, fontWeight: FontWeight.bold),
-                            ),
-                          ),
-                        if (totalTransferenciaDelDia > 0)
-                          Padding(
-                            padding: const EdgeInsets.all(16.0),
-                            child: Text(
-                              "Transferencia: \$${formatCurrency(totalTransferenciaDelDia)}",
-                              style: TextStyle(
-                                  fontSize: 20, fontWeight: FontWeight.bold),
-                            ),
-                          ),
-                      ],
-                    ),
-                    Row(
-                      children: [
-                        if (totalEfectivoDelDia > 0)
-                          Padding(
-                            padding: const EdgeInsets.all(16.0),
-                            child: Text(
-                              "Efectivo: \$${formatCurrency(totalEfectivoDelDia)}",
-                              style: TextStyle(
-                                  fontSize: 20, fontWeight: FontWeight.bold),
-                            ),
-                          ),
-                        if (cambio > 0)
-                          Padding(
-                            padding: const EdgeInsets.all(16.0),
-                            child: Text(
-                              "Cambio: \$${formatCurrency(cambio)}",
-                              style: TextStyle(
-                                  fontSize: 20, fontWeight: FontWeight.bold),
-                            ),
-                          ),
-                      ],
-                    ),
+                    if (totalTarjetaDelDia > 0)
+                      Padding(
+                        padding: const EdgeInsets.all(4.0),
+                        child: Text(
+                          "Total Tarjeta: \$${formatCurrency(totalTarjetaDelDia)}",
+                          style: TextStyle(
+                              fontSize: 20, fontWeight: FontWeight.bold),
+                        ),
+                      ),
+                    if (totalTransferenciaDelDia > 0)
+                      Padding(
+                        padding: const EdgeInsets.all(4.0),
+                        child: Text(
+                          "Total Transferencia: \$${formatCurrency(totalTransferenciaDelDia)}",
+                          style: TextStyle(
+                              fontSize: 20, fontWeight: FontWeight.bold),
+                        ),
+                      ),
+                    if (totalEfectivoDelDia > 0)
+                      Padding(
+                        padding: const EdgeInsets.all(4.0),
+                        child: Text(
+                          "Total Efectivo: \$${formatCurrency(totalEfectivoDelDia)}",
+                          style: TextStyle(
+                              fontSize: 20, fontWeight: FontWeight.bold),
+                        ),
+                      ),
+                    if (cambio > 0)
+                      Padding(
+                        padding: const EdgeInsets.all(4.0),
+                        child: Text(
+                          "Cambio: \$${formatCurrency(cambio)}",
+                          style: TextStyle(
+                              fontSize: 20, fontWeight: FontWeight.bold),
+                        ),
+                      ),
                     if (totalCancelados > 0)
                       Padding(
-                        padding: const EdgeInsets.all(16.0),
+                        padding: const EdgeInsets.all(4.0),
                         child: Text(
                           "Total cancelados: \$${formatCurrency(totalCancelados)}",
                           style: TextStyle(
@@ -257,6 +250,15 @@ class _VentaScreenState extends State<VentaScreen> {
                               color: Colors.red),
                         ),
                       ),
+                    if (efesinCambio > 0)
+                      Padding(
+                        padding: const EdgeInsets.all(4.0),
+                        child: Text(
+                          "Efectivo en Caja: \$${formatCurrency(efesinCambio)}",
+                          style: TextStyle(
+                              fontSize: 20, fontWeight: FontWeight.bold),
+                        ),
+                      ),
                   ],
                 ),
               ],
@@ -297,6 +299,7 @@ class _VentaScreenState extends State<VentaScreen> {
     totalTransferenciaDelDia = 0.0;
     cambio = 0.0;
     totalSinCambio = 0.0;
+    efesinCambio = 0.0;
 
     for (var pedido in pedidosNoCancelados) {
       totalDelDia += pedido.totalPedido ?? 0.0;
@@ -307,10 +310,13 @@ class _VentaScreenState extends State<VentaScreen> {
           totalEfectivoDelDia + totalTarjetaDelDia + totalTransferenciaDelDia;
 
       cambio = totalSinCambio - totalDelDia;
+
+      efesinCambio = totalEfectivoDelDia - cambio;
     }
 
     print("Total del dia sin cambios $totalSinCambio");
     print("Cambio $cambio");
+    print("efectivo en caja $efesinCambio");
 
     totalCancelados = pedidosCancelados.fold(
         0.0, (sum, current) => sum + (current.totalPedido ?? 0.0));

+ 226 - 122
lib/widgets/app_drawer.dart

@@ -9,20 +9,19 @@ import 'package:conalep_pos/views/venta/venta_screen.dart';
 import 'package:flutter/material.dart';
 import '../models/usuario_model.dart';
 import 'package:provider/provider.dart';
+import '../services/repo_service.dart';
+import '../services/services.dart';
 import '../themes/themes.dart';
 import '../viewmodels/login_view_model.dart';
+import '../viewmodels/permiso_view_model.dart';
 import '../views/descuento/descuento_screen.dart';
+import '../views/sucursal/sucursal_screen.dart';
 import '../views/variable/variable_screen.dart';
 import 'widgets_components.dart';
 import 'copia_db.dart';
 
-class AppDrawer extends StatefulWidget {
-  @override
-  _AppDrawerState createState() => _AppDrawerState();
-}
-
-class _AppDrawerState extends State<AppDrawer> {
-  int _versionTapCount = 0;
+class AppDrawer extends StatelessWidget {
+  AppDrawer({super.key});
 
   Future<bool> _showExitConfirmationDialog(BuildContext context) async {
     bool shouldPop = false;
@@ -38,12 +37,12 @@ class _AppDrawerState extends State<AppDrawer> {
             child: const Text('Cancelar', style: TextStyle(color: Colors.red)),
           ),
           TextButton(
-            onPressed: () {
-              Navigator.pop(context);
-              Navigator.pop(context);
+            onPressed: () async {
               Provider.of<LoginViewModel>(context, listen: false).logOut();
-              Navigator.of(context)
-                  .pushNamedAndRemoveUntil('main', (route) => false);
+              Navigator.of(context).pushNamedAndRemoveUntil(
+                'login',
+                (route) => false,
+              );
             },
             child: const Text('Aceptar'),
           ),
@@ -55,10 +54,10 @@ class _AppDrawerState extends State<AppDrawer> {
 
   @override
   Widget build(BuildContext context) {
-    String? nombre = Provider.of<LoginViewModel>(context).nombre.toString();
-    String? correo = Provider.of<LoginViewModel>(context).correo.toString();
-    //final avm = Provider.of<AdministracionViewModel>(context);
-    //List<String> permisos = avm.lospermisos;
+    final permisoViewModel = Provider.of<PermisoViewModel>(context);
+    List<String> userPermisos = permisoViewModel.userPermisos;
+    BaseService baseService = BaseService(); // Instancia de BaseService
+    String prefijoVersion = baseService.prefijoVersion();
     return Drawer(
       surfaceTintColor: Colors.white,
       backgroundColor: Colors.white,
@@ -90,135 +89,240 @@ class _AppDrawerState extends State<AppDrawer> {
               ],
             ),
           ),
-          //HEADER
+          // 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'),
-              onTap: () => {
-                Navigator.pop(context),
-                Navigator.of(context).push(
-                  MaterialPageRoute(
-                    builder: (context) => const PedidoScreen(),
-                  ),
-                ),
-              },
-            ),
-            ListTile(
-              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('Categoría Producto'),
-              onTap: () => {
-                Navigator.pop(context),
-                Navigator.of(context).push(
-                  MaterialPageRoute(
-                    builder: (context) => CategoriaProductoScreen(),
-                  ),
+            child: ListView(
+              children: [
+                ListTile(
+                  leading: circulo(const Icon(Icons.restaurant_menu)),
+                  title: const Text('Pedidos'),
+                  onTap: () => {
+                    Navigator.pop(context),
+                    Navigator.of(context).push(
+                      MaterialPageRoute(
+                        builder: (context) => const PedidoScreen(),
+                      ),
+                    ),
+                  },
                 ),
-              },
-            ),
-            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: 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.point_of_sale_rounded)),
-            //   title: const Text('Corte De Caja'),
-            //   onTap: () => {
-            //     Navigator.pop(context),
-            //     Navigator.of(context).push(
-            //       MaterialPageRoute(
-            //         builder: (context) => CorteCajaScreen(),
-            //       ),
-            //     ),
-            //   },
-            // ),
-            ExpansionTile(
-              leading: circulo(const Icon(Icons.admin_panel_settings)),
-              title: const Text('Administración'),
-              children: [
+                //if (userPermisos.contains(Usuario.VER_CATEGORIAS))
                 ListTile(
-                  leading: circulo(const Icon(Icons.discount)),
-                  title: const Text('Descuentos'),
+                  leading:
+                      circulo(const Icon(Icons.format_list_bulleted_rounded)),
+                  title: const Text('Categoría Producto'),
                   onTap: () => {
                     Navigator.pop(context),
                     Navigator.of(context).push(
                       MaterialPageRoute(
-                        builder: (context) => DescuentoScreen(),
+                        builder: (context) => CategoriaProductoScreen(),
                       ),
                     ),
                   },
                 ),
                 ListTile(
-                  leading: circulo(const Icon(Icons.discount)),
-                  title: const Text('Variables'),
+                  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) => VariablesScreen(),
+                        builder: (context) => VentaScreen(),
                       ),
                     ),
                   },
                 ),
+                //if (userPermisos.contains(Usuario.VER_ADMIN))
+                ExpansionTile(
+                  leading: circulo(const Icon(Icons.admin_panel_settings)),
+                  title: const Text('Administración'),
+                  children: [
+                    ListTile(
+                      leading: circulo(const Icon(Icons.point_of_sale)),
+                      title: const Text('Corte de caja'),
+                      onTap: () => {
+                        Navigator.pop(context),
+                        Navigator.of(context).push(
+                          MaterialPageRoute(
+                            builder: (context) => CorteCajaScreen(),
+                          ),
+                        ),
+                      },
+                    ),
+                    ListTile(
+                      leading: circulo(const Icon(Icons.discount)),
+                      title: const Text('Descuentos'),
+                      onTap: () => {
+                        Navigator.pop(context),
+                        Navigator.of(context).push(
+                          MaterialPageRoute(
+                            builder: (context) => DescuentoScreen(),
+                          ),
+                        ),
+                      },
+                    ),
+                    ListTile(
+                      leading: circulo(const Icon(Icons.discount)),
+                      title: const Text('Variables'),
+                      onTap: () => {
+                        Navigator.pop(context),
+                        Navigator.of(context).push(
+                          MaterialPageRoute(
+                            builder: (context) => VariablesScreen(),
+                          ),
+                        ),
+                      },
+                    ),
+                    //if (userPermisos.contains(Usuario.VER_SUCURSALES))
+                    ListTile(
+                      leading: circulo(const Icon(Icons.storefront_outlined)),
+                      title: const Text('Sucursales'),
+                      onTap: () => {
+                        Navigator.pop(context),
+                        Navigator.of(context).push(
+                          MaterialPageRoute(
+                            builder: (context) => SucursalesPage(),
+                          ),
+                        ),
+                      },
+                    ),
+                    //if (userPermisos.contains(Usuario.FORZAR_SINCRONIZACION))
+                    ListTile(
+                      leading: circulo(const Icon(Icons.sync)),
+                      title: const Text('Forzar Sincronización'),
+                      onTap: () async {
+                        bool confirmado = await showDialog(
+                              context: context,
+                              builder: (context) {
+                                return AlertDialog(
+                                  title: const Text("Forzar Sincronización",
+                                      style: TextStyle(
+                                          fontWeight: FontWeight.w500,
+                                          fontSize: 22)),
+                                  content: const Text(
+                                      '¿Estás seguro de que deseas forzar la sincronización?',
+                                      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: MaterialStateProperty.all(
+                                                EdgeInsets.fromLTRB(
+                                                    20, 10, 20, 10)),
+                                            backgroundColor:
+                                                MaterialStateProperty.all(
+                                                    Colors.red),
+                                            foregroundColor:
+                                                MaterialStateProperty.all(
+                                                    AppTheme.secondary),
+                                          ),
+                                        ),
+                                        TextButton(
+                                          onPressed: () =>
+                                              Navigator.of(context).pop(true),
+                                          child: const Text('Sí',
+                                              style: TextStyle(fontSize: 18)),
+                                          style: ButtonStyle(
+                                            padding: MaterialStateProperty.all(
+                                                EdgeInsets.fromLTRB(
+                                                    20, 10, 20, 10)),
+                                            backgroundColor:
+                                                MaterialStateProperty.all(
+                                                    AppTheme.tertiary),
+                                            foregroundColor:
+                                                MaterialStateProperty.all(
+                                                    AppTheme.quaternary),
+                                          ),
+                                        ),
+                                      ],
+                                    ),
+                                  ],
+                                );
+                              },
+                            ) ??
+                            false;
+
+                        if (confirmado) {
+                          showDialog(
+                            context: context,
+                            barrierDismissible: false,
+                            builder: (context) => AlertDialog(
+                              title: const Text("Sincronizando",
+                                  style: TextStyle(
+                                      fontWeight: FontWeight.w500,
+                                      fontSize: 22)),
+                              content: Padding(
+                                padding: const EdgeInsetsDirectional.symmetric(
+                                    horizontal: 120),
+                                child: const CircularProgressIndicator(),
+                              ),
+                            ),
+                          );
+
+                          try {
+                            await RepoService().forzarSincronizacion();
+
+                            Navigator.of(context, rootNavigator: true).pop();
+                            Navigator.of(context).pop();
+
+                            ScaffoldMessenger.of(context).showSnackBar(
+                              SnackBar(
+                                content: const Text(
+                                    'Sincronización completada con éxito'),
+                                backgroundColor: Colors.green,
+                              ),
+                            );
+                          } catch (e) {
+                            Navigator.of(context, rootNavigator: true).pop();
+                            Navigator.of(context).pop();
+
+                            ScaffoldMessenger.of(context).showSnackBar(
+                              SnackBar(
+                                content: Text('Error en la sincronización: $e'),
+                                backgroundColor: Colors.red,
+                              ),
+                            );
+                          }
+                        }
+                      },
+                    ),
+                  ],
+                ),
+                ListTile(
+                  leading: const Icon(Icons.logout),
+                  title: const Text('Cerrar sesión'),
+                  onTap: () {
+                    _showExitConfirmationDialog(context);
+                  },
+                ),
               ],
             ),
-            // ListTile(
-            //   leading: const Icon(Icons.logout),
-            //   title: const Text('Cerrar sesión'),
-            //   onTap: () {
-            //     _showExitConfirmationDialog(context);
-            //   },
-            // ),
-          ])),
-          GestureDetector(
-            onTap: () {
-              _versionTapCount++;
-              if (_versionTapCount == 5) {
-                copyDatabase(context);
-                _versionTapCount = 0;
-              }
-            },
-            child: const Padding(
-              padding: EdgeInsets.only(bottom: 10),
-              child: Align(
-                alignment: Alignment.bottomCenter,
-                child: Text(
-                  'v1.24.10.24',
-                  style: TextStyle(fontWeight: FontWeight.w300),
-                ),
+          ),
+          Padding(
+            padding: const EdgeInsets.only(bottom: 10),
+            child: Align(
+              alignment: Alignment.bottomCenter,
+              child: Text(
+                '$prefijoVersion.1.24.11.06',
+                style: const TextStyle(fontWeight: FontWeight.w300),
               ),
             ),
           ),

+ 66 - 29
lib/widgets/app_textfield.dart

@@ -2,9 +2,11 @@
 
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
+import 'package:intl/intl.dart';
 import '../themes/themes.dart';
 
-class AppTextField extends StatelessWidget {
+class AppTextField extends StatefulWidget {
+  final bool separarMiles;
   final Icon? prefixIcon;
   final Widget? suffixIcon;
   final String? labelText;
@@ -32,6 +34,7 @@ class AppTextField extends StatelessWidget {
 
   AppTextField({
     super.key,
+    this.separarMiles = false,
     this.etiqueta,
     this.labelText,
     this.prefixIcon,
@@ -58,6 +61,13 @@ class AppTextField extends StatelessWidget {
     this.onSubmitted,
   });
 
+  @override
+  _AppTextFieldState createState() => _AppTextFieldState();
+}
+
+class _AppTextFieldState extends State<AppTextField> {
+  bool isFirst = true;
+
   double _getFontSize(BuildContext context) {
     double screenWidth = MediaQuery.of(context).size.width;
     if (screenWidth < 480) {
@@ -71,47 +81,74 @@ class AppTextField extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     double fontSize = _getFontSize(context);
-    etiqueta ??= "";
+    widget.etiqueta ??= "";
+
     return Column(
       crossAxisAlignment: CrossAxisAlignment.start,
       children: [
-        if (etiqueta != '')
+        if (widget.etiqueta != '')
           Text(
-            etiqueta.toString(),
+            widget.etiqueta!,
             style: TextStyle(
                 fontSize: fontSize,
                 fontWeight: FontWeight.bold,
-                color: enabled ? Colors.black : Colors.grey),
+                color: widget.enabled ? Colors.black : Colors.grey[700]),
           ),
-        if (etiqueta != '')
+        if (widget.etiqueta != '')
           const SizedBox(
             height: 5,
           ),
         TextFormField(
-          onFieldSubmitted: onSubmitted,
-          autofillHints: autofillHints,
-          validator: validator,
-          enabled: enabled,
+          onFieldSubmitted: widget.onSubmitted,
+          autofillHints: widget.autofillHints,
+          validator: widget.validator,
+          enabled: widget.enabled,
           style: TextStyle(
             fontSize: fontSize,
           ),
-          maxLength: maxLength,
-          maxLines: maxLines,
-          inputFormatters: inputFormatters,
-          initialValue: initialValue,
-          controller: controller,
-          onTap: onTap,
-          readOnly: readOnly,
-          keyboardType: keyboardType,
-          textCapitalization: textCapitalization,
+          maxLength: widget.maxLength,
+          maxLines: widget.maxLines,
+          inputFormatters: widget.inputFormatters ??
+              (widget.separarMiles
+                  ? [FilteringTextInputFormatter.digitsOnly]
+                  : null),
+          initialValue: widget.initialValue,
+          controller: widget.controller,
+          onTap: widget.onTap,
+          readOnly: widget.readOnly,
+          keyboardType: widget.keyboardType,
+          textCapitalization: widget.textCapitalization,
           cursorColor: AppTheme.tertiary,
-          obscureText: obscureText,
+          obscureText: widget.obscureText,
           autocorrect: true,
-          onChanged: onChanged,
+          onChanged: (value) {
+            if (widget.separarMiles) {
+              String newValue = value.replaceAll(',', '').replaceAll('.', '');
+
+              if (value.isEmpty || newValue == '00') {
+                widget.controller?.clear();
+                isFirst = true;
+                return;
+              }
+
+              double value1 = double.parse(newValue);
+
+              if (!isFirst) value1 = value1 * 100;
+              value = NumberFormat.currency(customPattern: '###,###.##')
+                  .format(value1 / 100);
+
+              widget.controller?.value = TextEditingValue(
+                text: value,
+                selection: TextSelection.collapsed(offset: value.length),
+              );
+            } else if (widget.onChanged != null) {
+              widget.onChanged!(value);
+            }
+          },
           decoration: InputDecoration(
-            contentPadding: vertical == null
+            contentPadding: widget.vertical == null
                 ? null
-                : EdgeInsets.symmetric(vertical: vertical!),
+                : EdgeInsets.symmetric(vertical: widget.vertical!),
             focusedBorder: OutlineInputBorder(
               borderRadius: BorderRadius.circular(10),
               borderSide: BorderSide(
@@ -142,18 +179,18 @@ class AppTextField extends StatelessWidget {
                 color: Colors.grey[200]!,
               ),
             ),
-            errorText: errorText,
-            labelText: labelText,
-            hintText: hintText,
+            errorText: widget.errorText,
+            labelText: widget.labelText,
+            hintText: widget.hintText,
             floatingLabelStyle: TextStyle(
               color: AppTheme.tertiary,
               fontSize: fontSize,
             ),
             filled: true,
-            fillColor: fillColor,
-            prefixIcon: prefixIcon,
+            fillColor: widget.fillColor,
+            prefixIcon: widget.prefixIcon,
             prefixIconColor: AppTheme.tertiary,
-            suffixIcon: suffixIcon,
+            suffixIcon: widget.suffixIcon,
             suffixIconColor: AppTheme.tertiary,
             border: OutlineInputBorder(
               borderRadius: BorderRadius.circular(15),

+ 55 - 3
lib/widgets/widgets_components.dart

@@ -92,7 +92,7 @@ class Cargando extends StatelessWidget {
           child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
             Padding(
                 padding: const EdgeInsets.fromLTRB(16, 0, 16, 10),
-                child: Image.asset("assets/icono-BN.png", height: 200)),
+                child: Image.asset("assets/icono.png", height: 200)),
             const CircularProgressIndicator(backgroundColor: Colors.grey),
             Container(margin: const EdgeInsets.only(bottom: 8.0)),
             const Text("Cargando contenido...",
@@ -117,7 +117,7 @@ boton(String etiqueta, Function()? accion,
     alignment: Alignment.topLeft,
     child: SizedBox(
       height: height,
-      width: 380,
+      width: width,
       child: ElevatedButton(
         style: ButtonStyle(
           shape: MaterialStatePropertyAll(
@@ -132,7 +132,7 @@ boton(String etiqueta, Function()? accion,
           mainAxisAlignment: MainAxisAlignment.center,
           children: [
             Text(etiqueta,
-                style: TextStyle(fontSize: 18, color: AppTheme.secondary)),
+                style: TextStyle(fontSize: 18, color: AppTheme.quaternary)),
           ],
         ),
       ),
@@ -344,6 +344,53 @@ alerta(BuildContext context, {String? etiqueta = "Capturar búsqueda"}) {
       });
 }
 
+cuadroConfirmacion(BuildContext context,
+    {String? etiqueta = "¿Estás seguro?", required VoidCallback onConfirm}) {
+  return showDialog(
+    context: context,
+    builder: (context) {
+      return AlertDialog(
+        title: Text(etiqueta.toString()),
+        actions: [
+          Row(
+            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            children: [
+              Expanded(
+                child: TextButton(
+                  style: ButtonStyle(
+                      backgroundColor: MaterialStatePropertyAll(Colors.red)),
+                  onPressed: () {
+                    Navigator.pop(context);
+                  },
+                  child: const Text(
+                    'No',
+                    style: TextStyle(color: Colors.white),
+                  ),
+                ),
+              ),
+              SizedBox(width: 10),
+              Expanded(
+                child: TextButton(
+                  style: ButtonStyle(
+                      backgroundColor: MaterialStatePropertyAll(Colors.green)),
+                  onPressed: () {
+                    onConfirm();
+                    Navigator.pop(context);
+                  },
+                  child: const Text(
+                    'Sí',
+                    style: TextStyle(color: Colors.white),
+                  ),
+                ),
+              ),
+            ],
+          ),
+        ],
+      );
+    },
+  );
+}
+
 botonElevated(
     {Function()? accion,
     String? titulo = "Buscar",
@@ -760,3 +807,8 @@ Future eliminarMedia(
     },
   );
 }
+
+String formatoMiles(double? value) {
+  final formatter = NumberFormat('#,##0.00', 'en_US');
+  return formatter.format(value);
+}