57 次代碼提交 530fe76048 ... c2cfde911e

作者 SHA1 備註 提交日期
  Hugo Quijada c2cfde911e Flutter single instance no ejecutar en android 4 月之前
  OscarGil03 7e01fdcb7d Cambio version sdk 4 月之前
  OscarGil03 1d6c3d91b8 Propina en Corte de caja 4 月之前
  OscarGil03 509e6b87e7 Instalador 5 月之前
  OscarGil03 8f381b1e1e Actualización configuración 5 月之前
  OscarGil03 92ded34352 Pedidos ahora llevan corte caja id y se pueden pasar de un corte de caja a otro 5 月之前
  OscarGil03 eda469f3f5 Cambio version 6 月之前
  OscarGil03 0e18511ca2 Modulo corte de caja, pedidos con mesa y token totp seguridad 6 月之前
  OscarGil03 b0e13e5055 Script generacion setup 6 月之前
  OscarGil03 35d6312c17 Eliminacion de pedidos y mostrar mesa en grid 7 月之前
  OscarGil03 9e2916fd6d Validacion mesas e impresion de ticket 7 月之前
  OscarGil03 e937f2d6b8 Pedido Mesas 7 月之前
  OscarGil03 319ceeed89 Actualizacion mesas 7 月之前
  OscarGil03 166db8d37a Actualizacion version con corte de caja y ajuste en la creacion de pedido 7 月之前
  OscarGil03 ac5bef4da5 Sincronizacion forzosa con imagenes 8 月之前
  OscarGil03 777cedeceb Boton para mostrar contraseña 8 月之前
  OscarGil03 e4e8ac923f cambio versión 8 月之前
  OscarGil03 9bea99c012 Se guarda idUsuario en pedido y se manda a sincronización 8 月之前
  OscarGil03 f059d41e6f Validación de clave en login 8 月之前
  OscarGil03 e9fde029c1 Forzar sincronización y sincronizacion de clave usuario 8 月之前
  OscarGil03 a79399eb4d Campo minimo para validar un minimo obligatorio al generar un pedido 8 月之前
  OscarGil03 4e9cf6c077 Actualizacion de pantalla al sincronizar productos y categorias 8 月之前
  OscarGil03 bf6b1deab3 Sobreescribe la la sincronizacion de toppings para evitar duplicados 8 月之前
  OscarGil03 7318f130d9 Lista de toppings descolapsada por default y muestra el maximo por categoria 8 月之前
  OscarGil03 5d8fea1497 Cambio de version 8 月之前
  OscarGil03 a0e47e578f Clave sucursal cambia dependiendo del seleccionado en modulo de sucursales 8 月之前
  OscarGil03 ffa2a4cf95 Prefijo en la versión dependiendo a donde apunte la API 8 月之前
  OscarGil03 47472236c7 Permiso de administracion, cancelar pedido y ver reportes 8 月之前
  OscarGil03 be2fb5533f Sincronizacion de toppings seleccionables y sincronizacion de toppings del pedido seleccionados 8 月之前
  OscarGil03 a21320fb30 Sincronizacion de toppings e imagenes 8 月之前
  OscarGil03 5df5fa09c2 Se empezó a implementar las validaciones con permisos 8 月之前
  OscarGil03 db2b9090e7 Actualizacion de permisos, usuarios y usuarios permisos cuando la fecha de modificado sea mas reciente 8 月之前
  OscarGil03 fd910044da Utilización SessionStorage para mantener sesión iniciada 8 月之前
  OscarGil03 d78be1be14 Login Local con correo y selector de sucursal 8 月之前
  OscarGil03 009baec027 Sincronizacion de permisos, usuarios y usuarios permisos 8 月之前
  OscarGil03 a221a44d4f Selector de sucursales 8 月之前
  OscarGil03 89eba6770c Selector de sucursales 8 月之前
  OscarGil03 6c117ee208 claveSucursal en parametros de productos y categorias y sincronizacion de sucursales 8 月之前
  OscarGil03 afb9bf17cf Iconos App 8 月之前
  OscarGil03 20aca5b4ce Cambio nombre 8 月之前
  OscarGil03 2ea88fb8e7 Sincronizacion productos y ajustes variables 8 月之前
  OscarGil03 b62a3ab36c Ajustes variables, pedidos, productos y categorias 9 月之前
  OscarGil03 549cf23782 Sincronizacion de pedidos, productos y categorías 9 月之前
  OscarGil03 663978bdeb Descuentos, tipos de pago y variables 10 月之前
  OscarGil03 ca5ce8da9c ticket pedido 11 月之前
  OscarGil03 7f6d87310c Version nueva 11 月之前
  OscarGil03 2eca21dfd2 local bd y pedidos 1 年之前
  OscarGil03 06d2d3edd2 Se arregló limite de obtencion de topings 1 年之前
  OscarGil03 5fd9286b68 Se arregló el limite en la obtencion de topings 1 年之前
  OscarGil03 23d40ed69d Buscador productos en pedido form 1 年之前
  OscarGil03 bf87d5e4e6 Forms de topings y categoria topings actualizado 1 年之前
  OscarGil03 750f798fb5 Categoría de topings y topings en pedido form 1 年之前
  OscarGil03 1c10fe75bd Topings y categoria de topings 1 年之前
  OscarGil03 a65237adcf Productos y categoría de productos 1 年之前
  OscarGil03 db179bdc59 Categoria Productos 1 年之前
  OscarGil03 d57b1093d4 primer commit 1 年之前
  OscarGil03 aa729f12a1 first commit 1 年之前
共有 51 個文件被更改,包括 3819 次插入1351 次删除
  1. 7 3
      android/app/build.gradle
  2. 91 1
      android/build.gradle
  3. 3 1
      android/gradle.properties
  4. 1 1
      android/gradle/wrapper/gradle-wrapper.properties
  5. 1 0
      android/settings.gradle
  6. 0 52
      installers/Joshi_inno_Script.iss
  7. 0 52
      installers/Joshi_inno_Scripts.iss
  8. 3 2
      installers/Turquesa_POS_Inno_Setup.iss
  9. 二進制
      installers/TurquessaCoffeeSetup/TurquessaCoffeeSetup-2024-11-13.exe
  10. 二進制
      installers/TurquessaPOS_Setup_2024-11-13-1.exe
  11. 二進制
      installers/Turquessa_Setup-1.exe
  12. 二進制
      installers/Turquessa_Setup_2024_11_13.exe
  13. 二進制
      installers/TurquessaPOS_Setup_2024-11-22.exe
  14. 0 52
      installers/Turquessa_inno_Script1.iss
  15. 0 64
      installers/turq_installer.iss
  16. 9 0
      lib/main.dart
  17. 4 0
      lib/models/categoria_producto_model.dart
  18. 20 1
      lib/models/corte_caja_model.dart
  19. 12 2
      lib/models/item_carrito_model.dart
  20. 1 0
      lib/models/models.dart
  21. 20 5
      lib/models/pedido_model.dart
  22. 3 0
      lib/models/pedido_producto_model.dart
  23. 0 1
      lib/models/permiso_model.dart
  24. 10 0
      lib/models/producto_model.dart
  25. 54 0
      lib/models/propina_model.dart
  26. 249 4
      lib/services/repo_service.dart
  27. 206 44
      lib/viewmodels/corte_caja_view_model.dart
  28. 16 7
      lib/viewmodels/mesa_view_model.dart
  29. 119 10
      lib/viewmodels/pedido_view_model.dart
  30. 98 1
      lib/viewmodels/producto_view_model.dart
  31. 27 0
      lib/viewmodels/propina_view_model.dart
  32. 1 0
      lib/viewmodels/viewmodels.dart
  33. 68 86
      lib/views/categoria_producto/categoria_producto_screen.dart
  34. 239 0
      lib/views/corte_caja/corte_caja_finalizado_screen.dart
  35. 518 82
      lib/views/corte_caja/corte_caja_form.dart
  36. 4 3
      lib/views/home/home_screen.dart
  37. 60 77
      lib/views/mesa/mesa_screen.dart
  38. 559 39
      lib/views/pedido/pedido_detalle_screen.dart
  39. 770 364
      lib/views/pedido/pedido_form.dart
  40. 99 23
      lib/views/pedido/pedido_screen.dart
  41. 12 4
      lib/views/pedido/pedido_sync.dart
  42. 27 18
      lib/views/pedido/pedido_ticket.dart
  43. 2 2
      lib/views/producto/producto_form.dart
  44. 66 79
      lib/views/producto/producto_screen.dart
  45. 2 2
      lib/views/variable/variable_screen.dart
  46. 1 1
      lib/views/venta/venta_screen.dart
  47. 13 13
      lib/widgets/app_drawer.dart
  48. 5 9
      lib/widgets/app_dropdown_modelo.dart
  49. 103 2
      lib/widgets/widgets_components.dart
  50. 308 236
      pubspec.lock
  51. 8 8
      pubspec.yaml

+ 7 - 3
android/app/build.gradle

@@ -28,12 +28,16 @@ android {
     ndkVersion flutter.ndkVersion
     ndkVersion flutter.ndkVersion
 
 
     compileOptions {
     compileOptions {
-        sourceCompatibility JavaVersion.VERSION_1_8
-        targetCompatibility JavaVersion.VERSION_1_8
+        sourceCompatibility JavaVersion.VERSION_17
+        targetCompatibility JavaVersion.VERSION_17
     }
     }
 
 
     kotlinOptions {
     kotlinOptions {
-        jvmTarget = '1.8'
+        jvmTarget = JavaVersion.VERSION_17
+    }
+
+    kotlin {
+        jvmToolchain(17)
     }
     }
 
 
     sourceSets {
     sourceSets {

+ 91 - 1
android/build.gradle

@@ -1,11 +1,12 @@
 buildscript {
 buildscript {
-    ext.kotlin_version = '1.7.10'
+    ext.kotlin_version = '1.8.22'
     repositories {
     repositories {
         google()
         google()
         mavenCentral()
         mavenCentral()
     }
     }
 
 
     dependencies {
     dependencies {
+        classpath 'com.android.tools.build:gradle:8.1.0'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
     }
     }
 }
 }
@@ -20,7 +21,96 @@ allprojects {
 rootProject.buildDir = '../build'
 rootProject.buildDir = '../build'
 subprojects {
 subprojects {
     project.buildDir = "${rootProject.buildDir}/${project.name}"
     project.buildDir = "${rootProject.buildDir}/${project.name}"
+
+    // Ajuste de dependencias
+    project.configurations.all {
+        resolutionStrategy.eachDependency { details ->
+            if (details.requested.group == 'com.android.support' && !details.requested.name.contains('multidex')) {
+                details.useVersion "27.1.1"
+            }
+            if (details.requested.group == "org.jetbrains.kotlin") {
+                details.useVersion "1.8.22"
+            }
+        }
+    }
+
+    // Ajuste del namespace y eliminación del atributo 'package' en manifests
+    project.plugins.withId("com.android.library") {
+        project.android {
+            if (namespace == null || namespace.isEmpty()) {
+                namespace = "${project.group}.${project.name}".replace('-', '_')
+            }
+
+            project.tasks.configureEach { task ->
+                if (task.name.contains("processDebugManifest") || task.name.contains("processReleaseManifest")) {
+                    task.doFirst {
+                        File manifestFile = file("${projectDir}/src/main/AndroidManifest.xml")
+                        if (manifestFile.exists()) {
+                            String manifestContent = manifestFile.text
+                            if (manifestContent.contains('package=')) {
+                                manifestContent = manifestContent.replaceAll(/package="[^"]*"/, "")
+                                manifestFile.write(manifestContent)
+                                println "Removed 'package' attribute from ${manifestFile}"
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    // Fuerza Java 17 en proyectos Android
+    project.plugins.withId("com.android.application") {
+        project.android {
+            compileOptions {
+                sourceCompatibility JavaVersion.VERSION_17
+                targetCompatibility JavaVersion.VERSION_17
+            }
+        }
+    }
+
+    project.plugins.withId("com.android.library") {
+        project.android {
+            compileOptions {
+                sourceCompatibility JavaVersion.VERSION_17
+                targetCompatibility JavaVersion.VERSION_17
+            }
+        }
+    }
+
+    // Fuerza Kotlin JVM target a 17
+    project.plugins.withId("org.jetbrains.kotlin.jvm") {
+        project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
+            kotlinOptions {
+                jvmTarget = "17"
+            }
+        }
+    }
+
+    project.plugins.withId("org.jetbrains.kotlin.android") {
+        project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
+            kotlinOptions {
+                jvmTarget = "17"
+            }
+        }
+    }
+
+    // Fuerza Java para proyectos con plugin 'java'
+    project.plugins.withId("java") {
+        java {
+            toolchain {
+                languageVersion = JavaLanguageVersion.of(17)
+            }
+        }
+    }
+
+    // Fuerza nivel de compatibilidad en tareas Java
+    project.tasks.withType(JavaCompile).configureEach {
+        sourceCompatibility = JavaVersion.VERSION_17
+        targetCompatibility = JavaVersion.VERSION_17
+    }
 }
 }
+
 subprojects {
 subprojects {
     project.evaluationDependsOn(':app')
     project.evaluationDependsOn(':app')
 }
 }

+ 3 - 1
android/gradle.properties

@@ -1,3 +1,5 @@
-org.gradle.jvmargs=-Xmx4G
+org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8
 android.useAndroidX=true
 android.useAndroidX=true
 android.enableJetifier=true
 android.enableJetifier=true
+org.gradle.java.installations.auto-download=true
+org.gradle.java.installations.auto-detect=true

+ 1 - 1
android/gradle/wrapper/gradle-wrapper.properties

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip

+ 1 - 0
android/settings.gradle

@@ -18,6 +18,7 @@ pluginManagement {
 
 
     plugins {
     plugins {
         id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false
         id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false
+        id 'org.gradle.toolchains.foojay-resolver-convention' version '0.5.0'
     }
     }
 }
 }
 
 

+ 0 - 52
installers/Joshi_inno_Script.iss

@@ -1,52 +0,0 @@
-; Script generated by the Inno Setup Script Wizard.
-; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
-
-#define MyAppName "JoshiPapasPOS"
-#define MyAppVersion "1.0"
-#define MyAppPublisher "JoshiPapas"
-#define MyAppExeName "yoshi_papas_app.exe"
-
-[Setup]
-; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
-; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
-AppId={{702582E6-37C5-43C7-B36D-3C94AB62D406}
-AppName={#MyAppName}
-AppVersion={#MyAppVersion}
-;AppVerName={#MyAppName} {#MyAppVersion}
-AppPublisher={#MyAppPublisher}
-DefaultDirName={autopf}\{#MyAppName}
-DisableProgramGroupPage=yes
-; Uncomment the following line to run in non administrative install mode (install for current user only.)
-;PrivilegesRequired=lowest
-OutputDir=C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\YoshiPapas\yoshi_papas_app\installers
-OutputBaseFilename=JoshiPapasSetup
-SetupIconFile=C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\YoshiPapas\yoshi_papas_app\assets\JoshiLogo.ico
-Compression=lzma
-SolidCompression=yes
-WizardStyle=modern
-
-[Languages]
-Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl"
-
-[Tasks]
-Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
-
-[Files]
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\YoshiPapas\yoshi_papas_app\build\windows\x64\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\YoshiPapas\yoshi_papas_app\build\windows\x64\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\YoshiPapas\yoshi_papas_app\build\windows\x64\runner\Release\pdfium.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\YoshiPapas\yoshi_papas_app\build\windows\x64\runner\Release\permission_handler_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\YoshiPapas\yoshi_papas_app\build\windows\x64\runner\Release\sqlite3.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\YoshiPapas\yoshi_papas_app\build\windows\x64\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\YoshiPapas\yoshi_papas_app\build\windows\x64\runner\Release\printing_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\YoshiPapas\yoshi_papas_app\build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\YoshiPapas\yoshi_papas_app\build\windows\x64\runner\Release\sqlite3.def"; DestDir: "{app}"; Flags: ignoreversion
-; NOTE: Don't use "Flags: ignoreversion" on any shared system files
-
-[Icons]
-Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
-Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
-
-[Run]
-Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
-

+ 0 - 52
installers/Joshi_inno_Scripts.iss

@@ -1,52 +0,0 @@
-; Script generated by the Inno Setup Script Wizard.
-; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
-
-#define MyAppName "JoshiPapasPOS"
-#define MyAppVersion "1.1"
-#define MyAppPublisher "JoshiPapas"
-#define MyAppExeName "yoshi_papas_app.exe"
-
-[Setup]
-; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
-; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
-AppId={{94577222-7355-4608-BA29-68F4D6596669}
-AppName={#MyAppName}
-AppVersion={#MyAppVersion}
-;AppVerName={#MyAppName} {#MyAppVersion}
-AppPublisher={#MyAppPublisher}
-DefaultDirName={autopf}\{#MyAppName}
-DisableProgramGroupPage=yes
-; Uncomment the following line to run in non administrative install mode (install for current user only.)
-;PrivilegesRequired=lowest
-OutputDir=C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\YoshiPapas\yoshi_papas_app\installers
-OutputBaseFilename=JoshiPapasSetup
-SetupIconFile=C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\YoshiPapas\yoshi_papas_app\assets\JoshiLogo.png
-Compression=lzma
-SolidCompression=yes
-WizardStyle=modern
-
-[Languages]
-Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl"
-
-[Tasks]
-Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
-
-[Files]
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\YoshiPapas\yoshi_papas_app\build\windows\x64\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\YoshiPapas\yoshi_papas_app\build\windows\x64\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\YoshiPapas\yoshi_papas_app\build\windows\x64\runner\Release\pdfium.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\YoshiPapas\yoshi_papas_app\build\windows\x64\runner\Release\permission_handler_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\YoshiPapas\yoshi_papas_app\build\windows\x64\runner\Release\printing_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\YoshiPapas\yoshi_papas_app\build\windows\x64\runner\Release\sqlite3.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\YoshiPapas\yoshi_papas_app\build\windows\x64\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\YoshiPapas\yoshi_papas_app\build\windows\x64\runner\Release\sqlite3.def"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\YoshiPapas\yoshi_papas_app\build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs
-; NOTE: Don't use "Flags: ignoreversion" on any shared system files
-
-[Icons]
-Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
-Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
-
-[Run]
-Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
-

+ 3 - 2
installers/Turquesa_POS_Inno_Setup.iss

@@ -2,7 +2,7 @@
 ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
 ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
 
 
 #define MyAppName "TurquessaPOS"
 #define MyAppName "TurquessaPOS"
-#define MyAppVersion "1.2024.11.22"
+#define MyAppVersion "1.24.12.27"
 #define MyAppExeName "turquessa_app.exe"
 #define MyAppExeName "turquessa_app.exe"
 
 
 [Setup]
 [Setup]
@@ -17,7 +17,7 @@ DisableProgramGroupPage=yes
 ; Uncomment the following line to run in non administrative install mode (install for current user only.)
 ; Uncomment the following line to run in non administrative install mode (install for current user only.)
 ;PrivilegesRequired=lowest
 ;PrivilegesRequired=lowest
 OutputDir=C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\installers
 OutputDir=C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\installers
-OutputBaseFilename=TurquessaPOS_Setup_2024-11-20
+OutputBaseFilename=TurquessaPOS_Setup_24.12.27
 SetupIconFile=C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\assets\icono.ico
 SetupIconFile=C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\assets\icono.ico
 Compression=lzma
 Compression=lzma
 SolidCompression=yes
 SolidCompression=yes
@@ -39,6 +39,7 @@ Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_
 Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\sqlite3.dll"; DestDir: "{app}"; Flags: ignoreversion
 Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\sqlite3.dll"; DestDir: "{app}"; Flags: ignoreversion
 Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
 Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
 Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\vcruntime140_1.dll"; DestDir: "{app}"; Flags: ignoreversion
 Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\vcruntime140_1.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\msvcp140.dll"; DestDir: "{app}"; Flags: ignoreversion
 Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs
 Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs
 ; NOTE: Don't use "Flags: ignoreversion" on any shared system files
 ; NOTE: Don't use "Flags: ignoreversion" on any shared system files
 
 

二進制
installers/TurquessaCoffeeSetup/TurquessaCoffeeSetup-2024-11-13.exe


二進制
installers/TurquessaPOS_Setup_2024-11-13-1.exe


二進制
installers/Turquessa_Setup-1.exe


二進制
installers/Turquessa_Setup_2024_11_13.exe


二進制
installers/TurquessaPOS_Setup_2024-11-22.exe


+ 0 - 52
installers/Turquessa_inno_Script1.iss

@@ -1,52 +0,0 @@
-; Script generated by the Inno Setup Script Wizard.
-; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
-
-#define MyAppName "TurquessaCoffeePOS"
-#define MyAppVersion "1.24.11.13"
-#define MyAppPublisher "TurquessaCoffee"
-#define MyAppExeName "turquessa_coffee_app.exe"
-
-[Setup]
-; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
-; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
-AppId={{A1520D9F-748A-4E3A-8BF6-21BA8A5DE484}
-AppName={#MyAppName}
-AppVersion={#MyAppVersion}
-;AppVerName={#MyAppName} {#MyAppVersion}
-AppPublisher={#MyAppPublisher}
-DefaultDirName={autopf}\{#MyAppName}
-DisableProgramGroupPage=yes
-; Uncomment the following line to run in non administrative install mode (install for current user only.)
-;PrivilegesRequired=lowest
-OutputDir=C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\installers
-OutputBaseFilename=TurquessaCoffeeSetup
-SetupIconFile=C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\assets\icono.ico
-Compression=lzma
-SolidCompression=yes
-WizardStyle=modern
-
-[Languages]
-Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl"
-
-[Tasks]
-Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
-
-[Files]
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\pdfium.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\permission_handler_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\printing_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\sqlite3.def"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\sqlite3.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs
-; NOTE: Don't use "Flags: ignoreversion" on any shared system files
-
-[Icons]
-Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
-Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
-
-[Run]
-Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
-

+ 0 - 64
installers/turq_installer.iss

@@ -1,64 +0,0 @@
-; Script generated by the Inno Setup Script Wizard.
-; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
-
-#define MyAppName "TurquessaCoffeeSetup"
-#define MyAppVersion "1.0"
-#define MyAppPublisher "Turquessa Coffee"
-#define MyAppExeName "turquessa_coffee_app.exe"
-#define MyAppAssocName MyAppName + " File"
-#define MyAppAssocExt ".myp"
-#define MyAppAssocKey StringChange(MyAppAssocName, " ", "") + MyAppAssocExt
-
-[Setup]
-; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
-; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
-AppId={{BCAA57CD-BF4C-4777-A45B-1FBB46A4339B}
-AppName={#MyAppName}
-AppVersion={#MyAppVersion}
-;AppVerName={#MyAppName} {#MyAppVersion}
-AppPublisher={#MyAppPublisher}
-DefaultDirName={autopf}\{#MyAppName}
-ChangesAssociations=yes
-DisableProgramGroupPage=yes
-; Uncomment the following line to run in non administrative install mode (install for current user only.)
-;PrivilegesRequired=lowest
-OutputDir=TurquessaCoffeeSetup
-OutputBaseFilename=TurquessaCoffeeSetup
-SetupIconFile=C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\assets\icono.ico
-Compression=lzma
-SolidCompression=yes
-WizardStyle=modern
-
-[Languages]
-Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl"
-
-[Tasks]
-Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
-
-[Files]
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\pdfium.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\permission_handler_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\printing_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\sqlite3.def"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\sqlite3.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\turquessa_coffee_app.exe"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
-Source: "C:\Users\gusos\OneDrive\Escritorio\eDesarrollos\Turquesa\pos_turquessa_flutter\build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs
-; NOTE: Don't use "Flags: ignoreversion" on any shared system files
-
-[Registry]
-Root: HKA; Subkey: "Software\Classes\{#MyAppAssocExt}\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocKey}"; ValueData: ""; Flags: uninsdeletevalue
-Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}"; ValueType: string; ValueName: ""; ValueData: "{#MyAppAssocName}"; Flags: uninsdeletekey
-Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"
-Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""
-Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".myp"; ValueData: ""
-
-[Icons]
-Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
-Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
-
-[Run]
-Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
-

+ 9 - 0
lib/main.dart

@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
 import 'package:provider/provider.dart';
 import 'package:provider/provider.dart';
 import 'package:sqflite_common_ffi/sqflite_ffi.dart';
 import 'package:sqflite_common_ffi/sqflite_ffi.dart';
 import 'dart:io';
 import 'dart:io';
+import 'package:flutter_single_instance/flutter_single_instance.dart';
 
 
 import 'services/productos_service.dart';
 import 'services/productos_service.dart';
 import 'views/home/home_screen.dart';
 import 'views/home/home_screen.dart';
@@ -16,6 +17,13 @@ void main() async {
   WidgetsFlutterBinding.ensureInitialized();
   WidgetsFlutterBinding.ensureInitialized();
   tzdata.initializeTimeZones();
   tzdata.initializeTimeZones();
 
 
+  if(!Platform.isAndroid) {
+    if (!await FlutterSingleInstance.platform.isFirstInstance()) {
+      print("La aplicación ya se está ejecutando");
+      exit(0);
+    }
+  }
+
   // Inicialización de la base de datos para plataformas de escritorio con FFI
   // Inicialización de la base de datos para plataformas de escritorio con FFI
   if (Platform.isWindows || Platform.isLinux) {
   if (Platform.isWindows || Platform.isLinux) {
     sqfliteFfiInit();
     sqfliteFfiInit();
@@ -48,6 +56,7 @@ void main() async {
       ChangeNotifierProvider(create: (_) => PermisoViewModel()),
       ChangeNotifierProvider(create: (_) => PermisoViewModel()),
       ChangeNotifierProvider(create: (_) => CorteCajaViewModel()),
       ChangeNotifierProvider(create: (_) => CorteCajaViewModel()),
       ChangeNotifierProvider(create: (_) => MesaViewModel()),
       ChangeNotifierProvider(create: (_) => MesaViewModel()),
+      ChangeNotifierProvider(create: (_) => PropinaViewModel()),
       // Agrega aquí cualquier otro provider que necesites
       // Agrega aquí cualquier otro provider que necesites
     ], child: const MyApp()));
     ], child: const MyApp()));
   });
   });

+ 4 - 0
lib/models/categoria_producto_model.dart

@@ -7,6 +7,8 @@ class CategoriaProducto extends Basico {
   int? esToping;
   int? esToping;
   int? maximo;
   int? maximo;
   int? minimo;
   int? minimo;
+  int? idWeb;
+  String? sincronizado;
 
 
   CategoriaProducto({
   CategoriaProducto({
     super.id,
     super.id,
@@ -15,6 +17,8 @@ class CategoriaProducto extends Basico {
     this.esToping,
     this.esToping,
     this.maximo,
     this.maximo,
     this.minimo,
     this.minimo,
+    this.idWeb,
+    this.sincronizado,
   });
   });
 
 
   @override
   @override

+ 20 - 1
lib/models/corte_caja_model.dart

@@ -1,4 +1,5 @@
-import 'basico_model.dart';
+import 'package:turquessa_app/models/models.dart';
+
 import '../services/services.dart';
 import '../services/services.dart';
 
 
 class CorteCaja {
 class CorteCaja {
@@ -20,6 +21,12 @@ class CorteCaja {
   DateTime? creado;
   DateTime? creado;
   DateTime? eliminado;
   DateTime? eliminado;
   DateTime? modificado;
   DateTime? modificado;
+  String? idWeb;
+  String? sincronizado;
+
+  List<Deposito> depositos = [];
+  List<Retiro> retiros = [];
+  List<Gasto> gastos = [];
 
 
   CorteCaja({
   CorteCaja({
     this.id,
     this.id,
@@ -40,6 +47,11 @@ class CorteCaja {
     this.creado,
     this.creado,
     this.modificado,
     this.modificado,
     this.eliminado,
     this.eliminado,
+    this.idWeb,
+    this.sincronizado,
+    this.depositos = const [],
+    this.retiros = const [],
+    this.gastos = const [],
   });
   });
 
 
   Map<String, dynamic> toJson() {
   Map<String, dynamic> toJson() {
@@ -62,6 +74,8 @@ class CorteCaja {
       'creado': creado?.toIso8601String(),
       'creado': creado?.toIso8601String(),
       'modificado': modificado?.toIso8601String(),
       'modificado': modificado?.toIso8601String(),
       'eliminado': eliminado?.toIso8601String(),
       'eliminado': eliminado?.toIso8601String(),
+      'sincronizado': sincronizado,
+      'idWeb': idWeb,
     };
     };
   }
   }
 
 
@@ -85,6 +99,9 @@ class CorteCaja {
       'creado': creado,
       'creado': creado,
       'modificado': modificado,
       'modificado': modificado,
       'eliminado': eliminado,
       'eliminado': eliminado,
+      'depositos': depositos.map((d) => d.toApi()).toList(),
+      'retiros': retiros.map((r) => r.toApi()).toList(),
+      'gastos': gastos.map((g) => g.toApi()).toList(),
     };
     };
   }
   }
 
 
@@ -107,6 +124,8 @@ class CorteCaja {
     creado = Basico.parseDate(json['creado']);
     creado = Basico.parseDate(json['creado']);
     eliminado = Basico.parseDate(json['eliminado']);
     eliminado = Basico.parseDate(json['eliminado']);
     modificado = Basico.parseDate(json['modificado']);
     modificado = Basico.parseDate(json['modificado']);
+    idWeb = Basico.parseString(json['idWeb']);
+    sincronizado = Basico.parseString(json['sincronizado']);
   }
   }
 
 
   Future<void> guardar() async {
   Future<void> guardar() async {

+ 12 - 2
lib/models/item_carrito_model.dart

@@ -1,4 +1,4 @@
-import 'toping_model.dart';
+import 'package:flutter/material.dart';
 import 'producto_model.dart';
 import 'producto_model.dart';
 
 
 class ItemCarrito {
 class ItemCarrito {
@@ -6,12 +6,22 @@ class ItemCarrito {
   int cantidad;
   int cantidad;
   Map<int, Set<int>> selectedToppings;
   Map<int, Set<int>> selectedToppings;
   Map<int, List<Producto>> selectableToppings;
   Map<int, List<Producto>> selectableToppings;
+  String? comentario;
+  bool expandido;
+  TextEditingController comentarioController;
 
 
   ItemCarrito({
   ItemCarrito({
     required this.producto,
     required this.producto,
     this.cantidad = 1,
     this.cantidad = 1,
+    this.comentario,
+    this.expandido = false,
     Map<int, Set<int>>? selectedToppings,
     Map<int, Set<int>>? selectedToppings,
     Map<int, List<Producto>>? selectableToppings,
     Map<int, List<Producto>>? selectableToppings,
   })  : selectedToppings = selectedToppings ?? {},
   })  : selectedToppings = selectedToppings ?? {},
-        selectableToppings = selectableToppings ?? {};
+        selectableToppings = selectableToppings ?? {},
+        comentarioController = TextEditingController(text: comentario ?? '');
+
+  void dispose() {
+    comentarioController.dispose();
+  }
 }
 }

+ 1 - 0
lib/models/models.dart

@@ -24,3 +24,4 @@ export '../models/permiso_model.dart';
 export '../models/usuario_permiso_model.dart';
 export '../models/usuario_permiso_model.dart';
 export '../models/retiro_model.dart';
 export '../models/retiro_model.dart';
 export '../models/mesa_model.dart';
 export '../models/mesa_model.dart';
+export '../models/propina_model.dart';

+ 20 - 5
lib/models/pedido_model.dart

@@ -1,8 +1,4 @@
-import 'package:intl/intl.dart';
-
-import 'basico_model.dart';
-import 'pedido_producto_model.dart';
-import '../services/services.dart';
+import 'models.dart';
 
 
 class Pedido extends Basico {
 class Pedido extends Basico {
   int? folio;
   int? folio;
@@ -25,6 +21,9 @@ class Pedido extends Basico {
   double? cantTransferencia;
   double? cantTransferencia;
   List<PedidoProducto> productos = [];
   List<PedidoProducto> productos = [];
   int? idWeb;
   int? idWeb;
+  String? uuid;
+  String? idCorteCaja;
+  List<Propinas> propinas = [];
 
 
   String? sincronizado;
   String? sincronizado;
 
 
@@ -51,6 +50,8 @@ class Pedido extends Basico {
     this.productos = const [],
     this.productos = const [],
     this.idWeb,
     this.idWeb,
     this.sincronizado,
     this.sincronizado,
+    this.uuid,
+    this.idCorteCaja,
   });
   });
 
 
   @override
   @override
@@ -77,6 +78,8 @@ class Pedido extends Basico {
       'cantTransferencia': cantTransferencia,
       'cantTransferencia': cantTransferencia,
       'sincronizado': sincronizado,
       'sincronizado': sincronizado,
       'idWeb': idWeb,
       'idWeb': idWeb,
+      'uuid': uuid,
+      'idCorteCaja': idCorteCaja,
     }..addAll(super.toJson());
     }..addAll(super.toJson());
   }
   }
 
 
@@ -97,7 +100,11 @@ class Pedido extends Basico {
       'cantEfectivo': cantEfectivo,
       'cantEfectivo': cantEfectivo,
       'cantTarjeta': cantTarjeta,
       'cantTarjeta': cantTarjeta,
       'cantTransferencia': cantTransferencia,
       'cantTransferencia': cantTransferencia,
+      'idMesa': idMesa,
+      'uuid': uuid,
+      'idCorteCaja': idCorteCaja,
       'productos': productos.map((producto) => producto.toApi()).toList(),
       'productos': productos.map((producto) => producto.toApi()).toList(),
+      'propinas': propinas.map((propina) => propina.toApi()).toList(),
     };
     };
     Map<String, dynamic> basicoMap = super.toJson();
     Map<String, dynamic> basicoMap = super.toJson();
     basicoMap.remove('id');
     basicoMap.remove('id');
@@ -128,8 +135,10 @@ class Pedido extends Basico {
     cantEfectivo = Basico.parseDouble(json['cantEfectivo']);
     cantEfectivo = Basico.parseDouble(json['cantEfectivo']);
     cantTarjeta = Basico.parseDouble(json['cantTarjeta']);
     cantTarjeta = Basico.parseDouble(json['cantTarjeta']);
     cantTransferencia = Basico.parseDouble(json['cantTransferencia']);
     cantTransferencia = Basico.parseDouble(json['cantTransferencia']);
+    uuid = Basico.parseString(json['uuid']);
     idWeb = Basico.parseInt(json['idWeb']);
     idWeb = Basico.parseInt(json['idWeb']);
     sincronizado = Basico.parseString(json['sincronizado']);
     sincronizado = Basico.parseString(json['sincronizado']);
+    idCorteCaja = Basico.parseString(json['idCorteCaja']);
 
 
     List<PedidoProducto> _productos = [];
     List<PedidoProducto> _productos = [];
     if (json["productos"] != null && (json["productos"] as List).isNotEmpty) {
     if (json["productos"] != null && (json["productos"] as List).isNotEmpty) {
@@ -139,5 +148,11 @@ class Pedido extends Basico {
       }
       }
     }
     }
     productos = _productos;
     productos = _productos;
+
+    if (json["propinas"] != null && (json["propinas"] as List).isNotEmpty) {
+      propinas = (json["propinas"] as List)
+          .map((item) => Propinas.fromJson(item))
+          .toList();
+    }
   }
   }
 }
 }

+ 3 - 0
lib/models/pedido_producto_model.dart

@@ -26,6 +26,7 @@ class PedidoProducto extends Basico {
     this.toppings = const [],
     this.toppings = const [],
     this.idWeb,
     this.idWeb,
     this.sincronizado,
     this.sincronizado,
+    super.eliminado,
   });
   });
 
 
   @override
   @override
@@ -41,6 +42,7 @@ class PedidoProducto extends Basico {
       'comentario': comentario,
       'comentario': comentario,
       'idWeb': idWeb,
       'idWeb': idWeb,
       'sincronizado': sincronizado,
       'sincronizado': sincronizado,
+      'eliminado': eliminado?.toIso8601String(),
     }..addAll(super.toJson());
     }..addAll(super.toJson());
   }
   }
 
 
@@ -52,6 +54,7 @@ class PedidoProducto extends Basico {
       'descuento': descuento,
       'descuento': descuento,
       'cantidad': cantidad,
       'cantidad': cantidad,
       'comentario': comentario,
       'comentario': comentario,
+      'eliminado': eliminado,
       'toppings': toppings.map((topping) => topping.toApi()).toList(),
       'toppings': toppings.map((topping) => topping.toApi()).toList(),
     };
     };
   }
   }

+ 0 - 1
lib/models/permiso_model.dart

@@ -21,7 +21,6 @@ class Permiso {
       this.creado,
       this.creado,
       this.modificado});
       this.modificado});
 
 
-  @override
   Map<String, dynamic> toJson() {
   Map<String, dynamic> toJson() {
     return {
     return {
       'id': id,
       'id': id,

+ 10 - 0
lib/models/producto_model.dart

@@ -19,6 +19,8 @@ class Producto extends Basico {
   List<Producto>? topings;
   List<Producto>? topings;
   int? activo;
   int? activo;
   List<Media>? media;
   List<Media>? media;
+  int? idWeb;
+  String? sincronizado;
 
 
   Producto({
   Producto({
     super.id,
     super.id,
@@ -36,6 +38,8 @@ class Producto extends Basico {
     this.topings,
     this.topings,
     this.activo,
     this.activo,
     this.media,
     this.media,
+    this.idWeb,
+    this.sincronizado,
   });
   });
 
 
   @override
   @override
@@ -64,6 +68,8 @@ class Producto extends Basico {
       'creado': creado?.toIso8601String(),
       'creado': creado?.toIso8601String(),
       'modificado': modificado?.toIso8601String(),
       'modificado': modificado?.toIso8601String(),
       'eliminado': eliminado?.toIso8601String(),
       'eliminado': eliminado?.toIso8601String(),
+      'sincronizado': sincronizado,
+      'idWeb': idWeb,
     };
     };
 
 
     return data..addAll(super.toJson());
     return data..addAll(super.toJson());
@@ -106,6 +112,8 @@ class Producto extends Basico {
     codigo = Basico.parseString(json['codigo']);
     codigo = Basico.parseString(json['codigo']);
     descuento = Basico.parseString(json['descuento']);
     descuento = Basico.parseString(json['descuento']);
     activo = Basico.parseInt(json['activo']);
     activo = Basico.parseInt(json['activo']);
+    idWeb = Basico.parseInt(json['idWeb']);
+    sincronizado = Basico.parseString(json['sincronizado']);
     if (json['toping'] is bool) {
     if (json['toping'] is bool) {
       toping = json['toping'] ? 1 : 0;
       toping = json['toping'] ? 1 : 0;
     } else {
     } else {
@@ -134,6 +142,8 @@ class Producto extends Basico {
     creado = Basico.parseDate(json['creado']);
     creado = Basico.parseDate(json['creado']);
     modificado = Basico.parseDate(json['modificado']);
     modificado = Basico.parseDate(json['modificado']);
     eliminado = Basico.parseDate(json['eliminado']);
     eliminado = Basico.parseDate(json['eliminado']);
+    idWeb = Basico.parseInt(json['idWeb']);
+    sincronizado = Basico.parseString(json['sincronizado']);
     if (json['media'] != null) {
     if (json['media'] != null) {
       media = (json['media'] as List).map((i) => Media.fromJson(i)).toList();
       media = (json['media'] as List).map((i) => Media.fromJson(i)).toList();
     }
     }

+ 54 - 0
lib/models/propina_model.dart

@@ -0,0 +1,54 @@
+import 'models.dart';
+
+class Propinas extends Basico {
+  int? idPedido;
+  double? cantidad;
+  String? comentario;
+  String? sincronizado;
+  int? idWeb;
+
+  Propinas({
+    super.id,
+    this.idPedido,
+    this.cantidad,
+    this.comentario,
+    this.sincronizado,
+    this.idWeb,
+    super.eliminado,
+    super.creado,
+  });
+
+  @override
+  Map<String, dynamic> toJson() {
+    return {
+      'id': id,
+      'idPedido': idPedido,
+      'cantidad': cantidad,
+      'comentario': comentario,
+      'sincronizado': sincronizado,
+      'creado': creado?.toIso8601String(),
+      'eliminado': eliminado?.toIso8601String(),
+      'idWeb': idWeb,
+    }..addAll(super.toJson());
+  }
+
+  Map<String, dynamic> toApi() {
+    return {
+      'idPedido': idPedido,
+      'cantidad': cantidad,
+      'comentario': comentario,
+      'sincronizado': sincronizado,
+      'creado': creado?.toIso8601String(),
+      'eliminado': eliminado?.toIso8601String(),
+    };
+  }
+
+  Propinas.fromJson(Map<String, dynamic> json) {
+    super.parseJson(json);
+    idPedido = Basico.parseInt(json['idPedido']);
+    cantidad = Basico.parseDouble(json['cantidad']);
+    comentario = Basico.parseString(json['comentario']);
+    idWeb = Basico.parseInt(json['idWeb']);
+    sincronizado = Basico.parseString(json['sincronizado']);
+  }
+}

+ 249 - 4
lib/services/repo_service.dart

@@ -9,7 +9,7 @@ import 'services.dart';
 import '../views/producto/producto_imagen.dart';
 import '../views/producto/producto_imagen.dart';
 
 
 class RepoService<T> {
 class RepoService<T> {
-  static int dbVersion = 23;
+  static int dbVersion = 26;
   static String dbName = 'posTurquessa.db';
   static String dbName = 'posTurquessa.db';
   static const String id = Basico.identificadorWeb;
   static const String id = Basico.identificadorWeb;
   static const String idLocal = Basico.identificadorLocal;
   static const String idLocal = Basico.identificadorLocal;
@@ -27,6 +27,7 @@ class RepoService<T> {
     'Variable': Variable(),
     'Variable': Variable(),
     'Descuento': Descuento(),
     'Descuento': Descuento(),
     'Mesa': Mesa(),
     'Mesa': Mesa(),
+    'Propinas': Propinas(),
   };
   };
 
 
   Future<Database?> get db async {
   Future<Database?> get db async {
@@ -91,6 +92,14 @@ class RepoService<T> {
       'idLocal': -1,
       'idLocal': -1,
     });
     });
 
 
+    await db.insert('Variable', {
+      'nombre': 'Mesas',
+      'clave': 'MESAS',
+      'descripcion': 'Uso de mesas en el punto de venta',
+      'activo': 1,
+      'idLocal': -1,
+    });
+
     await db.insert('Descuento', {'porcentaje': 0});
     await db.insert('Descuento', {'porcentaje': 0});
     await db.insert('Descuento', {'porcentaje': 5});
     await db.insert('Descuento', {'porcentaje': 5});
     await db.insert('Descuento', {'porcentaje': 10});
     await db.insert('Descuento', {'porcentaje': 10});
@@ -627,6 +636,62 @@ class RepoService<T> {
           ALTER TABLE Usuario ADD COLUMN token TEXT;
           ALTER TABLE Usuario ADD COLUMN token TEXT;
         ''');
         ''');
 
 
+          await db.insert('Variable', {
+            'nombre': 'Mesas',
+            'clave': 'MESAS',
+            'descripcion': 'Uso de mesas en el punto de venta',
+            'activo': 1,
+            'idLocal': -1,
+          });
+
+          break;
+
+        case 23:
+          await db.execute('''
+          ALTER TABLE Pedido ADD COLUMN uuid TEXT;
+        ''');
+
+          break;
+
+        case 24:
+          await db.execute('''
+          CREATE TABLE Propinas (
+            id INTEGER PRIMARY KEY AUTOINCREMENT,
+            idLocal INTEGER,
+            idPedido INTEGER,
+            cantidad REAL,
+            comentario TEXT,
+            sincronizado TEXT,
+            idWeb INTEGER,
+            creado TEXT,
+            modificado TEXT,
+            eliminado TEXT
+          )
+        ''');
+
+          break;
+
+        case 25:
+          await db.execute('''
+          ALTER TABLE Pedido ADD COLUMN idCorteCaja TEXT;
+        ''');
+
+          await db.execute('''
+          ALTER TABLE Producto ADD COLUMN idWeb INTEGER;
+        ''');
+
+          await db.execute('''
+          ALTER TABLE Producto ADD COLUMN sincronizado TEXT;
+        ''');
+
+          await db.execute('''
+          ALTER TABLE CategoriaProducto ADD COLUMN idWeb INTEGER;
+        ''');
+
+          await db.execute('''
+          ALTER TABLE CategoriaProducto ADD COLUMN sincronizado TEXT;
+        ''');
+
           break;
           break;
       }
       }
       oldVersion++;
       oldVersion++;
@@ -706,6 +771,71 @@ class RepoService<T> {
     }
     }
   }
   }
 
 
+  Future<int> guardarPedidoProducto(PedidoProducto pedidoProducto) async {
+    try {
+      var dbClient = await db;
+
+      // Busca registros existentes
+      List<Map<String, dynamic>> existingRecords = await dbClient!.query(
+        'PedidoProducto',
+        where: 'idPedido = ? AND idProducto = ?',
+        whereArgs: [pedidoProducto.idPedido, pedidoProducto.idProducto],
+      );
+
+      if (existingRecords.isNotEmpty) {
+        // Obtiene el ID existente
+        int existingId = existingRecords.first['id'] as int;
+
+        // Clona el mapa JSON y elimina el campo 'id' para la actualización
+        Map<String, dynamic> dataToUpdate = Map.from(pedidoProducto.toJson());
+        dataToUpdate.remove('id');
+
+        // Actualiza el registro
+        await dbClient.update(
+          'PedidoProducto',
+          dataToUpdate,
+          where: 'id = ?',
+          whereArgs: [existingId],
+        );
+
+        print(
+            "Registro actualizado para idPedido: ${pedidoProducto.idPedido}, idProducto: ${pedidoProducto.idProducto}");
+        return existingId;
+      } else {
+        // Inserta un nuevo registro eliminando el campo 'id' antes de insertar
+        Map<String, dynamic> dataToInsert = Map.from(pedidoProducto.toJson());
+        dataToInsert.remove('id');
+
+        int id = await dbClient.insert('PedidoProducto', dataToInsert);
+
+        print(
+            "Registro insertado para idPedido: ${pedidoProducto.idPedido}, idProducto: ${pedidoProducto.idProducto}");
+        return id;
+      }
+    } catch (e) {
+      print('Error al guardar PedidoProducto: $e');
+      return 0;
+    }
+  }
+
+  Future<PedidoProducto?> obtenerPorIdPedidoYProducto({
+    required int idPedido,
+    required int idProducto,
+  }) async {
+    Database? dbClient = await db;
+    List<Map<String, dynamic>> maps = await dbClient!.query(
+      'PedidoProducto',
+      where: 'idPedido = ? AND idProducto = ?',
+      whereArgs: [idPedido, idProducto],
+    );
+
+    if (maps.isNotEmpty) {
+      return PedidoProducto.fromJson(Map<String, dynamic>.from(maps.first));
+    }
+
+    return null; // Retorna null si no se encuentra ningún registro
+  }
+
   void asignarFechasLocalmente(Basico model) {
   void asignarFechasLocalmente(Basico model) {
     DateTime ahora = DateTime.now();
     DateTime ahora = DateTime.now();
     if (model.creado == null) {
     if (model.creado == null) {
@@ -779,11 +909,21 @@ class RepoService<T> {
     return item.toJson();
     return item.toJson();
   }
   }
 
 
-  Future<List<T>> obtenerTodos({String orderBy = 'id DESC'}) async {
+  Future<List<T>> obtenerTodos<T>({
+    String where = 'eliminado IS NULL',
+    List<dynamic>? whereArgs,
+    String orderBy = 'id DESC',
+  }) async {
     var db = await this.db;
     var db = await this.db;
     String tableName = T.toString();
     String tableName = T.toString();
-    var result = await db!
-        .query(tableName, where: 'eliminado IS NULL', orderBy: orderBy);
+
+    var result = await db!.query(
+      tableName,
+      where: where,
+      whereArgs: whereArgs,
+      orderBy: orderBy,
+    );
+
     return result.map((map) => fromMap<T>(map)).toList();
     return result.map((map) => fromMap<T>(map)).toList();
   }
   }
 
 
@@ -797,6 +937,8 @@ class RepoService<T> {
         return Usuario.fromJson(map) as T;
         return Usuario.fromJson(map) as T;
       case CorteCaja:
       case CorteCaja:
         return CorteCaja.fromJson(map) as T;
         return CorteCaja.fromJson(map) as T;
+      case Propinas:
+        return Propinas.fromJson(map) as T;
       default:
       default:
         throw Exception('Tipo no soportado');
         throw Exception('Tipo no soportado');
     }
     }
@@ -823,6 +965,17 @@ class RepoService<T> {
 
 
   //CORTE CAJA
   //CORTE CAJA
 
 
+  Future<List<Pedido>> obtenerPedidosPorCorteCaja(String idCorteCaja) async {
+    var dbClient = await db;
+    List<Map<String, dynamic>> maps = await dbClient!.query(
+      'Pedido',
+      where: 'idCorteCaja = ? AND eliminado IS NULL AND estatus != ?',
+      whereArgs: [idCorteCaja, 'CANCELADO'],
+    );
+
+    return maps.map((map) => Pedido.fromJson(map)).toList();
+  }
+
   Future<List<Deposito>> obtenerDepositosPorIdCorteCaja(
   Future<List<Deposito>> obtenerDepositosPorIdCorteCaja(
       String idCorteCaja) async {
       String idCorteCaja) async {
     var dbClient = await db;
     var dbClient = await db;
@@ -1459,4 +1612,96 @@ class RepoService<T> {
       }
       }
     }
     }
   }
   }
+
+  Future<List<Producto>>
+      obtenerProductosNoSincronizadosOrdenadosPorFecha() async {
+    var dbClient = await db;
+    List<Map<String, dynamic>> result = await dbClient!.query(
+      'Producto',
+      where: 'sincronizado IS NULL AND eliminado IS NULL',
+      orderBy: 'creado ASC',
+    );
+    return result.map((map) => Producto.fromJson(map)).toList();
+  }
+
+  Future<List<CategoriaProducto>>
+      obtenerCategoriasNoSincronizadasOrdenadasPorFecha() async {
+    var dbClient = await db;
+    // Similar a productos, ordenando por 'modificado'
+    List<Map<String, dynamic>> result = await dbClient!.query(
+      'CategoriaProducto',
+      where: 'sincronizado IS NULL AND eliminado IS NULL',
+      orderBy: 'creado ASC',
+    );
+    return result.map((map) => CategoriaProducto.fromJson(map)).toList();
+  }
+
+  Future<void> actualizarProductoSincronizado(
+      int idProducto, int idWeb, String sincronizado) async {
+    var dbClient = await db;
+    await dbClient!.update(
+      'Producto',
+      {
+        'idWeb': idWeb,
+        'sincronizado': sincronizado,
+      },
+      where: 'id = ?',
+      whereArgs: [idProducto],
+    );
+  }
+
+  Future<void> actualizarCategoriaSincronizada(
+      int idCategoria, int idWeb, String sincronizado) async {
+    var dbClient = await db;
+    await dbClient!.update(
+      'CategoriaProducto',
+      {
+        'idWeb': idWeb,
+        'sincronizado': sincronizado,
+      },
+      where: 'id = ?',
+      whereArgs: [idCategoria],
+    );
+  }
+
+  Future<List<Pedido>> obtenerPedidosConPropinasPorCorte(
+      String idCorteCaja) async {
+    Database? dbClient = await db;
+
+    List<Map<String, dynamic>> maps = await dbClient!.rawQuery('''
+    SELECT *
+    FROM Pedido
+    WHERE idCorteCaja = ?
+      AND estatus = 'TERMINADO'
+      AND eliminado IS NULL
+      AND id IN (
+        SELECT idPedido
+        FROM Propinas
+        WHERE eliminado IS NULL
+      )
+    ORDER BY id DESC
+  ''', [idCorteCaja]);
+
+    List<Pedido> pedidos = [];
+    for (var map in maps) {
+      Pedido pedido = Pedido.fromJson(map);
+
+      List<Propinas> propinas = await obtenerPropinasPorPedido(pedido.id!);
+      pedido.propinas = propinas;
+
+      pedidos.add(pedido);
+    }
+
+    return pedidos;
+  }
+
+  Future<List<Propinas>> obtenerPropinasPorPedido(int idPedido) async {
+    Database? dbClient = await db;
+    List<Map<String, dynamic>> maps = await dbClient!.query(
+      'Propinas',
+      where: 'idPedido = ? AND eliminado IS NULL',
+      whereArgs: [idPedido],
+    );
+    return maps.map((map) => Propinas.fromJson(map)).toList();
+  }
 }
 }

+ 206 - 44
lib/viewmodels/corte_caja_view_model.dart

@@ -1,10 +1,14 @@
+import 'package:collection/collection.dart';
+
 import '../../widgets/widgets.dart';
 import '../../widgets/widgets.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
-import 'package:intl/intl.dart';
 import 'package:uuid/uuid.dart';
 import 'package:uuid/uuid.dart';
+import '../data/api_response.dart';
 import '../models/models.dart';
 import '../models/models.dart';
-import '../services/repo_service.dart';
+import '../services/services.dart';
 import '../views/corte_caja/corte_caja_ticket.dart';
 import '../views/corte_caja/corte_caja_ticket.dart';
+import 'dart:convert';
+import 'package:http/http.dart' as http;
 
 
 class CorteCajaViewModel extends ChangeNotifier {
 class CorteCajaViewModel extends ChangeNotifier {
   List<CorteCaja> _cortes = [];
   List<CorteCaja> _cortes = [];
@@ -58,6 +62,12 @@ class CorteCajaViewModel extends ChangeNotifier {
   TextEditingController get fondoDiaSigController => _fondoDiaSigController;
   TextEditingController get fondoDiaSigController => _fondoDiaSigController;
   TextEditingController get ventaPuntosController => ventaPuntosController;
   TextEditingController get ventaPuntosController => ventaPuntosController;
 
 
+  List<Pedido> _pedidosConPropinas = [];
+  double _totalPropinas = 0.0;
+
+  List<Pedido> get pedidosConPropinas => _pedidosConPropinas;
+  double get totalPropinas => _totalPropinas;
+
   void setIsLoading(bool loading) {
   void setIsLoading(bool loading) {
     _isLoading = loading;
     _isLoading = loading;
     notifyListeners();
     notifyListeners();
@@ -174,11 +184,102 @@ class CorteCajaViewModel extends ChangeNotifier {
     notifyListeners();
     notifyListeners();
   }
   }
 
 
+  Future<CorteCaja?> fetchCorteCajaConDetalles(String idCorteCaja) async {
+    // Similar a fetchDepositosAndRetiros, pero devuelve el corte ya cargado
+    await fetchDepositosAndRetiros(idCorteCaja);
+    return _cortes.firstWhereOrNull((c) => c.id == idCorteCaja);
+  }
+
   void selectCorte(CorteCaja corte) {
   void selectCorte(CorteCaja corte) {
     _selectedCorte = corte;
     _selectedCorte = corte;
     notifyListeners();
     notifyListeners();
   }
   }
 
 
+  Future<List<CorteCaja>>
+      fetchAllLocalCortesCajasNoSincronizadosOrdenadosPorFecha() async {
+    setIsLoading(true);
+    var db = await RepoService().db;
+
+    List<Map<String, dynamic>> result = await db!.query('CorteCaja',
+        where: 'sincronizado IS NULL', orderBy: 'fechaApertura ASC');
+
+    List<CorteCaja> cortesNoSync =
+        result.map((map) => CorteCaja.fromJson(map)).toList();
+    setIsLoading(false);
+    return cortesNoSync;
+  }
+
+  Future<bool> sincronizarCorteCajas() async {
+    List<CorteCaja> cortesNoSincronizados =
+        await fetchAllLocalCortesCajasNoSincronizadosOrdenadosPorFecha();
+
+    if (cortesNoSincronizados.isNotEmpty) {
+      CorteCaja corteNoSincronizado = cortesNoSincronizados.first;
+
+      if (corteNoSincronizado.id != null &&
+          corteNoSincronizado.id!.isNotEmpty) {
+        await fetchCorteCajaConDetalles(corteNoSincronizado.id!);
+        corteNoSincronizado =
+            _cortes.firstWhere((c) => c.id == corteNoSincronizado.id);
+
+        corteNoSincronizado.depositos = this.depositos;
+        corteNoSincronizado.retiros = this.retiros;
+        corteNoSincronizado.gastos = this.gastos;
+      }
+
+      Map<String, dynamic> corteJson =
+          await prepararCorteCajaParaApi(corteNoSincronizado);
+
+      print('JSON Corte Caja enviado: $corteJson');
+
+      var response = ApiResponse(await BaseService()
+          .post('/pos/corte-caja/sincronizar2', body: corteJson));
+
+      if (response.isOk && response.detalle != null) {
+        String idWeb = response.detalle!['idWeb'];
+        String sincronizado = response.detalle!['sincronizado'];
+
+        await actualizarCorteCajaSincronizado(
+            corteNoSincronizado.id!, idWeb, sincronizado);
+
+        return true;
+      } else {
+        print(
+            'Error en la sincronización del corte de caja: ${response.mensaje}');
+        return true;
+      }
+    } else {
+      print('No se encontraron cortes de caja no sincronizados.');
+      return false;
+    }
+  }
+
+  Future<void> actualizarCorteCajaSincronizado(
+      String idCorteCaja, String idWeb, String sincronizado) async {
+    var db = await RepoService().db;
+
+    await db!.update(
+      'CorteCaja',
+      {
+        'idWeb': idWeb,
+        'sincronizado': sincronizado,
+      },
+      where: 'id = ?',
+      whereArgs: [idCorteCaja],
+    );
+  }
+
+  Future<Map<String, dynamic>> prepararCorteCajaParaApi(CorteCaja corte) async {
+    String? claveSucursal =
+        await RepoService().obtenerClaveSucursalSeleccionada();
+    Map<String, dynamic> apiMap = corte.toApi();
+    apiMap['claveSucursal'] = claveSucursal;
+    if (corte.idWeb != null && corte.idWeb!.isNotEmpty) {
+      apiMap['idWeb'] = corte.idWeb;
+    }
+    return apiMap;
+  }
+
   Future<void> addDeposito(double monto, String descripcion, String? persona,
   Future<void> addDeposito(double monto, String descripcion, String? persona,
       String corteCajaId) async {
       String corteCajaId) async {
     Deposito nuevoDeposito = Deposito(
     Deposito nuevoDeposito = Deposito(
@@ -196,6 +297,11 @@ class CorteCajaViewModel extends ChangeNotifier {
 
 
     calcularCorteFinal();
     calcularCorteFinal();
 
 
+    if (_selectedCorte != null) {
+      _selectedCorte!.sincronizado = null;
+      await _selectedCorte!.guardar();
+    }
+
     notifyListeners();
     notifyListeners();
   }
   }
 
 
@@ -216,6 +322,11 @@ class CorteCajaViewModel extends ChangeNotifier {
 
 
     calcularCorteFinal();
     calcularCorteFinal();
 
 
+    if (_selectedCorte != null) {
+      _selectedCorte!.sincronizado = null;
+      await _selectedCorte!.guardar();
+    }
+
     notifyListeners();
     notifyListeners();
   }
   }
 
 
@@ -236,53 +347,50 @@ class CorteCajaViewModel extends ChangeNotifier {
 
 
     calcularCorteFinal();
     calcularCorteFinal();
 
 
+    if (_selectedCorte != null) {
+      _selectedCorte!.sincronizado = null;
+      await _selectedCorte!.guardar();
+    }
+
     notifyListeners();
     notifyListeners();
   }
   }
 
 
-  Future<void> cargarVentasDesdeFechaApertura(DateTime fechaApertura) async {
+  Future<void> cargarVentasPorCorteId(String idCorteCaja) async {
     setIsLoading(true);
     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;
+    try {
+      RepoService<Pedido> repoPedido = RepoService<Pedido>();
+      List<Pedido> pedidos =
+          await repoPedido.obtenerPedidosPorCorteCaja(idCorteCaja);
+
+      pedidos = pedidos.where((p) => p.estatus == 'TERMINADO').toList();
+
+      double totalEfectivo = 0;
+      double totalTarjeta = 0;
+      double totalTransferencia = 0;
+      double totalSinCambio = 0;
+
+      for (var pedido in pedidos) {
+        totalEfectivo += pedido.cantEfectivo ?? 0;
+        totalTarjeta += pedido.cantTarjeta ?? 0;
+        totalTransferencia += pedido.cantTransferencia ?? 0;
+        totalSinCambio += pedido.totalPedido ?? 0;
+      }
+
+      double totalDia = totalEfectivo + totalTarjeta + totalTransferencia;
+      double cambio = totalDia - totalSinCambio;
+      double efectivoSinCambio = totalEfectivo - cambio;
+
+      _ventaEfeController.text = formatoMiles(efectivoSinCambio);
+      _ventaTarjController.text = formatoMiles(totalTarjeta);
+      _ventaTransfController.text = formatoMiles(totalTransferencia);
+      _totalVentaController.text =
+          formatoMiles(efectivoSinCambio + totalTarjeta + totalTransferencia);
+    } catch (e) {
+      print("Error al cargar pedidos por idCorteCaja: $e");
+    } finally {
+      setIsLoading(false);
+      notifyListeners();
     }
     }
-
-    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();
   }
   }
 
 
   bool hasOpenCorteCaja() {
   bool hasOpenCorteCaja() {
@@ -315,6 +423,7 @@ class CorteCajaViewModel extends ChangeNotifier {
     _selectedCorte?.gasto = gasto;
     _selectedCorte?.gasto = gasto;
     _selectedCorte?.retiro = retiro;
     _selectedCorte?.retiro = retiro;
     _selectedCorte?.deposito = deposito;
     _selectedCorte?.deposito = deposito;
+    _selectedCorte!.sincronizado = null;
 
 
     calcularCorteFinal();
     calcularCorteFinal();
 
 
@@ -366,6 +475,7 @@ class CorteCajaViewModel extends ChangeNotifier {
     if (corteCajaId != null) {
     if (corteCajaId != null) {
       await fetchDepositosAndRetiros(corteCajaId);
       await fetchDepositosAndRetiros(corteCajaId);
       _selectedCorte = _cortes.firstWhere((corte) => corte.id == corteCajaId);
       _selectedCorte = _cortes.firstWhere((corte) => corte.id == corteCajaId);
+      await cargarVentasPorCorteId(corteCajaId);
     }
     }
 
 
     if (_selectedCorte == null) {
     if (_selectedCorte == null) {
@@ -423,4 +533,56 @@ class CorteCajaViewModel extends ChangeNotifier {
       'ventaEfe': ventaEfe,
       'ventaEfe': ventaEfe,
     };
     };
   }
   }
+
+  Future<List<Pedido>> fetchPedidosCancelados(String idCorteCaja) async {
+    var db = await RepoService().db;
+    List<Map<String, dynamic>> maps = await db!.query(
+      'Pedido',
+      where: 'idCorteCaja = ? AND eliminado IS NULL AND estatus = ?',
+      whereArgs: [idCorteCaja, 'CANCELADO'],
+    );
+    return maps.map((map) => Pedido.fromJson(map)).toList();
+  }
+
+  Future<DateTime?> fetchHoraLocal() async {
+    try {
+      final url = Uri.parse('https://worldtimeapi.org/api/ip');
+      final response = await http.get(url);
+
+      if (response.statusCode == 200) {
+        final data = jsonDecode(response.body);
+        final dateTimeString = data['datetime'] as String;
+        DateTime serverDateTime = DateTime.parse(dateTimeString);
+        final localTime = serverDateTime.toLocal();
+
+        return localTime;
+      } else {
+        print("Error al consultar la hora: ${response.statusCode}");
+        return null;
+      }
+    } catch (e) {
+      print("Excepción al consultar la hora: $e");
+      return null;
+    }
+  }
+
+  Future<void> fetchPedidosConPropinas(String idCorteCaja) async {
+    RepoService<Pedido> repoPedido = RepoService<Pedido>();
+
+    List<Pedido> pedidosPropina =
+        await repoPedido.obtenerPedidosConPropinasPorCorte(idCorteCaja);
+
+    double sumaPropinas = 0.0;
+    for (var pedido in pedidosPropina) {
+      double sumLocal = pedido.propinas.fold(
+        0.0,
+        (prev, propina) => prev + (propina.cantidad ?? 0.0),
+      );
+      sumaPropinas += sumLocal;
+    }
+    _pedidosConPropinas = pedidosPropina;
+    _totalPropinas = sumaPropinas;
+
+    notifyListeners();
+  }
 }
 }

+ 16 - 7
lib/viewmodels/mesa_view_model.dart

@@ -34,19 +34,28 @@ class MesaViewModel extends ChangeNotifier {
     notifyListeners();
     notifyListeners();
   }
   }
 
 
-  Future<void> fetchLocalAll({int page = 1}) async {
+  Future<void> fetchLocalAll({int page = 1, bool sinLimite = false}) async {
     _currentPage = page;
     _currentPage = page;
     var db = await RepoService().db;
     var db = await RepoService().db;
 
 
-    int? count =
-        Sqflite.firstIntValue(await db!.rawQuery('SELECT COUNT(*) FROM Mesa'));
-    _totalMesas = count ?? 0;
+    if (!sinLimite) {
+      int? count = Sqflite.firstIntValue(
+          await db!.rawQuery('SELECT COUNT(*) FROM Mesa'));
+      _totalMesas = count ?? 0;
+    }
 
 
-    int offset = (_limit * (page - 1));
+    String? limitOffsetClause;
+    if (!sinLimite) {
+      int offset = (_limit * (page - 1));
+      limitOffsetClause = 'LIMIT $_limit OFFSET $offset';
+    } else {
+      limitOffsetClause = '';
+    }
 
 
-    var query = await db.query('Mesa',
-        orderBy: 'id asc', limit: _limit, offset: offset);
+    var query = await db!
+        .rawQuery('SELECT * FROM Mesa ORDER BY id ASC $limitOffsetClause');
     _mesas = query.map((element) => Mesa.fromJson(element)).toList();
     _mesas = query.map((element) => Mesa.fromJson(element)).toList();
+
     notifyListeners();
     notifyListeners();
   }
   }
 
 

+ 119 - 10
lib/viewmodels/pedido_view_model.dart

@@ -1,3 +1,4 @@
+import 'package:collection/collection.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:intl/intl.dart';
 import 'package:intl/intl.dart';
 import 'package:sqflite/sqflite.dart';
 import 'package:sqflite/sqflite.dart';
@@ -92,6 +93,62 @@ class PedidoViewModel extends ChangeNotifier {
     }
     }
   }
   }
 
 
+  Future<bool> guardarPedidoConProductos({
+    required Pedido pedido,
+    required List<ItemCarrito> carrito,
+  }) async {
+    try {
+      RepoService<Pedido> repoPedido = RepoService<Pedido>();
+      RepoService<PedidoProducto> repoPedidoProducto =
+          RepoService<PedidoProducto>();
+      RepoService<PedidoProductoTopping> repoPedidoProductoTopping =
+          RepoService<PedidoProductoTopping>();
+
+      // Guardar el pedido en la base de datos
+      if (pedido.id == null || pedido.id == 0) {
+        int nextFolio = await repoPedido.obtenerProximoFolio();
+        pedido.folio = nextFolio;
+        pedido.id = await repoPedido.guardarLocal(pedido);
+      } else {
+        await repoPedido.guardar(pedido);
+      }
+
+      // Manejar los productos del carrito
+      for (var item in carrito) {
+        PedidoProducto pedidoProducto = PedidoProducto(
+          idPedido: pedido.id,
+          idProducto: item.producto.id,
+          cantidad: item.cantidad,
+          costoUnitario: item.producto.precio.toString(),
+          sincronizado: null,
+        );
+
+        // Guardar o actualizar el producto
+        int idPedidoProducto =
+            await repoPedidoProducto.guardarPedidoProducto(pedidoProducto);
+
+        // Manejar los toppings seleccionados
+        for (var entry in item.selectedToppings.entries) {
+          int categoriaId = entry.key;
+          for (int toppingId in entry.value) {
+            PedidoProductoTopping topping = PedidoProductoTopping(
+              idPedidoProducto: idPedidoProducto,
+              idTopping: toppingId,
+              idCategoria: categoriaId,
+            );
+            await repoPedidoProductoTopping.guardarLocal(topping);
+          }
+        }
+      }
+
+      notifyListeners();
+      return true;
+    } catch (e) {
+      print("Error al guardar el pedido con productos: $e");
+      return false;
+    }
+  }
+
   Future<void> fetchLocalPedidos({int page = 1}) async {
   Future<void> fetchLocalPedidos({int page = 1}) async {
     _isLoading = true;
     _isLoading = true;
     _currentPage = page;
     _currentPage = page;
@@ -120,6 +177,23 @@ class PedidoViewModel extends ChangeNotifier {
     }
     }
   }
   }
 
 
+  Future<void> eliminarProductoDelPedido(int idProducto, int idPedido) async {
+    RepoService<PedidoProducto> repoPedidoProducto =
+        RepoService<PedidoProducto>();
+
+    PedidoProducto? producto =
+        await repoPedidoProducto.obtenerPorIdPedidoYProducto(
+      idPedido: idPedido,
+      idProducto: idProducto,
+    );
+
+    if (producto != null) {
+      producto.eliminado = DateTime.now().toUtc();
+      await repoPedidoProducto.guardar(producto);
+      notifyListeners();
+    }
+  }
+
   Future<void> fetchLocalPedidosForScreen({int page = 1}) async {
   Future<void> fetchLocalPedidosForScreen({int page = 1}) async {
     setIsLoading(true);
     setIsLoading(true);
     RepoService<Pedido> repoPedido = RepoService<Pedido>();
     RepoService<Pedido> repoPedido = RepoService<Pedido>();
@@ -164,9 +238,12 @@ class PedidoViewModel extends ChangeNotifier {
     if (pedido != null) {
     if (pedido != null) {
       RepoService<PedidoProducto> repoProducto = RepoService<PedidoProducto>();
       RepoService<PedidoProducto> repoProducto = RepoService<PedidoProducto>();
       RepoService<Producto> repoProductoInfo = RepoService<Producto>();
       RepoService<Producto> repoProductoInfo = RepoService<Producto>();
+      RepoService<Propinas> repoPropina = RepoService<Propinas>();
       List<PedidoProducto> productos =
       List<PedidoProducto> productos =
           await repoProducto.obtenerPorIdPedido(idPedido);
           await repoProducto.obtenerPorIdPedido(idPedido);
 
 
+      productos = productos.where((p) => p.eliminado == null).toList();
+
       for (var producto in productos) {
       for (var producto in productos) {
         Producto? prodInfo =
         Producto? prodInfo =
             await repoProductoInfo.obtenerProductoPorId(producto.idProducto!);
             await repoProductoInfo.obtenerProductoPorId(producto.idProducto!);
@@ -188,6 +265,9 @@ class PedidoViewModel extends ChangeNotifier {
         }
         }
 
 
         producto.toppings = toppings;
         producto.toppings = toppings;
+        List<Propinas> propinas = await repoPropina.obtenerTodos<Propinas>(
+            where: 'idPedido = ?', whereArgs: [idPedido]);
+        pedido.propinas = propinas;
       }
       }
 
 
       pedido.productos = productos;
       pedido.productos = productos;
@@ -267,7 +347,7 @@ class PedidoViewModel extends ChangeNotifier {
       Map<String, dynamic> pedidoJson =
       Map<String, dynamic> pedidoJson =
           await prepararPedidoParaApi(pedidoNoSincronizado);
           await prepararPedidoParaApi(pedidoNoSincronizado);
 
 
-      print('JSON enviado: $pedidoJson');
+      //print('JSON enviado: $pedidoJson');
 
 
       var response = ApiResponse(await BaseService()
       var response = ApiResponse(await BaseService()
           .post('/pos/pedido/sincronizar', body: pedidoJson));
           .post('/pos/pedido/sincronizar', body: pedidoJson));
@@ -315,19 +395,48 @@ class PedidoViewModel extends ChangeNotifier {
     setIsLoading(false);
     setIsLoading(false);
     return allPedidos;
     return allPedidos;
   }
   }
-}
 
 
-Future<Map<String, dynamic>> prepararPedidoParaApi(Pedido pedido) async {
-  String? claveSucursal =
-      await RepoService().obtenerClaveSucursalSeleccionada();
+  Future<List<Pedido>> fetchPedidosNuevosByCorteId(String idCorteCaja) async {
+    var db = await RepoService().db;
 
 
-  Map<String, dynamic> apiMap = pedido.toApi();
+    List<Map<String, dynamic>> maps = await db!.query(
+      'Pedido',
+      where: 'estatus = ? AND idCorteCaja = ? AND eliminado IS NULL',
+      whereArgs: ['NUEVO', idCorteCaja],
+    );
+
+    if (maps.isNotEmpty) {
+      return maps.map((map) => Pedido.fromJson(map)).toList();
+    }
+    return [];
+  }
 
 
-  apiMap['claveSucursal'] = claveSucursal;
+  Future<void> actualizarCorteCajaEnPedidos(
+      List<int> idsPedidos, String nuevoIdCorte) async {
+    var db = await RepoService().db;
 
 
-  if (pedido.idWeb != null && pedido.idWeb! > 0) {
-    apiMap['idWeb'] = pedido.idWeb;
+    for (int idPedido in idsPedidos) {
+      await db!.update(
+        'Pedido',
+        {'idCorteCaja': nuevoIdCorte, 'sincronizado': null},
+        where: 'id = ?',
+        whereArgs: [idPedido],
+      );
+    }
   }
   }
 
 
-  return apiMap;
+  Future<Map<String, dynamic>> prepararPedidoParaApi(Pedido pedido) async {
+    String? claveSucursal =
+        await RepoService().obtenerClaveSucursalSeleccionada();
+
+    Map<String, dynamic> apiMap = pedido.toApi();
+
+    apiMap['claveSucursal'] = claveSucursal;
+
+    if (pedido.idWeb != null && pedido.idWeb! > 0) {
+      apiMap['idWeb'] = pedido.idWeb;
+    }
+
+    return apiMap;
+  }
 }
 }

+ 98 - 1
lib/viewmodels/producto_view_model.dart

@@ -284,7 +284,6 @@ class ProductoViewModel<T> extends ChangeNotifier {
         if (toppingApi.isNotEmpty) {
         if (toppingApi.isNotEmpty) {
           print("Producto Toppings API obtenidos: ${toppingApi.length}");
           print("Producto Toppings API obtenidos: ${toppingApi.length}");
 
 
-          // Delegamos la sincronización de los toppings al RepoService
           await RepoService().sincronizarProductoTopping(toppingApi);
           await RepoService().sincronizarProductoTopping(toppingApi);
           notifyListeners();
           notifyListeners();
           return true;
           return true;
@@ -324,6 +323,104 @@ class ProductoViewModel<T> extends ChangeNotifier {
     }
     }
   }
   }
 
 
+  Future<bool> sincronizarProductosLocales() async {
+    List<Producto> productosNoSincronizados =
+        await RepoService().obtenerProductosNoSincronizadosOrdenadosPorFecha();
+
+    if (productosNoSincronizados.isNotEmpty) {
+      Producto productoNoSincronizado = productosNoSincronizados.first;
+
+      Map<String, dynamic> productoJson =
+          await prepararProductoParaApi(productoNoSincronizado);
+
+      print('JSON Producto enviado: $productoJson');
+
+      // Llamada a la API
+      var response = ApiResponse(await BaseService()
+          .post('/pos/producto/sincronizar', body: productoJson));
+
+      if (response.isOk && response.detalle != null) {
+        int idWeb = response.detalle!['idWeb'];
+        String sincronizado = response.detalle!['sincronizado'];
+        await RepoService().actualizarProductoSincronizado(
+            productoNoSincronizado.id!, idWeb, sincronizado);
+        return true;
+      } else {
+        print('Error en la sincronización del producto: ${response.mensaje}');
+        return true;
+      }
+    } else {
+      print('No se encontraron productos no sincronizados.');
+      return false;
+    }
+  }
+
+  Future<Map<String, dynamic>> prepararProductoParaApi(
+      Producto producto) async {
+    String? claveSucursal =
+        await RepoService().obtenerClaveSucursalSeleccionada();
+    Map<String, dynamic> apiMap = producto.toJson();
+
+    apiMap['claveSucursal'] = claveSucursal;
+
+    if (producto.idWeb != null && producto.idWeb! > 0) {
+      apiMap['idWeb'] = producto.idWeb;
+    }
+
+    return apiMap;
+  }
+
+  Future<Map<String, dynamic>> prepararCategoriaParaApi(
+      CategoriaProducto categoria) async {
+    String? claveSucursal =
+        await RepoService().obtenerClaveSucursalSeleccionada();
+    Map<String, dynamic> apiMap = categoria.toJson();
+    // Asegúrate que CategoriaProducto tenga un toJson y si necesitas toApi, créalo similar a Pedido.
+
+    apiMap['claveSucursal'] = claveSucursal;
+
+    if (categoria.idWeb != null && categoria.idWeb! > 0) {
+      apiMap['idWeb'] = categoria.idWeb;
+    }
+
+    return apiMap;
+  }
+
+  Future<bool> sincronizarCategoriasLocales() async {
+    // Obtener categorias no sincronizadas
+    List<CategoriaProducto> categoriasNoSincronizadas =
+        await RepoService().obtenerCategoriasNoSincronizadasOrdenadasPorFecha();
+
+    if (categoriasNoSincronizadas.isNotEmpty) {
+      CategoriaProducto categoriaNoSincronizada =
+          categoriasNoSincronizadas.first;
+
+      Map<String, dynamic> categoriaJson =
+          await prepararCategoriaParaApi(categoriaNoSincronizada);
+
+      print('JSON Categoria enviado: $categoriaJson');
+
+      // Llamada a la API
+      var response = ApiResponse(await BaseService()
+          .post('/pos/categoria/sincronizar', body: categoriaJson));
+
+      if (response.isOk && response.detalle != null) {
+        int idWeb = response.detalle!['idWeb'];
+        String sincronizado = response.detalle!['sincronizado'];
+        await RepoService().actualizarCategoriaSincronizada(
+            categoriaNoSincronizada.id!, idWeb, sincronizado);
+        return true;
+      } else {
+        print(
+            'Error en la sincronización de la categoría: ${response.mensaje}');
+        return true;
+      }
+    } else {
+      print('No se encontraron categorias no sincronizadas.');
+      return false;
+    }
+  }
+
   void setIsLoading(bool loading) {
   void setIsLoading(bool loading) {
     _isLoading = loading;
     _isLoading = loading;
     notifyListeners();
     notifyListeners();

+ 27 - 0
lib/viewmodels/propina_view_model.dart

@@ -0,0 +1,27 @@
+import 'package:flutter/foundation.dart';
+import '/models/models.dart';
+import '/services/repo_service.dart';
+
+class PropinaViewModel extends ChangeNotifier {
+  final RepoService _repoService = RepoService();
+
+  Future<void> guardarPropina(Propinas propina) async {
+    try {
+      await _repoService.guardar(propina);
+    } catch (e) {
+      print('Error al guardar la propina: $e');
+    }
+  }
+
+  Future<List<Propinas>> obtenerPropinasPorPedido(int idPedido) async {
+    try {
+      return await _repoService.obtenerTodos<Propinas>(
+        where: 'idPedido = ?',
+        whereArgs: [idPedido],
+      );
+    } catch (e) {
+      print('Error al obtener propinas: $e');
+      return [];
+    }
+  }
+}

+ 1 - 0
lib/viewmodels/viewmodels.dart

@@ -13,3 +13,4 @@ export '../viewmodels/sucursal_view_model.dart';
 export '../viewmodels/permiso_view_model.dart';
 export '../viewmodels/permiso_view_model.dart';
 export '../viewmodels/corte_caja_view_model.dart';
 export '../viewmodels/corte_caja_view_model.dart';
 export '../viewmodels/mesa_view_model.dart';
 export '../viewmodels/mesa_view_model.dart';
+export '../viewmodels/propina_view_model.dart';

+ 68 - 86
lib/views/categoria_producto/categoria_producto_screen.dart

@@ -27,15 +27,28 @@ class _CategoriaProductoScreenState extends State<CategoriaProductoScreen> {
   }
   }
 
 
   void go(CategoriaProducto categoriaProducto) {
   void go(CategoriaProducto categoriaProducto) {
-    Navigator.push(
-      context,
-      MaterialPageRoute(
-        builder: (context) =>
-            CategoriaProductoForm(categoriaProducto: categoriaProducto),
-      ),
-    ).then((_) =>
-        Provider.of<CategoriaProductoViewModel>(context, listen: false)
-            .fetchLocalAll());
+    showDialog(
+      context: context,
+      builder: (context) {
+        return TotpCuadroConfirmacion(
+          title: "Editar Categoría Producto",
+          content:
+              "Por favor, ingresa el código de autenticación para continuar.",
+          onSuccess: () {
+            Navigator.push(
+              context,
+              MaterialPageRoute(
+                builder: (context) =>
+                    CategoriaProductoForm(categoriaProducto: categoriaProducto),
+              ),
+            ).then((_) {
+              Provider.of<CategoriaProductoViewModel>(context, listen: false)
+                  .fetchLocalAll();
+            });
+          },
+        );
+      },
+    );
   }
   }
 
 
   void clearSearchAndReset() {
   void clearSearchAndReset() {
@@ -108,72 +121,29 @@ class _CategoriaProductoScreenState extends State<CategoriaProductoScreen> {
               PopupMenuItem(
               PopupMenuItem(
                 child: const Text('Eliminar'),
                 child: const Text('Eliminar'),
                 onTap: () async {
                 onTap: () async {
-                  await Future.delayed(Duration(milliseconds: 100));
-                  bool confirmado = await showDialog<bool>(
-                        context: context,
-                        builder: (context) {
-                          return AlertDialog(
-                            title: const Text("Eliminar",
-                                style: TextStyle(
-                                    fontWeight: FontWeight.w500, fontSize: 22)),
-                            content: const Text(
-                                '¿Estás seguro de que deseas eliminar esta categoría?',
-                                style: TextStyle(fontSize: 18)),
-                            actions: [
-                              Row(
-                                mainAxisAlignment:
-                                    MainAxisAlignment.spaceBetween,
-                                children: [
-                                  TextButton(
-                                    onPressed: () =>
-                                        Navigator.of(context).pop(false),
-                                    child: const Text('No',
-                                        style: TextStyle(fontSize: 18)),
-                                    style: ButtonStyle(
-                                        padding: MaterialStatePropertyAll(
-                                            EdgeInsets.fromLTRB(
-                                                20, 10, 20, 10)),
-                                        backgroundColor:
-                                            MaterialStatePropertyAll(
-                                                Colors.red),
-                                        foregroundColor:
-                                            MaterialStatePropertyAll(
-                                                AppTheme.secondary)),
-                                  ),
-                                  TextButton(
-                                    onPressed: () =>
-                                        Navigator.of(context).pop(true),
-                                    child: const Text('Sí',
-                                        style: TextStyle(fontSize: 18)),
-                                    style: ButtonStyle(
-                                        padding: MaterialStatePropertyAll(
-                                            EdgeInsets.fromLTRB(
-                                                20, 10, 20, 10)),
-                                        backgroundColor:
-                                            MaterialStatePropertyAll(
-                                                AppTheme.tertiary),
-                                        foregroundColor:
-                                            MaterialStatePropertyAll(
-                                                AppTheme.quaternary)),
-                                  ),
-                                ],
-                              )
-                            ],
-                          );
-                        },
-                      ) ??
-                      false;
-
-                  if (confirmado) {
-                    await Provider.of<CategoriaProductoViewModel>(context,
-                            listen: false)
-                        .deleteCategoriaProducto(item.id);
-                    Provider.of<CategoriaProductoViewModel>(context,
-                            listen: false)
-                        .fetchLocalAll();
-                  }
+                  Future.delayed(Duration.zero, () {
+                    showDialog(
+                      context: context,
+                      builder: (context) {
+                        return TotpCuadroConfirmacion(
+                          title: "Eliminar Categoría Producto",
+                          content:
+                              "Por favor, ingresa el código de autenticación para continuar.",
+                          onSuccess: () async {
+                            await Provider.of<CategoriaProductoViewModel>(
+                                    context,
+                                    listen: false)
+                                .deleteCategoriaProducto(item.id);
+                            Provider.of<CategoriaProductoViewModel>(context,
+                                    listen: false)
+                                .fetchLocalAll();
+                          },
+                        );
+                      },
+                    );
+                  });
                 },
                 },
-              )
+              ),
             ],
             ],
             icon: const Icon(Icons.more_vert),
             icon: const Icon(Icons.more_vert),
           ),
           ),
@@ -380,16 +350,28 @@ class _CategoriaProductoScreenState extends State<CategoriaProductoScreen> {
       ),
       ),
       floatingActionButton: FloatingActionButton.extended(
       floatingActionButton: FloatingActionButton.extended(
         onPressed: () async {
         onPressed: () async {
-          CategoriaProducto nuevoProducto = CategoriaProducto();
-          Navigator.push(
-            context,
-            MaterialPageRoute(
-              builder: (context) =>
-                  CategoriaProductoForm(categoriaProducto: nuevoProducto),
-            ),
-          ).then((_) =>
-              Provider.of<CategoriaProductoViewModel>(context, listen: false)
-                  .fetchLocalAll());
+          showDialog(
+            context: context,
+            builder: (context) {
+              return TotpCuadroConfirmacion(
+                title: "Agregar Categoría",
+                content:
+                    "Por favor, ingresa el código de autenticación para continuar.",
+                onSuccess: () {
+                  CategoriaProducto nuevoProducto = CategoriaProducto();
+                  Navigator.push(
+                    context,
+                    MaterialPageRoute(
+                      builder: (context) => CategoriaProductoForm(
+                          categoriaProducto: nuevoProducto),
+                    ),
+                  ).then((_) => Provider.of<CategoriaProductoViewModel>(context,
+                          listen: false)
+                      .fetchLocalAll());
+                },
+              );
+            },
+          );
         },
         },
         icon: Icon(Icons.add, size: 30, color: AppTheme.quaternary),
         icon: Icon(Icons.add, size: 30, color: AppTheme.quaternary),
         label: Text(
         label: Text(
@@ -437,7 +419,7 @@ class _CategoriaProductoScreenState extends State<CategoriaProductoScreen> {
                     shape: RoundedRectangleBorder(
                     shape: RoundedRectangleBorder(
                       borderRadius: BorderRadius.circular(20.0),
                       borderRadius: BorderRadius.circular(20.0),
                     ),
                     ),
-                    primary: AppTheme.tertiary,
+                    backgroundColor: AppTheme.tertiary,
                     padding: const EdgeInsets.symmetric(vertical: 25),
                     padding: const EdgeInsets.symmetric(vertical: 25),
                   ),
                   ),
                   child: Text('Limpiar',
                   child: Text('Limpiar',
@@ -465,7 +447,7 @@ class _CategoriaProductoScreenState extends State<CategoriaProductoScreen> {
                     shape: RoundedRectangleBorder(
                     shape: RoundedRectangleBorder(
                       borderRadius: BorderRadius.circular(20.0),
                       borderRadius: BorderRadius.circular(20.0),
                     ),
                     ),
-                    primary: AppTheme.tertiary,
+                    backgroundColor: AppTheme.tertiary,
                     padding: const EdgeInsets.symmetric(vertical: 25),
                     padding: const EdgeInsets.symmetric(vertical: 25),
                   ),
                   ),
                   child: Text('Buscar',
                   child: Text('Buscar',

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

@@ -1,3 +1,4 @@
+import '../../models/models.dart';
 import '../../widgets/widgets.dart';
 import '../../widgets/widgets.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:provider/provider.dart';
 import 'package:provider/provider.dart';
@@ -19,11 +20,21 @@ class CorteCajaFinalizadoScreen extends StatefulWidget {
 class _CorteCajaFinalizadoScreenState extends State<CorteCajaFinalizadoScreen> {
 class _CorteCajaFinalizadoScreenState extends State<CorteCajaFinalizadoScreen> {
   Map<String, bool> _expandedSections = {};
   Map<String, bool> _expandedSections = {};
 
 
+  List<Pedido> _pedidosCancelados = [];
+
   @override
   @override
   void initState() {
   void initState() {
     super.initState();
     super.initState();
     final viewModel = Provider.of<CorteCajaViewModel>(context, listen: false);
     final viewModel = Provider.of<CorteCajaViewModel>(context, listen: false);
     viewModel.fetchDepositosAndRetiros(widget.corteCajaId!);
     viewModel.fetchDepositosAndRetiros(widget.corteCajaId!);
+
+    viewModel.fetchPedidosCancelados(widget.corteCajaId!).then((pedidos) {
+      setState(() {
+        _pedidosCancelados = pedidos;
+      });
+    });
+
+    viewModel.fetchPedidosConPropinas(widget.corteCajaId!);
   }
   }
 
 
   @override
   @override
@@ -76,6 +87,15 @@ class _CorteCajaFinalizadoScreenState extends State<CorteCajaFinalizadoScreen> {
                   _buildExpandableTable(
                   _buildExpandableTable(
                       "Retiros", viewModel.retiros, totalRetiro),
                       "Retiros", viewModel.retiros, totalRetiro),
                   _buildExpandableTable("Gastos", viewModel.gastos, totalGasto),
                   _buildExpandableTable("Gastos", viewModel.gastos, totalGasto),
+                  _buildExpandablePropinas(
+                    "Propinas",
+                    viewModel.pedidosConPropinas,
+                    viewModel.totalPropinas,
+                  ),
+                  _buildExpandablePedidosCancelados(
+                    "Pedidos Cancelados",
+                    _pedidosCancelados,
+                  ),
                   Table(
                   Table(
                     defaultVerticalAlignment: TableCellVerticalAlignment.middle,
                     defaultVerticalAlignment: TableCellVerticalAlignment.middle,
                     columnWidths: {
                     columnWidths: {
@@ -208,4 +228,223 @@ class _CorteCajaFinalizadoScreenState extends State<CorteCajaFinalizadoScreen> {
       ],
       ],
     );
     );
   }
   }
+
+  Widget _buildExpandablePedidosCancelados(String title, List<Pedido> pedidos) {
+    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: ${pedidos.length}",
+                            style: TextStyle(
+                                fontWeight: FontWeight.bold, fontSize: 18),
+                          ),
+                          Icon(isExpanded
+                              ? Icons.expand_less
+                              : Icons.expand_more),
+                        ],
+                      ),
+                    ),
+                  ],
+                ),
+              ],
+            ),
+          ),
+        ),
+
+        // Contenido expandido
+        if (isExpanded)
+          pedidos.isNotEmpty
+              ? Table(
+                  defaultVerticalAlignment: TableCellVerticalAlignment.middle,
+                  columnWidths: {0: FlexColumnWidth(4), 1: FlexColumnWidth(2)},
+                  children: pedidos.map((pedido) {
+                    // Construir fila con folio, totalPedido y tipoPago si existe
+                    // Observa que en la primer celda pondremos el texto completo
+                    // y la segunda celda podría quedar vacía o un " - "
+                    final tipoPago = (pedido.tipoPago?.isNotEmpty ?? false)
+                        ? " - ${pedido.tipoPago}"
+                        : "";
+                    return TableRow(
+                      children: [
+                        Container(
+                          padding: const EdgeInsets.all(8.5),
+                          decoration: BoxDecoration(
+                              border: Border.all(color: Colors.grey)),
+                          child: Text(
+                            "Folio: ${pedido.folio} "
+                            "$tipoPago",
+                            style: TextStyle(
+                                fontSize: 16, fontWeight: FontWeight.w600),
+                          ),
+                        ),
+                        Container(
+                          padding: const EdgeInsets.all(8.5),
+                          decoration: BoxDecoration(
+                              border: Border.all(color: Colors.grey)),
+                          child: Text(
+                            "\$${pedido.totalPedido?.toStringAsFixed(2) ?? '0.00'}",
+                            style: TextStyle(
+                                fontSize: 16, fontWeight: FontWeight.w600),
+                          ),
+                        ),
+                      ],
+                    );
+                  }).toList(),
+                )
+              : Padding(
+                  padding: const EdgeInsets.all(8.0),
+                  child: Text(
+                    "No hay pedidos cancelados para este corte.",
+                    style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
+                  ),
+                ),
+      ],
+    );
+  }
+
+  Widget _buildExpandablePropinas(
+      String title, List<Pedido> pedidos, double totalPropinas) {
+    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: [
+                          // Mostramos el total general de propinas
+                          Text(
+                            "\$${totalPropinas.toStringAsFixed(2)}",
+                            style: TextStyle(
+                              fontWeight: FontWeight.bold,
+                              fontSize: 18,
+                            ),
+                          ),
+                          Icon(isExpanded
+                              ? Icons.expand_less
+                              : Icons.expand_more),
+                        ],
+                      ),
+                    ),
+                  ],
+                ),
+              ],
+            ),
+          ),
+        ),
+        if (isExpanded)
+          pedidos.isNotEmpty
+              ? Table(
+                  defaultVerticalAlignment: TableCellVerticalAlignment.middle,
+                  columnWidths: {0: FlexColumnWidth(4), 1: FlexColumnWidth(2)},
+                  children: pedidos.map((pedido) {
+                    // Sumar propinas para este pedido en particular
+                    double sumaPropinasPedido = pedido.propinas.fold(
+                      0.0,
+                      (prev, propina) => prev + (propina.cantidad ?? 0),
+                    );
+                    return TableRow(
+                      children: [
+                        Container(
+                          padding: const EdgeInsets.all(8.5),
+                          decoration: BoxDecoration(
+                            border: Border.all(color: Colors.grey),
+                          ),
+                          child: Text(
+                            "Folio: ${pedido.folio} - Total: \$${(pedido.totalPedido ?? 0).toStringAsFixed(2)}",
+                            style: TextStyle(
+                              fontSize: 16,
+                              fontWeight: FontWeight.w600,
+                            ),
+                          ),
+                        ),
+                        Container(
+                          padding: const EdgeInsets.all(8.5),
+                          decoration: BoxDecoration(
+                            border: Border.all(color: Colors.grey),
+                          ),
+                          child: Text(
+                            "\$${sumaPropinasPedido.toStringAsFixed(2)}",
+                            style: TextStyle(
+                              fontSize: 16,
+                              fontWeight: FontWeight.w600,
+                            ),
+                          ),
+                        ),
+                      ],
+                    );
+                  }).toList(),
+                )
+              : Padding(
+                  padding: const EdgeInsets.all(8.0),
+                  child: Text(
+                    "No hay propinas registradas para este corte.",
+                    style: TextStyle(
+                      fontSize: 16,
+                      fontWeight: FontWeight.w600,
+                    ),
+                  ),
+                ),
+      ],
+    );
+  }
 }
 }

+ 518 - 82
lib/views/corte_caja/corte_caja_form.dart

@@ -1,12 +1,17 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/services.dart';
 import 'package:provider/provider.dart';
 import 'package:provider/provider.dart';
+import '../../models/models.dart';
 import '../../themes/themes.dart';
 import '../../themes/themes.dart';
 import 'package:intl/intl.dart';
 import 'package:intl/intl.dart';
 import '../../models/corte_caja_model.dart';
 import '../../models/corte_caja_model.dart';
 import '../../viewmodels/corte_caja_view_model.dart';
 import '../../viewmodels/corte_caja_view_model.dart';
+import '../../viewmodels/viewmodels.dart';
 import '../../widgets/app_textfield.dart';
 import '../../widgets/app_textfield.dart';
 import '../../widgets/widgets.dart';
 import '../../widgets/widgets.dart';
+import 'dart:async';
+
+import '../pedido/pedido_screen.dart';
 
 
 class CorteCajaForm extends StatefulWidget {
 class CorteCajaForm extends StatefulWidget {
   final String? corteCajaId;
   final String? corteCajaId;
@@ -33,8 +38,13 @@ class _CorteCajaFormState extends State<CorteCajaForm> {
   final TextEditingController _descripcionGastoController =
   final TextEditingController _descripcionGastoController =
       TextEditingController();
       TextEditingController();
   final TextEditingController _personaGastoController = TextEditingController();
   final TextEditingController _personaGastoController = TextEditingController();
+  late double fondoInicial;
+  Timer? _validationTimer;
+  bool isFondoValid = true;
   DateTime? fechaApertura;
   DateTime? fechaApertura;
   bool isFirst = true;
   bool isFirst = true;
+  List<Pedido> _pedidosCancelados = [];
+
   String formatNumber(double? value) {
   String formatNumber(double? value) {
     final formatter = NumberFormat('#,##0.00', 'en_US');
     final formatter = NumberFormat('#,##0.00', 'en_US');
     return formatter.format(value);
     return formatter.format(value);
@@ -56,6 +66,8 @@ class _CorteCajaFormState extends State<CorteCajaForm> {
           viewModel.selectCorte(corte);
           viewModel.selectCorte(corte);
           fechaApertura = viewModel.selectedCorte?.fechaApertura;
           fechaApertura = viewModel.selectedCorte?.fechaApertura;
 
 
+          fondoInicial = viewModel.selectedCorte?.fondo ?? 0.0;
+
           viewModel.fondoController.text =
           viewModel.fondoController.text =
               viewModel.selectedCorte?.fondo != null
               viewModel.selectedCorte?.fondo != null
                   ? formatNumber(viewModel.selectedCorte!.fondo!)
                   ? formatNumber(viewModel.selectedCorte!.fondo!)
@@ -66,14 +78,206 @@ class _CorteCajaFormState extends State<CorteCajaForm> {
                   ? formatNumber(viewModel.selectedCorte!.fondoDiaSig!)
                   ? formatNumber(viewModel.selectedCorte!.fondoDiaSig!)
                   : '';
                   : '';
 
 
-          if (fechaApertura != null) {
-            viewModel.cargarVentasDesdeFechaApertura(fechaApertura!).then((_) {
-              viewModel.calcularCorteFinal();
+          viewModel.cargarVentasPorCorteId(corte.id!).then((_) {
+            viewModel.calcularCorteFinal();
+          });
+          viewModel.fetchPedidosCancelados(widget.corteCajaId!).then((pedidos) {
+            setState(() {
+              _pedidosCancelados = pedidos;
             });
             });
-          }
+          });
         });
         });
       });
       });
+
+      viewModel.fetchPedidosConPropinas(widget.corteCajaId!).then((_) {
+        setState(() {});
+      });
+    }
+  }
+
+  void _validateFondo(CorteCajaViewModel viewModel) {
+    if (_validationTimer != null) {
+      _validationTimer!.cancel();
     }
     }
+
+    _validationTimer = Timer(Duration(seconds: 2), () {
+      final fondoActual = double.tryParse(
+            viewModel.fondoController.text.replaceAll(',', ''),
+          ) ??
+          0.0;
+
+      if (fondoActual < fondoInicial) {
+        setState(() {
+          isFondoValid = false;
+        });
+
+        alerta(context,
+            etiqueta: "El fondo no puede ser menor al que ya se tenía.");
+        viewModel.fondoController.text = formatNumber(fondoInicial);
+      } else {
+        setState(() {
+          isFondoValid = true;
+        });
+      }
+    });
+  }
+
+  void _mostrarDialogoPedidosNuevos(
+    BuildContext context,
+    List<Pedido> pedidos, {
+    required Function(List<Pedido>) onAbrirNuevaCaja,
+  }) {
+    showDialog(
+      context: context,
+      builder: (ctx) {
+        return AlertDialog(
+          title: Text(
+            'Atención: Aún cuentas con pedidos sin terminar.',
+            style: TextStyle(fontWeight: FontWeight.bold),
+          ),
+          content: SingleChildScrollView(
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                Text(
+                  '¿Deseas terminar los pedidos o abrir una nueva caja con ellos?',
+                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
+                ),
+                SizedBox(height: 10),
+                Text(
+                  'Pedidos pendientes:',
+                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
+                ),
+                SizedBox(height: 5),
+                Column(
+                  children: pedidos.map((p) {
+                    return Row(
+                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                      children: [
+                        Text(
+                          'Folio: ${p.folio}',
+                          style: TextStyle(
+                              fontSize: 16, fontWeight: FontWeight.w600),
+                        ),
+                        Text(
+                          '\$${p.totalPedido?.toStringAsFixed(2) ?? '0.00'}',
+                          style: TextStyle(
+                              fontSize: 16, fontWeight: FontWeight.w600),
+                        ),
+                      ],
+                    );
+                  }).toList(),
+                ),
+              ],
+            ),
+          ),
+          actions: [
+            TextButton(
+              style: ButtonStyle(
+                backgroundColor: MaterialStatePropertyAll(AppTheme.rojo),
+                foregroundColor: MaterialStatePropertyAll(AppTheme.quaternary),
+              ),
+              child: Text(
+                'Terminar mis pedidos primero',
+                style: TextStyle(fontSize: 16),
+              ),
+              onPressed: () {
+                Navigator.of(ctx).pop();
+                Navigator.push(
+                  context,
+                  MaterialPageRoute(
+                    builder: (context) => PedidoScreen(),
+                  ),
+                );
+              },
+            ),
+            TextButton(
+              style: ButtonStyle(
+                  backgroundColor: MaterialStatePropertyAll(AppTheme.verde),
+                  foregroundColor:
+                      MaterialStatePropertyAll(AppTheme.quaternary)),
+              child: Text(
+                'Abrir una caja nueva con esos pedidos',
+                style: TextStyle(fontSize: 16),
+              ),
+              onPressed: () {
+                Navigator.of(ctx).pop();
+                onAbrirNuevaCaja(pedidos);
+              },
+            ),
+          ],
+        );
+      },
+    );
+  }
+
+  void _mostrarDialogoDespuesDe6pm(
+    BuildContext context,
+    List<Pedido> pedidos,
+  ) {
+    showDialog(
+      context: context,
+      builder: (ctx) {
+        return AlertDialog(
+          title: Text(
+            'Pedidos sin terminar',
+            style: TextStyle(fontWeight: FontWeight.bold),
+          ),
+          content: SingleChildScrollView(
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                Text(
+                  'Debes terminar estos pedidos para poder realizar el corte:',
+                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
+                ),
+                SizedBox(height: 10),
+                Column(
+                  children: pedidos.map((p) {
+                    return Row(
+                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                      children: [
+                        Text(
+                          'Folio: ${p.folio}',
+                          style: TextStyle(
+                              fontSize: 16, fontWeight: FontWeight.w600),
+                        ),
+                        Text(
+                          '\$${p.totalPedido?.toStringAsFixed(2) ?? '0.00'}',
+                          style: TextStyle(
+                              fontSize: 16, fontWeight: FontWeight.w600),
+                        ),
+                      ],
+                    );
+                  }).toList(),
+                ),
+              ],
+            ),
+          ),
+          actions: [
+            TextButton(
+              style: ButtonStyle(
+                backgroundColor: MaterialStatePropertyAll(AppTheme.rojo),
+                foregroundColor: MaterialStatePropertyAll(AppTheme.quaternary),
+              ),
+              child: Text(
+                'Terminar mis pedidos',
+                style: TextStyle(fontSize: 16),
+              ),
+              onPressed: () {
+                Navigator.of(ctx).pop();
+                Navigator.push(
+                  context,
+                  MaterialPageRoute(
+                    builder: (context) => PedidoScreen(),
+                  ),
+                );
+              },
+            ),
+          ],
+        );
+      },
+    );
   }
   }
 
 
   @override
   @override
@@ -148,6 +352,7 @@ class _CorteCajaFormState extends State<CorteCajaForm> {
                         keyboardType: TextInputType.number,
                         keyboardType: TextInputType.number,
                         separarMiles: true,
                         separarMiles: true,
                         onChanged: (value) {
                         onChanged: (value) {
+                          _validateFondo(viewModel);
                           viewModel.calcularCorteFinal();
                           viewModel.calcularCorteFinal();
                         },
                         },
                       ),
                       ),
@@ -167,7 +372,16 @@ class _CorteCajaFormState extends State<CorteCajaForm> {
                     ),
                     ),
                   ],
                   ],
                 ),
                 ),
-                SizedBox(height: 20),
+                SizedBox(height: 10),
+                if (!isFondoValid)
+                  Text(
+                    "El fondo no puede ser menor a $fondoInicial",
+                    style: TextStyle(
+                        color: Colors.red,
+                        fontSize: 16,
+                        fontWeight: FontWeight.bold),
+                  ),
+                SizedBox(height: 10),
                 Row(
                 Row(
                   children: [
                   children: [
                     Expanded(
                     Expanded(
@@ -274,13 +488,19 @@ class _CorteCajaFormState extends State<CorteCajaForm> {
                             trailing: IconButton(
                             trailing: IconButton(
                               icon: Icon(Icons.remove_circle_outline,
                               icon: Icon(Icons.remove_circle_outline,
                                   color: Colors.red),
                                   color: Colors.red),
-                              onPressed: () {
-                                cuadroConfirmacion(
-                                  context,
-                                  etiqueta:
-                                      "¿Estás seguro de eliminar el depósito?",
-                                  onConfirm: () {
-                                    viewModel.eliminarDeposito(deposito.id!);
+                              onPressed: () async {
+                                showDialog(
+                                  context: context,
+                                  builder: (context) {
+                                    return TotpCuadroConfirmacion(
+                                      title: "Eliminar Deposito",
+                                      content:
+                                          "Por favor, ingresa el código de autenticación para continuar.",
+                                      onSuccess: () {
+                                        viewModel
+                                            .eliminarDeposito(deposito.id!);
+                                      },
+                                    );
                                   },
                                   },
                                 );
                                 );
                               },
                               },
@@ -363,13 +583,18 @@ class _CorteCajaFormState extends State<CorteCajaForm> {
                             trailing: IconButton(
                             trailing: IconButton(
                               icon: Icon(Icons.remove_circle_outline,
                               icon: Icon(Icons.remove_circle_outline,
                                   color: Colors.red),
                                   color: Colors.red),
-                              onPressed: () {
-                                cuadroConfirmacion(
-                                  context,
-                                  etiqueta:
-                                      "¿Estás seguro de eliminar el retiro?",
-                                  onConfirm: () {
-                                    viewModel.eliminarRetiro(retiro.id!);
+                              onPressed: () async {
+                                showDialog(
+                                  context: context,
+                                  builder: (context) {
+                                    return TotpCuadroConfirmacion(
+                                      title: "Eliminar Retiro",
+                                      content:
+                                          "Por favor, ingresa el código de autenticación para continuar.",
+                                      onSuccess: () {
+                                        viewModel.eliminarRetiro(retiro.id!);
+                                      },
+                                    );
                                   },
                                   },
                                 );
                                 );
                               },
                               },
@@ -451,13 +676,18 @@ class _CorteCajaFormState extends State<CorteCajaForm> {
                             trailing: IconButton(
                             trailing: IconButton(
                               icon: Icon(Icons.remove_circle_outline,
                               icon: Icon(Icons.remove_circle_outline,
                                   color: Colors.red),
                                   color: Colors.red),
-                              onPressed: () {
-                                cuadroConfirmacion(
-                                  context,
-                                  etiqueta:
-                                      "¿Estás seguro de eliminar el gasto?",
-                                  onConfirm: () {
-                                    viewModel.eliminarGasto(gasto.id!);
+                              onPressed: () async {
+                                showDialog(
+                                  context: context,
+                                  builder: (context) {
+                                    return TotpCuadroConfirmacion(
+                                      title: "Eliminar Gasto",
+                                      content:
+                                          "Por favor, ingresa el código de autenticación para continuar.",
+                                      onSuccess: () {
+                                        viewModel.eliminarGasto(gasto.id!);
+                                      },
+                                    );
                                   },
                                   },
                                 );
                                 );
                               },
                               },
@@ -480,6 +710,114 @@ class _CorteCajaFormState extends State<CorteCajaForm> {
                   ],
                   ],
                 ),
                 ),
                 const SizedBox(height: 20),
                 const SizedBox(height: 20),
+                ExpansionTile(
+                  backgroundColor: Colors.white,
+                  collapsedBackgroundColor: Colors.white,
+                  shape: RoundedRectangleBorder(
+                    borderRadius: BorderRadius.circular(10),
+                    side: BorderSide(color: Colors.grey),
+                  ),
+                  collapsedShape: RoundedRectangleBorder(
+                    borderRadius: BorderRadius.circular(10),
+                    side: BorderSide(color: Colors.grey),
+                  ),
+                  title: Row(
+                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                    children: [
+                      Text(
+                        "Propinas",
+                        style: TextStyle(
+                            fontWeight: FontWeight.bold, fontSize: 18),
+                      ),
+                      Text(
+                        "Total: \$${formatNumber(viewModel.totalPropinas)}",
+                        style: TextStyle(
+                            fontWeight: FontWeight.bold, fontSize: 18),
+                      ),
+                    ],
+                  ),
+                  children: [
+                    if (viewModel.pedidosConPropinas.isEmpty)
+                      Padding(
+                        padding: const EdgeInsets.all(8.0),
+                        child: Text(
+                            "No hay propinas registradas para este corte."),
+                      )
+                    else
+                      ...viewModel.pedidosConPropinas.map((pedido) {
+                        double totalPedidoPropinas = pedido.propinas.fold(
+                          0.0,
+                          (suma, prop) => suma + (prop.cantidad ?? 0.0),
+                        );
+                        return ListTile(
+                          title: Text(
+                            "Folio: ${pedido.folio} "
+                            "- Total: \$${(pedido.totalPedido ?? 0).toStringAsFixed(2)} "
+                            "- Propina: \$${totalPedidoPropinas.toStringAsFixed(2)}",
+                          ),
+                        );
+                      }).toList(),
+                  ],
+                ),
+                const SizedBox(height: 20),
+                ExpansionTile(
+                  backgroundColor: Colors.white,
+                  collapsedBackgroundColor: Colors.white,
+                  shape: RoundedRectangleBorder(
+                    borderRadius: BorderRadius.circular(10),
+                    side: BorderSide(color: Colors.grey),
+                  ),
+                  collapsedShape: RoundedRectangleBorder(
+                    borderRadius: BorderRadius.circular(10),
+                    side: BorderSide(color: Colors.grey),
+                  ),
+                  title: Row(
+                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                    children: [
+                      Text(
+                        "Pedidos Cancelados",
+                        style: TextStyle(
+                            fontWeight: FontWeight.bold, fontSize: 18),
+                      ),
+                      Row(
+                        children: [
+                          Text(
+                            "Total: ${_pedidosCancelados.length}",
+                            style: TextStyle(fontWeight: FontWeight.bold),
+                          ),
+                          SizedBox(width: 20),
+                        ],
+                      )
+                    ],
+                  ),
+                  children: [
+                    if (_pedidosCancelados.isNotEmpty)
+                      ListView.builder(
+                        shrinkWrap: true,
+                        itemCount: _pedidosCancelados.length,
+                        itemBuilder: (context, index) {
+                          final pedidoCancelado = _pedidosCancelados[index];
+                          final tipoPagoString =
+                              (pedidoCancelado.tipoPago?.isNotEmpty ?? false)
+                                  ? " - ${pedidoCancelado.tipoPago}"
+                                  : "";
+
+                          return ListTile(
+                            title: Text("Folio: ${pedidoCancelado.folio}"
+                                " - \$${pedidoCancelado.totalPedido?.toStringAsFixed(2) ?? '0.00'}"
+                                "$tipoPagoString"),
+                          );
+                        },
+                      )
+                    else
+                      Padding(
+                        padding: const EdgeInsets.all(8.0),
+                        child:
+                            Text("No hay pedidos cancelados para este corte."),
+                      ),
+                  ],
+                ),
+                const SizedBox(height: 20),
                 Align(
                 Align(
                   alignment: Alignment.centerRight,
                   alignment: Alignment.centerRight,
                   child: Column(
                   child: Column(
@@ -541,15 +879,62 @@ class _CorteCajaFormState extends State<CorteCajaForm> {
                     const SizedBox(width: 15),
                     const SizedBox(width: 15),
                     if (viewModel.selectedCorte?.modificado != null)
                     if (viewModel.selectedCorte?.modificado != null)
                       boton('Realizar Corte', () async {
                       boton('Realizar Corte', () async {
-                        cuadroConfirmacion(
-                          context,
-                          etiqueta: "¿Estás seguro realizar el corte?",
-                          onConfirm: () async {
-                            await viewModel.guardarCorte(esCorte: true);
-                            Navigator.pop(context);
-                            await viewModel.imprimirCorteCajaTicket();
-                          },
-                        );
+                        // 1) Verificar si hay pedidos con estatus NUEVO de este corte
+                        final pedidosViewModel = Provider.of<PedidoViewModel>(
+                            context,
+                            listen: false);
+                        List<Pedido> pedidosNuevos =
+                            await pedidosViewModel.fetchPedidosNuevosByCorteId(
+                                viewModel.selectedCorte!.id!);
+
+                        // 2) Si no hay pedidos con estatus NUEVO se continúa flujo normal
+                        if (pedidosNuevos.isEmpty) {
+                          cuadroConfirmacion(
+                            context,
+                            etiqueta: "¿Estás seguro de realizar el corte?",
+                            onConfirm: () async {
+                              await viewModel.guardarCorte(esCorte: true);
+                              Navigator.pop(context);
+                              await viewModel.imprimirCorteCajaTicket();
+                            },
+                          );
+                          return;
+                        }
+
+                        // 3) Hay pedidos con estatus NUEVO se va a revisar hora local
+                        final serverDateTime = await viewModel.fetchHoraLocal();
+                        print(DateFormat('dd/MM/yyyy HH:mm:ss')
+                            .format(serverDateTime!));
+                        if (serverDateTime.hour < 18) {
+                          // Si son antes de las 6pm mostramos el diálogo para pasar los pedidos al siguiente corte
+                          _mostrarDialogoPedidosNuevos(
+                            context,
+                            pedidosNuevos,
+                            onAbrirNuevaCaja: (nuevosPedidos) async {
+                              final oldCorteId = viewModel.selectedCorte!.id!;
+
+                              await viewModel.guardarCorte(esCorte: true);
+
+                              String? nuevaCajaId =
+                                  await viewModel.createCorteCaja();
+                              if (nuevaCajaId == null) return;
+
+                              List<int> ids =
+                                  nuevosPedidos.map((p) => p.id!).toList();
+                              await pedidosViewModel
+                                  .actualizarCorteCajaEnPedidos(
+                                      ids, nuevaCajaId);
+
+                              Navigator.pop(context);
+
+                              await viewModel.imprimirCorteCajaTicket(
+                                  corteCajaId: oldCorteId);
+                            },
+                          );
+                        } else {
+                          // Si son de las 6pm mostramos el diálogo que forza el terminar esos pedidos
+                          _mostrarDialogoDespuesDe6pm(context, pedidosNuevos);
+                        }
                       }, height: 60, width: 250),
                       }, height: 60, width: 250),
                   ],
                   ],
                 ),
                 ),
@@ -597,7 +982,6 @@ class _CorteCajaFormState extends State<CorteCajaForm> {
                   controller: _personaDepositoController,
                   controller: _personaDepositoController,
                 ),
                 ),
                 SizedBox(height: 20),
                 SizedBox(height: 20),
-                // Lista de depósitos
                 Text(
                 Text(
                   "Lista de Depósitos",
                   "Lista de Depósitos",
                   style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
                   style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
@@ -620,14 +1004,19 @@ class _CorteCajaFormState extends State<CorteCajaForm> {
                                 trailing: IconButton(
                                 trailing: IconButton(
                                   icon: Icon(Icons.remove_circle_outline,
                                   icon: Icon(Icons.remove_circle_outline,
                                       color: Colors.red),
                                       color: Colors.red),
-                                  onPressed: () {
-                                    cuadroConfirmacion(
-                                      context,
-                                      etiqueta:
-                                          "¿Estás seguro de eliminar el depósito?",
-                                      onConfirm: () {
-                                        viewModel
-                                            .eliminarDeposito(deposito.id!);
+                                  onPressed: () async {
+                                    showDialog(
+                                      context: context,
+                                      builder: (context) {
+                                        return TotpCuadroConfirmacion(
+                                          title: "Eliminar Deposito",
+                                          content:
+                                              "Por favor, ingresa el código de autenticación para continuar.",
+                                          onSuccess: () {
+                                            viewModel
+                                                .eliminarDeposito(deposito.id!);
+                                          },
+                                        );
                                       },
                                       },
                                     );
                                     );
                                   },
                                   },
@@ -667,15 +1056,27 @@ class _CorteCajaFormState extends State<CorteCajaForm> {
                             _descripcionDepositoController.text;
                             _descripcionDepositoController.text;
                         String persona = _personaDepositoController.text;
                         String persona = _personaDepositoController.text;
 
 
-                        viewModel.addDeposito(
-                          monto,
-                          descripcion,
-                          persona,
-                          widget.corteCajaId!,
+                        showDialog(
+                          context: context,
+                          builder: (context) {
+                            return TotpCuadroConfirmacion(
+                              title: "Agregar Depósito",
+                              content:
+                                  "Por favor, ingresa el código de autenticación para continuar.",
+                              onSuccess: () {
+                                viewModel.addDeposito(
+                                  monto,
+                                  descripcion,
+                                  persona,
+                                  widget.corteCajaId!,
+                                );
+                                _montoDepositoController.clear();
+                                _descripcionDepositoController.clear();
+                                _personaDepositoController.clear();
+                              },
+                            );
+                          },
                         );
                         );
-                        _montoDepositoController.clear();
-                        _descripcionDepositoController.clear();
-                        _personaDepositoController.clear();
                       },
                       },
                       child: Text(
                       child: Text(
                         "Agregar",
                         "Agregar",
@@ -777,13 +1178,19 @@ class _CorteCajaFormState extends State<CorteCajaForm> {
                                 trailing: IconButton(
                                 trailing: IconButton(
                                   icon: Icon(Icons.remove_circle_outline,
                                   icon: Icon(Icons.remove_circle_outline,
                                       color: Colors.red),
                                       color: Colors.red),
-                                  onPressed: () {
-                                    cuadroConfirmacion(
-                                      context,
-                                      etiqueta:
-                                          "¿Estás seguro de eliminar el retiro?",
-                                      onConfirm: () {
-                                        viewModel.eliminarRetiro(retiro.id!);
+                                  onPressed: () async {
+                                    showDialog(
+                                      context: context,
+                                      builder: (context) {
+                                        return TotpCuadroConfirmacion(
+                                          title: "Eliminar Retiro",
+                                          content:
+                                              "Por favor, ingresa el código de autenticación para continuar.",
+                                          onSuccess: () {
+                                            viewModel
+                                                .eliminarRetiro(retiro.id!);
+                                          },
+                                        );
                                       },
                                       },
                                     );
                                     );
                                   },
                                   },
@@ -822,15 +1229,27 @@ class _CorteCajaFormState extends State<CorteCajaForm> {
                         String descripcion = _descripcionRetiroController.text;
                         String descripcion = _descripcionRetiroController.text;
                         String persona = _personaRetiroController.text;
                         String persona = _personaRetiroController.text;
 
 
-                        viewModel.addRetiro(
-                          monto,
-                          descripcion,
-                          persona,
-                          widget.corteCajaId!,
+                        showDialog(
+                          context: context,
+                          builder: (context) {
+                            return TotpCuadroConfirmacion(
+                              title: "Agregar Retiro",
+                              content:
+                                  "Por favor, ingresa el código de autenticación para continuar.",
+                              onSuccess: () {
+                                viewModel.addRetiro(
+                                  monto,
+                                  descripcion,
+                                  persona,
+                                  widget.corteCajaId!,
+                                );
+                                _montoRetiroController.clear();
+                                _descripcionRetiroController.clear();
+                                _personaRetiroController.clear();
+                              },
+                            );
+                          },
                         );
                         );
-                        _montoRetiroController.clear();
-                        _descripcionRetiroController.clear();
-                        _personaRetiroController.clear();
                       },
                       },
                       child: Text(
                       child: Text(
                         "Agregar",
                         "Agregar",
@@ -932,13 +1351,18 @@ class _CorteCajaFormState extends State<CorteCajaForm> {
                                 trailing: IconButton(
                                 trailing: IconButton(
                                   icon: Icon(Icons.remove_circle_outline,
                                   icon: Icon(Icons.remove_circle_outline,
                                       color: Colors.red),
                                       color: Colors.red),
-                                  onPressed: () {
-                                    cuadroConfirmacion(
-                                      context,
-                                      etiqueta:
-                                          "¿Estás seguro de eliminar el gasto?",
-                                      onConfirm: () {
-                                        viewModel.eliminarGasto(gasto.id!);
+                                  onPressed: () async {
+                                    showDialog(
+                                      context: context,
+                                      builder: (context) {
+                                        return TotpCuadroConfirmacion(
+                                          title: "Eliminar Gasto",
+                                          content:
+                                              "Por favor, ingresa el código de autenticación para continuar.",
+                                          onSuccess: () {
+                                            viewModel.eliminarGasto(gasto.id!);
+                                          },
+                                        );
                                       },
                                       },
                                     );
                                     );
                                   },
                                   },
@@ -977,15 +1401,27 @@ class _CorteCajaFormState extends State<CorteCajaForm> {
                         String descripcion = _descripcionGastoController.text;
                         String descripcion = _descripcionGastoController.text;
                         String persona = _personaGastoController.text;
                         String persona = _personaGastoController.text;
 
 
-                        viewModel.addGasto(
-                          monto,
-                          descripcion,
-                          persona,
-                          widget.corteCajaId!,
+                        showDialog(
+                          context: context,
+                          builder: (context) {
+                            return TotpCuadroConfirmacion(
+                              title: "Agregar Gasto",
+                              content:
+                                  "Por favor, ingresa el código de autenticación para continuar.",
+                              onSuccess: () {
+                                viewModel.addGasto(
+                                  monto,
+                                  descripcion,
+                                  persona,
+                                  widget.corteCajaId!,
+                                );
+                                _montoGastoController.clear();
+                                _descripcionGastoController.clear();
+                                _personaGastoController.clear();
+                              },
+                            );
+                          },
                         );
                         );
-                        _montoGastoController.clear();
-                        _descripcionGastoController.clear();
-                        _personaGastoController.clear();
                       },
                       },
                       child: Text(
                       child: Text(
                         "Agregar",
                         "Agregar",

+ 4 - 3
lib/views/home/home_screen.dart

@@ -21,8 +21,8 @@ class Formulario extends State<HomeScreen> {
     // Future(() async {
     // Future(() async {
     //   await Provider.of<LoginViewModel>(context, listen: false).setValores();
     //   await Provider.of<LoginViewModel>(context, listen: false).setValores();
     // });
     // });
-    PedidoSync()
-        .startSync(Provider.of<PedidoViewModel>(context, listen: false));
+    PedidoSync().startSync(Provider.of<PedidoViewModel>(context, listen: false),
+        Provider.of<CorteCajaViewModel>(context, listen: false));
 
 
     WidgetsBinding.instance.addPostFrameCallback((_) {
     WidgetsBinding.instance.addPostFrameCallback((_) {
       Provider.of<ProductoViewModel>(context, listen: false)
       Provider.of<ProductoViewModel>(context, listen: false)
@@ -37,7 +37,8 @@ class Formulario extends State<HomeScreen> {
       }
       }
 
 
       Provider.of<CorteCajaViewModel>(context, listen: false).fetchCortes();
       Provider.of<CorteCajaViewModel>(context, listen: false).fetchCortes();
-      Provider.of<MesaViewModel>(context, listen: false).fetchLocalAll();
+      Provider.of<MesaViewModel>(context, listen: false)
+          .fetchLocalAll(sinLimite: true);
     });
     });
   }
   }
 
 

+ 60 - 77
lib/views/mesa/mesa_screen.dart

@@ -22,14 +22,28 @@ class _MesasScreenState extends State<MesasScreen> {
     Provider.of<MesaViewModel>(context, listen: false).fetchLocalAll();
     Provider.of<MesaViewModel>(context, listen: false).fetchLocalAll();
   }
   }
 
 
-  void go(Mesa variable) {
-    Navigator.push(
-      context,
-      MaterialPageRoute(
-        builder: (context) => MesaForm(mesa: variable),
-      ),
-    ).then((_) =>
-        Provider.of<MesaViewModel>(context, listen: false).fetchLocalAll());
+  void go(Mesa mesa) {
+    showDialog(
+      context: context,
+      builder: (context) {
+        return TotpCuadroConfirmacion(
+          title: "Editar Mesa",
+          content:
+              "Por favor, ingresa el código de autenticación para continuar.",
+          onSuccess: () {
+            Navigator.push(
+              context,
+              MaterialPageRoute(
+                builder: (context) => MesaForm(mesa: mesa),
+              ),
+            ).then((_) {
+              Provider.of<MesaViewModel>(context, listen: false)
+                  .fetchLocalAll();
+            });
+          },
+        );
+      },
+    );
   }
   }
 
 
   void clearSearchAndReset() {
   void clearSearchAndReset() {
@@ -61,68 +75,24 @@ class _MesasScreenState extends State<MesasScreen> {
               PopupMenuItem(
               PopupMenuItem(
                 child: const Text('Eliminar'),
                 child: const Text('Eliminar'),
                 onTap: () async {
                 onTap: () async {
-                  await Future.delayed(Duration(milliseconds: 100));
-                  bool confirmado = await showDialog<bool>(
-                        context: context,
-                        builder: (context) {
-                          return AlertDialog(
-                            title: const Text("Eliminar",
-                                style: TextStyle(
-                                    fontWeight: FontWeight.w500, fontSize: 22)),
-                            content: const Text(
-                                '¿Estás seguro de que deseas eliminar esta variable?',
-                                style: TextStyle(fontSize: 18)),
-                            actions: [
-                              Row(
-                                mainAxisAlignment:
-                                    MainAxisAlignment.spaceBetween,
-                                children: [
-                                  TextButton(
-                                    onPressed: () =>
-                                        Navigator.of(context).pop(false),
-                                    child: const Text('No',
-                                        style: TextStyle(fontSize: 18)),
-                                    style: ButtonStyle(
-                                        padding: MaterialStatePropertyAll(
-                                            EdgeInsets.fromLTRB(
-                                                20, 10, 20, 10)),
-                                        backgroundColor:
-                                            MaterialStatePropertyAll(
-                                                AppTheme.primary),
-                                        foregroundColor:
-                                            MaterialStatePropertyAll(
-                                                AppTheme.secondary)),
-                                  ),
-                                  TextButton(
-                                    onPressed: () =>
-                                        Navigator.of(context).pop(true),
-                                    child: const Text('Sí',
-                                        style: TextStyle(fontSize: 18)),
-                                    style: ButtonStyle(
-                                        padding: MaterialStatePropertyAll(
-                                            EdgeInsets.fromLTRB(
-                                                20, 10, 20, 10)),
-                                        backgroundColor:
-                                            MaterialStatePropertyAll(
-                                                AppTheme.tertiary),
-                                        foregroundColor:
-                                            MaterialStatePropertyAll(
-                                                AppTheme.quaternary)),
-                                  ),
-                                ],
-                              )
-                            ],
-                          );
-                        },
-                      ) ??
-                      false;
-
-                  if (confirmado) {
-                    await model.deleteMesa(item.id!);
-                    model.fetchLocalAll();
-                  }
+                  Future.delayed(Duration.zero, () {
+                    showDialog(
+                      context: context,
+                      builder: (context) {
+                        return TotpCuadroConfirmacion(
+                          title: "Eliminar Mesa",
+                          content:
+                              "Por favor, ingresa el código de autenticación para continuar.",
+                          onSuccess: () async {
+                            await model.deleteMesa(item.id!);
+                            model.fetchLocalAll();
+                          },
+                        );
+                      },
+                    );
+                  });
                 },
                 },
-              )
+              ),
             ],
             ],
             icon: const Icon(Icons.more_vert),
             icon: const Icon(Icons.more_vert),
           ),
           ),
@@ -311,14 +281,27 @@ class _MesasScreenState extends State<MesasScreen> {
       ),
       ),
       floatingActionButton: FloatingActionButton.extended(
       floatingActionButton: FloatingActionButton.extended(
         onPressed: () async {
         onPressed: () async {
-          Mesa nuevaMesa = Mesa();
-          Navigator.push(
-            context,
-            MaterialPageRoute(
-              builder: (context) => MesaForm(mesa: nuevaMesa),
-            ),
-          ).then((_) => Provider.of<MesaViewModel>(context, listen: false)
-              .fetchLocalAll());
+          showDialog(
+            context: context,
+            builder: (context) {
+              return TotpCuadroConfirmacion(
+                title: "Agregar Mesa",
+                content:
+                    "Por favor, ingresa el código de autenticación para continuar.",
+                onSuccess: () {
+                  Mesa nuevaMesa = Mesa();
+                  Navigator.push(
+                    context,
+                    MaterialPageRoute(
+                      builder: (context) => MesaForm(mesa: nuevaMesa),
+                    ),
+                  ).then((_) =>
+                      Provider.of<MesaViewModel>(context, listen: false)
+                          .fetchLocalAll());
+                },
+              );
+            },
+          );
         },
         },
         icon: Icon(Icons.add, size: 30, color: AppTheme.quaternary),
         icon: Icon(Icons.add, size: 30, color: AppTheme.quaternary),
         label: Text(
         label: Text(

+ 559 - 39
lib/views/pedido/pedido_detalle_screen.dart

@@ -1,19 +1,39 @@
+import 'package:collection/collection.dart';
 import 'package:intl/intl.dart';
 import 'package:intl/intl.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
 import '../../models/models.dart';
 import '../../models/models.dart';
+import '../../services/services.dart';
 import '../../themes/themes.dart';
 import '../../themes/themes.dart';
+import '../../viewmodels/viewmodels.dart';
+import '../../widgets/widgets.dart';
 import '../pedido/pedido_ticket.dart';
 import '../pedido/pedido_ticket.dart';
 
 
-class PedidoDetalleScreen extends StatelessWidget {
+class PedidoDetalleScreen extends StatefulWidget {
   final Pedido pedido;
   final Pedido pedido;
 
 
-  const PedidoDetalleScreen({Key? key, required this.pedido}) : super(key: key);
+  PedidoDetalleScreen({Key? key, required this.pedido}) : super(key: key);
+
+  @override
+  _PedidoDetalleScreenState createState() => _PedidoDetalleScreenState();
+}
+
+class _PedidoDetalleScreenState extends State<PedidoDetalleScreen> {
+  late Pedido pedido;
+
+  @override
+  void initState() {
+    super.initState();
+    pedido = widget.pedido;
+  }
 
 
   String formatCurrency(double amount) {
   String formatCurrency(double amount) {
     final format = NumberFormat("#,##0.00", "es_MX");
     final format = NumberFormat("#,##0.00", "es_MX");
     return format.format(amount);
     return format.format(amount);
   }
   }
 
 
+  final List<String> tiposDePago = ['Efectivo', 'Tarjeta', 'Transferencia'];
+
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     double totalSinDescuento =
     double totalSinDescuento =
@@ -33,6 +53,14 @@ class PedidoDetalleScreen extends StatelessWidget {
     double precioDescuento = totalSinDescuento * (descuento / 100);
     double precioDescuento = totalSinDescuento * (descuento / 100);
     double totalConDescuento = totalSinDescuento - precioDescuento;
     double totalConDescuento = totalSinDescuento - precioDescuento;
 
 
+    double totalPropina = 0.0;
+    if (pedido.propinas.isNotEmpty) {
+      totalPropina = pedido.propinas.fold(
+        0.0,
+        (previousValue, propina) => previousValue + (propina.cantidad ?? 0.0),
+      );
+    }
+
     return Scaffold(
     return Scaffold(
       appBar: AppBar(
       appBar: AppBar(
         title: Text(
         title: Text(
@@ -145,6 +173,12 @@ class PedidoDetalleScreen extends StatelessWidget {
                                   ),
                                   ),
                                 ],
                                 ],
                               ),
                               ),
+                              if (producto.comentario!.isNotEmpty)
+                                Text(
+                                  'Comentarios: ${producto.comentario!}',
+                                  style: TextStyle(
+                                      fontSize: 15, color: AppTheme.tertiary),
+                                ),
                               if (producto.toppings.isNotEmpty)
                               if (producto.toppings.isNotEmpty)
                                 Padding(
                                 Padding(
                                   padding: const EdgeInsets.only(top: 4.0),
                                   padding: const EdgeInsets.only(top: 4.0),
@@ -232,6 +266,27 @@ class PedidoDetalleScreen extends StatelessWidget {
                                       fontWeight: FontWeight.bold)),
                                       fontWeight: FontWeight.bold)),
                             ],
                             ],
                           ),
                           ),
+                          if (pedido.propinas.isNotEmpty) ...[
+                            const Divider(),
+                            Row(
+                              mainAxisAlignment: MainAxisAlignment.end,
+                              children: [
+                                const Text(
+                                  'Propina:',
+                                  style: TextStyle(
+                                      fontSize: 16,
+                                      fontWeight: FontWeight.bold),
+                                ),
+                                const SizedBox(width: 5),
+                                Text(
+                                  '\$${formatCurrency(totalPropina)}',
+                                  style: const TextStyle(
+                                      fontSize: 16,
+                                      fontWeight: FontWeight.bold),
+                                ),
+                              ],
+                            ),
+                          ],
                         ],
                         ],
                       ),
                       ),
                     ),
                     ),
@@ -245,15 +300,47 @@ class PedidoDetalleScreen extends StatelessWidget {
               color: Colors.white,
               color: Colors.white,
               child: Padding(
               child: Padding(
                 padding: const EdgeInsets.all(8.0),
                 padding: const EdgeInsets.all(8.0),
-                child: Column(
-                  crossAxisAlignment: CrossAxisAlignment.start,
-                  children: [
-                    Text('Pago',
-                        style: TextStyle(
-                            fontSize: 22, fontWeight: FontWeight.bold)),
-                    const SizedBox(height: 10),
-                    _buildPaymentDetails(),
-                  ],
+                child: Consumer<PedidoViewModel>(
+                  builder: (context, viewModel, _) {
+                    return Column(
+                      crossAxisAlignment: CrossAxisAlignment.start,
+                      children: [
+                        Row(
+                          children: [
+                            Text('Pago',
+                                style: TextStyle(
+                                    fontSize: 22, fontWeight: FontWeight.bold)),
+                            Spacer(),
+                            ElevatedButton(
+                              onPressed: () {
+                                _mostrarModalCambiarMetodoPago(context);
+                              },
+                              child: Text('Cambiar Método Pago',
+                                  style: TextStyle(
+                                      color: AppTheme.quaternary,
+                                      fontWeight: FontWeight.w500,
+                                      fontSize: 16)),
+                              style: ElevatedButton.styleFrom(
+                                backgroundColor: AppTheme.tertiary,
+                                padding:
+                                    const EdgeInsets.fromLTRB(20, 10, 20, 10),
+                              ),
+                            ),
+                          ],
+                        ),
+                        const SizedBox(height: 10),
+                        if ((pedido.cantEfectivo ?? 0) > 0)
+                          _buildReadOnlyPaymentRow(
+                              "Efectivo", pedido.cantEfectivo ?? 0.0),
+                        if ((pedido.cantTarjeta ?? 0) > 0)
+                          _buildReadOnlyPaymentRow(
+                              "Tarjeta", pedido.cantTarjeta ?? 0.0),
+                        if ((pedido.cantTransferencia ?? 0) > 0)
+                          _buildReadOnlyPaymentRow(
+                              "Transferencia", pedido.cantTransferencia ?? 0.0),
+                      ],
+                    );
+                  },
                 ),
                 ),
               ),
               ),
             ),
             ),
@@ -276,7 +363,7 @@ class PedidoDetalleScreen extends StatelessWidget {
                 ),
                 ),
                 style: ElevatedButton.styleFrom(
                 style: ElevatedButton.styleFrom(
                   padding: const EdgeInsets.fromLTRB(50, 20, 50, 20),
                   padding: const EdgeInsets.fromLTRB(50, 20, 50, 20),
-                  primary: AppTheme.tertiary,
+                  backgroundColor: AppTheme.tertiary,
                 ),
                 ),
               ),
               ),
             )
             )
@@ -286,41 +373,409 @@ class PedidoDetalleScreen extends StatelessWidget {
     );
     );
   }
   }
 
 
-  Widget _buildPaymentDetails() {
-    List<Widget> paymentDetails = [];
+  Widget _buildReadOnlyPaymentRow(String paymentType, double amount) {
+    return Padding(
+      padding: const EdgeInsets.symmetric(vertical: 6.0),
+      child: Row(
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: [
+          Text(paymentType,
+              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
+          Text('\$${formatCurrency(amount)}',
+              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
+        ],
+      ),
+    );
+  }
+
+  Future<void> _mostrarModalCambiarMetodoPago(BuildContext context) async {
+    double totalPedido = pedido.totalPedido ?? 0.0;
+    TextEditingController efectivoController = TextEditingController(
+        text: (pedido.cantEfectivo ?? 0) > 0
+            ? pedido.cantEfectivo!.toStringAsFixed(2)
+            : '');
+    TextEditingController tarjetaController = TextEditingController(
+        text: (pedido.cantTarjeta ?? 0) > 0
+            ? pedido.cantTarjeta!.toStringAsFixed(2)
+            : '');
+    TextEditingController transferenciaController = TextEditingController(
+        text: (pedido.cantTransferencia ?? 0) > 0
+            ? pedido.cantTransferencia!.toStringAsFixed(2)
+            : '');
 
 
-    if (pedido.cantEfectivo != null && pedido.cantEfectivo! > 0) {
-      paymentDetails.add(_buildPaymentRow("Efectivo", pedido.cantEfectivo!));
-    }
-    if (pedido.cantTarjeta != null && pedido.cantTarjeta! > 0) {
-      paymentDetails.add(_buildPaymentRow("Tarjeta", pedido.cantTarjeta!));
-    }
-    if (pedido.cantTransferencia != null && pedido.cantTransferencia! > 0) {
-      paymentDetails
-          .add(_buildPaymentRow("Transferencia", pedido.cantTransferencia!));
-    }
-    if (paymentDetails.isEmpty) {
-      paymentDetails.add(Text("No se especificaron métodos de pago.",
-          style: TextStyle(fontSize: 16, color: Colors.grey[600])));
+    bool efectivoSeleccionado = (pedido.cantEfectivo ?? 0) > 0;
+    bool tarjetaSeleccionada = (pedido.cantTarjeta ?? 0) > 0;
+    bool transferenciaSeleccionada = (pedido.cantTransferencia ?? 0) > 0;
+
+    bool efectivoCompleto = false;
+    bool tarjetaCompleto = false;
+    bool transferenciaCompleto = false;
+
+    double cambio = 0.0;
+    double faltante = totalPedido;
+    bool totalCompletado = false;
+
+    void _calcularCambio(StateSetter setState) {
+      double totalPagado = (double.tryParse(efectivoController.text) ?? 0) +
+          (double.tryParse(tarjetaController.text) ?? 0) +
+          (double.tryParse(transferenciaController.text) ?? 0);
+
+      setState(() {
+        cambio = totalPagado - totalPedido;
+        faltante = cambio < 0 ? totalPedido - totalPagado : 0;
+        totalCompletado = cambio >= 0;
+      });
     }
     }
 
 
-    return Column(
-      crossAxisAlignment: CrossAxisAlignment.start,
-      children: paymentDetails,
+    bool? shouldSave = await showDialog<bool>(
+      context: context,
+      builder: (BuildContext context) {
+        return StatefulBuilder(
+          builder: (context, setState) {
+            return AlertDialog(
+              actionsPadding: EdgeInsets.fromLTRB(50, 10, 50, 30),
+              title: const Text(
+                'Cambiar Método de Pago',
+                style: TextStyle(fontSize: 22, fontWeight: FontWeight.w500),
+              ),
+              content: SingleChildScrollView(
+                child: AnimatedSize(
+                  duration: const Duration(milliseconds: 300),
+                  curve: Curves.easeInOut,
+                  child: Column(
+                    crossAxisAlignment: CrossAxisAlignment.stretch,
+                    children: [
+                      Align(
+                        alignment: Alignment.center,
+                        child: Text(
+                          'Métodos de pago',
+                          style: TextStyle(
+                              fontWeight: FontWeight.bold, fontSize: 20),
+                        ),
+                      ),
+                      const SizedBox(height: 10),
+                      _buildPaymentMethodRow(
+                        setState,
+                        totalPedido,
+                        label: 'Efectivo',
+                        selected: efectivoSeleccionado,
+                        exactSelected: efectivoCompleto,
+                        controller: efectivoController,
+                        onSelected: (value) {
+                          setState(() {
+                            efectivoSeleccionado = value;
+                            if (!efectivoSeleccionado) {
+                              efectivoCompleto = false;
+                              efectivoController.clear();
+                            }
+                            _calcularCambio(setState);
+                          });
+                        },
+                        onExactSelected: (value) {
+                          setState(() {
+                            efectivoCompleto = value;
+                            if (efectivoCompleto) {
+                              efectivoController.text =
+                                  totalPedido.toStringAsFixed(2);
+                              efectivoSeleccionado = true;
+                              tarjetaSeleccionada = false;
+                              transferenciaSeleccionada = false;
+                              tarjetaController.clear();
+                              transferenciaController.clear();
+                            } else {
+                              efectivoController.clear();
+                            }
+                            _calcularCambio(setState);
+                          });
+                        },
+                        disableOtherMethods:
+                            tarjetaCompleto || transferenciaCompleto,
+                        onChangedMonto: () => _calcularCambio(setState),
+                      ),
+                      _buildPaymentMethodRow(
+                        setState,
+                        totalPedido,
+                        label: 'Tarjeta',
+                        selected: tarjetaSeleccionada,
+                        exactSelected: tarjetaCompleto,
+                        controller: tarjetaController,
+                        sinCambio: true,
+                        onSelected: (value) {
+                          setState(() {
+                            tarjetaSeleccionada = value;
+                            if (!tarjetaSeleccionada) {
+                              tarjetaCompleto = false;
+                              tarjetaController.clear();
+                            }
+                            _calcularCambio(setState);
+                          });
+                        },
+                        onExactSelected: (value) {
+                          setState(() {
+                            tarjetaCompleto = value;
+                            if (tarjetaCompleto) {
+                              tarjetaController.text =
+                                  totalPedido.toStringAsFixed(2);
+                              tarjetaSeleccionada = true;
+                              efectivoSeleccionado = false;
+                              transferenciaSeleccionada = false;
+                              efectivoController.clear();
+                              transferenciaController.clear();
+                            } else {
+                              tarjetaController.clear();
+                            }
+                            _calcularCambio(setState);
+                          });
+                        },
+                        disableOtherMethods:
+                            efectivoCompleto || transferenciaCompleto,
+                        onChangedMonto: () => _calcularCambio(setState),
+                      ),
+                      _buildPaymentMethodRow(
+                        setState,
+                        totalPedido,
+                        label: 'Transferencia',
+                        selected: transferenciaSeleccionada,
+                        exactSelected: transferenciaCompleto,
+                        controller: transferenciaController,
+                        sinCambio: true,
+                        onSelected: (value) {
+                          setState(() {
+                            transferenciaSeleccionada = value;
+                            if (!transferenciaSeleccionada) {
+                              transferenciaCompleto = false;
+                              transferenciaController.clear();
+                            }
+                            _calcularCambio(setState);
+                          });
+                        },
+                        onExactSelected: (value) {
+                          setState(() {
+                            transferenciaCompleto = value;
+                            if (transferenciaCompleto) {
+                              transferenciaController.text =
+                                  totalPedido.toStringAsFixed(2);
+                              transferenciaSeleccionada = true;
+                              efectivoSeleccionado = false;
+                              tarjetaSeleccionada = false;
+                              efectivoController.clear();
+                              tarjetaController.clear();
+                            } else {
+                              transferenciaController.clear();
+                            }
+                            _calcularCambio(setState);
+                          });
+                        },
+                        disableOtherMethods:
+                            efectivoCompleto || tarjetaCompleto,
+                        onChangedMonto: () => _calcularCambio(setState),
+                      ),
+                      const SizedBox(height: 10),
+                      Align(
+                        alignment: Alignment.centerRight,
+                        child: Column(
+                          crossAxisAlignment: CrossAxisAlignment.end,
+                          children: [
+                            Text(
+                              'Total del pedido: \$${totalPedido.toStringAsFixed(2)}',
+                              style: const TextStyle(
+                                  fontWeight: FontWeight.bold, fontSize: 18),
+                            ),
+                            if (faltante > 0)
+                              Text(
+                                'Faltante: \$${faltante.toStringAsFixed(2)}',
+                                style: const TextStyle(
+                                    color: Colors.red,
+                                    fontSize: 18,
+                                    fontWeight: FontWeight.bold),
+                              )
+                            else if (cambio > 0)
+                              Text(
+                                'Cambio: \$${cambio.toStringAsFixed(2)}',
+                                style: const TextStyle(
+                                    color: Colors.green,
+                                    fontSize: 18,
+                                    fontWeight: FontWeight.bold),
+                              ),
+                          ],
+                        ),
+                      ),
+                    ],
+                  ),
+                ),
+              ),
+              actions: [
+                TextButton(
+                  child: const Text('Cancelar', style: TextStyle(fontSize: 18)),
+                  onPressed: () => Navigator.of(context).pop(false),
+                  style: ButtonStyle(
+                    padding: MaterialStateProperty.all(
+                        EdgeInsets.fromLTRB(30, 20, 30, 20)),
+                    backgroundColor: MaterialStateProperty.all(Colors.red),
+                    foregroundColor:
+                        MaterialStateProperty.all(AppTheme.secondary),
+                  ),
+                ),
+                const SizedBox(width: 100),
+                TextButton(
+                  child: const Text('Guardar', style: TextStyle(fontSize: 18)),
+                  onPressed: totalCompletado
+                      ? () => Navigator.of(context).pop(true)
+                      : null,
+                  style: ButtonStyle(
+                    padding: MaterialStateProperty.all(
+                        EdgeInsets.fromLTRB(30, 20, 30, 20)),
+                    backgroundColor: MaterialStateProperty.all(
+                        totalCompletado ? AppTheme.tertiary : Colors.grey),
+                    foregroundColor:
+                        MaterialStateProperty.all(AppTheme.quaternary),
+                  ),
+                ),
+              ],
+            );
+          },
+        );
+      },
     );
     );
+
+    if (shouldSave ?? false) {
+      double cantEfectivo = efectivoSeleccionado
+          ? double.tryParse(efectivoController.text) ?? 0
+          : 0;
+      double cantTarjeta = tarjetaSeleccionada
+          ? double.tryParse(tarjetaController.text) ?? 0
+          : 0;
+      double cantTransferencia = transferenciaSeleccionada
+          ? double.tryParse(transferenciaController.text) ?? 0
+          : 0;
+
+      pedido.cantEfectivo = cantEfectivo;
+      pedido.cantTarjeta = cantTarjeta;
+      pedido.cantTransferencia = cantTransferencia;
+
+      List<String> nuevosMetodos = [];
+      if (cantEfectivo > 0) nuevosMetodos.add('Efectivo');
+      if (cantTarjeta > 0) nuevosMetodos.add('Tarjeta');
+      if (cantTransferencia > 0) nuevosMetodos.add('Transferencia');
+
+      pedido.tipoPago =
+          nuevosMetodos.isNotEmpty ? nuevosMetodos.join(',') : 'No Definido';
+
+      pedido.sincronizado = null;
+
+      await _guardarPedido(pedido, context);
+
+      // Recargamos el pedido desde la BD para tener sus datos actualizados
+      PedidoViewModel viewModel =
+          Provider.of<PedidoViewModel>(context, listen: false);
+      Pedido? pedidoActualizado =
+          await viewModel.fetchPedidoConProductos(pedido.id!);
+
+      if (pedidoActualizado != null) {
+        setState(() {
+          pedido = pedidoActualizado;
+        });
+      }
+    }
   }
   }
 
 
-  Widget _buildPaymentRow(String paymentType, double amount) {
+  Widget _buildPaymentMethodRow(
+    StateSetter setState,
+    double totalPedido, {
+    required String label,
+    required bool selected,
+    required bool exactSelected,
+    required TextEditingController controller,
+    required Function(bool) onSelected,
+    required Function(bool) onExactSelected,
+    required bool disableOtherMethods,
+    required Function() onChangedMonto,
+    bool sinCambio = false,
+  }) {
     return Row(
     return Row(
       mainAxisAlignment: MainAxisAlignment.spaceBetween,
       mainAxisAlignment: MainAxisAlignment.spaceBetween,
+      crossAxisAlignment: CrossAxisAlignment.center,
       children: [
       children: [
-        Text(
-          paymentType,
-          style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
+        Row(
+          children: [
+            Checkbox(
+              activeColor: AppTheme.primary,
+              value: selected,
+              onChanged: disableOtherMethods
+                  ? null
+                  : (value) {
+                      onSelected(value ?? false);
+                    },
+            ),
+            GestureDetector(
+              onTap: disableOtherMethods
+                  ? null
+                  : () {
+                      onSelected(!selected);
+                    },
+              child: Text(
+                label,
+                style:
+                    const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
+              ),
+            ),
+          ],
         ),
         ),
-        Text(
-          '\$${formatCurrency(amount)}',
-          style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
+        SizedBox(
+          width: 180,
+          child: Row(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Column(
+                children: [
+                  const Text(
+                    'Exacto',
+                    style: TextStyle(
+                        fontSize: 18,
+                        fontWeight: FontWeight.bold,
+                        color: Colors.black),
+                  ),
+                  const SizedBox(height: 17),
+                  Checkbox(
+                    activeColor: AppTheme.primary,
+                    value: exactSelected,
+                    onChanged: !disableOtherMethods
+                        ? (value) {
+                            onExactSelected(value ?? false);
+                            if (value == true) {
+                              setState(() {
+                                // nada especial, ya manejamos esto arriba
+                              });
+                            }
+                          }
+                        : null,
+                  ),
+                ],
+              ),
+              const SizedBox(width: 5),
+              Expanded(
+                child: AppTextField(
+                  controller: controller,
+                  enabled: selected,
+                  etiqueta: 'Cantidad',
+                  hintText: '0.00',
+                  keyboardType: TextInputType.number,
+                  onChanged: (value) {
+                    if (sinCambio) {
+                      double? input = double.tryParse(value) ?? 0;
+                      if (input > totalPedido) {
+                        controller.text = totalPedido.toStringAsFixed(2);
+                        controller.selection = TextSelection.fromPosition(
+                          TextPosition(offset: controller.text.length),
+                        );
+                      }
+                    }
+                    onChangedMonto();
+                  },
+                ),
+              ),
+            ],
+          ),
         ),
         ),
       ],
       ],
     );
     );
@@ -328,10 +783,75 @@ class PedidoDetalleScreen extends StatelessWidget {
 
 
   String _formatDateTime(String? dateTimeString) {
   String _formatDateTime(String? dateTimeString) {
     if (dateTimeString == null) return "Sin fecha";
     if (dateTimeString == null) return "Sin fecha";
-
     DateTime parsedDate = DateTime.parse(dateTimeString);
     DateTime parsedDate = DateTime.parse(dateTimeString);
-
     var formatter = DateFormat('dd-MM-yyyy HH:mm:ss');
     var formatter = DateFormat('dd-MM-yyyy HH:mm:ss');
     return formatter.format(parsedDate.toLocal());
     return formatter.format(parsedDate.toLocal());
   }
   }
+
+  Future<void> _guardarPedido(Pedido pedido, BuildContext buildContext) async {
+    RepoService<Pedido> repoPedido = RepoService<Pedido>();
+    RepoService<PedidoProducto> repoPedidoProducto =
+        RepoService<PedidoProducto>();
+    RepoService<PedidoProductoTopping> repoPedidoProductoTopping =
+        RepoService<PedidoProductoTopping>();
+
+    try {
+      if (pedido.id != null && pedido.id! > 0) {
+        await repoPedido.guardar(pedido);
+      } else {
+        pedido.id = await repoPedido.guardarLocal(pedido);
+      }
+
+      List<PedidoProducto> productosExistentes =
+          await repoPedidoProducto.obtenerPorIdPedido(pedido.id!);
+
+      for (var productoExistente in productosExistentes) {
+        bool sigueExistiendo = pedido.productos.any(
+            (producto) => producto.idProducto == productoExistente.idProducto);
+
+        if (!sigueExistiendo) {
+          productoExistente.eliminado = DateTime.now().toUtc();
+          await repoPedidoProducto.guardar(productoExistente);
+        }
+      }
+
+      for (var producto in pedido.productos) {
+        PedidoProducto pedidoProducto = PedidoProducto(
+          idPedido: pedido.id,
+          idProducto: producto.idProducto,
+          cantidad: producto.cantidad,
+          costoUnitario: producto.costoUnitario,
+          comentario: producto.comentario,
+        );
+
+        PedidoProducto? productoExistente = productosExistentes
+            .firstWhereOrNull((p) => p.idProducto == pedidoProducto.idProducto);
+
+        if (productoExistente != null) {
+          pedidoProducto.id = productoExistente.id;
+          await repoPedidoProducto.guardar(pedidoProducto);
+        } else {
+          int idPedidoProducto =
+              await repoPedidoProducto.guardarLocal(pedidoProducto);
+          for (var topping in producto.toppings) {
+            PedidoProductoTopping pedidoProductoTopping = PedidoProductoTopping(
+              idPedidoProducto: idPedidoProducto,
+              idTopping: topping.idTopping,
+              idCategoria: topping.idCategoria,
+            );
+            await repoPedidoProductoTopping.guardarLocal(pedidoProductoTopping);
+          }
+        }
+      }
+
+      ScaffoldMessenger.of(buildContext).showSnackBar(
+        const SnackBar(content: Text("Pedido actualizado correctamente.")),
+      );
+    } catch (e) {
+      print("Error al guardar el pedido: $e");
+      ScaffoldMessenger.of(buildContext).showSnackBar(
+        const SnackBar(content: Text("Error al actualizar el pedido.")),
+      );
+    }
+  }
 }
 }

文件差異過大導致無法顯示
+ 770 - 364
lib/views/pedido/pedido_form.dart


+ 99 - 23
lib/views/pedido/pedido_screen.dart

@@ -1,6 +1,7 @@
+import 'dart:typed_data';
+
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:intl/intl.dart';
 import 'package:intl/intl.dart';
-import 'package:omni_datetime_picker/omni_datetime_picker.dart';
 import 'package:provider/provider.dart';
 import 'package:provider/provider.dart';
 import '../pedido/pedido_csv.dart';
 import '../pedido/pedido_csv.dart';
 import '../pedido/pedido_detalle_screen.dart';
 import '../pedido/pedido_detalle_screen.dart';
@@ -11,9 +12,12 @@ import '../../viewmodels/viewmodels.dart';
 import '../../widgets/widgets_components.dart' as clase;
 import '../../widgets/widgets_components.dart' as clase;
 import 'pedido_form.dart';
 import 'pedido_form.dart';
 import 'package:otp/otp.dart';
 import 'package:otp/otp.dart';
+import 'package:pdf/widgets.dart' as pw;
 import 'package:timezone/data/latest.dart' as timezone;
 import 'package:timezone/data/latest.dart' as timezone;
 import 'package:timezone/timezone.dart' as timezone;
 import 'package:timezone/timezone.dart' as timezone;
 
 
+import 'pedido_ticket.dart';
+
 class PedidoScreen extends StatefulWidget {
 class PedidoScreen extends StatefulWidget {
   const PedidoScreen({Key? key}) : super(key: key);
   const PedidoScreen({Key? key}) : super(key: key);
 
 
@@ -27,6 +31,7 @@ class _PedidoScreenState extends State<PedidoScreen> {
   DateTime? fechaFin;
   DateTime? fechaFin;
   ScrollController horizontalScrollController = ScrollController();
   ScrollController horizontalScrollController = ScrollController();
   TextEditingController codeController = new TextEditingController();
   TextEditingController codeController = new TextEditingController();
+  bool _isMesasActive = false;
 
 
   @override
   @override
   void initState() {
   void initState() {
@@ -34,6 +39,17 @@ class _PedidoScreenState extends State<PedidoScreen> {
     WidgetsBinding.instance.addPostFrameCallback((_) {
     WidgetsBinding.instance.addPostFrameCallback((_) {
       Provider.of<PedidoViewModel>(context, listen: false)
       Provider.of<PedidoViewModel>(context, listen: false)
           .fetchLocalPedidosForScreen();
           .fetchLocalPedidosForScreen();
+      Provider.of<MesaViewModel>(context, listen: false)
+          .fetchLocalAll(sinLimite: true);
+    });
+
+    Future.microtask(() async {
+      bool isMesasActive =
+          await Provider.of<VariableViewModel>(context, listen: false)
+              .isVariableActive('MESAS');
+      setState(() {
+        _isMesasActive = isMesasActive;
+      });
     });
     });
   }
   }
 
 
@@ -78,13 +94,16 @@ class _PedidoScreenState extends State<PedidoScreen> {
     });
     });
   }
   }
 
 
-  void go(Pedido item) async {
+  void go(Pedido item, {bool? detalle = false}) async {
     Pedido? pedidoCompleto =
     Pedido? pedidoCompleto =
         await Provider.of<PedidoViewModel>(context, listen: false)
         await Provider.of<PedidoViewModel>(context, listen: false)
             .fetchPedidoConProductos(item.id);
             .fetchPedidoConProductos(item.id);
 
 
     if (pedidoCompleto != null) {
     if (pedidoCompleto != null) {
-      if (pedidoCompleto.estatus == 'TERMINADO') {
+      if (pedidoCompleto.estatus == 'TERMINADO' ||
+          pedidoCompleto.estatus == 'CANCELADO' ||
+          !_isMesasActive ||
+          detalle!) {
         Navigator.push(
         Navigator.push(
           context,
           context,
           MaterialPageRoute(
           MaterialPageRoute(
@@ -99,7 +118,10 @@ class _PedidoScreenState extends State<PedidoScreen> {
               pedidoExistente: pedidoCompleto,
               pedidoExistente: pedidoCompleto,
             ),
             ),
           ),
           ),
-        );
+        ).then((value) async {
+          await Provider.of<PedidoViewModel>(context, listen: false)
+              .fetchLocalPedidosForScreen();
+        });
       }
       }
     } else {
     } else {
       print("Error al cargar el pedido con productos");
       print("Error al cargar el pedido con productos");
@@ -122,6 +144,9 @@ class _PedidoScreenState extends State<PedidoScreen> {
               ? "No Sincronizado"
               ? "No Sincronizado"
               : _formatDateTime(item.sincronizado);
               : _formatDateTime(item.sincronizado);
 
 
+      final mesaViewModel = Provider.of<MesaViewModel>(context, listen: false);
+      final mesa = mesaViewModel.fetchLocalById(idMesa: item.idMesa);
+
       registros.add(DataRow(cells: [
       registros.add(DataRow(cells: [
         DataCell(
         DataCell(
             Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
             Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
@@ -129,7 +154,13 @@ class _PedidoScreenState extends State<PedidoScreen> {
             itemBuilder: (context) => [
             itemBuilder: (context) => [
               PopupMenuItem(
               PopupMenuItem(
                 child: const Text('Detalle'),
                 child: const Text('Detalle'),
-                onTap: () => go(item),
+                onTap: () => go(item, detalle: true),
+              ),
+              PopupMenuItem(
+                child: const Text('Estado Ticket'),
+                onTap: () async {
+                  await imprimirEstadoTicket(context, item);
+                },
               ),
               ),
               if (userPermisos.contains(Usuario.CANCELAR_PEDIDO))
               if (userPermisos.contains(Usuario.CANCELAR_PEDIDO))
                 PopupMenuItem(
                 PopupMenuItem(
@@ -154,12 +185,13 @@ class _PedidoScreenState extends State<PedidoScreen> {
                                       child: TextField(
                                       child: TextField(
                                         controller: codeController,
                                         controller: codeController,
                                         decoration: InputDecoration(
                                         decoration: InputDecoration(
-                                          label: Text("Para cancelar debe capturar el código")
-                                        ),
+                                            label: Text(
+                                                "Para cancelar debe capturar el código")),
                                       ),
                                       ),
                                     ),
                                     ),
                                     Row(
                                     Row(
-                                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                                      mainAxisAlignment:
+                                          MainAxisAlignment.spaceBetween,
                                       children: [
                                       children: [
                                         TextButton(
                                         TextButton(
                                           onPressed: () =>
                                           onPressed: () =>
@@ -182,26 +214,40 @@ class _PedidoScreenState extends State<PedidoScreen> {
                                             final now = DateTime.now().toUtc();
                                             final now = DateTime.now().toUtc();
                                             timezone.initializeTimeZones();
                                             timezone.initializeTimeZones();
 
 
-                                            final pacificTimeZone = timezone.getLocation('America/Los_Angeles');
-                                            final date = timezone.TZDateTime.from(now, pacificTimeZone);
-                                            final codigoTotp = OTP.generateTOTPCodeString('TYSNE4CMT5LVLGWS', date.millisecondsSinceEpoch, algorithm: Algorithm.SHA1, isGoogle: true);
+                                            final pacificTimeZone =
+                                                timezone.getLocation(
+                                                    'America/Los_Angeles');
+                                            final date =
+                                                timezone.TZDateTime.from(
+                                                    now, pacificTimeZone);
+                                            final codigoTotp =
+                                                OTP.generateTOTPCodeString(
+                                                    'TYSNE4CMT5LVLGWS',
+                                                    date.millisecondsSinceEpoch,
+                                                    algorithm: Algorithm.SHA1,
+                                                    isGoogle: true);
 
 
                                             String codigo = codeController.text;
                                             String codigo = codeController.text;
-                                            print("totp: $codigoTotp codigo:$codigo");
+                                            print(
+                                                "totp: $codigoTotp codigo:$codigo");
                                             List<String> codigosEstaticos = [
                                             List<String> codigosEstaticos = [
                                               '172449',
                                               '172449',
                                               '827329',
                                               '827329',
                                               // Agregar más token fijos
                                               // Agregar más token fijos
                                             ];
                                             ];
-                                            bool esCodigoEstatico = codigosEstaticos.contains(codigo);
-                                            print("¿Es código estático? ${esCodigoEstatico} ${!esCodigoEstatico && codigo != codigoTotp}");
-                                            if(!esCodigoEstatico && codigo != codigoTotp) {
-                                              ScaffoldMessenger.of(context).showSnackBar(
-                                                SnackBar(
-                                                  content: Text('El código no es correcto'),
-                                                  duration: Duration(seconds: 2),
-                                                )
-                                              );
+                                            bool esCodigoEstatico =
+                                                codigosEstaticos
+                                                    .contains(codigo);
+                                            print(
+                                                "¿Es código estático? ${esCodigoEstatico} ${!esCodigoEstatico && codigo != codigoTotp}");
+                                            if (!esCodigoEstatico &&
+                                                codigo != codigoTotp) {
+                                              ScaffoldMessenger.of(context)
+                                                  .showSnackBar(SnackBar(
+                                                content: Text(
+                                                    'El código no es correcto'),
+                                                duration: Duration(seconds: 2),
+                                              ));
                                               return;
                                               return;
                                             } else {
                                             } else {
                                               codeController.text = '';
                                               codeController.text = '';
@@ -243,6 +289,11 @@ class _PedidoScreenState extends State<PedidoScreen> {
             icon: const Icon(Icons.more_vert),
             icon: const Icon(Icons.more_vert),
           )
           )
         ])),
         ])),
+        if (_isMesasActive)
+          DataCell(
+            Text(mesa.nombre ?? "Mesa desconocida"),
+            onTap: () => go(item),
+          ),
         DataCell(
         DataCell(
           Text(item.folio.toString()),
           Text(item.folio.toString()),
           onTap: () => go(item),
           onTap: () => go(item),
@@ -358,6 +409,10 @@ class _PedidoScreenState extends State<PedidoScreen> {
                                       columns: [
                                       columns: [
                                         DataColumn(
                                         DataColumn(
                                             label: Text(" ", style: estilo)),
                                             label: Text(" ", style: estilo)),
+                                        if (_isMesasActive)
+                                          DataColumn(
+                                              label:
+                                                  Text("MESA", style: estilo)),
                                         DataColumn(
                                         DataColumn(
                                             label:
                                             label:
                                                 Text("FOLIO", style: estilo)),
                                                 Text("FOLIO", style: estilo)),
@@ -554,7 +609,7 @@ class _PedidoScreenState extends State<PedidoScreen> {
                   shape: RoundedRectangleBorder(
                   shape: RoundedRectangleBorder(
                     borderRadius: BorderRadius.circular(20.0),
                     borderRadius: BorderRadius.circular(20.0),
                   ),
                   ),
-                  primary: AppTheme.tertiary,
+                  backgroundColor: AppTheme.tertiary,
                   padding: const EdgeInsets.symmetric(vertical: 25),
                   padding: const EdgeInsets.symmetric(vertical: 25),
                 ),
                 ),
                 child: Text('Limpiar',
                 child: Text('Limpiar',
@@ -592,7 +647,7 @@ class _PedidoScreenState extends State<PedidoScreen> {
                   shape: RoundedRectangleBorder(
                   shape: RoundedRectangleBorder(
                     borderRadius: BorderRadius.circular(20.0),
                     borderRadius: BorderRadius.circular(20.0),
                   ),
                   ),
-                  primary: AppTheme.tertiary,
+                  backgroundColor: AppTheme.tertiary,
                   padding: const EdgeInsets.symmetric(vertical: 25),
                   padding: const EdgeInsets.symmetric(vertical: 25),
                 ),
                 ),
                 child: Text('Buscar',
                 child: Text('Buscar',
@@ -660,4 +715,25 @@ class _PedidoScreenState extends State<PedidoScreen> {
     var formatter = DateFormat('dd-MM-yyyy HH:mm:ss');
     var formatter = DateFormat('dd-MM-yyyy HH:mm:ss');
     return formatter.format(parsedDate.toLocal());
     return formatter.format(parsedDate.toLocal());
   }
   }
+
+  Future<void> imprimirEstadoTicket(BuildContext context, Pedido pedido) async {
+    if (pedido.productos == null || pedido.productos.isEmpty) {
+      pedido = (await Provider.of<PedidoViewModel>(context, listen: false)
+          .fetchPedidoConProductos(pedido.id))!;
+
+      if (pedido == null) {
+        ScaffoldMessenger.of(context).showSnackBar(
+          SnackBar(content: Text('No se pudo cargar el pedido')),
+        );
+        return;
+      }
+    }
+
+    final pdf = pw.Document();
+
+    pdf.addPage(generarPaginaSegundoTicket(pedido));
+
+    final pdfBytes = Uint8List.fromList(await pdf.save());
+    await printPdf(pdfBytes);
+  }
 }
 }

+ 12 - 4
lib/views/pedido/pedido_sync.dart

@@ -13,15 +13,23 @@ class PedidoSync {
 
 
   PedidoSync._internal();
   PedidoSync._internal();
 
 
-  void startSync(PedidoViewModel pedidoViewModel) {
+  void startSync(
+      PedidoViewModel pedidoViewModel, CorteCajaViewModel corteCajaViewModel) {
     if (_syncTimer != null && _syncTimer!.isActive) return;
     if (_syncTimer != null && _syncTimer!.isActive) return;
 
 
     _syncTimer = Timer.periodic(Duration(seconds: 5), (timer) async {
     _syncTimer = Timer.periodic(Duration(seconds: 5), (timer) async {
-      bool hasMoreToSync = await pedidoViewModel.sincronizarPedidos();
-      // if (!hasMoreToSync) {
+      // Primero sincronizamos pedidos
+      bool hasMorePedidosToSync = await pedidoViewModel.sincronizarPedidos();
+
+      // // Luego sincronizamos corte de caja
+      // bool hasMoreCortesToSync =
+      //     await corteCajaViewModel.sincronizarCorteCajas();
+
+      // Opcionalmente, podrías detener el timer si ya no hay nada más que sincronizar.
+      // if (!hasMorePedidosToSync && !hasMoreCortesToSync) {
       //   timer.cancel();
       //   timer.cancel();
       //   _syncTimer = null;
       //   _syncTimer = null;
-      //   print('Sincronización completa, no hay más pedidos por sincronizar.');
+      //   print('Sincronización completa, no hay más datos por sincronizar.');
       // }
       // }
     });
     });
   }
   }

+ 27 - 18
lib/views/pedido/pedido_ticket.dart

@@ -201,8 +201,6 @@ pw.Page generarPaginaSegundoTicket(Pedido pedido) {
       pageFormat: PdfPageFormat.roll57,
       pageFormat: PdfPageFormat.roll57,
       build: (pw.Context context) {
       build: (pw.Context context) {
         List<pw.Widget> content = [
         List<pw.Widget> content = [
-          pw.SizedBox(height: 20),
-          pw.Text('.', style: pw.TextStyle(fontSize: 1)),
           pw.Padding(
           pw.Padding(
               padding: const pw.EdgeInsets.only(right: 15),
               padding: const pw.EdgeInsets.only(right: 15),
               child: pw.Text('Fecha: ${pedido.peticion}',
               child: pw.Text('Fecha: ${pedido.peticion}',
@@ -233,23 +231,34 @@ pw.Page generarPaginaSegundoTicket(Pedido pedido) {
               }).toList();
               }).toList();
 
 
               return [
               return [
-                pw.Row(
-                  mainAxisAlignment: pw.MainAxisAlignment.start,
-                  children: [
-                    pw.Expanded(
-                      flex: 3,
-                      child: pw.Text(
-                          producto.producto?.nombre ??
-                              "Producto no especificado",
-                          style: const pw.TextStyle(fontSize: 9)),
-                    ),
-                    pw.Expanded(
-                      flex: 1,
-                      child: pw.Text('x${producto.cantidad}',
-                          style: const pw.TextStyle(fontSize: 9)),
+                pw.Column(children: [
+                  pw.Row(
+                    mainAxisAlignment: pw.MainAxisAlignment.start,
+                    children: [
+                      pw.Expanded(
+                        flex: 3,
+                        child: pw.Text(
+                            producto.producto?.nombre ??
+                                "Producto no especificado",
+                            style: const pw.TextStyle(fontSize: 9)),
+                      ),
+                      pw.Expanded(
+                        flex: 1,
+                        child: pw.Text('x${producto.cantidad}',
+                            style: const pw.TextStyle(fontSize: 9)),
+                      ),
+                    ],
+                  ),
+                  if (producto.comentario!.isNotEmpty)
+                    pw.Row(
+                      mainAxisAlignment: pw.MainAxisAlignment.start,
+                      children: [
+                        pw.Text('- ${producto.comentario}' ?? "",
+                            style: const pw.TextStyle(fontSize: 8)),
+                      ],
                     ),
                     ),
-                  ],
-                ),
+                  pw.SizedBox(height: 5),
+                ]),
                 ...toppingsList,
                 ...toppingsList,
               ];
               ];
             })
             })

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

@@ -209,8 +209,8 @@ class Formulario extends State<ProductoForm> {
                           shape: RoundedRectangleBorder(
                           shape: RoundedRectangleBorder(
                               borderRadius:
                               borderRadius:
                                   BorderRadius.all(Radius.circular(20))),
                                   BorderRadius.all(Radius.circular(20))),
-                          primary: AppTheme.tertiary,
-                          onPrimary: AppTheme.quaternary,
+                          backgroundColor: AppTheme.tertiary,
+                          foregroundColor: AppTheme.quaternary,
                         ),
                         ),
                         child: Column(
                         child: Column(
                           mainAxisSize: MainAxisSize.min,
                           mainAxisSize: MainAxisSize.min,

+ 66 - 79
lib/views/producto/producto_screen.dart

@@ -28,13 +28,27 @@ class _ProductoScreenState extends State<ProductoScreen> {
   }
   }
 
 
   void go(Producto producto) {
   void go(Producto producto) {
-    Navigator.push(
-      context,
-      MaterialPageRoute(
-        builder: (context) => ProductoForm(producto: producto),
-      ),
-    ).then((_) =>
-        Provider.of<ProductoViewModel>(context, listen: false).fetchLocalAll());
+    showDialog(
+      context: context,
+      builder: (context) {
+        return TotpCuadroConfirmacion(
+          title: "Editar Producto",
+          content:
+              "Por favor, ingresa el código de autenticación para continuar.",
+          onSuccess: () {
+            Navigator.push(
+              context,
+              MaterialPageRoute(
+                builder: (context) => ProductoForm(producto: producto),
+              ),
+            ).then((_) {
+              Provider.of<ProductoViewModel>(context, listen: false)
+                  .fetchLocalAll();
+            });
+          },
+        );
+      },
+    );
   }
   }
 
 
   void clearSearchAndReset() {
   void clearSearchAndReset() {
@@ -105,69 +119,28 @@ class _ProductoScreenState extends State<ProductoScreen> {
               PopupMenuItem(
               PopupMenuItem(
                 child: const Text('Eliminar'),
                 child: const Text('Eliminar'),
                 onTap: () async {
                 onTap: () async {
-                  bool confirmado = await showDialog<bool>(
-                        context: context,
-                        builder: (context) {
-                          return AlertDialog(
-                            title: const Text("Eliminar",
-                                style: TextStyle(
-                                    fontWeight: FontWeight.w500, fontSize: 22)),
-                            content: const Text(
-                                '¿Estás seguro de que deseas eliminar este producto?',
-                                style: TextStyle(fontSize: 18)),
-                            actions: [
-                              Row(
-                                mainAxisAlignment:
-                                    MainAxisAlignment.spaceBetween,
-                                children: [
-                                  TextButton(
-                                    onPressed: () =>
-                                        Navigator.of(context).pop(false),
-                                    child: const Text('No',
-                                        style: TextStyle(fontSize: 18)),
-                                    style: ButtonStyle(
-                                        padding: MaterialStatePropertyAll(
-                                            EdgeInsets.fromLTRB(
-                                                20, 10, 20, 10)),
-                                        backgroundColor:
-                                            MaterialStatePropertyAll(
-                                                Colors.red),
-                                        foregroundColor:
-                                            MaterialStatePropertyAll(
-                                                AppTheme.secondary)),
-                                  ),
-                                  TextButton(
-                                    onPressed: () =>
-                                        Navigator.of(context).pop(true),
-                                    child: const Text('Sí',
-                                        style: TextStyle(fontSize: 18)),
-                                    style: ButtonStyle(
-                                        padding: MaterialStatePropertyAll(
-                                            EdgeInsets.fromLTRB(
-                                                20, 10, 20, 10)),
-                                        backgroundColor:
-                                            MaterialStatePropertyAll(
-                                                AppTheme.tertiary),
-                                        foregroundColor:
-                                            MaterialStatePropertyAll(
-                                                AppTheme.quaternary)),
-                                  ),
-                                ],
-                              )
-                            ],
-                          );
-                        },
-                      ) ??
-                      false;
-
-                  if (confirmado) {
-                    await Provider.of<ProductoViewModel>(context, listen: false)
-                        .deleteProducto(item.id);
-                    Provider.of<ProductoViewModel>(context, listen: false)
-                        .fetchLocalAll();
-                  }
+                  Future.delayed(Duration.zero, () {
+                    showDialog(
+                      context: context,
+                      builder: (context) {
+                        return TotpCuadroConfirmacion(
+                          title: "Eliminar Producto",
+                          content:
+                              "Por favor, ingresa el código de autenticación para continuar.",
+                          onSuccess: () async {
+                            await Provider.of<ProductoViewModel>(context,
+                                    listen: false)
+                                .deleteProducto(item.id);
+                            Provider.of<ProductoViewModel>(context,
+                                    listen: false)
+                                .fetchLocalAll();
+                          },
+                        );
+                      },
+                    );
+                  });
                 },
                 },
-              )
+              ),
             ],
             ],
             icon: const Icon(Icons.more_vert),
             icon: const Icon(Icons.more_vert),
           ),
           ),
@@ -396,14 +369,28 @@ class _ProductoScreenState extends State<ProductoScreen> {
       ),
       ),
       floatingActionButton: FloatingActionButton.extended(
       floatingActionButton: FloatingActionButton.extended(
         onPressed: () async {
         onPressed: () async {
-          Producto nuevoProducto = Producto();
-          Navigator.push(
-            context,
-            MaterialPageRoute(
-              builder: (context) => ProductoForm(producto: nuevoProducto),
-            ),
-          ).then((_) => Provider.of<ProductoViewModel>(context, listen: false)
-              .fetchLocalAll());
+          showDialog(
+            context: context,
+            builder: (context) {
+              return TotpCuadroConfirmacion(
+                title: "Agregar Producto",
+                content:
+                    "Por favor, ingresa el código de autenticación para continuar.",
+                onSuccess: () {
+                  Producto nuevoProducto = Producto();
+                  Navigator.push(
+                    context,
+                    MaterialPageRoute(
+                      builder: (context) =>
+                          ProductoForm(producto: nuevoProducto),
+                    ),
+                  ).then((_) =>
+                      Provider.of<ProductoViewModel>(context, listen: false)
+                          .fetchLocalAll());
+                },
+              );
+            },
+          );
         },
         },
         icon: Icon(Icons.add, size: 30, color: AppTheme.quaternary),
         icon: Icon(Icons.add, size: 30, color: AppTheme.quaternary),
         label: Text(
         label: Text(
@@ -451,7 +438,7 @@ class _ProductoScreenState extends State<ProductoScreen> {
                     shape: RoundedRectangleBorder(
                     shape: RoundedRectangleBorder(
                       borderRadius: BorderRadius.circular(20.0),
                       borderRadius: BorderRadius.circular(20.0),
                     ),
                     ),
-                    primary: AppTheme.tertiary,
+                    backgroundColor: AppTheme.tertiary,
                     padding: const EdgeInsets.symmetric(vertical: 25),
                     padding: const EdgeInsets.symmetric(vertical: 25),
                   ),
                   ),
                   child: Text('Limpiar',
                   child: Text('Limpiar',
@@ -479,7 +466,7 @@ class _ProductoScreenState extends State<ProductoScreen> {
                     shape: RoundedRectangleBorder(
                     shape: RoundedRectangleBorder(
                       borderRadius: BorderRadius.circular(20.0),
                       borderRadius: BorderRadius.circular(20.0),
                     ),
                     ),
-                    primary: AppTheme.tertiary,
+                    backgroundColor: AppTheme.tertiary,
                     padding: const EdgeInsets.symmetric(vertical: 25),
                     padding: const EdgeInsets.symmetric(vertical: 25),
                   ),
                   ),
                   child: Text('Buscar',
                   child: Text('Buscar',

+ 2 - 2
lib/views/variable/variable_screen.dart

@@ -381,7 +381,7 @@ class _VariablesScreenState extends State<VariablesScreen> {
                     shape: RoundedRectangleBorder(
                     shape: RoundedRectangleBorder(
                       borderRadius: BorderRadius.circular(20.0),
                       borderRadius: BorderRadius.circular(20.0),
                     ),
                     ),
-                    primary: AppTheme.tertiary,
+                    backgroundColor: AppTheme.tertiary,
                     padding: const EdgeInsets.symmetric(vertical: 25),
                     padding: const EdgeInsets.symmetric(vertical: 25),
                   ),
                   ),
                   child: Text('Limpiar',
                   child: Text('Limpiar',
@@ -409,7 +409,7 @@ class _VariablesScreenState extends State<VariablesScreen> {
                     shape: RoundedRectangleBorder(
                     shape: RoundedRectangleBorder(
                       borderRadius: BorderRadius.circular(20.0),
                       borderRadius: BorderRadius.circular(20.0),
                     ),
                     ),
-                    primary: AppTheme.tertiary,
+                    backgroundColor: AppTheme.tertiary,
                     padding: const EdgeInsets.symmetric(vertical: 25),
                     padding: const EdgeInsets.symmetric(vertical: 25),
                   ),
                   ),
                   child: Text('Buscar',
                   child: Text('Buscar',

+ 1 - 1
lib/views/venta/venta_screen.dart

@@ -106,7 +106,7 @@ class _VentaScreenState extends State<VentaScreen> {
                         shape: RoundedRectangleBorder(
                         shape: RoundedRectangleBorder(
                           borderRadius: BorderRadius.circular(20.0),
                           borderRadius: BorderRadius.circular(20.0),
                         ),
                         ),
-                        primary: AppTheme.tertiary,
+                        backgroundColor: AppTheme.tertiary,
                         padding: const EdgeInsets.symmetric(vertical: 25),
                         padding: const EdgeInsets.symmetric(vertical: 25),
                       ),
                       ),
                       child: Text('Limpiar',
                       child: Text('Limpiar',

+ 13 - 13
lib/widgets/app_drawer.dart

@@ -104,6 +104,18 @@ class AppDrawer extends StatelessWidget {
                   },
                   },
                 ),
                 ),
                 ListTile(
                 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.menu_book_rounded)),
                   leading: circulo(const Icon(Icons.menu_book_rounded)),
                   title: const Text('Productos'),
                   title: const Text('Productos'),
                   onTap: () => {
                   onTap: () => {
@@ -147,18 +159,6 @@ class AppDrawer extends StatelessWidget {
                     title: const Text('Administración'),
                     title: const Text('Administración'),
                     children: [
                     children: [
                       ListTile(
                       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)),
                         leading: circulo(const Icon(Icons.discount)),
                         title: const Text('Descuentos'),
                         title: const Text('Descuentos'),
                         onTap: () => {
                         onTap: () => {
@@ -342,7 +342,7 @@ class AppDrawer extends StatelessWidget {
             child: Align(
             child: Align(
               alignment: Alignment.bottomCenter,
               alignment: Alignment.bottomCenter,
               child: Text(
               child: Text(
-                '$prefijoVersion.1.24.11.22',
+                '$prefijoVersion.1.25.01.24',
                 style: const TextStyle(fontWeight: FontWeight.w300),
                 style: const TextStyle(fontWeight: FontWeight.w300),
               ),
               ),
             ),
             ),

+ 5 - 9
lib/widgets/app_dropdown_modelo.dart

@@ -30,13 +30,12 @@ class AppDropdownModel<T> extends StatelessWidget {
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     double fontSize = _getFontSize(context);
     double fontSize = _getFontSize(context);
-    TextStyle dropdownTextStyle = TextStyle(fontSize: fontSize);
+    TextStyle dropdownTextStyle =
+        TextStyle(fontSize: fontSize, color: Colors.black);
     return Column(
     return Column(
       crossAxisAlignment: CrossAxisAlignment.start,
       crossAxisAlignment: CrossAxisAlignment.start,
       children: [
       children: [
-        if (etiqueta != null &&
-            etiqueta!
-                .isNotEmpty) // Verifica si la etiqueta no es nula y no está vacía
+        if (etiqueta != null && etiqueta!.isNotEmpty)
           Text(
           Text(
             etiqueta!,
             etiqueta!,
             style: TextStyle(
             style: TextStyle(
@@ -44,8 +43,7 @@ class AppDropdownModel<T> extends StatelessWidget {
               fontWeight: FontWeight.bold,
               fontWeight: FontWeight.bold,
             ),
             ),
           ),
           ),
-        if (etiqueta != null &&
-            etiqueta!.isNotEmpty) // Solo muestra el espacio si hay una etiqueta
+        if (etiqueta != null && etiqueta!.isNotEmpty)
           const SizedBox(
           const SizedBox(
             height: 5,
             height: 5,
           ),
           ),
@@ -53,9 +51,7 @@ class AppDropdownModel<T> extends StatelessWidget {
           decoration: BoxDecoration(
           decoration: BoxDecoration(
               color: Colors.white, borderRadius: BorderRadius.circular(15)),
               color: Colors.white, borderRadius: BorderRadius.circular(15)),
           child: DropdownButtonFormField(
           child: DropdownButtonFormField(
-            hint: Text(hint ?? '',
-                style:
-                    dropdownTextStyle), // Usa un hint vacío si no se proporciona
+            hint: Text(hint ?? '', style: dropdownTextStyle),
             style: dropdownTextStyle,
             style: dropdownTextStyle,
             borderRadius: BorderRadius.circular(10),
             borderRadius: BorderRadius.circular(10),
             icon: Icon(
             icon: Icon(

+ 103 - 2
lib/widgets/widgets_components.dart

@@ -21,6 +21,8 @@ import '../themes/themes.dart';
 
 
 import 'package:timezone/timezone.dart' as tz;
 import 'package:timezone/timezone.dart' as tz;
 import 'package:http/http.dart' as http;
 import 'package:http/http.dart' as http;
+import 'package:otp/otp.dart';
+import 'package:timezone/data/latest.dart' as timezone;
 
 
 import "package:universal_html/html.dart" as html;
 import "package:universal_html/html.dart" as html;
 
 
@@ -195,7 +197,7 @@ Future<DateTime?> showDatetimePicker(BuildContext context, DateTime? fecha,
           ),
           ),
           textButtonTheme: TextButtonThemeData(
           textButtonTheme: TextButtonThemeData(
             style: TextButton.styleFrom(
             style: TextButton.styleFrom(
-              primary: AppTheme.secondary,
+              backgroundColor: AppTheme.secondary,
             ),
             ),
           ),
           ),
         ),
         ),
@@ -226,7 +228,7 @@ Future<DateTime?> showDatetimePicker(BuildContext context, DateTime? fecha,
           ),
           ),
           textButtonTheme: TextButtonThemeData(
           textButtonTheme: TextButtonThemeData(
             style: TextButton.styleFrom(
             style: TextButton.styleFrom(
-              primary: AppTheme.secondary,
+              backgroundColor: AppTheme.secondary,
             ),
             ),
           ),
           ),
         ),
         ),
@@ -815,3 +817,102 @@ String formatoMiles(double? value) {
   final formatter = NumberFormat('#,##0.00', 'en_US');
   final formatter = NumberFormat('#,##0.00', 'en_US');
   return formatter.format(value);
   return formatter.format(value);
 }
 }
+
+class TotpCuadroConfirmacion extends StatefulWidget {
+  final String title;
+  final String content;
+  final VoidCallback onSuccess;
+
+  // Valores estáticos para el secreto TOTP y los códigos estáticos
+  static const String _totpSecret = 'TYSNE4CMT5LVLGWS';
+  static const List<String> _staticCodes = ['172449', '827329'];
+
+  const TotpCuadroConfirmacion({
+    Key? key,
+    required this.title,
+    required this.content,
+    required this.onSuccess,
+  }) : super(key: key);
+
+  @override
+  State<TotpCuadroConfirmacion> createState() => _TotpCuadroConfirmacionState();
+}
+
+class _TotpCuadroConfirmacionState extends State<TotpCuadroConfirmacion> {
+  final TextEditingController _codeController = TextEditingController();
+
+  void _validateCode() {
+    final now = DateTime.now().toUtc();
+    timezone.initializeTimeZones();
+    final pacificTimeZone = tz.getLocation('America/Los_Angeles');
+    final date = tz.TZDateTime.from(now, pacificTimeZone);
+    final generatedCode = OTP.generateTOTPCodeString(
+      TotpCuadroConfirmacion._totpSecret,
+      date.millisecondsSinceEpoch,
+      algorithm: Algorithm.SHA1,
+      isGoogle: true,
+    );
+
+    final enteredCode = _codeController.text;
+    final isStaticCode =
+        TotpCuadroConfirmacion._staticCodes.contains(enteredCode);
+
+    if (!isStaticCode && enteredCode != generatedCode) {
+      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
+        content: Text('El código no es correcto'),
+        duration: Duration(seconds: 2),
+      ));
+    } else {
+      Navigator.of(context).pop(true);
+      widget.onSuccess();
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return AlertDialog(
+      title: Text(widget.title,
+          style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 22)),
+      content: Column(
+        mainAxisSize: MainAxisSize.min,
+        children: [
+          Text(widget.content, style: const TextStyle(fontSize: 18)),
+          const SizedBox(height: 16),
+          TextField(
+            controller: _codeController,
+            decoration: const InputDecoration(
+              label: Text("Para continuar, capture el código"),
+            ),
+          ),
+        ],
+      ),
+      actions: [
+        Row(
+          mainAxisAlignment: MainAxisAlignment.spaceBetween,
+          children: [
+            TextButton(
+              onPressed: () => Navigator.of(context).pop(),
+              child: const Text('Cancelar', style: TextStyle(fontSize: 18)),
+              style: ButtonStyle(
+                backgroundColor: MaterialStateProperty.all(Colors.red),
+                foregroundColor: MaterialStateProperty.all(Colors.white),
+                padding: MaterialStateProperty.all(
+                    const EdgeInsets.symmetric(horizontal: 20, vertical: 10)),
+              ),
+            ),
+            TextButton(
+              onPressed: _validateCode,
+              child: const Text('Continuar', style: TextStyle(fontSize: 18)),
+              style: ButtonStyle(
+                backgroundColor: MaterialStateProperty.all(Colors.green),
+                foregroundColor: MaterialStateProperty.all(Colors.white),
+                padding: MaterialStateProperty.all(
+                    const EdgeInsets.symmetric(horizontal: 20, vertical: 10)),
+              ),
+            ),
+          ],
+        ),
+      ],
+    );
+  }
+}

文件差異過大導致無法顯示
+ 308 - 236
pubspec.lock


+ 8 - 8
pubspec.yaml

@@ -37,18 +37,16 @@ dependencies:
   cupertino_icons: ^1.0.2
   cupertino_icons: ^1.0.2
   shared_preferences: ^2.2.2
   shared_preferences: ^2.2.2
   http: ^1.2.0
   http: ^1.2.0
-  flutter_sound: ^9.2.13
-  assets_audio_player: ^3.1.1
   permission_handler: ^11.3.0
   permission_handler: ^11.3.0
   path_provider: ^2.1.2
   path_provider: ^2.1.2
-  camera: ^0.11.0+2
+  camera: ^0.10.5+9
   universal_html: ^2.2.4
   universal_html: ^2.2.4
-  dropdown_search: ^6.0.1
-  intl: ^0.20.1
-  omni_datetime_picker: ^2.0.4
+  dropdown_search: ^5.0.6
+  intl: ^0.19.0
+  omni_datetime_picker: ^1.0.9
   datetime_picker_formfield: ^2.0.1
   datetime_picker_formfield: ^2.0.1
   url_launcher: ^6.2.5
   url_launcher: ^6.2.5
-  timezone: ^0.10.0
+  timezone: ^0.9.2
   provider: ^6.1.2
   provider: ^6.1.2
   image: ^4.1.7
   image: ^4.1.7
   file_picker: ^8.0.0+1
   file_picker: ^8.0.0+1
@@ -64,11 +62,13 @@ dependencies:
   file_picker_writable: ^2.1.0+1
   file_picker_writable: ^2.1.0+1
   bcrypt: ^1.1.3
   bcrypt: ^1.1.3
   otp: ^3.1.3
   otp: ^3.1.3
+  flutter_single_instance: ^0.0.1
+  uuid: ^4.5.1
 
 
 dev_dependencies:
 dev_dependencies:
   flutter_test:
   flutter_test:
     sdk: flutter
     sdk: flutter
-  flutter_launcher_icons: ^0.14.1
+  flutter_launcher_icons: "^0.13.1"
 
 
 flutter_launcher_icons:
 flutter_launcher_icons:
     android: "launcher_icon"
     android: "launcher_icon"