diff --git a/HackOnLinces_app b/HackOnLinces_app
deleted file mode 160000
index 88b5802..0000000
--- a/HackOnLinces_app
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 88b580229c63f17b87742aa1f7b9821533484066
diff --git a/HackOnLinces_app/aplicacion_hack/.gitignore b/HackOnLinces_app/aplicacion_hack/.gitignore
new file mode 100644
index 0000000..3820a95
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/.gitignore
@@ -0,0 +1,45 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.build/
+.buildlog/
+.history
+.svn/
+.swiftpm/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins-dependencies
+.pub-cache/
+.pub/
+/build/
+/coverage/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/HackOnLinces_app/aplicacion_hack/.metadata b/HackOnLinces_app/aplicacion_hack/.metadata
new file mode 100644
index 0000000..768732a
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/.metadata
@@ -0,0 +1,45 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "559ffa3f75e7402d65a8def9c28389a9b2e6fe42"
+ channel: "stable"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
+ base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
+ - platform: android
+ create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
+ base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
+ - platform: ios
+ create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
+ base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
+ - platform: linux
+ create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
+ base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
+ - platform: macos
+ create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
+ base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
+ - platform: web
+ create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
+ base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
+ - platform: windows
+ create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
+ base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/HackOnLinces_app/aplicacion_hack/README.md b/HackOnLinces_app/aplicacion_hack/README.md
new file mode 100644
index 0000000..5c52d84
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/README.md
@@ -0,0 +1,17 @@
+# aplicacion_hack
+
+A new Flutter project.
+
+## Getting Started
+
+This project is a starting point for a Flutter application.
+
+A few resources to get you started if this is your first Flutter project:
+
+- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
+- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
+- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
+
+For help getting started with Flutter development, view the
+[online documentation](https://docs.flutter.dev/), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.
diff --git a/HackOnLinces_app/aplicacion_hack/analysis_options.yaml b/HackOnLinces_app/aplicacion_hack/analysis_options.yaml
new file mode 100644
index 0000000..0d29021
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/analysis_options.yaml
@@ -0,0 +1,28 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+ # The lint rules applied to this project can be customized in the
+ # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+ # included above or to enable additional rules. A list of all available lints
+ # and their documentation is published at https://dart.dev/lints.
+ #
+ # Instead of disabling a lint rule for the entire project in the
+ # section below, it can also be suppressed for a single line of code
+ # or a specific dart file by using the `// ignore: name_of_lint` and
+ # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+ # producing the lint.
+ rules:
+ # avoid_print: false # Uncomment to disable the `avoid_print` rule
+ # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/HackOnLinces_app/aplicacion_hack/android/.gitignore b/HackOnLinces_app/aplicacion_hack/android/.gitignore
new file mode 100644
index 0000000..be3943c
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/android/.gitignore
@@ -0,0 +1,14 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+.cxx/
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/to/reference-keystore
+key.properties
+**/*.keystore
+**/*.jks
diff --git a/HackOnLinces_app/aplicacion_hack/android/app/build.gradle.kts b/HackOnLinces_app/aplicacion_hack/android/app/build.gradle.kts
new file mode 100644
index 0000000..d6ba38a
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/android/app/build.gradle.kts
@@ -0,0 +1,50 @@
+plugins {
+ id("com.android.application")
+ id("com.google.gms.google-services")
+ // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
+ id("dev.flutter.flutter-gradle-plugin")
+}
+
+android {
+ namespace = "com.example.aplicacion_hack"
+ compileSdk = flutter.compileSdkVersion
+ ndkVersion = flutter.ndkVersion
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ isCoreLibraryDesugaringEnabled = true
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId = "com.example.aplicacion_hack"
+ // You can update the following values to match your application needs.
+ // For more information, see: https://flutter.dev/to/review-gradle-config.
+ minSdk = flutter.minSdkVersion
+ targetSdk = flutter.targetSdkVersion
+ versionCode = flutter.versionCode
+ versionName = flutter.versionName
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig = signingConfigs.getByName("debug")
+ }
+ }
+}
+
+kotlin {
+ compilerOptions {
+ jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
+ }
+}
+
+flutter {
+ source = "../.."
+}
+dependencies {
+ coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
+}
diff --git a/HackOnLinces_app/aplicacion_hack/android/app/google-services.json b/HackOnLinces_app/aplicacion_hack/android/app/google-services.json
new file mode 100644
index 0000000..83ed588
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/android/app/google-services.json
@@ -0,0 +1,29 @@
+{
+ "project_info": {
+ "project_number": "338042609701",
+ "project_id": "hackon-58b23",
+ "storage_bucket": "hackon-58b23.firebasestorage.app"
+ },
+ "client": [
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:338042609701:android:0f0f92d7f895794f371fcc",
+ "android_client_info": {
+ "package_name": "com.example.aplicacion_hack"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "AIzaSyBuT70SbADLeg92ll8keCySI8I4eYyqyLw"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ }
+ ],
+ "configuration_version": "1"
+}
\ No newline at end of file
diff --git a/HackOnLinces_app/aplicacion_hack/android/app/src/debug/AndroidManifest.xml b/HackOnLinces_app/aplicacion_hack/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/HackOnLinces_app/aplicacion_hack/android/app/src/main/AndroidManifest.xml b/HackOnLinces_app/aplicacion_hack/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..23a57a5
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HackOnLinces_app/aplicacion_hack/android/app/src/main/kotlin/com/example/aplicacion_hack/MainActivity.kt b/HackOnLinces_app/aplicacion_hack/android/app/src/main/kotlin/com/example/aplicacion_hack/MainActivity.kt
new file mode 100644
index 0000000..88cf8ba
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/android/app/src/main/kotlin/com/example/aplicacion_hack/MainActivity.kt
@@ -0,0 +1,5 @@
+package com.example.aplicacion_hack
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity : FlutterActivity()
diff --git a/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/drawable-v21/launch_background.xml b/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..f74085f
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/drawable/launch_background.xml b/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/values-night/styles.xml b/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..06952be
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/values/styles.xml b/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..cb1ef88
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/HackOnLinces_app/aplicacion_hack/android/app/src/profile/AndroidManifest.xml b/HackOnLinces_app/aplicacion_hack/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/HackOnLinces_app/aplicacion_hack/android/build.gradle.kts b/HackOnLinces_app/aplicacion_hack/android/build.gradle.kts
new file mode 100644
index 0000000..a2b1a8a
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/android/build.gradle.kts
@@ -0,0 +1,35 @@
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ // Añadimos la herramienta de Google Services para que Android la pueda usar
+ classpath("com.google.gms:google-services:4.4.1")
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+val newBuildDir: Directory =
+ rootProject.layout.buildDirectory
+ .dir("../../build")
+ .get()
+rootProject.layout.buildDirectory.value(newBuildDir)
+
+subprojects {
+ val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
+ project.layout.buildDirectory.value(newSubprojectBuildDir)
+}
+subprojects {
+ project.evaluationDependsOn(":app")
+}
+
+tasks.register("clean") {
+ delete(rootProject.layout.buildDirectory)
+}
\ No newline at end of file
diff --git a/HackOnLinces_app/aplicacion_hack/android/gradle.properties b/HackOnLinces_app/aplicacion_hack/android/gradle.properties
new file mode 100644
index 0000000..e96108c
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/android/gradle.properties
@@ -0,0 +1,6 @@
+org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
+android.useAndroidX=true
+# This newDsl flag was added by the Flutter template
+android.newDsl=false
+# This builtInKotlin flag was added by the Flutter template
+android.builtInKotlin=false
diff --git a/HackOnLinces_app/aplicacion_hack/android/gradle/wrapper/gradle-wrapper.properties b/HackOnLinces_app/aplicacion_hack/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..2d428bf
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
diff --git a/HackOnLinces_app/aplicacion_hack/android/settings.gradle.kts b/HackOnLinces_app/aplicacion_hack/android/settings.gradle.kts
new file mode 100644
index 0000000..5ea2823
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/android/settings.gradle.kts
@@ -0,0 +1,29 @@
+pluginManagement {
+ val flutterSdkPath =
+ run {
+ val properties = java.util.Properties()
+ file("local.properties").inputStream().use { properties.load(it) }
+ val flutterSdkPath = properties.getProperty("flutter.sdk")
+ require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
+ flutterSdkPath
+ }
+
+ includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
+
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+plugins {
+ id("dev.flutter.flutter-plugin-loader") version "1.0.0"
+ id("com.android.application") version "9.0.1" apply false
+ // START: FlutterFire Configuration
+ id("com.google.gms.google-services") version("4.3.15") apply false
+ // END: FlutterFire Configuration
+ id("org.jetbrains.kotlin.android") version "2.3.20" apply false
+}
+
+include(":app")
diff --git a/HackOnLinces_app/aplicacion_hack/assets/images/bottle.png b/HackOnLinces_app/aplicacion_hack/assets/images/bottle.png
new file mode 100644
index 0000000..2c8bb5a
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/assets/images/bottle.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/assets/images/globo.png b/HackOnLinces_app/aplicacion_hack/assets/images/globo.png
new file mode 100644
index 0000000..0dde173
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/assets/images/globo.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/assets/images/megafono.png b/HackOnLinces_app/aplicacion_hack/assets/images/megafono.png
new file mode 100644
index 0000000..a8c99ee
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/assets/images/megafono.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/assets/images/planta.png b/HackOnLinces_app/aplicacion_hack/assets/images/planta.png
new file mode 100644
index 0000000..e61720f
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/assets/images/planta.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/assets/images/recycle.jpg b/HackOnLinces_app/aplicacion_hack/assets/images/recycle.jpg
new file mode 100644
index 0000000..e09256d
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/assets/images/recycle.jpg differ
diff --git a/HackOnLinces_app/aplicacion_hack/assets/images/reloj.jpg b/HackOnLinces_app/aplicacion_hack/assets/images/reloj.jpg
new file mode 100644
index 0000000..0ff5df8
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/assets/images/reloj.jpg differ
diff --git a/HackOnLinces_app/aplicacion_hack/assets/images/reloj.png b/HackOnLinces_app/aplicacion_hack/assets/images/reloj.png
new file mode 100644
index 0000000..e64757c
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/assets/images/reloj.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/firebase.json b/HackOnLinces_app/aplicacion_hack/firebase.json
new file mode 100644
index 0000000..b44d29b
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/firebase.json
@@ -0,0 +1 @@
+{"flutter":{"platforms":{"android":{"default":{"projectId":"hackon-58b23","appId":"1:338042609701:android:0f0f92d7f895794f371fcc","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"hackon-58b23","configurations":{"android":"1:338042609701:android:0f0f92d7f895794f371fcc","ios":"1:338042609701:ios:d1ac418d368d1df5371fcc","macos":"1:338042609701:ios:d1ac418d368d1df5371fcc","web":"1:338042609701:web:5f0a245f364e7604371fcc","windows":"1:338042609701:web:b01c28c419fddd25371fcc"}}}}}}
\ No newline at end of file
diff --git a/HackOnLinces_app/aplicacion_hack/ios/.gitignore b/HackOnLinces_app/aplicacion_hack/ios/.gitignore
new file mode 100644
index 0000000..7a7f987
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/ios/.gitignore
@@ -0,0 +1,34 @@
+**/dgph
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/ephemeral/
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Flutter/AppFrameworkInfo.plist b/HackOnLinces_app/aplicacion_hack/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..391a902
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,24 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ App
+ CFBundleIdentifier
+ io.flutter.flutter.app
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ App
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+
+
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Flutter/Debug.xcconfig b/HackOnLinces_app/aplicacion_hack/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..592ceee
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/ios/Flutter/Debug.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Flutter/Release.xcconfig b/HackOnLinces_app/aplicacion_hack/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..592ceee
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/ios/Flutter/Release.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner.xcodeproj/project.pbxproj b/HackOnLinces_app/aplicacion_hack/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..f31ea00
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,644 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 54;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+ 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
+ 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 97C146E61CF9000F007C117D /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 97C146ED1CF9000F007C117D;
+ remoteInfo = Runner;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
+ 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
+ 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
+ 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 331C8082294A63A400263BE5 /* RunnerTests */ = {
+ isa = PBXGroup;
+ children = (
+ 331C807B294A618700263BE5 /* RunnerTests.swift */,
+ );
+ path = RunnerTests;
+ sourceTree = "";
+ };
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ 331C8082294A63A400263BE5 /* RunnerTests */,
+ );
+ sourceTree = "";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 97C146F01CF9000F007C117D /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146FA1CF9000F007C117D /* Main.storyboard */,
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */,
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+ 97C147021CF9000F007C117D /* Info.plist */,
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+ 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 331C8080294A63A400263BE5 /* RunnerTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+ buildPhases = (
+ 331C807D294A63A400263BE5 /* Sources */,
+ 331C807F294A63A400263BE5 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 331C8086294A63A400263BE5 /* PBXTargetDependency */,
+ );
+ name = RunnerTests;
+ productName = RunnerTests;
+ productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Runner;
+ packageProductDependencies = (
+ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
+ );
+ productName = Runner;
+ productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 97C146E61CF9000F007C117D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = YES;
+ LastUpgradeCheck = 1510;
+ ORGANIZATIONNAME = "";
+ TargetAttributes = {
+ 331C8080294A63A400263BE5 = {
+ CreatedOnToolsVersion = 14.0;
+ TestTargetID = 97C146ED1CF9000F007C117D;
+ };
+ 97C146ED1CF9000F007C117D = {
+ CreatedOnToolsVersion = 7.3.1;
+ LastSwiftMigration = 1100;
+ };
+ };
+ };
+ buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 97C146E51CF9000F007C117D;
+ packageReferences = (
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
+ );
+ productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 97C146ED1CF9000F007C117D /* Runner */,
+ 331C8080294A63A400263BE5 /* RunnerTests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 331C807F294A63A400263BE5 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 97C146EC1CF9000F007C117D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
+ );
+ name = "Thin Binary";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+ };
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Run Script";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 331C807D294A63A400263BE5 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 97C146EA1CF9000F007C117D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+ 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 97C146ED1CF9000F007C117D /* Runner */;
+ targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C146FB1CF9000F007C117D /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 249021D3217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Profile;
+ };
+ 249021D4217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.aplicacionHack;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Profile;
+ };
+ 331C8088294A63A400263BE5 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.aplicacionHack.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Debug;
+ };
+ 331C8089294A63A400263BE5 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.aplicacionHack.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Release;
+ };
+ 331C808A294A63A400263BE5 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.aplicacionHack.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Profile;
+ };
+ 97C147031CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 97C147041CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 97C147061CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.aplicacionHack;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Debug;
+ };
+ 97C147071CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.aplicacionHack;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 331C8088294A63A400263BE5 /* Debug */,
+ 331C8089294A63A400263BE5 /* Release */,
+ 331C808A294A63A400263BE5 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147031CF9000F007C117D /* Debug */,
+ 97C147041CF9000F007C117D /* Release */,
+ 249021D3217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147061CF9000F007C117D /* Debug */,
+ 97C147071CF9000F007C117D /* Release */,
+ 249021D4217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+
+/* Begin XCLocalSwiftPackageReference section */
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
+ isa = XCLocalSwiftPackageReference;
+ relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
+ };
+/* End XCLocalSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = FlutterGeneratedPluginSwiftPackage;
+ };
+/* End XCSwiftPackageProductDependency section */
+ };
+ rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/HackOnLinces_app/aplicacion_hack/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/HackOnLinces_app/aplicacion_hack/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/HackOnLinces_app/aplicacion_hack/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/HackOnLinces_app/aplicacion_hack/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..c3fedb2
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner.xcworkspace/contents.xcworkspacedata b/HackOnLinces_app/aplicacion_hack/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/HackOnLinces_app/aplicacion_hack/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/HackOnLinces_app/aplicacion_hack/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/AppDelegate.swift b/HackOnLinces_app/aplicacion_hack/ios/Runner/AppDelegate.swift
new file mode 100644
index 0000000..c30b367
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/ios/Runner/AppDelegate.swift
@@ -0,0 +1,16 @@
+import Flutter
+import UIKit
+
+@main
+@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+
+ func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
+ GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
+ }
+}
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d36b1fa
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-83.5x83.5@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "Icon-App-1024x1024@1x.png",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 0000000..dc9ada4
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000..7353c41
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000..797d452
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000..6ed2d93
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000..4cd7b00
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000..fe73094
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..321773c
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000..797d452
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000..502f463
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..0ec3034
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..0ec3034
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000..e9f5fea
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000..84ac32a
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000..8953cba
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000..0467bf1
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..0bedcf2
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Base.lproj/LaunchScreen.storyboard b/HackOnLinces_app/aplicacion_hack/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Base.lproj/Main.storyboard b/HackOnLinces_app/aplicacion_hack/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Info.plist b/HackOnLinces_app/aplicacion_hack/ios/Runner/Info.plist
new file mode 100644
index 0000000..34758c5
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/ios/Runner/Info.plist
@@ -0,0 +1,70 @@
+
+
+
+
+ CADisableMinimumFrameDurationOnPhone
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Aplicacion Hack
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ aplicacion_hack
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UIApplicationSceneManifest
+
+ UIApplicationSupportsMultipleScenes
+
+ UISceneConfigurations
+
+ UIWindowSceneSessionRoleApplication
+
+
+ UISceneClassName
+ UIWindowScene
+ UISceneConfigurationName
+ flutter
+ UISceneDelegateClassName
+ $(PRODUCT_MODULE_NAME).SceneDelegate
+ UISceneStoryboardFile
+ Main
+
+
+
+
+ UIApplicationSupportsIndirectInputEvents
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+
+
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/Runner-Bridging-Header.h b/HackOnLinces_app/aplicacion_hack/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 0000000..308a2a5
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/HackOnLinces_app/aplicacion_hack/ios/Runner/SceneDelegate.swift b/HackOnLinces_app/aplicacion_hack/ios/Runner/SceneDelegate.swift
new file mode 100644
index 0000000..b9ce8ea
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/ios/Runner/SceneDelegate.swift
@@ -0,0 +1,6 @@
+import Flutter
+import UIKit
+
+class SceneDelegate: FlutterSceneDelegate {
+
+}
diff --git a/HackOnLinces_app/aplicacion_hack/ios/RunnerTests/RunnerTests.swift b/HackOnLinces_app/aplicacion_hack/ios/RunnerTests/RunnerTests.swift
new file mode 100644
index 0000000..86a7c3b
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/ios/RunnerTests/RunnerTests.swift
@@ -0,0 +1,12 @@
+import Flutter
+import UIKit
+import XCTest
+
+class RunnerTests: XCTestCase {
+
+ func testExample() {
+ // If you add code to the Runner application, consider adding tests here.
+ // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
+ }
+
+}
diff --git a/HackOnLinces_app/aplicacion_hack/lib/firebase_options.dart b/HackOnLinces_app/aplicacion_hack/lib/firebase_options.dart
new file mode 100644
index 0000000..908492c
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/lib/firebase_options.dart
@@ -0,0 +1,88 @@
+// File generated by FlutterFire CLI.
+// ignore_for_file: type=lint
+import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
+import 'package:flutter/foundation.dart'
+ show defaultTargetPlatform, kIsWeb, TargetPlatform;
+
+/// Default [FirebaseOptions] for use with your Firebase apps.
+///
+/// Example:
+/// ```dart
+/// import 'firebase_options.dart';
+/// // ...
+/// await Firebase.initializeApp(
+/// options: DefaultFirebaseOptions.currentPlatform,
+/// );
+/// ```
+class DefaultFirebaseOptions {
+ static FirebaseOptions get currentPlatform {
+ if (kIsWeb) {
+ return web;
+ }
+ switch (defaultTargetPlatform) {
+ case TargetPlatform.android:
+ return android;
+ case TargetPlatform.iOS:
+ return ios;
+ case TargetPlatform.macOS:
+ return macos;
+ case TargetPlatform.windows:
+ return windows;
+ case TargetPlatform.linux:
+ throw UnsupportedError(
+ 'DefaultFirebaseOptions have not been configured for linux - '
+ 'you can reconfigure this by running the FlutterFire CLI again.',
+ );
+ default:
+ throw UnsupportedError(
+ 'DefaultFirebaseOptions are not supported for this platform.',
+ );
+ }
+ }
+
+ static const FirebaseOptions web = FirebaseOptions(
+ apiKey: 'AIzaSyDfpYR_--oRlGdTjLnAZY6z3RLh3LLz5gk',
+ appId: '1:338042609701:web:5f0a245f364e7604371fcc',
+ messagingSenderId: '338042609701',
+ projectId: 'hackon-58b23',
+ authDomain: 'hackon-58b23.firebaseapp.com',
+ storageBucket: 'hackon-58b23.firebasestorage.app',
+ measurementId: 'G-TBT47P2HX5',
+ );
+
+ static const FirebaseOptions android = FirebaseOptions(
+ apiKey: 'AIzaSyBuT70SbADLeg92ll8keCySI8I4eYyqyLw',
+ appId: '1:338042609701:android:0f0f92d7f895794f371fcc',
+ messagingSenderId: '338042609701',
+ projectId: 'hackon-58b23',
+ storageBucket: 'hackon-58b23.firebasestorage.app',
+ );
+
+ static const FirebaseOptions ios = FirebaseOptions(
+ apiKey: 'AIzaSyAB7czRjHfrkHEjiDOvPLiUz9zS1UGiZbw',
+ appId: '1:338042609701:ios:d1ac418d368d1df5371fcc',
+ messagingSenderId: '338042609701',
+ projectId: 'hackon-58b23',
+ storageBucket: 'hackon-58b23.firebasestorage.app',
+ iosBundleId: 'com.example.aplicacionHack',
+ );
+
+ static const FirebaseOptions macos = FirebaseOptions(
+ apiKey: 'AIzaSyAB7czRjHfrkHEjiDOvPLiUz9zS1UGiZbw',
+ appId: '1:338042609701:ios:d1ac418d368d1df5371fcc',
+ messagingSenderId: '338042609701',
+ projectId: 'hackon-58b23',
+ storageBucket: 'hackon-58b23.firebasestorage.app',
+ iosBundleId: 'com.example.aplicacionHack',
+ );
+
+ static const FirebaseOptions windows = FirebaseOptions(
+ apiKey: 'AIzaSyDfpYR_--oRlGdTjLnAZY6z3RLh3LLz5gk',
+ appId: '1:338042609701:web:b01c28c419fddd25371fcc',
+ messagingSenderId: '338042609701',
+ projectId: 'hackon-58b23',
+ authDomain: 'hackon-58b23.firebaseapp.com',
+ storageBucket: 'hackon-58b23.firebasestorage.app',
+ measurementId: 'G-DYGG6KBJ6C',
+ );
+}
diff --git a/HackOnLinces_app/aplicacion_hack/lib/main.dart b/HackOnLinces_app/aplicacion_hack/lib/main.dart
new file mode 100644
index 0000000..05b76a8
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/lib/main.dart
@@ -0,0 +1,114 @@
+// ================================================================
+// main.dart — EcoTrack
+// Sistema de Notificacion Privada de Recoleccion de Residuos
+// ================================================================
+
+import 'package:flutter/material.dart';
+import 'package:firebase_core/firebase_core.dart';
+import 'package:firebase_messaging/firebase_messaging.dart';
+import 'screens/login_screen.dart';
+import 'screens/home_screen.dart';
+import 'screens/route_list_screen.dart';
+import 'screens/info_screen.dart';
+import 'screens/analytics_screen.dart';
+import 'screens/reporte_screen.dart';
+import 'screens/mapa_rutas_screen.dart';
+import 'firebase_options.dart';
+import 'package:flutter_local_notifications/flutter_local_notifications.dart';
+
+const AndroidNotificationChannel _canal = AndroidNotificationChannel(
+ 'ecotrack_canal',
+ 'EcoTrack Notificaciones',
+ description: 'Notificaciones del camión de basura',
+ importance: Importance.high,
+);
+
+final FlutterLocalNotificationsPlugin _localNotifications =
+ FlutterLocalNotificationsPlugin();
+
+@pragma('vm:entry-point')
+Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
+ await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
+}
+
+void main() async {
+ WidgetsFlutterBinding.ensureInitialized();
+
+ await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
+ FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
+
+ // DESPUÉS
+ final androidPlugin =
+ _localNotifications.resolvePlatformSpecificImplementation<
+ AndroidFlutterLocalNotificationsPlugin>();
+ await androidPlugin?.createNotificationChannel(_canal);
+
+ await _localNotifications.initialize(
+ const InitializationSettings(
+ android: AndroidInitializationSettings('@mipmap/ic_launcher'),
+ ),
+ );
+
+ // ← EL QUE TE FALTABA
+ FirebaseMessaging.onMessage.listen((RemoteMessage message) {
+ final notification = message.notification;
+ if (notification == null) return;
+ _localNotifications.show(
+ notification.hashCode,
+ notification.title,
+ notification.body,
+ NotificationDetails(
+ android: AndroidNotificationDetails(
+ _canal.id,
+ _canal.name,
+ channelDescription: _canal.description,
+ importance: Importance.high,
+ priority: Priority.high,
+ icon: '@mipmap/ic_launcher',
+ ),
+ ),
+ );
+ });
+
+ await FirebaseMessaging.instance.requestPermission(
+ alert: true,
+ badge: true,
+ sound: true,
+ );
+
+ runApp(const EcoTrackApp());
+}
+
+class EcoTrackApp extends StatelessWidget {
+ const EcoTrackApp({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'EcoTrack',
+ debugShowCheckedModeBanner: false,
+ theme: ThemeData(
+ colorScheme: ColorScheme.fromSeed(
+ seedColor: const Color(0xFF2E7D32),
+ brightness: Brightness.light,
+ ),
+ useMaterial3: true,
+ fontFamily: 'Roboto',
+ appBarTheme: const AppBarTheme(
+ centerTitle: false,
+ elevation: 0,
+ ),
+ ),
+ initialRoute: '/',
+ routes: {
+ '/': (context) => const LoginScreen(),
+ '/home': (context) => const HomeScreen(),
+ '/routes': (context) => const RouteListScreen(),
+ '/info': (context) => const InfoScreen(),
+ '/analytics': (context) => const AnalyticsScreen(),
+ '/reporte': (context) => const ReporteScreen(),
+ '/mapa': (context) => const MapaRutasScreen(),
+ },
+ );
+ }
+}
diff --git a/HackOnLinces_app/aplicacion_hack/lib/screens/analytics_screen.dart b/HackOnLinces_app/aplicacion_hack/lib/screens/analytics_screen.dart
new file mode 100644
index 0000000..3e31487
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/lib/screens/analytics_screen.dart
@@ -0,0 +1,1092 @@
+// ================================================================
+// lib/screens/analytics_screen.dart
+// Dashboard de Reportes y Análisis Predictivo
+// ================================================================
+//
+// DEPENDENCIAS (agregar en pubspec.yaml):
+// fl_chart: ^0.69.0
+//
+// NAVEGAR DESDE main.dart:
+// '/analytics': (context) => const AnalyticsScreen()
+//
+// SECCIONES:
+// 1. Resumen ejecutivo (KPIs principales)
+// 2. Días con más residuos (BarChart)
+// 3. Zonas críticas (mapa de calor horizontal)
+// 4. Predicción próxima semana (LineChart)
+// 5. Recomendaciones de logística
+// ================================================================
+
+import 'dart:convert';
+import 'package:flutter/material.dart';
+import 'package:fl_chart/fl_chart.dart';
+import 'package:http/http.dart' as http;
+
+// ----------------------------------------------------------------
+// MODELOS
+// ----------------------------------------------------------------
+
+class ResumenReporte {
+ final double totalKg;
+ final double promedioKgDia;
+ final int coloniasAnalizadas;
+ final int zonasCriticas;
+ final String diaPico;
+ final String coloniaMayorVolumen;
+
+ ResumenReporte({
+ required this.totalKg,
+ required this.promedioKgDia,
+ required this.coloniasAnalizadas,
+ required this.zonasCriticas,
+ required this.diaPico,
+ required this.coloniaMayorVolumen,
+ });
+
+ factory ResumenReporte.fromJson(Map j) => ResumenReporte(
+ totalKg: (j['total_kg_recolectados'] as num).toDouble(),
+ promedioKgDia: (j['promedio_kg_dia'] as num).toDouble(),
+ coloniasAnalizadas: j['colonias_analizadas'],
+ zonasCriticas: j['zonas_criticas'],
+ diaPico: j['dia_pico'],
+ coloniaMayorVolumen: j['colonia_mayor_volumen'],
+ );
+}
+
+class DiaSemana {
+ final String dia;
+ final double promedioKg;
+ final double promedioIncidencias;
+
+ DiaSemana({
+ required this.dia,
+ required this.promedioKg,
+ required this.promedioIncidencias,
+ });
+
+ factory DiaSemana.fromJson(Map j) => DiaSemana(
+ dia: j['dia'],
+ promedioKg: (j['promedio_kg'] as num).toDouble(),
+ promedioIncidencias: (j['promedio_incidencias'] as num).toDouble(),
+ );
+}
+
+class ZonaCritica {
+ final String colonia;
+ final String rutaId;
+ final double promedioKgDia;
+ final double promedioIncidencias;
+ final double indiceCriticidad;
+ final String nivelCriticidad;
+
+ ZonaCritica({
+ required this.colonia,
+ required this.rutaId,
+ required this.promedioKgDia,
+ required this.promedioIncidencias,
+ required this.indiceCriticidad,
+ required this.nivelCriticidad,
+ });
+
+ factory ZonaCritica.fromJson(Map j) => ZonaCritica(
+ colonia: j['colonia'],
+ rutaId: j['ruta_id'],
+ promedioKgDia: (j['promedio_kg_dia'] as num).toDouble(),
+ promedioIncidencias: (j['promedio_incidencias_dia'] as num).toDouble(),
+ indiceCriticidad: (j['indice_criticidad'] as num).toDouble(),
+ nivelCriticidad: j['nivel_criticidad'],
+ );
+}
+
+class DiaPred {
+ final String fecha;
+ final String dia;
+ final double volumenKg;
+ final String confianza;
+
+ DiaPred({
+ required this.fecha,
+ required this.dia,
+ required this.volumenKg,
+ required this.confianza,
+ });
+
+ factory DiaPred.fromJson(Map j) => DiaPred(
+ fecha: j['fecha'],
+ dia: j['dia'],
+ volumenKg: (j['volumen_predicho_kg'] as num).toDouble(),
+ confianza: j['confianza'],
+ );
+}
+
+class PrediccionColonia {
+ final String colonia;
+ final String tendencia;
+ final double pendienteDiaria;
+ final List dias;
+ final double totalPredichoKg;
+
+ PrediccionColonia({
+ required this.colonia,
+ required this.tendencia,
+ required this.pendienteDiaria,
+ required this.dias,
+ required this.totalPredichoKg,
+ });
+
+ factory PrediccionColonia.fromJson(Map j) => PrediccionColonia(
+ colonia: j['colonia'],
+ tendencia: j['tendencia'],
+ pendienteDiaria: (j['pendiente_diaria_kg'] as num).toDouble(),
+ dias: (j['prediccion_7dias'] as List).map((d) => DiaPred.fromJson(d)).toList(),
+ totalPredichoKg: (j['volumen_predicho_total_kg'] as num).toDouble(),
+ );
+}
+
+class Recomendacion {
+ final String tipo;
+ final String prioridad;
+ final String emoji;
+ final String titulo;
+ final String descripcion;
+ final String? rutaAfectada;
+
+ Recomendacion({
+ required this.tipo,
+ required this.prioridad,
+ required this.emoji,
+ required this.titulo,
+ required this.descripcion,
+ this.rutaAfectada,
+ });
+
+ factory Recomendacion.fromJson(Map j) => Recomendacion(
+ tipo: j['tipo'],
+ prioridad: j['prioridad'],
+ emoji: j['emoji'],
+ titulo: j['titulo'],
+ descripcion: j['descripcion'],
+ rutaAfectada: j['ruta_afectada'],
+ );
+}
+
+// ================================================================
+// PANTALLA PRINCIPAL
+// ================================================================
+class AnalyticsScreen extends StatefulWidget {
+ const AnalyticsScreen({super.key});
+
+ @override
+ State createState() => _AnalyticsScreenState();
+}
+
+class _AnalyticsScreenState extends State
+ with SingleTickerProviderStateMixin {
+ static const _baseUrl = 'http://192.168.198.55:8000';
+
+ bool _cargando = true;
+ String? _error;
+ late TabController _tabController;
+
+ ResumenReporte? _resumen;
+ List _diasSemana = [];
+ List _zonas = [];
+ List _predicciones = [];
+ List _recomendaciones = [];
+
+ // Colonia seleccionada para el detalle de predicción
+ int _coloniaIdx = 0;
+
+ @override
+ void initState() {
+ super.initState();
+ _tabController = TabController(length: 4, vsync: this);
+ _cargarReporte();
+ }
+
+ @override
+ void dispose() {
+ _tabController.dispose();
+ super.dispose();
+ }
+
+ Future _cargarReporte() async {
+ setState(() { _cargando = true; _error = null; });
+ try {
+ final resp = await http
+ .get(Uri.parse('$_baseUrl/api/analytics/reporte-real'))
+ .timeout(const Duration(seconds: 20));
+
+ if (resp.statusCode != 200) throw Exception('Error ${resp.statusCode}');
+ final data = json.decode(resp.body);
+
+ setState(() {
+ _resumen = ResumenReporte.fromJson(data['resumen']);
+ _diasSemana = (data['dias_semana'] as List)
+ .map((d) => DiaSemana.fromJson(d))
+ .toList();
+ _zonas = (data['zonas_criticas'] as List)
+ .map((z) => ZonaCritica.fromJson(z))
+ .toList();
+ _predicciones = (data['prediccion_proxima_semana'] as List)
+ .map((p) => PrediccionColonia.fromJson(p))
+ .toList();
+ _recomendaciones = (data['recomendaciones'] as List)
+ .map((r) => Recomendacion.fromJson(r))
+ .toList();
+ _cargando = false;
+ });
+ } catch (e) {
+ setState(() { _error = e.toString(); _cargando = false; });
+ }
+ }
+
+ // ================================================================
+ // BUILD
+ // ================================================================
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: const Color(0xFFF3F6F3),
+ appBar: AppBar(
+ title: const Text('Análisis y Reportes'),
+ backgroundColor: const Color(0xFF1B5E20),
+ foregroundColor: Colors.white,
+ actions: [
+ IconButton(icon: const Icon(Icons.refresh), onPressed: _cargarReporte),
+ ],
+ bottom: _cargando || _error != null
+ ? null
+ : TabBar(
+ controller: _tabController,
+ indicatorColor: Colors.white,
+ labelColor: Colors.white,
+ unselectedLabelColor: Colors.white60,
+ isScrollable: true,
+ tabs: const [
+ Tab(text: 'Resumen'),
+ Tab(text: 'Días pico'),
+ Tab(text: 'Zonas'),
+ Tab(text: 'Predicción'),
+ ],
+ ),
+ ),
+ body: _cargando
+ ? _buildCargando()
+ : _error != null
+ ? _buildError()
+ : TabBarView(
+ controller: _tabController,
+ children: [
+ _buildResumen(),
+ _buildDiasPico(),
+ _buildZonas(),
+ _buildPrediccion(),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildCargando() => const Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ CircularProgressIndicator(color: Color(0xFF2E7D32)),
+ SizedBox(height: 16),
+ Text('Calculando análisis predictivo...', style: TextStyle(color: Colors.grey)),
+ SizedBox(height: 8),
+ Text('Procesando 90 días de datos', style: TextStyle(color: Colors.grey, fontSize: 12)),
+ ],
+ ),
+ );
+
+ Widget _buildError() => Center(
+ child: Padding(
+ padding: const EdgeInsets.all(32),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(Icons.bar_chart, size: 72, color: Colors.grey),
+ const SizedBox(height: 16),
+ const Text('No se pudo cargar el reporte',
+ style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
+ const SizedBox(height: 8),
+ Text('Verifica que el backend esté corriendo',
+ style: TextStyle(color: Colors.grey.shade600)),
+ const SizedBox(height: 24),
+ ElevatedButton.icon(
+ onPressed: _cargarReporte,
+ icon: const Icon(Icons.refresh),
+ label: const Text('Reintentar'),
+ style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF2E7D32)),
+ ),
+ ],
+ ),
+ ),
+ );
+
+ // ================================================================
+ // TAB 1: RESUMEN EJECUTIVO
+ // ================================================================
+ Widget _buildResumen() {
+ if (_resumen == null) return const SizedBox();
+ return ListView(
+ padding: const EdgeInsets.all(16),
+ children: [
+ // KPIs en grid 2x2
+ GridView.count(
+ crossAxisCount: 2,
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ crossAxisSpacing: 12,
+ mainAxisSpacing: 12,
+ childAspectRatio: 1.4,
+ children: [
+ _KpiCard(
+ emoji: '🗑️',
+ valor: '${(_resumen!.totalKg / 1000).toStringAsFixed(1)} t',
+ etiqueta: 'Total recolectado\n(90 días)',
+ color: const Color(0xFF1565C0),
+ ),
+ _KpiCard(
+ emoji: '📅',
+ valor: '${_resumen!.promedioKgDia.toStringAsFixed(0)} kg',
+ etiqueta: 'Promedio\npor día',
+ color: const Color(0xFF2E7D32),
+ ),
+ _KpiCard(
+ emoji: '⚠️',
+ valor: '${_resumen!.zonasCriticas}',
+ etiqueta: 'Zonas\ncríticas',
+ color: const Color(0xFFE65100),
+ ),
+ _KpiCard(
+ emoji: '📆',
+ valor: _resumen!.diaPico,
+ etiqueta: 'Día con más\nresiduos',
+ color: const Color(0xFF6A1B9A),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 20),
+
+ // Datos destacados
+ _SeccionTitulo('Hallazgos principales'),
+ _InfoCard(
+ emoji: '🏆',
+ titulo: 'Colonia con mayor volumen',
+ subtitulo: _resumen!.coloniaMayorVolumen,
+ color: const Color(0xFFC62828),
+ ),
+ const SizedBox(height: 8),
+ _InfoCard(
+ emoji: '📈',
+ titulo: 'Día crítico de la semana',
+ subtitulo: '${_resumen!.diaPico} — mayor acumulación tras el fin de semana',
+ color: const Color(0xFF1565C0),
+ ),
+
+ const SizedBox(height: 20),
+ _SeccionTitulo('Recomendaciones'),
+ ..._recomendaciones.map((r) => _TarjetaRecomendacion(rec: r)),
+ ],
+ );
+ }
+
+ // ================================================================
+ // TAB 2: DÍAS PICO — BarChart
+ // ================================================================
+ Widget _buildDiasPico() {
+ // Ordenar por día de la semana (lun-dom)
+ final diasOrdenados = List.from(_diasSemana);
+ const orden = ['Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'];
+ diasOrdenados.sort((a, b) => orden.indexOf(a.dia).compareTo(orden.indexOf(b.dia)));
+
+ final maxKg = diasOrdenados.map((d) => d.promedioKg).reduce((a, b) => a > b ? a : b);
+
+ return ListView(
+ padding: const EdgeInsets.all(16),
+ children: [
+ _SeccionTitulo('Promedio de residuos por día'),
+ const Text(
+ 'Basado en 90 días de histórico. El lunes concentra mayor volumen por acumulación del fin de semana.',
+ style: TextStyle(color: Colors.grey, fontSize: 13),
+ ),
+ const SizedBox(height: 20),
+
+ // Gráfica de barras
+ Container(
+ height: 260,
+ padding: const EdgeInsets.fromLTRB(8, 16, 16, 8),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(16),
+ boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.06), blurRadius: 8)],
+ ),
+ child: BarChart(
+ BarChartData(
+ maxY: maxKg * 1.15,
+ barTouchData: BarTouchData(
+ touchTooltipData: BarTouchTooltipData(
+ getTooltipItem: (group, groupIndex, rod, rodIndex) {
+ final d = diasOrdenados[group.x];
+ return BarTooltipItem(
+ '${d.dia}\n${rod.toY.toStringAsFixed(0)} kg',
+ const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12),
+ );
+ },
+ ),
+ ),
+ titlesData: FlTitlesData(
+ bottomTitles: AxisTitles(
+ sideTitles: SideTitles(
+ showTitles: true,
+ getTitlesWidget: (val, meta) {
+ final abrevs = ['L', 'Ma', 'Mi', 'J', 'V', 'S', 'D'];
+ final idx = val.toInt();
+ if (idx < 0 || idx >= abrevs.length) return const SizedBox();
+ return Text(abrevs[idx],
+ style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600));
+ },
+ ),
+ ),
+ leftTitles: AxisTitles(
+ sideTitles: SideTitles(
+ showTitles: true,
+ reservedSize: 52,
+ getTitlesWidget: (val, meta) => Text(
+ '${(val / 1000).toStringAsFixed(1)}t',
+ style: const TextStyle(fontSize: 10, color: Colors.grey),
+ ),
+ ),
+ ),
+ topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
+ rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
+ ),
+ gridData: FlGridData(
+ drawVerticalLine: false,
+ horizontalInterval: maxKg / 4,
+ getDrawingHorizontalLine: (v) => FlLine(color: Colors.grey.shade200, strokeWidth: 1),
+ ),
+ borderData: FlBorderData(show: false),
+ barGroups: List.generate(diasOrdenados.length, (i) {
+ final d = diasOrdenados[i];
+ final esPico = d.dia == _resumen?.diaPico;
+ return BarChartGroupData(
+ x: i,
+ barRods: [
+ BarChartRodData(
+ toY: d.promedioKg,
+ color: esPico ? const Color(0xFFE65100) : const Color(0xFF2E7D32),
+ width: 28,
+ borderRadius: const BorderRadius.vertical(top: Radius.circular(6)),
+ ),
+ ],
+ );
+ }),
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 20),
+ _SeccionTitulo('Incidencias por día'),
+ const Text(
+ 'Reportes de basura tirada fuera de horario.',
+ style: TextStyle(color: Colors.grey, fontSize: 13),
+ ),
+ const SizedBox(height: 12),
+
+ // Tabla de incidencias
+ Container(
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(12),
+ boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 6)],
+ ),
+ child: Column(
+ children: diasOrdenados.asMap().entries.map((entry) {
+ final i = entry.key;
+ final d = entry.value;
+ final maxInc = diasOrdenados
+ .map((x) => x.promedioIncidencias)
+ .reduce((a, b) => a > b ? a : b);
+ final pct = maxInc > 0 ? d.promedioIncidencias / maxInc : 0.0;
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
+ decoration: BoxDecoration(
+ border: i < diasOrdenados.length - 1
+ ? Border(bottom: BorderSide(color: Colors.grey.shade100))
+ : null,
+ ),
+ child: Row(
+ children: [
+ SizedBox(width: 60, child: Text(d.dia.substring(0, 3),
+ style: const TextStyle(fontWeight: FontWeight.w600))),
+ Expanded(
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(4),
+ child: LinearProgressIndicator(
+ value: pct,
+ backgroundColor: Colors.grey.shade100,
+ valueColor: AlwaysStoppedAnimation(
+ pct > 0.7 ? const Color(0xFFC62828) : const Color(0xFF2E7D32)),
+ minHeight: 10,
+ ),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Text('${d.promedioIncidencias.toStringAsFixed(1)}/día',
+ style: const TextStyle(fontSize: 12, color: Colors.grey)),
+ ],
+ ),
+ );
+ }).toList(),
+ ),
+ ),
+ ],
+ );
+ }
+
+ // ================================================================
+ // TAB 3: ZONAS CRÍTICAS
+ // ================================================================
+ Widget _buildZonas() {
+ return ListView(
+ padding: const EdgeInsets.all(16),
+ children: [
+ _SeccionTitulo('Mapa de calor por colonia'),
+ const Text(
+ 'Índice de criticidad basado en volumen e incidencias (0–100).',
+ style: TextStyle(color: Colors.grey, fontSize: 13),
+ ),
+ const SizedBox(height: 16),
+ ..._zonas.map((z) => _TarjetaZona(zona: z)),
+ const SizedBox(height: 16),
+ _SeccionTitulo('Tiempo promedio de recolección'),
+ Container(
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(12),
+ boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 6)],
+ ),
+ child: Column(
+ children: _zonas.asMap().entries.map((entry) {
+ final i = entry.key;
+ final z = entry.value;
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ decoration: BoxDecoration(
+ border: i < _zonas.length - 1
+ ? Border(bottom: BorderSide(color: Colors.grey.shade100))
+ : null,
+ ),
+ child: Row(
+ children: [
+ Expanded(child: Text(z.colonia,
+ style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 13))),
+ Text('${z.promedioKgDia.toStringAsFixed(0)} kg/día',
+ style: TextStyle(color: Colors.grey.shade600, fontSize: 12)),
+ ],
+ ),
+ );
+ }).toList(),
+ ),
+ ),
+ ],
+ );
+ }
+
+ // ================================================================
+ // TAB 4: PREDICCIÓN — LineChart
+ // ================================================================
+ Widget _buildPrediccion() {
+ if (_predicciones.isEmpty) return const Center(child: Text('Sin predicciones'));
+ final pred = _predicciones[_coloniaIdx];
+
+ return ListView(
+ padding: const EdgeInsets.all(16),
+ children: [
+ _SeccionTitulo('Predicción próximos 7 días'),
+ const Text(
+ 'Regresión lineal sobre 90 días históricos, ajustada por estacionalidad semanal.',
+ style: TextStyle(color: Colors.grey, fontSize: 13),
+ ),
+ const SizedBox(height: 12),
+
+ // Selector de colonia
+ SizedBox(
+ height: 36,
+ child: ListView.builder(
+ scrollDirection: Axis.horizontal,
+ itemCount: _predicciones.length,
+ itemBuilder: (context, i) {
+ final p = _predicciones[i];
+ final sel = i == _coloniaIdx;
+ return GestureDetector(
+ onTap: () => setState(() => _coloniaIdx = i),
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 150),
+ margin: const EdgeInsets.only(right: 8),
+ padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
+ decoration: BoxDecoration(
+ color: sel ? const Color(0xFF2E7D32) : Colors.white,
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(
+ color: sel ? const Color(0xFF2E7D32) : Colors.grey.shade300,
+ ),
+ ),
+ child: Text(
+ p.colonia.split(' ').first,
+ style: TextStyle(
+ color: sel ? Colors.white : Colors.grey.shade700,
+ fontWeight: FontWeight.w600,
+ fontSize: 12,
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+
+ const SizedBox(height: 16),
+
+ // Badge de tendencia
+ Row(
+ children: [
+ _TendenciaBadge(tendencia: pred.tendencia, pendiente: pred.pendienteDiaria),
+ const SizedBox(width: 8),
+ Text('Total predicho: ${pred.totalPredichoKg.toStringAsFixed(0)} kg',
+ style: TextStyle(color: Colors.grey.shade600, fontSize: 13)),
+ ],
+ ),
+
+ const SizedBox(height: 16),
+
+ // Gráfica de línea
+ Container(
+ height: 240,
+ padding: const EdgeInsets.fromLTRB(8, 16, 16, 8),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(16),
+ boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.06), blurRadius: 8)],
+ ),
+ child: LineChart(
+ LineChartData(
+ minY: 0,
+ lineTouchData: LineTouchData(
+ touchTooltipData: LineTouchTooltipData(
+ getTooltipItems: (spots) => spots.map((s) {
+ final d = pred.dias[s.x.toInt()];
+ return LineTooltipItem(
+ '${d.dia}\n${s.y.toStringAsFixed(0)} kg',
+ const TextStyle(color: Colors.white, fontSize: 12),
+ );
+ }).toList(),
+ ),
+ ),
+ titlesData: FlTitlesData(
+ bottomTitles: AxisTitles(
+ sideTitles: SideTitles(
+ showTitles: true,
+ getTitlesWidget: (val, meta) {
+ final idx = val.toInt();
+ if (idx < 0 || idx >= pred.dias.length) return const SizedBox();
+ return Text(
+ pred.dias[idx].dia.substring(0, 2),
+ style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w600),
+ );
+ },
+ ),
+ ),
+ leftTitles: AxisTitles(
+ sideTitles: SideTitles(
+ showTitles: true,
+ reservedSize: 52,
+ getTitlesWidget: (val, meta) => Text(
+ '${(val / 1000).toStringAsFixed(1)}t',
+ style: const TextStyle(fontSize: 10, color: Colors.grey),
+ ),
+ ),
+ ),
+ topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
+ rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
+ ),
+ gridData: FlGridData(
+ drawVerticalLine: false,
+ getDrawingHorizontalLine: (v) =>
+ FlLine(color: Colors.grey.shade100, strokeWidth: 1),
+ ),
+ borderData: FlBorderData(show: false),
+ lineBarsData: [
+ LineChartBarData(
+ spots: pred.dias.asMap().entries.map((e) =>
+ FlSpot(e.key.toDouble(), e.value.volumenKg)).toList(),
+ isCurved: true,
+ color: const Color(0xFF2E7D32),
+ barWidth: 3,
+ dotData: FlDotData(
+ getDotPainter: (spot, pct, bar, idx) => FlDotCirclePainter(
+ radius: 5,
+ color: Colors.white,
+ strokeWidth: 2.5,
+ strokeColor: const Color(0xFF2E7D32),
+ ),
+ ),
+ belowBarData: BarAreaData(
+ show: true,
+ color: const Color(0xFF2E7D32).withValues(alpha: 0.1),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 16),
+
+ // Tabla de predicciones
+ Container(
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(12),
+ boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 6)],
+ ),
+ child: Column(
+ children: pred.dias.asMap().entries.map((entry) {
+ final i = entry.key;
+ final d = entry.value;
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
+ decoration: BoxDecoration(
+ border: i < pred.dias.length - 1
+ ? Border(bottom: BorderSide(color: Colors.grey.shade100))
+ : null,
+ ),
+ child: Row(
+ children: [
+ SizedBox(
+ width: 32,
+ child: Text(d.dia.substring(0, 2),
+ style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
+ ),
+ const SizedBox(width: 4),
+ Text(d.fecha, style: TextStyle(color: Colors.grey.shade500, fontSize: 12)),
+ const Spacer(),
+ Text('${d.volumenKg.toStringAsFixed(0)} kg',
+ style: const TextStyle(fontWeight: FontWeight.w600)),
+ const SizedBox(width: 8),
+ _ConfianzaBadge(confianza: d.confianza),
+ ],
+ ),
+ );
+ }).toList(),
+ ),
+ ),
+ ],
+ );
+ }
+}
+
+// ================================================================
+// WIDGETS AUXILIARES
+// ================================================================
+
+class _KpiCard extends StatelessWidget {
+ final String emoji;
+ final String valor;
+ final String etiqueta;
+ final Color color;
+
+ const _KpiCard({
+ required this.emoji,
+ required this.valor,
+ required this.etiqueta,
+ required this.color,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ padding: const EdgeInsets.all(14),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(16),
+ boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.06), blurRadius: 8)],
+ border: Border(left: BorderSide(color: color, width: 4)),
+ ),
+ child: ClipRect(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(emoji, style: const TextStyle(fontSize: 20)),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(valor,
+ style: TextStyle(
+ fontSize: 18, fontWeight: FontWeight.bold, color: color)),
+ Text(etiqueta,
+ style: TextStyle(fontSize: 10, color: Colors.grey.shade600, height: 1.2)),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _InfoCard extends StatelessWidget {
+ final String emoji;
+ final String titulo;
+ final String subtitulo;
+ final Color color;
+
+ const _InfoCard({
+ required this.emoji,
+ required this.titulo,
+ required this.subtitulo,
+ required this.color,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(14),
+ margin: const EdgeInsets.only(bottom: 8),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(12),
+ boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 6)],
+ ),
+ child: Row(
+ children: [
+ Text(emoji, style: const TextStyle(fontSize: 28)),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(titulo,
+ style: TextStyle(fontWeight: FontWeight.bold, color: color, fontSize: 13)),
+ const SizedBox(height: 2),
+ Text(subtitulo, style: TextStyle(color: Colors.grey.shade600, fontSize: 13)),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _SeccionTitulo extends StatelessWidget {
+ final String titulo;
+ const _SeccionTitulo(this.titulo);
+
+ @override
+ Widget build(BuildContext context) => Padding(
+ padding: const EdgeInsets.only(bottom: 10),
+ child: Text(titulo,
+ style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFF1A1A1A))),
+ );
+}
+
+class _TarjetaZona extends StatelessWidget {
+ final ZonaCritica zona;
+ const _TarjetaZona({required this.zona});
+
+ Color get _color {
+ switch (zona.nivelCriticidad) {
+ case 'CRÍTICO': return const Color(0xFFC62828);
+ case 'ALTO': return const Color(0xFFE65100);
+ case 'MEDIO': return const Color(0xFFF57F17);
+ default: return const Color(0xFF2E7D32);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ margin: const EdgeInsets.only(bottom: 10),
+ padding: const EdgeInsets.all(14),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(12),
+ boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 6)],
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Expanded(
+ child: Text(zona.colonia,
+ style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
+ ),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
+ decoration: BoxDecoration(
+ color: _color.withValues(alpha: 0.12),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Text(zona.nivelCriticidad,
+ style: TextStyle(color: _color, fontWeight: FontWeight.bold, fontSize: 11)),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ // Barra de criticidad
+ ClipRRect(
+ borderRadius: BorderRadius.circular(4),
+ child: LinearProgressIndicator(
+ value: zona.indiceCriticidad / 100,
+ backgroundColor: Colors.grey.shade100,
+ valueColor: AlwaysStoppedAnimation(_color),
+ minHeight: 8,
+ ),
+ ),
+ const SizedBox(height: 6),
+ Row(
+ children: [
+ Text('${zona.promedioKgDia.toStringAsFixed(0)} kg/día',
+ style: TextStyle(color: Colors.grey.shade600, fontSize: 12)),
+ const Spacer(),
+ Text('Índice: ${zona.indiceCriticidad.toStringAsFixed(0)}/100',
+ style: TextStyle(color: _color, fontWeight: FontWeight.w600, fontSize: 12)),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _TarjetaRecomendacion extends StatelessWidget {
+ final Recomendacion rec;
+ const _TarjetaRecomendacion({required this.rec});
+
+ Color get _colorPrioridad {
+ switch (rec.prioridad) {
+ case 'ALTA': return const Color(0xFFC62828);
+ case 'MEDIA': return const Color(0xFFE65100);
+ default: return const Color(0xFF2E7D32);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ width: double.infinity,
+ margin: const EdgeInsets.only(bottom: 10),
+ padding: const EdgeInsets.all(14),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(12),
+ border: Border(left: BorderSide(color: _colorPrioridad, width: 4)),
+ boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 6)],
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Text(rec.emoji, style: const TextStyle(fontSize: 20)),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(rec.titulo,
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ color: _colorPrioridad,
+ fontSize: 14)),
+ ),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
+ decoration: BoxDecoration(
+ color: _colorPrioridad.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(6),
+ ),
+ child: Text(rec.prioridad,
+ style: TextStyle(color: _colorPrioridad, fontSize: 10, fontWeight: FontWeight.bold)),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Text(rec.descripcion,
+ style: TextStyle(color: Colors.grey.shade600, fontSize: 13, height: 1.4)),
+ if (rec.rutaAfectada != null) ...[
+ const SizedBox(height: 6),
+ Text('Ruta: ${rec.rutaAfectada}',
+ style: TextStyle(color: _colorPrioridad, fontSize: 12, fontWeight: FontWeight.w600)),
+ ],
+ ],
+ ),
+ );
+ }
+}
+
+class _TendenciaBadge extends StatelessWidget {
+ final String tendencia;
+ final double pendiente;
+ const _TendenciaBadge({required this.tendencia, required this.pendiente});
+
+ @override
+ Widget build(BuildContext context) {
+ final color = tendencia == 'CRECIENTE'
+ ? const Color(0xFFC62828)
+ : tendencia == 'DECRECIENTE'
+ ? const Color(0xFF2E7D32)
+ : const Color(0xFF1565C0);
+ final icon = tendencia == 'CRECIENTE'
+ ? Icons.trending_up
+ : tendencia == 'DECRECIENTE'
+ ? Icons.trending_down
+ : Icons.trending_flat;
+
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(color: color.withValues(alpha: 0.3)),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(icon, color: color, size: 16),
+ const SizedBox(width: 4),
+ Text(tendencia,
+ style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 12)),
+ ],
+ ),
+ );
+ }
+}
+
+class _ConfianzaBadge extends StatelessWidget {
+ final String confianza;
+ const _ConfianzaBadge({required this.confianza});
+
+ @override
+ Widget build(BuildContext context) {
+ final color = confianza == 'Alta'
+ ? const Color(0xFF2E7D32)
+ : confianza == 'Media'
+ ? const Color(0xFFE65100)
+ : Colors.grey;
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(6),
+ ),
+ child: Text(confianza,
+ style: TextStyle(color: color, fontSize: 10, fontWeight: FontWeight.w600)),
+ );
+ }
+}
diff --git a/HackOnLinces_app/aplicacion_hack/lib/screens/home_screen.dart b/HackOnLinces_app/aplicacion_hack/lib/screens/home_screen.dart
new file mode 100644
index 0000000..090f95a
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/lib/screens/home_screen.dart
@@ -0,0 +1,927 @@
+// ================================================================
+// lib/screens/home_screen.dart (v3 — rediseño visual)
+// ================================================================
+
+import 'dart:async';
+import 'dart:ui';
+import 'package:flutter/material.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+import 'package:firebase_messaging/firebase_messaging.dart';
+import '../services/api_service.dart';
+
+class HomeScreen extends StatefulWidget {
+ const HomeScreen({super.key});
+
+ @override
+ State createState() => _HomeScreenState();
+}
+
+class _HomeScreenState extends State with TickerProviderStateMixin {
+ int? _usuarioId;
+ String _nombreUsuario = '';
+ ETAInfo? _etaInfo;
+ List _direcciones = [];
+ List _colonias = [];
+ bool _cargando = true;
+ bool _cargandoColonias = true;
+ String? _error;
+ String? _errorColonias;
+
+ Timer? _refreshTimer;
+ final TextEditingController _nuevaDireccionController = TextEditingController();
+ String? _nuevaColoniaSeleccionada;
+
+ late AnimationController _pulseController;
+ late AnimationController _fadeController;
+ late Animation _pulseAnimation;
+ late Animation _fadeAnimation;
+
+ final ApiService _apiService = ApiService();
+
+ @override
+ void initState() {
+ super.initState();
+ _pulseController = AnimationController(
+ vsync: this,
+ duration: const Duration(seconds: 2),
+ )..repeat(reverse: true);
+ _pulseAnimation = Tween(begin: 1.0, end: 1.06).animate(
+ CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
+ );
+
+ _fadeController = AnimationController(
+ vsync: this,
+ duration: const Duration(milliseconds: 600),
+ );
+ _fadeAnimation = CurvedAnimation(parent: _fadeController, curve: Curves.easeOut);
+ }
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ if (_usuarioId == null) {
+ final args = ModalRoute.of(context)?.settings.arguments;
+ if (args is int) {
+ _usuarioId = args;
+ _inicializar();
+ } else {
+ _cargarDesdeStorage();
+ }
+ }
+ }
+
+ @override
+ void dispose() {
+ _pulseController.dispose();
+ _fadeController.dispose();
+ _refreshTimer?.cancel();
+ _nuevaDireccionController.dispose();
+ super.dispose();
+ }
+
+ Future _inicializar() async {
+ final prefs = await SharedPreferences.getInstance();
+ if (mounted) setState(() => _nombreUsuario = prefs.getString('nombre') ?? '');
+ _cargarETA();
+ _iniciarAutoRefresh();
+ _registrarFCMToken();
+ _cargarUsuario();
+ _cargarColonias();
+ }
+
+ Future _cargarDesdeStorage() async {
+ final prefs = await SharedPreferences.getInstance();
+ final id = prefs.getInt('usuario_id');
+ if (id != null) {
+ _usuarioId = id;
+ _inicializar();
+ } else {
+ if (mounted) Navigator.pushReplacementNamed(context, '/');
+ }
+ }
+
+ Future _cargarETA() async {
+ if (!mounted) return;
+ setState(() { _cargando = true; _error = null; });
+ try {
+ final prefs = await SharedPreferences.getInstance();
+ final usuarioId = prefs.getInt('usuario_id') ?? 1;
+ final etaInfo = await _apiService.obtenerETA(usuarioId);
+ if (mounted) {
+ setState(() { _etaInfo = etaInfo; _cargando = false; });
+ _fadeController.forward(from: 0);
+ }
+ } catch (e) {
+ if (mounted) {
+ setState(() {
+ _etaInfo = ETAInfo(
+ usuarioId: 1,
+ colonia: "Centro",
+ rutaNombre: "Ruta Poniente - Camión #4",
+ rutaStatus: "EN_RUTA",
+ gpsOk: true,
+ etaTexto: "12 minutos aprox.",
+ etaMinutos: 12,
+ mensajePreventivo: "⚠️ El camión está a 3 cuadras. ¡Prepara tus bolsas!",
+ );
+ _cargando = false;
+ });
+ _fadeController.forward(from: 0);
+ }
+ }
+ }
+
+ Future _cargarUsuario() async {
+ if (_usuarioId == null) return;
+ try {
+ final usuario = await _apiService.obtenerUsuario(_usuarioId!);
+ if (mounted) {
+ setState(() {
+ _direcciones = usuario.direcciones;
+ if (usuario.nombre.isNotEmpty) _nombreUsuario = usuario.nombre;
+ });
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setString('nombre', usuario.nombre);
+ }
+ } catch (e) {
+ debugPrint('Error cargando usuario: $e');
+ }
+ }
+
+ Future _cargarColonias() async {
+ try {
+ final colonias = await _apiService.obtenerColonias();
+ if (mounted) setState(() { _colonias = colonias; _cargandoColonias = false; });
+ } catch (e) {
+ if (mounted) {
+ setState(() {
+ _colonias = ['Zona Centro', 'Las Arboledas', 'Trojes', 'San Juanico', 'Los Olivos', 'Rancho Seco', 'Las Insurgentes'];
+ _cargandoColonias = false;
+ _errorColonias = 'Sin conexión. Usando lista local.';
+ });
+ }
+ }
+ }
+
+ void _iniciarAutoRefresh() {
+ _refreshTimer = Timer.periodic(const Duration(seconds: 60), (_) => _cargarETA());
+ }
+
+ Future _registrarFCMToken() async {
+ try {
+ final messaging = FirebaseMessaging.instance;
+ final settings = await messaging.requestPermission(alert: true, sound: true, badge: true);
+ if (settings.authorizationStatus == AuthorizationStatus.authorized) {
+ final token = await messaging.getToken();
+ if (token != null && _usuarioId != null) {
+ await _apiService.registrarFcmToken(_usuarioId!, token);
+ }
+ }
+ FirebaseMessaging.onMessage.listen((RemoteMessage message) {
+ if (message.notification != null && mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(
+ content: Text('🚛 ${message.notification!.body}'),
+ backgroundColor: Colors.green.shade700,
+ duration: const Duration(seconds: 5),
+ behavior: SnackBarBehavior.floating,
+ ));
+ _cargarETA();
+ }
+ });
+ } catch (e) {
+ debugPrint('Error FCM: $e');
+ }
+ }
+
+ // ── DIÁLOGOS ────────────────────────────────────────────────────
+
+ Future _confirmarCerrarSesion() async {
+ final confirmar = await showDialog(
+ context: context,
+ builder: (ctx) => AlertDialog(
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
+ title: const Text('Cerrar sesión'),
+ content: const Text('¿Seguro que quieres cerrar sesión?'),
+ actions: [
+ TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Cancelar')),
+ ElevatedButton(
+ onPressed: () => Navigator.of(ctx).pop(true),
+ style: ElevatedButton.styleFrom(backgroundColor: Colors.red.shade700, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
+ child: const Text('Cerrar sesión', style: TextStyle(color: Colors.white)),
+ ),
+ ],
+ ),
+ );
+ if (confirmar == true) {
+ _refreshTimer?.cancel();
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.clear();
+ if (mounted) Navigator.pushReplacementNamed(context, '/');
+ }
+ }
+
+ Future _mostrarCambioPassword() async {
+ final actualCtrl = TextEditingController();
+ final nuevoCtrl = TextEditingController();
+ bool mostrarActual = false;
+ bool mostrarNuevo = false;
+
+ await showDialog(
+ context: context,
+ builder: (ctx) => StatefulBuilder(
+ builder: (ctx, setS) => AlertDialog(
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
+ title: const Text('Cambiar contraseña'),
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ TextField(
+ controller: actualCtrl,
+ obscureText: !mostrarActual,
+ decoration: InputDecoration(
+ labelText: 'Contraseña actual',
+ prefixIcon: const Icon(Icons.lock_outline),
+ suffixIcon: IconButton(
+ icon: Icon(mostrarActual ? Icons.visibility_off : Icons.visibility),
+ onPressed: () => setS(() => mostrarActual = !mostrarActual),
+ ),
+ border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
+ ),
+ ),
+ const SizedBox(height: 12),
+ TextField(
+ controller: nuevoCtrl,
+ obscureText: !mostrarNuevo,
+ decoration: InputDecoration(
+ labelText: 'Nueva contraseña',
+ hintText: 'Mínimo 6 caracteres',
+ prefixIcon: const Icon(Icons.lock_reset),
+ suffixIcon: IconButton(
+ icon: Icon(mostrarNuevo ? Icons.visibility_off : Icons.visibility),
+ onPressed: () => setS(() => mostrarNuevo = !mostrarNuevo),
+ ),
+ border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
+ ),
+ ),
+ ],
+ ),
+ actions: [
+ TextButton(onPressed: () => Navigator.of(ctx).pop(), child: const Text('Cancelar')),
+ ElevatedButton(
+ onPressed: () async {
+ if (_usuarioId == null) return;
+ if (actualCtrl.text.isEmpty || nuevoCtrl.text.isEmpty) return;
+ if (nuevoCtrl.text.length < 6) {
+ ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Mínimo 6 caracteres.')));
+ return;
+ }
+ try {
+ await _apiService.actualizarPassword(_usuarioId!, actualCtrl.text, nuevoCtrl.text);
+ if (mounted) {
+ Navigator.of(ctx).pop();
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('✅ Contraseña actualizada.'), backgroundColor: Colors.green),
+ );
+ }
+ } catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(e.toString().replaceFirst('Exception: ', '')), backgroundColor: Colors.red.shade700),
+ );
+ }
+ }
+ },
+ child: const Text('Guardar'),
+ ),
+ ],
+ ),
+ ),
+ );
+ actualCtrl.dispose();
+ nuevoCtrl.dispose();
+ }
+
+ Future _agregarDireccion() async {
+ if (_usuarioId == null) return;
+ final messenger = ScaffoldMessenger.of(context);
+ final direccion = _nuevaDireccionController.text.trim();
+ if (_nuevaColoniaSeleccionada == null) {
+ messenger.showSnackBar(const SnackBar(content: Text('Selecciona una colonia.')));
+ return;
+ }
+ if (direccion.isEmpty) {
+ messenger.showSnackBar(const SnackBar(content: Text('Ingresa la dirección.')));
+ return;
+ }
+ try {
+ await _apiService.agregarDireccion(_usuarioId!, _nuevaColoniaSeleccionada!, direccion);
+ _nuevaDireccionController.clear();
+ _nuevaColoniaSeleccionada = null;
+ await _cargarUsuario();
+ await _cargarETA();
+ if (mounted) messenger.showSnackBar(const SnackBar(content: Text('✅ Dirección agregada.')));
+ } catch (e) {
+ if (mounted) messenger.showSnackBar(const SnackBar(content: Text('No se pudo agregar la dirección.')));
+ }
+ }
+
+ Future _mostrarAgregarDireccionDialog() async {
+ if (_cargandoColonias) await _cargarColonias();
+ if (!mounted) return;
+ _nuevaDireccionController.clear();
+ _nuevaColoniaSeleccionada = null;
+
+ await showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
+ title: const Text('Agregar dirección'),
+ content: SingleChildScrollView(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (_errorColonias != null)
+ Padding(
+ padding: const EdgeInsets.only(bottom: 8),
+ child: Text(_errorColonias!, style: TextStyle(color: Colors.orange.shade700, fontSize: 12)),
+ ),
+ TextField(
+ controller: _nuevaDireccionController,
+ decoration: const InputDecoration(labelText: 'Dirección', hintText: 'Calle, número'),
+ ),
+ const SizedBox(height: 16),
+ DropdownButtonFormField(
+ initialValue: _nuevaColoniaSeleccionada,
+ hint: const Text('Selecciona tu colonia'),
+ items: _colonias.map((c) => DropdownMenuItem(value: c, child: Text(c))).toList(),
+ onChanged: (v) => setState(() => _nuevaColoniaSeleccionada = v),
+ ),
+ ],
+ ),
+ ),
+ actions: [
+ TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Cancelar')),
+ ElevatedButton(
+ onPressed: () async {
+ final nav = Navigator.of(context);
+ await _agregarDireccion();
+ if (mounted) nav.pop();
+ },
+ child: const Text('Guardar'),
+ ),
+ ],
+ ),
+ );
+ }
+
+ // ── HELPERS ─────────────────────────────────────────────────────
+
+ Color _colorSegunETA(int eta) {
+ if (eta <= 5) return const Color(0xFFB71C1C);
+ if (eta <= 15) return const Color(0xFFE65100);
+ if (eta <= 30) return const Color(0xFF1B5E20);
+ return const Color(0xFF1A237E);
+ }
+
+ String _emojiSegunETA(int eta) {
+ if (eta <= 5) return '🔴';
+ if (eta <= 15) return '🟡';
+ if (eta <= 30) return '🟢';
+ return '🔵';
+ }
+
+ String _textoEstado(int eta) {
+ if (eta <= 5) return '¡Saca tu basura AHORA!';
+ if (eta <= 15) return 'Prepárate, viene pronto';
+ if (eta <= 30) return 'En camino a tu zona';
+ return 'Aún falta un rato';
+ }
+
+ // ================================================================
+ // BUILD
+ // ================================================================
+
+ @override
+ Widget build(BuildContext context) {
+ final baseColor = _etaInfo != null
+ ? _colorSegunETA(_etaInfo!.etaMinutos)
+ : const Color(0xFF1B5E20);
+
+ return Scaffold(
+ body: AnimatedContainer(
+ duration: const Duration(milliseconds: 800),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.topCenter,
+ end: Alignment.bottomCenter,
+ colors: [baseColor, baseColor.withValues(alpha: 0.85), const Color(0xFF0D1B0F)],
+ stops: const [0.0, 0.5, 1.0],
+ ),
+ ),
+ child: SafeArea(
+ child: _cargando
+ ? _buildCargando()
+ : _error != null
+ ? _buildError()
+ : FadeTransition(opacity: _fadeAnimation, child: _buildContenido()),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildCargando() => const Center(
+ child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
+ CircularProgressIndicator(color: Colors.white, strokeWidth: 2),
+ SizedBox(height: 16),
+ Text('Consultando estado del camión...', style: TextStyle(color: Colors.white60, fontSize: 15)),
+ ]),
+ );
+
+ Widget _buildError() => Center(
+ child: Padding(
+ padding: const EdgeInsets.all(32),
+ child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
+ const Icon(Icons.wifi_off_rounded, size: 72, color: Colors.white38),
+ const SizedBox(height: 16),
+ Text(_error!, textAlign: TextAlign.center, style: const TextStyle(color: Colors.white, fontSize: 17)),
+ const SizedBox(height: 24),
+ ElevatedButton.icon(
+ onPressed: _cargarETA,
+ icon: const Icon(Icons.refresh),
+ label: const Text('Reintentar'),
+ style: ElevatedButton.styleFrom(backgroundColor: Colors.white, foregroundColor: Colors.red.shade700),
+ ),
+ ]),
+ ),
+ );
+
+ Widget _buildContenido() {
+ if (_etaInfo == null) return const SizedBox.shrink();
+ final eta = _etaInfo!;
+
+ return SingleChildScrollView(
+ physics: const BouncingScrollPhysics(),
+ child: Column(
+ children: [
+ // ── HEADER ──────────────────────────────────────────────
+ Padding(
+ padding: const EdgeInsets.fromLTRB(20, 12, 8, 0),
+ child: Row(
+ children: [
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ if (_nombreUsuario.isNotEmpty)
+ Text(
+ 'Hola, ${_nombreUsuario.split(' ').first} 👋',
+ style: const TextStyle(color: Colors.white60, fontSize: 13),
+ ),
+ Row(
+ children: [
+ const Icon(Icons.location_on_rounded, color: Colors.white70, size: 15),
+ const SizedBox(width: 3),
+ Text(
+ eta.colonia,
+ style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 17),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ IconButton(
+ onPressed: () => Navigator.pushNamed(context, '/mapa'),
+ icon: const Icon(Icons.map_rounded, color: Colors.white60),
+ tooltip: 'Ver mapa',
+ ),
+ IconButton(
+ onPressed: () => Navigator.pushNamed(context, '/routes'),
+ icon: const Icon(Icons.local_shipping_rounded, color: Colors.white60),
+ tooltip: 'Ver rutas',
+ ),
+ PopupMenuButton(
+ icon: const Icon(Icons.more_vert, color: Colors.white60),
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
+ onSelected: (v) {
+ if (v == 'password') _mostrarCambioPassword();
+ if (v == 'logout') _confirmarCerrarSesion();
+ },
+ itemBuilder: (_) => [
+ const PopupMenuItem(
+ value: 'password',
+ child: Row(children: [Icon(Icons.lock_reset, size: 18), SizedBox(width: 10), Text('Cambiar contraseña')]),
+ ),
+ const PopupMenuDivider(),
+ const PopupMenuItem(
+ value: 'logout',
+ child: Row(children: [Icon(Icons.logout, size: 18, color: Colors.red), SizedBox(width: 10), Text('Cerrar sesión', style: TextStyle(color: Colors.red))]),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+
+ // ── CHIP DE ESTADO GPS ───────────────────────────────────
+ if (!eta.gpsOk)
+ Padding(
+ padding: const EdgeInsets.fromLTRB(20, 8, 20, 0),
+ child: _GlassChip(
+ icon: Icons.gps_off,
+ label: 'GPS del camión desconectado',
+ color: Colors.red.shade300,
+ ),
+ ),
+
+ // ── CÍRCULO ETA PRINCIPAL ────────────────────────────────
+ const SizedBox(height: 24),
+ ScaleTransition(
+ scale: _pulseAnimation,
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ // Anillo exterior difuso
+ Container(
+ width: 200,
+ height: 200,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: Colors.white.withValues(alpha: 0.06),
+ ),
+ ),
+ // Anillo con blur
+ ClipOval(
+ child: BackdropFilter(
+ filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8),
+ child: Container(
+ width: 180,
+ height: 180,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: Colors.white.withValues(alpha: 0.15),
+ border: Border.all(color: Colors.white.withValues(alpha: 0.4), width: 2),
+ ),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(_emojiSegunETA(eta.etaMinutos), style: const TextStyle(fontSize: 36)),
+ const SizedBox(height: 2),
+ Text(
+ '${eta.etaMinutos}',
+ style: const TextStyle(fontSize: 58, fontWeight: FontWeight.w900, color: Colors.white, height: 1),
+ ),
+ const Text('minutos', style: TextStyle(fontSize: 14, color: Colors.white70, letterSpacing: 1)),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 14),
+ Text(
+ _textoEstado(eta.etaMinutos),
+ style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ eta.etaTexto,
+ style: const TextStyle(color: Colors.white60, fontSize: 13),
+ ),
+
+ // ── CARD MENSAJE PREVENTIVO ──────────────────────────────
+ const SizedBox(height: 20),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: _GlassCard(
+ child: Row(
+ children: [
+ Container(
+ width: 44, height: 44,
+ decoration: BoxDecoration(
+ color: Colors.white.withValues(alpha: 0.15),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: const Icon(Icons.notifications_active_rounded, color: Colors.white, size: 22),
+ ),
+ const SizedBox(width: 14),
+ Expanded(
+ child: Text(
+ eta.mensajePreventivo,
+ style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500, height: 1.4),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ // ── INFO DE RUTA ─────────────────────────────────────────
+ const SizedBox(height: 12),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: _GlassCard(
+ child: Row(
+ children: [
+ Container(
+ width: 44, height: 44,
+ decoration: BoxDecoration(
+ color: Colors.white.withValues(alpha: 0.15),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: const Icon(Icons.local_shipping_rounded, color: Colors.white, size: 22),
+ ),
+ const SizedBox(width: 14),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(eta.rutaNombre, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 13)),
+ const SizedBox(height: 2),
+ Row(
+ children: [
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
+ decoration: BoxDecoration(
+ color: eta.rutaStatus == 'EN_RUTA'
+ ? Colors.green.withValues(alpha: 0.3)
+ : Colors.blue.withValues(alpha: 0.3),
+ borderRadius: BorderRadius.circular(6),
+ ),
+ child: Text(
+ eta.rutaStatus,
+ style: TextStyle(
+ color: eta.rutaStatus == 'EN_RUTA' ? Colors.greenAccent : Colors.lightBlueAccent,
+ fontSize: 11, fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ // ── ACCESOS RÁPIDOS ──────────────────────────────────────
+ const SizedBox(height: 20),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Padding(
+ padding: EdgeInsets.only(left: 2, bottom: 12),
+ child: Text('Accesos rápidos', style: TextStyle(color: Colors.white60, fontSize: 12, letterSpacing: 0.8)),
+ ),
+ GridView.count(
+ crossAxisCount: 2,
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ crossAxisSpacing: 12,
+ mainAxisSpacing: 12,
+ childAspectRatio: 1.55,
+ children: [
+ _AccesoRapido(
+ icon: Icons.local_shipping_rounded,
+ label: 'Rutas',
+ sublabel: 'Estado del camión',
+ color: const Color(0xFF1565C0),
+ onTap: () => Navigator.pushNamed(context, '/routes'),
+ ),
+ _AccesoRapido(
+ icon: Icons.analytics_rounded,
+ label: 'Análisis',
+ sublabel: 'Reportes y predicción',
+ color: const Color(0xFF6A1B9A),
+ onTap: () => Navigator.pushNamed(context, '/analytics'),
+ ),
+ _AccesoRapido(
+ icon: Icons.report_problem_rounded,
+ label: 'Reportar',
+ sublabel: 'Enviar un reporte',
+ color: const Color(0xFFE65100),
+ onTap: () => Navigator.pushNamed(context, '/reporte'),
+ ),
+ _AccesoRapido(
+ icon: Icons.eco_rounded,
+ label: 'Información',
+ sublabel: 'Guía de residuos',
+ color: const Color(0xFF2E7D32),
+ onTap: () => Navigator.pushNamed(context, '/info'),
+ ),
+ _AccesoRapido(
+ icon: Icons.map_rounded,
+ label: 'Mapa',
+ sublabel: 'Ver rutas en mapa',
+ color: const Color(0xFF00838F),
+ onTap: () => Navigator.pushNamed(context, '/mapa'),
+ ),
+ _AccesoRapido(
+ icon: Icons.add_location_alt_rounded,
+ label: 'Dirección',
+ sublabel: 'Agregar domicilio',
+ color: const Color(0xFFC62828),
+ onTap: _mostrarAgregarDireccionDialog,
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+
+ // ── DIRECCIONES ──────────────────────────────────────────
+ if (_direcciones.isNotEmpty) ...[
+ const SizedBox(height: 20),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: _GlassCard(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ const Icon(Icons.home_rounded, color: Colors.white70, size: 16),
+ const SizedBox(width: 8),
+ const Text('Mis direcciones', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14)),
+ const Spacer(),
+ GestureDetector(
+ onTap: _mostrarAgregarDireccionDialog,
+ child: const Icon(Icons.add_circle_outline, color: Colors.white54, size: 20),
+ ),
+ ],
+ ),
+ const SizedBox(height: 10),
+ ..._direcciones.map((d) => Padding(
+ padding: const EdgeInsets.only(bottom: 8),
+ child: Row(
+ children: [
+ Container(
+ width: 6, height: 6,
+ decoration: const BoxDecoration(color: Colors.white54, shape: BoxShape.circle),
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(d.colonia, style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600)),
+ Text(d.direccion, style: const TextStyle(color: Colors.white54, fontSize: 12)),
+ ],
+ ),
+ ),
+ ],
+ ),
+ )),
+ ],
+ ),
+ ),
+ ),
+ ],
+
+ // ── FOOTER ───────────────────────────────────────────────
+ const SizedBox(height: 20),
+ Padding(
+ padding: const EdgeInsets.only(bottom: 28),
+ child: Column(children: [
+ const Text('🔄 Actualización automática cada minuto',
+ style: TextStyle(color: Colors.white30, fontSize: 11)),
+ const SizedBox(height: 10),
+ TextButton.icon(
+ onPressed: _cargarETA,
+ icon: const Icon(Icons.refresh, color: Colors.white38, size: 16),
+ label: const Text('Actualizar ahora', style: TextStyle(color: Colors.white38, fontSize: 12)),
+ ),
+ ]),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+// ================================================================
+// WIDGETS AUXILIARES
+// ================================================================
+
+/// Tarjeta con efecto glassmorphism
+class _GlassCard extends StatelessWidget {
+ final Widget child;
+ final EdgeInsets? padding;
+
+ const _GlassCard({required this.child, this.padding});
+
+ @override
+ Widget build(BuildContext context) {
+ return ClipRRect(
+ borderRadius: BorderRadius.circular(18),
+ child: BackdropFilter(
+ filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
+ child: Container(
+ width: double.infinity,
+ padding: padding ?? const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.white.withValues(alpha: 0.12),
+ borderRadius: BorderRadius.circular(18),
+ border: Border.all(color: Colors.white.withValues(alpha: 0.2), width: 1),
+ ),
+ child: child,
+ ),
+ ),
+ );
+ }
+}
+
+/// Chip de estado pequeño
+class _GlassChip extends StatelessWidget {
+ final IconData icon;
+ final String label;
+ final Color color;
+
+ const _GlassChip({required this.icon, required this.label, required this.color});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.15),
+ borderRadius: BorderRadius.circular(10),
+ border: Border.all(color: color.withValues(alpha: 0.4)),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(icon, color: color, size: 15),
+ const SizedBox(width: 6),
+ Text(label, style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.w600)),
+ ],
+ ),
+ );
+ }
+}
+
+/// Tarjeta de acceso rápido en el grid
+class _AccesoRapido extends StatelessWidget {
+ final IconData icon;
+ final String label;
+ final String sublabel;
+ final Color color;
+ final VoidCallback onTap;
+
+ const _AccesoRapido({
+ required this.icon,
+ required this.label,
+ required this.sublabel,
+ required this.color,
+ required this.onTap,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: onTap,
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(16),
+ child: BackdropFilter(
+ filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8),
+ child: Container(
+ padding: const EdgeInsets.all(14),
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.18),
+ borderRadius: BorderRadius.circular(16),
+ border: Border.all(color: color.withValues(alpha: 0.35), width: 1),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Container(
+ width: 36, height: 36,
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.25),
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: Icon(icon, color: Colors.white, size: 20),
+ ),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(label, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)),
+ Text(sublabel, style: const TextStyle(color: Colors.white54, fontSize: 10), overflow: TextOverflow.ellipsis),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/HackOnLinces_app/aplicacion_hack/lib/screens/info_screen.dart b/HackOnLinces_app/aplicacion_hack/lib/screens/info_screen.dart
new file mode 100644
index 0000000..87c8a15
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/lib/screens/info_screen.dart
@@ -0,0 +1,808 @@
+// ================================================================
+// lib/screens/info_screen.dart — EcoTrack
+// Pantalla de Informacion + Tutorial interactivo
+// ================================================================
+//
+// ASSETS USADOS:
+// assets/images/recycle.jpg → Separacion / reciclaje
+// assets/images/bottle.png → Plasticos
+// assets/images/planta.png → Composta / Medio ambiente
+// assets/images/megafono.png → Residuos peligrosos / Horarios
+//
+// SECCIONES:
+// 1. Tutorial interactivo (swipe de pasos)
+// 2. Articulos de informacion por categoria
+// 3. Detalle de articulo con imagen
+// ================================================================
+
+import 'package:flutter/material.dart';
+
+// ================================================================
+// MODELOS
+// ================================================================
+
+class _Subseccion {
+ final String subtitulo;
+ final String texto;
+ const _Subseccion(this.subtitulo, this.texto);
+}
+
+class _Articulo {
+ final String id;
+ final String categoria;
+ final String titulo;
+ final String resumen;
+ final String imagenAsset;
+ final Color color;
+ final List<_Subseccion> contenido;
+ final String consejoRapido;
+
+ const _Articulo({
+ required this.id,
+ required this.categoria,
+ required this.titulo,
+ required this.resumen,
+ required this.imagenAsset,
+ required this.color,
+ required this.contenido,
+ required this.consejoRapido,
+ });
+}
+
+// ================================================================
+// DATOS
+// ================================================================
+
+const _articulos = [
+ _Articulo(
+ id: 'separacion',
+ categoria: 'Separacion',
+ titulo: 'Como separar correctamente tu basura',
+ resumen: 'La separacion correcta es el primer paso para reciclar y reducir el impacto ambiental.',
+ imagenAsset: 'assets/images/recycle.jpg',
+ color: Color(0xFF2E7D32),
+ consejoRapido: 'Regla facil: si vino de la naturaleza y se pudre, es organico. Si es artificial y esta limpio, es reciclable.',
+ contenido: [
+ _Subseccion('Residuos Organicos', 'Restos de comida, cascaras de frutas y verduras, posos de cafe, bolsas de te, restos de jardin. Van en bolsa oscura o cafe. Se convierten en composta.'),
+ _Subseccion('Inorganicos Reciclables', 'Plasticos (botellas PET, envases), papel y carton limpios, vidrio, latas de aluminio y hojalata. Van en bolsa transparente. Deben estar limpios y secos.'),
+ _Subseccion('No Reciclables', 'Papel higienico usado, panales, colillas de cigarro, envolturas metalizadas. Van en bolsa negra. No tienen valor de reciclaje.'),
+ _Subseccion('Residuos Especiales', 'Pilas, medicamentos caducados, electronicos, aceite de cocina. NUNCA los mezcles con la basura regular. Lleva pilas a puntos de acopio en supermercados.'),
+ ],
+ ),
+ _Articulo(
+ id: 'horarios',
+ categoria: 'Horarios',
+ titulo: 'Cuando sacar tu basura',
+ resumen: 'Sacar la basura en el momento correcto evita plagas, malos olores y que el camion se la pierda.',
+ imagenAsset: 'assets/images/megafono.png',
+ color: Color(0xFF1565C0),
+ consejoRapido: 'Espera la alerta de EcoTrack antes de salir con tus bolsas. Te ahorra tiempo y evita dejar basura expuesta.',
+ contenido: [
+ _Subseccion('El momento ideal', 'Saca tu basura cuando recibas la alerta de "Camion Cercano" en EcoTrack. Eso significa que el camion esta a menos de 15 minutos de tu domicilio.'),
+ _Subseccion('Por que no de noche', 'Las bolsas en la acera de noche atraen perros y fauna nocturna que las rompen y dispersan los residuos. Ademas el plastico se deteriora con la humedad nocturna.'),
+ _Subseccion('Si me lo pierdo', 'Si el camion ya paso, guarda tu basura hasta el siguiente dia. Nunca dejes bolsas en la via publica fuera del horario de recoleccion.'),
+ _Subseccion('Dias festivos', 'En dias festivos el servicio puede retrasarse o cancelarse. Activa las notificaciones de EcoTrack para recibir alertas de retraso o cambio de horario.'),
+ ],
+ ),
+ _Articulo(
+ id: 'plasticos',
+ categoria: 'Reciclaje',
+ titulo: 'Guia de plasticos: cuales si y cuales no',
+ resumen: 'No todos los plasticos son iguales. Aprende a leer el numero en el triangulo de reciclaje.',
+ imagenAsset: 'assets/images/bottle.png',
+ color: Color(0xFF00838F),
+ consejoRapido: 'Busca el numero dentro del triangulo en el fondo del envase. Los numeros 1 y 2 siempre van al reciclaje.',
+ contenido: [
+ _Subseccion('Plastico #1 PET', 'Botellas de agua y refrescos. El mas reciclado. Aplastalo para ahorrar espacio. Quita la tapa porque es un material diferente.'),
+ _Subseccion('Plastico #2 HDPE', 'Garrafones, botellas de leche, shampoo. Tambien muy reciclable. Enjuagalo antes de separarlo.'),
+ _Subseccion('Plastico #5 PP', 'Tapas de botellas, envases de yogur. Si se recicla pero menos centros lo aceptan.'),
+ _Subseccion('Plasticos 3, 6 y 7', 'PVC, unicel, policarbonato. Dificiles o imposibles de reciclar. Van a basura no reciclable.'),
+ _Subseccion('Bolsas de plastico', 'No van en el reciclaje de casa porque tapan las maquinas clasificadoras. Lleva tus bolsas a centros de acopio en supermercados.'),
+ ],
+ ),
+ _Articulo(
+ id: 'composta',
+ categoria: 'Compostaje',
+ titulo: 'Haz composta en casa',
+ resumen: 'Convierte tus residuos organicos en abono natural. Es mas facil de lo que crees.',
+ imagenAsset: 'assets/images/planta.png',
+ color: Color(0xFF558B2F),
+ consejoRapido: 'La composta lista huele a tierra mojada, no a podrido. Si huele mal, agrega mas material seco y volteala.',
+ contenido: [
+ _Subseccion('Que necesitas', 'Un contenedor con tapa, residuos organicos, tierra o tierra de hojarasca, y un poco de paciencia.'),
+ _Subseccion('Que puedes compostar', 'Cascaras de frutas y verduras, restos de comida cocida sin carne, posos de cafe y filtros de papel, cascaras de huevo, hojas secas.'),
+ _Subseccion('Que NO debes compostar', 'Carnes, pescados, lacteos, aceites ya que atraen plagas, excrementos de mascotas, plasticos ni metales.'),
+ _Subseccion('El proceso', 'Alterna capas de residuos organicos humedos con capas de material seco. Voltea la mezcla cada semana. En 2 a 3 meses tendras composta lista para tus plantas.'),
+ ],
+ ),
+ _Articulo(
+ id: 'peligrosos',
+ categoria: 'Residuos Especiales',
+ titulo: 'Residuos peligrosos: como deshacerte de ellos',
+ resumen: 'Pilas, medicamentos y electronicos requieren un manejo especial para no contaminar el suelo y el agua.',
+ imagenAsset: 'assets/images/megafono.png',
+ color: Color(0xFFE65100),
+ consejoRapido: 'Guarda una caja en casa exclusiva para residuos peligrosos. Cuando este llena, busca el punto de acopio mas cercano.',
+ contenido: [
+ _Subseccion('Pilas y baterias', 'Una sola pila AA puede contaminar 600,000 litros de agua. Guardalas en una bolsa y lleva a los puntos de acopio en Walmart, Soriana, Home Depot o OXXO.'),
+ _Subseccion('Medicamentos caducados', 'No los tires al drenaje ni a la basura regular. Farmacias del Ahorro y Benavides cuentan con contenedores REPARED para medicamentos.'),
+ _Subseccion('Electronicos RAEE', 'Celulares, computadoras, cables, focos LED. Contienen plomo, mercurio y cadmio. Lleva a tiendas de electronicos o espera las jornadas municipales.'),
+ _Subseccion('Aceite de cocina', 'Un litro de aceite contamina hasta 1,000 litros de agua potable. Viertelo en una botella PET con tapa y lleva a centros de acopio.'),
+ ],
+ ),
+ _Articulo(
+ id: 'impacto',
+ categoria: 'Medio Ambiente',
+ titulo: 'El impacto real de reciclar',
+ resumen: 'Numeros concretos para entender por que vale la pena separar tu basura cada dia.',
+ imagenAsset: 'assets/images/planta.png',
+ color: Color(0xFF4527A0),
+ consejoRapido: 'Cada lata de aluminio que reciclas ahorra energia equivalente a medio litro de gasolina. Si importa.',
+ contenido: [
+ _Subseccion('Papel y carton', 'Reciclar 1 tonelada de papel salva 17 arboles y ahorra 26,000 litros de agua. Una familia promedio genera 500 kg de papel al ano.'),
+ _Subseccion('Aluminio', 'Reciclar una lata de aluminio ahorra la energia suficiente para que un foco LED funcione 20 horas. El aluminio puede reciclarse infinitas veces.'),
+ _Subseccion('Vidrio', 'El vidrio tarda mas de 4,000 anos en degradarse. Reciclarlo reduce en 20% las emisiones de CO2 de su produccion.'),
+ _Subseccion('Residuos en Mexico', 'Mexico genera 120,000 toneladas de basura al dia. Solo el 9% se recicla formalmente. Si cada hogar separara correctamente, ese porcentaje podria triplicarse.'),
+ ],
+ ),
+];
+
+// ── Pasos del tutorial ─────────────────────────────────────────
+
+class _PasoTutorial {
+ final String titulo;
+ final String descripcion;
+ final String imagenAsset;
+ final Color color;
+ final IconData icono;
+
+ const _PasoTutorial({
+ required this.titulo,
+ required this.descripcion,
+ required this.imagenAsset,
+ required this.color,
+ required this.icono,
+ });
+}
+
+const _pasosTutorial = [
+ _PasoTutorial(
+ titulo: 'Bienvenido a EcoTrack',
+ descripcion: 'EcoTrack te notifica cuando el camion recolector esta cerca de tu domicilio para que saques tu basura en el momento exacto.',
+ imagenAsset: 'assets/images/recycle.jpg',
+ color: Color(0xFF2E7D32),
+ icono: Icons.recycling_rounded,
+ ),
+ _PasoTutorial(
+ titulo: 'Separa tus residuos',
+ descripcion: 'Separa tu basura en organicos, reciclables y no reciclables. Esto facilita el trabajo del camion y reduce el impacto ambiental.',
+ imagenAsset: 'assets/images/bottle.png',
+ color: Color(0xFF00838F),
+ icono: Icons.category_rounded,
+ ),
+ _PasoTutorial(
+ titulo: 'Espera la alerta',
+ descripcion: 'Activa las notificaciones de EcoTrack. Te avisaremos cuando el camion este a menos de 15 minutos de tu casa.',
+ imagenAsset: 'assets/images/megafono.png',
+ color: Color(0xFF1565C0),
+ icono: Icons.notifications_active_rounded,
+ ),
+ _PasoTutorial(
+ titulo: 'Saca la basura a tiempo',
+ descripcion: 'Al recibir la alerta, saca tus bolsas a la acera. Evita sacarlas muy antes para no atraer fauna y mantener limpia la calle.',
+ imagenAsset: 'assets/images/recycle.jpg',
+ color: Color(0xFFE65100),
+ icono: Icons.access_time_filled_rounded,
+ ),
+ _PasoTutorial(
+ titulo: 'Envia reportes',
+ descripcion: 'Si el camion no paso o detectas alguna irregularidad, usa la seccion de Reportes para informar al municipio. Tu participacion mejora el servicio.',
+ imagenAsset: 'assets/images/megafono.png',
+ color: Color(0xFF6A1B9A),
+ icono: Icons.report_problem_rounded,
+ ),
+];
+
+// ================================================================
+// PANTALLA PRINCIPAL
+// ================================================================
+class InfoScreen extends StatefulWidget {
+ const InfoScreen({super.key});
+
+ @override
+ State createState() => _InfoScreenState();
+}
+
+class _InfoScreenState extends State with SingleTickerProviderStateMixin {
+ late TabController _tabController;
+ String? _categoriaFiltro;
+
+ @override
+ void initState() {
+ super.initState();
+ _tabController = TabController(length: 2, vsync: this);
+ }
+
+ @override
+ void dispose() {
+ _tabController.dispose();
+ super.dispose();
+ }
+
+ List get _categorias =>
+ _articulos.map((a) => a.categoria).toSet().toList();
+
+ List<_Articulo> get _articulosFiltrados =>
+ _categoriaFiltro == null
+ ? _articulos
+ : _articulos.where((a) => a.categoria == _categoriaFiltro).toList();
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: const Color(0xFFF5F7F5),
+ appBar: AppBar(
+ title: const Text('EcoTrack — Aprende'),
+ backgroundColor: const Color(0xFF2E7D32),
+ foregroundColor: Colors.white,
+ elevation: 0,
+ bottom: TabBar(
+ controller: _tabController,
+ indicatorColor: Colors.white,
+ labelColor: Colors.white,
+ unselectedLabelColor: Colors.white60,
+ tabs: const [
+ Tab(icon: Icon(Icons.play_circle_outline_rounded), text: 'Tutorial'),
+ Tab(icon: Icon(Icons.menu_book_rounded), text: 'Guias'),
+ ],
+ ),
+ ),
+ body: TabBarView(
+ controller: _tabController,
+ children: [
+ _TutorialView(),
+ _GuiasView(
+ categorias: _categorias,
+ categoriaFiltro: _categoriaFiltro,
+ articulos: _articulosFiltrados,
+ onFiltro: (c) => setState(() => _categoriaFiltro = c),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+// ================================================================
+// TAB 1: TUTORIAL INTERACTIVO
+// ================================================================
+class _TutorialView extends StatefulWidget {
+ @override
+ State<_TutorialView> createState() => _TutorialViewState();
+}
+
+class _TutorialViewState extends State<_TutorialView> {
+ final PageController _pageCtrl = PageController();
+ int _paginaActual = 0;
+
+ @override
+ void dispose() {
+ _pageCtrl.dispose();
+ super.dispose();
+ }
+
+ void _siguiente() {
+ if (_paginaActual < _pasosTutorial.length - 1) {
+ _pageCtrl.nextPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
+ }
+ }
+
+ void _anterior() {
+ if (_paginaActual > 0) {
+ _pageCtrl.previousPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ // Indicador de progreso
+ Padding(
+ padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
+ child: Row(
+ children: List.generate(_pasosTutorial.length, (i) {
+ final activo = i == _paginaActual;
+ final completado = i < _paginaActual;
+ return Expanded(
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 300),
+ margin: const EdgeInsets.symmetric(horizontal: 3),
+ height: 4,
+ decoration: BoxDecoration(
+ color: completado
+ ? const Color(0xFF2E7D32)
+ : activo
+ ? _pasosTutorial[_paginaActual].color
+ : Colors.grey.shade200,
+ borderRadius: BorderRadius.circular(2),
+ ),
+ ),
+ );
+ }),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ 'Paso ${_paginaActual + 1} de ${_pasosTutorial.length}',
+ style: const TextStyle(color: Colors.grey, fontSize: 12),
+ ),
+ Text(
+ _paginaActual == _pasosTutorial.length - 1 ? 'Completado' : '',
+ style: const TextStyle(color: Color(0xFF2E7D32), fontSize: 12, fontWeight: FontWeight.bold),
+ ),
+ ],
+ ),
+ ),
+
+ // Páginas del tutorial
+ Expanded(
+ child: PageView.builder(
+ controller: _pageCtrl,
+ itemCount: _pasosTutorial.length,
+ onPageChanged: (i) => setState(() => _paginaActual = i),
+ itemBuilder: (context, i) => _PaginaTutorial(paso: _pasosTutorial[i]),
+ ),
+ ),
+
+ // Controles de navegación
+ Padding(
+ padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
+ child: Row(
+ children: [
+ if (_paginaActual > 0)
+ Expanded(
+ child: OutlinedButton.icon(
+ onPressed: _anterior,
+ icon: const Icon(Icons.arrow_back_rounded, size: 18),
+ label: const Text('Anterior'),
+ style: OutlinedButton.styleFrom(
+ padding: const EdgeInsets.symmetric(vertical: 14),
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
+ ),
+ ),
+ ),
+ if (_paginaActual > 0) const SizedBox(width: 12),
+ Expanded(
+ flex: _paginaActual == 0 ? 1 : 1,
+ child: ElevatedButton.icon(
+ onPressed: _paginaActual == _pasosTutorial.length - 1 ? null : _siguiente,
+ icon: Icon(
+ _paginaActual == _pasosTutorial.length - 1
+ ? Icons.check_circle_rounded
+ : Icons.arrow_forward_rounded,
+ size: 18,
+ ),
+ label: Text(
+ _paginaActual == _pasosTutorial.length - 1 ? 'Listo' : 'Siguiente',
+ ),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: _pasosTutorial[_paginaActual].color,
+ foregroundColor: Colors.white,
+ padding: const EdgeInsets.symmetric(vertical: 14),
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
+ elevation: 0,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+}
+
+class _PaginaTutorial extends StatelessWidget {
+ final _PasoTutorial paso;
+ const _PaginaTutorial({required this.paso});
+
+ @override
+ Widget build(BuildContext context) {
+ return SingleChildScrollView(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Column(
+ children: [
+ // Imagen con overlay de icono
+ Stack(
+ alignment: Alignment.bottomRight,
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(20),
+ child: Image.asset(
+ paso.imagenAsset,
+ height: 220,
+ width: double.infinity,
+ fit: BoxFit.cover,
+ errorBuilder: (_, __, ___) => Container(
+ height: 220,
+ decoration: BoxDecoration(
+ color: paso.color.withValues(alpha: 0.12),
+ borderRadius: BorderRadius.circular(20),
+ ),
+ child: Icon(paso.icono, size: 80, color: paso.color.withValues(alpha: 0.4)),
+ ),
+ ),
+ ),
+ // Badge del icono
+ Positioned(
+ bottom: 16,
+ right: 16,
+ child: Container(
+ width: 52, height: 52,
+ decoration: BoxDecoration(
+ color: paso.color,
+ shape: BoxShape.circle,
+ boxShadow: [BoxShadow(color: paso.color.withValues(alpha: 0.4), blurRadius: 12)],
+ ),
+ child: Icon(paso.icono, color: Colors.white, size: 26),
+ ),
+ ),
+ ],
+ ),
+
+ const SizedBox(height: 24),
+
+ // Titulo
+ Text(
+ paso.titulo,
+ textAlign: TextAlign.center,
+ style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: paso.color, height: 1.2),
+ ),
+ const SizedBox(height: 12),
+
+ // Descripcion
+ Container(
+ padding: const EdgeInsets.all(18),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(16),
+ boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 8)],
+ ),
+ child: Text(
+ paso.descripcion,
+ textAlign: TextAlign.center,
+ style: const TextStyle(fontSize: 15, color: Color(0xFF444444), height: 1.6),
+ ),
+ ),
+ const SizedBox(height: 20),
+ ],
+ ),
+ );
+ }
+}
+
+// ================================================================
+// TAB 2: GUIAS DE INFORMACION
+// ================================================================
+class _GuiasView extends StatelessWidget {
+ final List categorias;
+ final String? categoriaFiltro;
+ final List<_Articulo> articulos;
+ final void Function(String?) onFiltro;
+
+ const _GuiasView({
+ required this.categorias,
+ required this.categoriaFiltro,
+ required this.articulos,
+ required this.onFiltro,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ // Filtros de categoria
+ SizedBox(
+ height: 48,
+ child: ListView(
+ scrollDirection: Axis.horizontal,
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ children: [
+ _FiltroChip(
+ label: 'Todas',
+ seleccionado: categoriaFiltro == null,
+ color: const Color(0xFF2E7D32),
+ onTap: () => onFiltro(null),
+ ),
+ ...categorias.map((cat) {
+ final a = _articulos.firstWhere((x) => x.categoria == cat, orElse: () => _articulos.first);
+ return _FiltroChip(
+ label: cat,
+ seleccionado: categoriaFiltro == cat,
+ color: a.color,
+ onTap: () => onFiltro(cat),
+ );
+ }),
+ ],
+ ),
+ ),
+
+ // Lista de articulos
+ Expanded(
+ child: ListView.builder(
+ padding: const EdgeInsets.fromLTRB(16, 4, 16, 24),
+ itemCount: articulos.length,
+ itemBuilder: (context, i) => _TarjetaArticulo(
+ articulo: articulos[i],
+ onTap: () => Navigator.push(
+ context,
+ MaterialPageRoute(builder: (_) => _DetalleArticuloScreen(articulo: articulos[i])),
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+}
+
+// ================================================================
+// WIDGET: Chip de filtro
+// ================================================================
+class _FiltroChip extends StatelessWidget {
+ final String label;
+ final bool seleccionado;
+ final Color color;
+ final VoidCallback onTap;
+
+ const _FiltroChip({required this.label, required this.seleccionado, required this.color, required this.onTap});
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.only(right: 8),
+ child: GestureDetector(
+ onTap: onTap,
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 200),
+ padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
+ decoration: BoxDecoration(
+ color: seleccionado ? color : Colors.white,
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(color: color, width: 1.5),
+ ),
+ child: Text(
+ label,
+ style: TextStyle(color: seleccionado ? Colors.white : color, fontWeight: FontWeight.w600, fontSize: 12),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+// ================================================================
+// WIDGET: Tarjeta de articulo
+// ================================================================
+class _TarjetaArticulo extends StatelessWidget {
+ final _Articulo articulo;
+ final VoidCallback onTap;
+
+ const _TarjetaArticulo({required this.articulo, required this.onTap});
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 12),
+ child: GestureDetector(
+ onTap: onTap,
+ child: Container(
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(16),
+ boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.06), blurRadius: 8, offset: const Offset(0, 2))],
+ ),
+ child: Row(
+ children: [
+ // Imagen lateral
+ ClipRRect(
+ borderRadius: const BorderRadius.horizontal(left: Radius.circular(16)),
+ child: Image.asset(
+ articulo.imagenAsset,
+ width: 90,
+ height: 100,
+ fit: BoxFit.cover,
+ errorBuilder: (_, __, ___) => Container(
+ width: 90, height: 100,
+ color: articulo.color.withValues(alpha: 0.1),
+ child: Icon(Icons.eco_rounded, color: articulo.color, size: 36),
+ ),
+ ),
+ ),
+ // Contenido
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.all(14),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
+ decoration: BoxDecoration(
+ color: articulo.color.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(6),
+ ),
+ child: Text(articulo.categoria,
+ style: TextStyle(color: articulo.color, fontSize: 10, fontWeight: FontWeight.w700)),
+ ),
+ const SizedBox(height: 6),
+ Text(articulo.titulo,
+ style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Color(0xFF1A1A1A)),
+ maxLines: 2, overflow: TextOverflow.ellipsis),
+ const SizedBox(height: 4),
+ Text(articulo.resumen,
+ style: TextStyle(fontSize: 12, color: Colors.grey.shade600, height: 1.3),
+ maxLines: 2, overflow: TextOverflow.ellipsis),
+ ],
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.only(right: 12),
+ child: Icon(Icons.chevron_right_rounded, color: Colors.grey.shade400),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+// ================================================================
+// PANTALLA DE DETALLE
+// ================================================================
+class _DetalleArticuloScreen extends StatelessWidget {
+ final _Articulo articulo;
+ const _DetalleArticuloScreen({required this.articulo});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: const Color(0xFFF5F7F5),
+ body: CustomScrollView(
+ slivers: [
+ SliverAppBar(
+ expandedHeight: 220,
+ pinned: true,
+ backgroundColor: articulo.color,
+ foregroundColor: Colors.white,
+ flexibleSpace: FlexibleSpaceBar(
+ title: Text(
+ articulo.titulo,
+ style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold),
+ maxLines: 2,
+ ),
+ background: Stack(
+ fit: StackFit.expand,
+ children: [
+ Image.asset(
+ articulo.imagenAsset,
+ fit: BoxFit.cover,
+ errorBuilder: (_, __, ___) => Container(color: articulo.color),
+ ),
+ // Gradiente oscuro para legibilidad del titulo
+ Container(
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.topCenter,
+ end: Alignment.bottomCenter,
+ colors: [Colors.transparent, articulo.color.withValues(alpha: 0.85)],
+ stops: const [0.4, 1.0],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ SliverToBoxAdapter(
+ child: Padding(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Badge categoria
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
+ decoration: BoxDecoration(
+ color: articulo.color.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(20),
+ ),
+ child: Text(articulo.categoria,
+ style: TextStyle(color: articulo.color, fontWeight: FontWeight.w700, fontSize: 12)),
+ ),
+ const SizedBox(height: 12),
+
+ // Resumen
+ Text(articulo.resumen,
+ style: const TextStyle(fontSize: 15, color: Color(0xFF333333), height: 1.5)),
+ const SizedBox(height: 20),
+
+ // Secciones
+ ...articulo.contenido.map((s) => _SeccionCard(seccion: s, color: articulo.color)),
+
+ const SizedBox(height: 8),
+
+ // Consejo destacado
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(18),
+ decoration: BoxDecoration(
+ color: articulo.color,
+ borderRadius: BorderRadius.circular(16),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Icon(Icons.lightbulb_outline_rounded, color: Colors.white.withValues(alpha: 0.9), size: 20),
+ const SizedBox(width: 8),
+ const Text('Consejo rapido',
+ style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 15)),
+ ],
+ ),
+ const SizedBox(height: 10),
+ Text(articulo.consejoRapido,
+ style: const TextStyle(color: Colors.white, fontSize: 14, height: 1.5)),
+ ],
+ ),
+ ),
+ const SizedBox(height: 32),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _SeccionCard extends StatelessWidget {
+ final _Subseccion seccion;
+ final Color color;
+ const _SeccionCard({required this.seccion, required this.color});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ width: double.infinity,
+ margin: const EdgeInsets.only(bottom: 12),
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(12),
+ border: Border(left: BorderSide(color: color, width: 4)),
+ boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.04), blurRadius: 6, offset: const Offset(0, 2))],
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(seccion.subtitulo,
+ style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: color)),
+ const SizedBox(height: 6),
+ Text(seccion.texto,
+ style: const TextStyle(fontSize: 13, color: Color(0xFF444444), height: 1.5)),
+ ],
+ ),
+ );
+ }
+}
diff --git a/HackOnLinces_app/aplicacion_hack/lib/screens/login_screen.dart b/HackOnLinces_app/aplicacion_hack/lib/screens/login_screen.dart
new file mode 100644
index 0000000..61ef896
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/lib/screens/login_screen.dart
@@ -0,0 +1,402 @@
+// ================================================================
+// lib/screens/login_screen.dart — EcoTrack
+// ================================================================
+
+import 'package:flutter/material.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+import '../services/api_service.dart';
+
+class LoginScreen extends StatefulWidget {
+ const LoginScreen({super.key});
+
+ @override
+ State createState() => _LoginScreenState();
+}
+
+class _LoginScreenState extends State {
+ final _emailCtrl = TextEditingController();
+ final _passwordCtrl = TextEditingController();
+ final _nameCtrl = TextEditingController();
+ final _dirCtrl = TextEditingController();
+
+ String? _coloniaSeleccionada;
+ List _colonias = [];
+ bool _esRegistro = false;
+ bool _cargandoColonias = true;
+ String? _errorColonias;
+ bool _logueando = false;
+ bool _mostrarPassword = false;
+
+ final ApiService _apiService = ApiService();
+
+ @override
+ void initState() {
+ super.initState();
+ _cargarColonias();
+ _verificarSesion();
+ }
+
+ @override
+ void dispose() {
+ _emailCtrl.dispose();
+ _passwordCtrl.dispose();
+ _nameCtrl.dispose();
+ _dirCtrl.dispose();
+ super.dispose();
+ }
+
+ Future _verificarSesion() async {
+ final prefs = await SharedPreferences.getInstance();
+ final id = prefs.getInt('usuario_id');
+ if (id != null && mounted) {
+ Navigator.pushReplacementNamed(context, '/home', arguments: id);
+ }
+ }
+
+ Future _cargarColonias() async {
+ try {
+ final colonias = await _apiService.obtenerColonias();
+ if (mounted) setState(() { _colonias = colonias; _cargandoColonias = false; });
+ } catch (e) {
+ if (mounted) {
+ setState(() {
+ _colonias = ['Zona Centro', 'Las Arboledas', 'Trojes', 'San Juanico', 'Los Olivos', 'Rancho Seco', 'Las Insurgentes'];
+ _cargandoColonias = false;
+ _errorColonias = 'Sin conexión al backend. Usando lista local.';
+ });
+ }
+ }
+ }
+
+ Future _iniciarSesion() async {
+ final email = _emailCtrl.text.trim();
+ final password = _passwordCtrl.text;
+ if (email.isEmpty) { _error('Ingresa tu correo.'); return; }
+ if (password.isEmpty) { _error('Ingresa tu contraseña.'); return; }
+
+ setState(() => _logueando = true);
+ try {
+ final r = await _apiService.loginConCorreo(email, password);
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setInt('usuario_id', r['usuario_id']);
+ await prefs.setString('nombre', r['nombre'] ?? '');
+ await prefs.setString('email', email);
+ if (mounted) Navigator.pushReplacementNamed(context, '/home', arguments: r['usuario_id']);
+ } catch (e) {
+ _error(e.toString().replaceFirst('Exception: ', ''));
+ } finally {
+ if (mounted) setState(() => _logueando = false);
+ }
+ }
+
+ Future _registrarse() async {
+ final nombre = _nameCtrl.text.trim();
+ final email = _emailCtrl.text.trim();
+ final password = _passwordCtrl.text;
+ final dir = _dirCtrl.text.trim();
+
+ if (nombre.isEmpty) { _error('Ingresa tu nombre.'); return; }
+ if (email.isEmpty) { _error('Ingresa tu correo.'); return; }
+ if (password.length < 6) { _error('Contraseña de al menos 6 caracteres.'); return; }
+ if (_coloniaSeleccionada == null) { _error('Selecciona tu colonia.'); return; }
+ if (dir.isEmpty) { _error('Ingresa tu dirección.'); return; }
+
+ setState(() => _logueando = true);
+ try {
+ final id = await _apiService.registrarUsuario(nombre, email, password, dir, _coloniaSeleccionada!);
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setInt('usuario_id', id);
+ await prefs.setString('nombre', nombre);
+ await prefs.setString('email', email);
+ if (mounted) Navigator.pushReplacementNamed(context, '/home', arguments: id);
+ } catch (e) {
+ _error(e.toString().replaceFirst('Exception: ', ''));
+ } finally {
+ if (mounted) setState(() => _logueando = false);
+ }
+ }
+
+ void _error(String msg) {
+ if (!mounted) return;
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(
+ content: Text(msg),
+ backgroundColor: Colors.red.shade700,
+ behavior: SnackBarBehavior.floating,
+ ));
+ }
+
+ // ================================================================
+ // BUILD
+ // ================================================================
+ @override
+ Widget build(BuildContext context) {
+ final color = Theme.of(context).colorScheme.primary;
+
+ return Scaffold(
+ backgroundColor: const Color(0xFFF6FAF6),
+ body: SafeArea(
+ child: SingleChildScrollView(
+ padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 32),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ const SizedBox(height: 16),
+
+ // ── LOGO ─────────────────────────────────────────────
+ Center(
+ child: Container(
+ width: 84, height: 84,
+ decoration: BoxDecoration(
+ color: color,
+ borderRadius: BorderRadius.circular(22),
+ boxShadow: [BoxShadow(color: color.withValues(alpha: 0.3), blurRadius: 20, offset: const Offset(0, 8))],
+ ),
+ child: const Icon(Icons.recycling_rounded, size: 48, color: Colors.white),
+ ),
+ ),
+ const SizedBox(height: 18),
+ Text('EcoTrack',
+ textAlign: TextAlign.center,
+ style: TextStyle(fontSize: 32, fontWeight: FontWeight.w900, color: color, letterSpacing: -0.5),
+ ),
+ const SizedBox(height: 4),
+ const Text('Recolección inteligente de residuos',
+ textAlign: TextAlign.center,
+ style: TextStyle(color: Colors.grey, fontSize: 13),
+ ),
+ const SizedBox(height: 32),
+
+ // ── TABS LOGIN / REGISTRO ─────────────────────────────
+ Container(
+ decoration: BoxDecoration(
+ color: Colors.grey.shade100,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Row(
+ children: [
+ _Tab(label: 'Iniciar sesión', seleccionado: !_esRegistro, color: color,
+ onTap: () => setState(() { _esRegistro = false; _passwordCtrl.clear(); })),
+ _Tab(label: 'Registrarse', seleccionado: _esRegistro, color: color,
+ onTap: () => setState(() { _esRegistro = true; _passwordCtrl.clear(); })),
+ ],
+ ),
+ ),
+ const SizedBox(height: 24),
+
+ // ── CAMPOS COMUNES ────────────────────────────────────
+ if (_esRegistro) ...[
+ _Campo(ctrl: _nameCtrl, label: 'Nombre completo', icon: Icons.person_outline),
+ const SizedBox(height: 14),
+ ],
+ _Campo(ctrl: _emailCtrl, label: 'Correo electrónico', icon: Icons.email_outlined, tipo: TextInputType.emailAddress),
+ const SizedBox(height: 14),
+ _Campo(
+ ctrl: _passwordCtrl,
+ label: 'Contraseña',
+ icon: Icons.lock_outline,
+ obscure: !_mostrarPassword,
+ sufijo: IconButton(
+ icon: Icon(_mostrarPassword ? Icons.visibility_off : Icons.visibility, size: 20),
+ onPressed: () => setState(() => _mostrarPassword = !_mostrarPassword),
+ ),
+ ),
+
+ // ── CAMPOS EXTRA REGISTRO ─────────────────────────────
+ if (_esRegistro) ...[
+ const SizedBox(height: 14),
+ _Campo(ctrl: _dirCtrl, label: 'Dirección', icon: Icons.home_outlined),
+ const SizedBox(height: 14),
+ if (_cargandoColonias)
+ const Center(child: Padding(padding: EdgeInsets.all(12), child: CircularProgressIndicator()))
+ else ...[
+ if (_errorColonias != null)
+ Padding(
+ padding: const EdgeInsets.only(bottom: 6),
+ child: Text(_errorColonias!, style: TextStyle(fontSize: 11, color: Colors.orange.shade700)),
+ ),
+ Container(
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: Colors.grey.shade200),
+ ),
+ child: DropdownButtonFormField(
+ value: _coloniaSeleccionada,
+ hint: const Text('Selecciona tu colonia'),
+ decoration: const InputDecoration(
+ prefixIcon: Icon(Icons.location_city_outlined),
+ border: InputBorder.none,
+ contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
+ ),
+ items: _colonias.map((c) => DropdownMenuItem(value: c, child: Text(c))).toList(),
+ onChanged: (v) => setState(() => _coloniaSeleccionada = v),
+ ),
+ ),
+ ],
+ const SizedBox(height: 8),
+ _IndicadorFortaleza(password: _passwordCtrl.text),
+ ],
+
+ const SizedBox(height: 24),
+
+ // ── BOTÓN PRINCIPAL ───────────────────────────────────
+ SizedBox(
+ height: 52,
+ child: ElevatedButton(
+ onPressed: _logueando ? null : (_esRegistro ? _registrarse : _iniciarSesion),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: color,
+ foregroundColor: Colors.white,
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
+ elevation: 0,
+ ),
+ child: _logueando
+ ? const SizedBox(width: 22, height: 22, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
+ : Text(_esRegistro ? 'Crear cuenta' : 'Entrar',
+ style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
+ ),
+ ),
+
+ const SizedBox(height: 20),
+ Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.06),
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: Text(
+ _esRegistro
+ ? 'Tu contraseña se almacena de forma segura con bcrypt. Minimo 6 caracteres.'
+ : 'Tus datos son privados y solo se usan para notificarte cuando el camion se acerca.',
+ style: TextStyle(fontSize: 11, color: color),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ const SizedBox(height: 20),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+// ================================================================
+// WIDGETS AUXILIARES
+// ================================================================
+
+class _Tab extends StatelessWidget {
+ final String label;
+ final bool seleccionado;
+ final Color color;
+ final VoidCallback onTap;
+
+ const _Tab({required this.label, required this.seleccionado, required this.color, required this.onTap});
+
+ @override
+ Widget build(BuildContext context) {
+ return Expanded(
+ child: GestureDetector(
+ onTap: onTap,
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 200),
+ margin: const EdgeInsets.all(4),
+ padding: const EdgeInsets.symmetric(vertical: 10),
+ decoration: BoxDecoration(
+ color: seleccionado ? Colors.white : Colors.transparent,
+ borderRadius: BorderRadius.circular(9),
+ boxShadow: seleccionado ? [BoxShadow(color: Colors.black.withValues(alpha: 0.08), blurRadius: 6)] : [],
+ ),
+ child: Text(label,
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ fontWeight: seleccionado ? FontWeight.bold : FontWeight.normal,
+ color: seleccionado ? color : Colors.grey.shade500,
+ fontSize: 13,
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _Campo extends StatelessWidget {
+ final TextEditingController ctrl;
+ final String label;
+ final IconData icon;
+ final bool obscure;
+ final TextInputType tipo;
+ final Widget? sufijo;
+
+ const _Campo({
+ required this.ctrl,
+ required this.label,
+ required this.icon,
+ this.obscure = false,
+ this.tipo = TextInputType.text,
+ this.sufijo,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return TextField(
+ controller: ctrl,
+ obscureText: obscure,
+ keyboardType: tipo,
+ textCapitalization: tipo == TextInputType.text ? TextCapitalization.words : TextCapitalization.none,
+ decoration: InputDecoration(
+ labelText: label,
+ prefixIcon: Icon(icon, size: 20),
+ suffixIcon: sufijo,
+ filled: true,
+ fillColor: Colors.white,
+ border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade200)),
+ enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade200)),
+ focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 1.5)),
+ contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
+ ),
+ );
+ }
+}
+
+class _IndicadorFortaleza extends StatelessWidget {
+ final String password;
+ const _IndicadorFortaleza({required this.password});
+
+ (int, String, Color) _evaluar() {
+ if (password.isEmpty) return (0, '', Colors.grey);
+ int pts = 0;
+ if (password.length >= 8) pts++;
+ if (password.contains(RegExp(r'[A-Z]'))) pts++;
+ if (password.contains(RegExp(r'[0-9]'))) pts++;
+ if (password.contains(RegExp(r'[!@#\$%^&*]'))) pts++;
+ if (pts <= 1) return (1, 'Debil', Colors.red);
+ if (pts == 2) return (2, 'Regular', Colors.orange);
+ if (pts == 3) return (3, 'Buena', Colors.lightGreen);
+ return (4, 'Excelente', Colors.green);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (password.isEmpty) return const SizedBox.shrink();
+ final (nivel, etiqueta, color) = _evaluar();
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(children: List.generate(4, (i) => Expanded(
+ child: Container(
+ margin: const EdgeInsets.only(right: 4),
+ height: 4,
+ decoration: BoxDecoration(
+ color: i < nivel ? color : Colors.grey.shade200,
+ borderRadius: BorderRadius.circular(2),
+ ),
+ ),
+ ))),
+ const SizedBox(height: 4),
+ Text('Contrasena: $etiqueta', style: TextStyle(fontSize: 11, color: color)),
+ ],
+ );
+ }
+}
diff --git a/HackOnLinces_app/aplicacion_hack/lib/screens/mapa_rutas_screen.dart b/HackOnLinces_app/aplicacion_hack/lib/screens/mapa_rutas_screen.dart
new file mode 100644
index 0000000..0044f19
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/lib/screens/mapa_rutas_screen.dart
@@ -0,0 +1,523 @@
+// ================================================================
+// lib/screens/mapa_rutas_screen.dart (v2 — flutter_map)
+// Mapa real de Celaya con rutas estilo metro sobre OpenStreetMap
+// ================================================================
+//
+// DEPENDENCIAS (agregar en pubspec.yaml):
+// flutter_map: ^7.0.2
+// latlong2: ^0.9.1
+//
+// SIN API KEY — usa tiles de OpenStreetMap (gratis, libre)
+//
+// FILTRADO:
+// Solo las rutas del usuario se ven en color vivo.
+// Las demás se muestran al 20% de opacidad.
+// ================================================================
+
+import 'package:flutter/material.dart';
+import 'package:flutter_map/flutter_map.dart';
+import 'package:latlong2/latlong.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+import '../services/api_service.dart';
+
+// ----------------------------------------------------------------
+// DATOS DE RUTAS
+// ----------------------------------------------------------------
+class _RutaInfo {
+ final String routeId;
+ final String nombre;
+ final String colonia;
+ final Color color;
+ final List puntos;
+
+ const _RutaInfo({
+ required this.routeId,
+ required this.nombre,
+ required this.colonia,
+ required this.color,
+ required this.puntos,
+ });
+}
+
+final _todasLasRutas = [
+ _RutaInfo(
+ routeId: 'RUTA-01',
+ nombre: 'Zona Centro - Las Arboledas',
+ colonia: 'Zona Centro',
+ color: const Color(0xFF1565C0),
+ puntos: [
+ LatLng(20.5111, -100.9037),
+ LatLng(20.5185, -100.8450),
+ LatLng(20.5215, -100.8142),
+ LatLng(20.5212, -100.8175),
+ LatLng(20.5210, -100.8210),
+ LatLng(20.5235, -100.8212),
+ LatLng(20.5260, -100.8215),
+ LatLng(20.5111, -100.9037),
+ ],
+ ),
+ _RutaInfo(
+ routeId: 'RUTA-03',
+ nombre: 'Sector Poniente - San Juanico',
+ colonia: 'San Juanico',
+ color: const Color(0xFF2E7D32),
+ puntos: [
+ LatLng(20.5111, -100.9037),
+ LatLng(20.5250, -100.8510),
+ LatLng(20.5290, -100.8320),
+ LatLng(20.5315, -100.8355),
+ LatLng(20.5340, -100.8390),
+ LatLng(20.5362, -100.8425),
+ LatLng(20.5330, -100.8430),
+ LatLng(20.5111, -100.9037),
+ ],
+ ),
+ _RutaInfo(
+ routeId: 'RUTA-04',
+ nombre: 'Oriente - Los Olivos',
+ colonia: 'Los Olivos',
+ color: const Color(0xFFE65100),
+ puntos: [
+ LatLng(20.5111, -100.9037),
+ LatLng(20.5260, -100.8010),
+ LatLng(20.5295, -100.7890),
+ LatLng(20.5320, -100.7850),
+ LatLng(20.5350, -100.7790),
+ LatLng(20.5310, -100.7760),
+ LatLng(20.5270, -100.7820),
+ LatLng(20.5111, -100.9037),
+ ],
+ ),
+ _RutaInfo(
+ routeId: 'RUTA-05',
+ nombre: 'Sector Sur - Rancho Seco',
+ colonia: 'Rancho Seco',
+ color: const Color(0xFF6A1B9A),
+ puntos: [
+ LatLng(20.5111, -100.9037),
+ LatLng(20.5050, -100.8620),
+ LatLng(20.5020, -100.8350),
+ LatLng(20.4995, -100.8210),
+ LatLng(20.4970, -100.8150),
+ LatLng(20.5010, -100.8120),
+ LatLng(20.5060, -100.8160),
+ LatLng(20.5111, -100.9037),
+ ],
+ ),
+ _RutaInfo(
+ routeId: 'RUTA-12',
+ nombre: 'Nororiente - Las Insurgentes',
+ colonia: 'Las Insurgentes',
+ color: const Color(0xFFC62828),
+ puntos: [
+ LatLng(20.5111, -100.9037),
+ LatLng(20.5280, -100.8080),
+ LatLng(20.5320, -100.7980),
+ LatLng(20.5340, -100.7940),
+ LatLng(20.5360, -100.7900),
+ LatLng(20.5310, -100.7920),
+ LatLng(20.5270, -100.8020),
+ LatLng(20.5111, -100.9037),
+ ],
+ ),
+ _RutaInfo(
+ routeId: 'RUTA-13',
+ nombre: 'Sector Norte - Trojes e Irrigación',
+ colonia: 'Trojes',
+ color: const Color(0xFF00838F),
+ puntos: [
+ LatLng(20.5111, -100.9037),
+ LatLng(20.5360, -100.8190),
+ LatLng(20.5420, -100.8080),
+ LatLng(20.5440, -100.8040),
+ LatLng(20.5460, -100.8000),
+ LatLng(20.5410, -100.8020),
+ LatLng(20.5370, -100.8120),
+ LatLng(20.5111, -100.9037),
+ ],
+ ),
+];
+
+const _coloniaARuta = {
+ 'Zona Centro': 'RUTA-01',
+ 'Las Arboledas': 'RUTA-01',
+ 'San Juanico': 'RUTA-03',
+ 'Los Olivos': 'RUTA-04',
+ 'Rancho Seco': 'RUTA-05',
+ 'Las Insurgentes': 'RUTA-12',
+ 'Trojes': 'RUTA-13',
+};
+
+// ================================================================
+// PANTALLA
+// ================================================================
+class MapaRutasScreen extends StatefulWidget {
+ const MapaRutasScreen({super.key});
+
+ @override
+ State createState() => _MapaRutasScreenState();
+}
+
+class _MapaRutasScreenState extends State {
+ Set _rutasDelUsuario = {};
+ List _estadosBackend = [];
+ String? _rutaSeleccionada;
+ bool _cargando = true;
+ bool _mostrarTodas = false;
+
+ final _mapController = MapController();
+ final ApiService _apiService = ApiService();
+
+ static const _centro = LatLng(20.5230, -100.8550);
+
+ @override
+ void initState() {
+ super.initState();
+ _cargarDatos();
+ }
+
+ Future _cargarDatos() async {
+ setState(() => _cargando = true);
+ final prefs = await SharedPreferences.getInstance();
+ final usuarioId = prefs.getInt('usuario_id');
+ if (usuarioId == null) {
+ if (mounted) setState(() => _cargando = false);
+ return;
+ }
+ try {
+ final rutas = await _apiService.obtenerRutas(usuarioId);
+ final usuario = await _apiService.obtenerUsuario(usuarioId);
+ final ids = {};
+ for (final d in usuario.direcciones) {
+ final r = _coloniaARuta[d.colonia];
+ if (r != null) ids.add(r);
+ }
+ for (final r in rutas) ids.add(r.routeId);
+ if (mounted) {
+ setState(() {
+ _estadosBackend = rutas;
+ _rutasDelUsuario = ids;
+ _cargando = false;
+ });
+ }
+ } catch (_) {
+ if (mounted) setState(() => _cargando = false);
+ }
+ }
+
+ RouteInfo? _estadoRuta(String routeId) {
+ try {
+ return _estadosBackend.firstWhere((r) => r.routeId == routeId);
+ } catch (_) {
+ return null;
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Mapa de rutas — Celaya'),
+ backgroundColor: const Color(0xFF1B5E20),
+ foregroundColor: Colors.white,
+ actions: [
+ IconButton(
+ icon: Icon(_mostrarTodas ? Icons.visibility_off : Icons.visibility),
+ tooltip: _mostrarTodas ? 'Solo mis rutas' : 'Ver todas',
+ onPressed: () => setState(() => _mostrarTodas = !_mostrarTodas),
+ ),
+ IconButton(
+ icon: const Icon(Icons.my_location),
+ onPressed: () => _mapController.move(_centro, 12.5),
+ ),
+ IconButton(icon: const Icon(Icons.refresh), onPressed: _cargarDatos),
+ ],
+ ),
+ body: _cargando
+ ? const Center(child: CircularProgressIndicator())
+ : Column(
+ children: [
+ Expanded(flex: 3, child: _buildMapa()),
+ _buildLeyenda(),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildMapa() {
+ return FlutterMap(
+ mapController: _mapController,
+ options: const MapOptions(
+ initialCenter: _centro,
+ initialZoom: 12.5,
+ minZoom: 11,
+ maxZoom: 17,
+ ),
+ children: [
+ TileLayer(
+ urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
+ userAgentPackageName: 'com.example.aplicacion_hack',
+ maxZoom: 19,
+ ),
+ PolylineLayer(polylines: _buildPolylines()),
+ MarkerLayer(markers: _buildMarkers()),
+ ],
+ );
+ }
+
+ List _buildPolylines() {
+ final list = [];
+ for (final ruta in _todasLasRutas) {
+ final esDelUsuario = _rutasDelUsuario.contains(ruta.routeId);
+ if (!_mostrarTodas && !esDelUsuario) continue;
+
+ final estado = _estadoRuta(ruta.routeId);
+ final posActual = estado?.lastPositionId ?? 0;
+ final seleccionada = _rutaSeleccionada == ruta.routeId;
+ final grosorBase = esDelUsuario ? 5.0 : 2.0;
+ final grosor = seleccionada ? grosorBase + 2 : grosorBase;
+
+ if (esDelUsuario && posActual > 1) {
+ // Tramo recorrido
+ final recorridos = ruta.puntos.take(posActual).toList();
+ if (recorridos.length >= 2) {
+ list.add(Polyline(
+ points: recorridos,
+ color: ruta.color,
+ strokeWidth: grosor,
+ ));
+ }
+ // Tramo pendiente (punteado)
+ final pendientes = ruta.puntos.skip(posActual - 1).toList();
+ if (pendientes.length >= 2) {
+ list.add(Polyline(
+ points: pendientes,
+ color: ruta.color.withValues(alpha: 0.35),
+ strokeWidth: grosor - 1,
+ pattern: StrokePattern.dashed(segments: [12, 8]),
+ ));
+ }
+ } else {
+ list.add(Polyline(
+ points: ruta.puntos,
+ color: esDelUsuario ? ruta.color : ruta.color.withValues(alpha: 0.2),
+ strokeWidth: grosor,
+ ));
+ }
+ }
+ return list;
+ }
+
+ List _buildMarkers() {
+ final list = [];
+
+ // Base / Relleno Sanitario
+ list.add(Marker(
+ point: LatLng(20.5111, -100.9037),
+ width: 70,
+ height: 36,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
+ decoration: BoxDecoration(
+ color: Colors.grey.shade800,
+ borderRadius: BorderRadius.circular(5),
+ ),
+ child: const Text('🏭 Base',
+ style: TextStyle(color: Colors.white, fontSize: 9, fontWeight: FontWeight.bold)),
+ ),
+ Container(width: 2, height: 4, color: Colors.grey.shade800),
+ Container(
+ width: 8, height: 8,
+ decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle),
+ ),
+ ],
+ ),
+ ));
+
+ for (final ruta in _todasLasRutas) {
+ final esDelUsuario = _rutasDelUsuario.contains(ruta.routeId);
+ if (!esDelUsuario) continue;
+
+ final estado = _estadoRuta(ruta.routeId);
+ final posActual = estado?.lastPositionId ?? 0;
+
+ for (int i = 1; i < ruta.puntos.length - 1; i++) {
+ final punto = ruta.puntos[i];
+ final posId = i + 1;
+ final esCamion = posId == posActual;
+
+ if (esCamion) {
+ list.add(Marker(
+ point: punto,
+ width: 46,
+ height: 54,
+ child: GestureDetector(
+ onTap: () => setState(() =>
+ _rutaSeleccionada = _rutaSeleccionada == ruta.routeId ? null : ruta.routeId),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
+ decoration: BoxDecoration(
+ color: ruta.color,
+ borderRadius: BorderRadius.circular(5),
+ boxShadow: [BoxShadow(color: ruta.color.withValues(alpha: 0.5), blurRadius: 6)],
+ ),
+ child: Text(
+ ruta.routeId.replaceAll('RUTA-', 'R'),
+ style: const TextStyle(color: Colors.white, fontSize: 8, fontWeight: FontWeight.bold),
+ ),
+ ),
+ const Text('🚛', style: TextStyle(fontSize: 20)),
+ ],
+ ),
+ ),
+ ));
+ } else {
+ list.add(Marker(
+ point: punto,
+ width: 14,
+ height: 14,
+ child: GestureDetector(
+ onTap: () => setState(() =>
+ _rutaSeleccionada = _rutaSeleccionada == ruta.routeId ? null : ruta.routeId),
+ child: Container(
+ decoration: BoxDecoration(
+ color: posId < posActual ? ruta.color : Colors.white,
+ shape: BoxShape.circle,
+ border: Border.all(color: ruta.color, width: 2.5),
+ boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 2)],
+ ),
+ ),
+ ),
+ ));
+ }
+ }
+ }
+ return list;
+ }
+
+ Widget _buildLeyenda() {
+ final rutasUsuario = _todasLasRutas
+ .where((r) => _rutasDelUsuario.contains(r.routeId))
+ .toList();
+
+ if (rutasUsuario.isEmpty) {
+ return Container(
+ height: 52,
+ color: const Color(0xFF1B5E20),
+ alignment: Alignment.center,
+ child: const Text('Sin rutas asignadas',
+ style: TextStyle(color: Colors.white54, fontSize: 13)),
+ );
+ }
+
+ return Container(
+ height: 96,
+ color: const Color(0xFF1B5E20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: const EdgeInsets.fromLTRB(14, 5, 0, 2),
+ child: Text(
+ _mostrarTodas ? 'Todas las rutas • Las tuyas están resaltadas' : 'Tus rutas — toca para centrar',
+ style: const TextStyle(color: Colors.white54, fontSize: 10),
+ ),
+ ),
+ Expanded(
+ child: ListView.builder(
+ scrollDirection: Axis.horizontal,
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3),
+ itemCount: rutasUsuario.length,
+ itemBuilder: (context, i) {
+ final ruta = rutasUsuario[i];
+ final estado = _estadoRuta(ruta.routeId);
+ final posActual = estado?.lastPositionId ?? 0;
+ final status = estado?.status ?? 'EN_RUTA';
+ final seleccionada = _rutaSeleccionada == ruta.routeId;
+
+ return GestureDetector(
+ onTap: () {
+ setState(() =>
+ _rutaSeleccionada = seleccionada ? null : ruta.routeId);
+ if (!seleccionada && ruta.puntos.length > 1) {
+ _mapController.move(ruta.puntos[1], 13.5);
+ }
+ },
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 200),
+ margin: const EdgeInsets.only(right: 8),
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
+ decoration: BoxDecoration(
+ color: seleccionada
+ ? ruta.color.withValues(alpha: 0.3)
+ : Colors.white.withValues(alpha: 0.08),
+ borderRadius: BorderRadius.circular(10),
+ border: Border.all(
+ color: seleccionada ? ruta.color : Colors.white24,
+ width: seleccionada ? 2 : 1,
+ ),
+ ),
+ child: ClipRect(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ width: 9, height: 9,
+ decoration: BoxDecoration(color: ruta.color, shape: BoxShape.circle),
+ ),
+ const SizedBox(width: 5),
+ Text(ruta.routeId,
+ style: TextStyle(color: ruta.color, fontWeight: FontWeight.bold, fontSize: 11)),
+ const SizedBox(width: 4),
+ Text(status == 'COMPLETADO' ? '✅' : '🚛',
+ style: const TextStyle(fontSize: 10)),
+ ],
+ ),
+ const SizedBox(height: 1),
+ SizedBox(
+ width: 115,
+ child: Text(ruta.colonia,
+ style: const TextStyle(color: Colors.white70, fontSize: 10),
+ overflow: TextOverflow.ellipsis),
+ ),
+ const SizedBox(height: 1),
+ SizedBox(
+ width: 115,
+ height: 3,
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(2),
+ child: LinearProgressIndicator(
+ value: (posActual / 8).clamp(0.0, 1.0),
+ backgroundColor: Colors.white12,
+ valueColor: AlwaysStoppedAnimation(ruta.color),
+ ),
+ ),
+ ),
+ const SizedBox(height: 1),
+ Text(
+ posActual > 0 ? 'Pos. $posActual / 8' : 'Sin datos',
+ style: TextStyle(color: ruta.color.withValues(alpha: 0.8), fontSize: 9),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/HackOnLinces_app/aplicacion_hack/lib/screens/reporte_screen.dart b/HackOnLinces_app/aplicacion_hack/lib/screens/reporte_screen.dart
new file mode 100644
index 0000000..e4005cf
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/lib/screens/reporte_screen.dart
@@ -0,0 +1,538 @@
+// ================================================================
+// lib/screens/reporte_screen.dart
+// Pantalla para que el ciudadano envíe reportes manuales
+// ================================================================
+//
+// NAVEGAR DESDE home_screen.dart:
+// Navigator.pushNamed(context, '/reporte')
+//
+// AGREGAR EN main.dart:
+// '/reporte': (context) => const ReporteScreen(),
+//
+// SECCIONES:
+// 1. Formulario de nuevo reporte
+// 2. Historial de reportes del usuario
+// ================================================================
+
+import 'dart:convert';
+import 'package:flutter/material.dart';
+import 'package:http/http.dart' as http;
+import 'package:shared_preferences/shared_preferences.dart';
+
+// ----------------------------------------------------------------
+// MODELOS
+// ----------------------------------------------------------------
+
+class ReporteInfo {
+ final int reporteId;
+ final String fecha;
+ final String hora;
+ final String colonia;
+ final String tipo;
+ final String? descripcion;
+ final String estado;
+
+ ReporteInfo({
+ required this.reporteId,
+ required this.fecha,
+ required this.hora,
+ required this.colonia,
+ required this.tipo,
+ this.descripcion,
+ required this.estado,
+ });
+
+ factory ReporteInfo.fromJson(Map j) => ReporteInfo(
+ reporteId: j['reporte_id'],
+ fecha: j['fecha'],
+ hora: j['hora'],
+ colonia: j['colonia'],
+ tipo: j['tipo'],
+ descripcion: j['descripcion'],
+ estado: j['estado'],
+ );
+}
+
+// ----------------------------------------------------------------
+// DATOS LOCALES
+// ----------------------------------------------------------------
+
+const _tiposReporte = [
+ {'valor': 'CAMION_NO_PASO', 'label': 'El camión no pasó', 'emoji': '🚫', 'color': 0xFFC62828},
+ {'valor': 'VOLUMEN_ALTO', 'label': 'Volumen inusualmente alto', 'emoji': '📦', 'color': 0xFFE65100},
+ {'valor': 'BASURA_FUERA_HORARIO', 'label': 'Basura fuera de horario', 'emoji': '⏰', 'color': 0xFFF57F17},
+ {'valor': 'OTRO', 'label': 'Otro problema', 'emoji': '📝', 'color': 0xFF37474F},
+];
+
+const _colonias = [
+ 'Zona Centro', 'Las Arboledas', 'Trojes',
+ 'San Juanico', 'Los Olivos', 'Rancho Seco', 'Las Insurgentes',
+];
+
+// ================================================================
+// PANTALLA PRINCIPAL
+// ================================================================
+class ReporteScreen extends StatefulWidget {
+ const ReporteScreen({super.key});
+
+ @override
+ State createState() => _ReporteScreenState();
+}
+
+class _ReporteScreenState extends State
+ with SingleTickerProviderStateMixin {
+ static const _baseUrl = 'http://192.168.198.55:8000';
+
+ late TabController _tabController;
+ int? _usuarioId;
+
+ // Formulario
+ String? _tipoSeleccionado;
+ String? _coloniaSeleccionada;
+ final _descController = TextEditingController();
+ bool _enviando = false;
+
+ // Historial
+ List _reportes = [];
+ bool _cargandoHistorial = true;
+
+ @override
+ void initState() {
+ super.initState();
+ _tabController = TabController(length: 2, vsync: this);
+ _cargarUsuario();
+ }
+
+ @override
+ void dispose() {
+ _tabController.dispose();
+ _descController.dispose();
+ super.dispose();
+ }
+
+ Future _cargarUsuario() async {
+ final prefs = await SharedPreferences.getInstance();
+ final id = prefs.getInt('usuario_id');
+ if (id != null && mounted) {
+ setState(() => _usuarioId = id);
+ _cargarHistorial();
+ }
+ }
+
+ Future _cargarHistorial() async {
+ if (_usuarioId == null) return;
+ setState(() => _cargandoHistorial = true);
+ try {
+ final resp = await http
+ .get(Uri.parse('$_baseUrl/api/reportes/usuario/$_usuarioId'))
+ .timeout(const Duration(seconds: 10));
+ if (resp.statusCode == 200 && mounted) {
+ final lista = json.decode(resp.body) as List;
+ setState(() {
+ _reportes = lista.map((r) => ReporteInfo.fromJson(r)).toList();
+ _cargandoHistorial = false;
+ });
+ } else {
+ if (mounted) setState(() => _cargandoHistorial = false);
+ }
+ } catch (_) {
+ if (mounted) setState(() => _cargandoHistorial = false);
+ }
+ }
+
+ Future _enviarReporte() async {
+ if (_tipoSeleccionado == null) {
+ _snack('Selecciona el tipo de problema.', error: true);
+ return;
+ }
+ if (_coloniaSeleccionada == null) {
+ _snack('Selecciona la colonia.', error: true);
+ return;
+ }
+ if (_usuarioId == null) {
+ _snack('No hay sesión activa.', error: true);
+ return;
+ }
+
+ setState(() => _enviando = true);
+
+ try {
+ final resp = await http.post(
+ Uri.parse('$_baseUrl/api/reportes?usuario_id=$_usuarioId'),
+ headers: {'Content-Type': 'application/json'},
+ body: json.encode({
+ 'colonia': _coloniaSeleccionada,
+ 'tipo': _tipoSeleccionado,
+ 'descripcion': _descController.text.trim().isEmpty
+ ? null
+ : _descController.text.trim(),
+ }),
+ ).timeout(const Duration(seconds: 10));
+
+ if (resp.statusCode == 200 && mounted) {
+ _snack('✅ Reporte enviado correctamente. ¡Gracias!');
+ setState(() {
+ _tipoSeleccionado = null;
+ _coloniaSeleccionada = null;
+ });
+ _descController.clear();
+ _cargarHistorial();
+ _tabController.animateTo(1); // ir al historial
+ } else {
+ final body = json.decode(resp.body);
+ _snack(body['detail'] ?? 'Error al enviar el reporte.', error: true);
+ }
+ } catch (e) {
+ _snack('Sin conexión al servidor.', error: true);
+ } finally {
+ if (mounted) setState(() => _enviando = false);
+ }
+ }
+
+ void _snack(String msg, {bool error = false}) {
+ if (!mounted) return;
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(
+ content: Text(msg),
+ backgroundColor: error ? Colors.red.shade700 : Colors.green.shade700,
+ behavior: SnackBarBehavior.floating,
+ ));
+ }
+
+ // ================================================================
+ // BUILD
+ // ================================================================
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: const Color(0xFFF5F7F5),
+ appBar: AppBar(
+ title: const Text('Reportes ciudadanos'),
+ backgroundColor: const Color(0xFF1B5E20),
+ foregroundColor: Colors.white,
+ bottom: TabBar(
+ controller: _tabController,
+ indicatorColor: Colors.white,
+ labelColor: Colors.white,
+ unselectedLabelColor: Colors.white60,
+ tabs: const [
+ Tab(icon: Icon(Icons.add_circle_outline), text: 'Nuevo reporte'),
+ Tab(icon: Icon(Icons.history), text: 'Mis reportes'),
+ ],
+ ),
+ ),
+ body: TabBarView(
+ controller: _tabController,
+ children: [_buildFormulario(), _buildHistorial()],
+ ),
+ );
+ }
+
+ // ================================================================
+ // TAB 1: FORMULARIO
+ // ================================================================
+ Widget _buildFormulario() {
+ return SingleChildScrollView(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Encabezado
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: const Color(0xFF2E7D32).withValues(alpha: 0.08),
+ borderRadius: BorderRadius.circular(14),
+ border: Border.all(color: const Color(0xFF2E7D32).withValues(alpha: 0.2)),
+ ),
+ child: const Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('📋 Envía un reporte',
+ style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
+ SizedBox(height: 6),
+ Text(
+ 'Tu reporte se guarda en la base de datos y ayuda a mejorar '
+ 'la logística de recolección en tu colonia.',
+ style: TextStyle(fontSize: 13, color: Colors.black54, height: 1.4),
+ ),
+ ],
+ ),
+ ),
+
+ const SizedBox(height: 24),
+ _Label('¿Qué problema ocurrió?'),
+ const SizedBox(height: 10),
+
+ // Selector de tipo
+ ...(_tiposReporte.map((tipo) {
+ final seleccionado = _tipoSeleccionado == tipo['valor'];
+ final color = Color(tipo['color'] as int);
+ return GestureDetector(
+ onTap: () => setState(() => _tipoSeleccionado = tipo['valor'] as String),
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 150),
+ margin: const EdgeInsets.only(bottom: 10),
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
+ decoration: BoxDecoration(
+ color: seleccionado ? color.withValues(alpha: 0.1) : Colors.white,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(
+ color: seleccionado ? color : Colors.grey.shade200,
+ width: seleccionado ? 2 : 1,
+ ),
+ boxShadow: seleccionado
+ ? [BoxShadow(color: color.withValues(alpha: 0.15), blurRadius: 8)]
+ : [BoxShadow(color: Colors.black.withValues(alpha: 0.04), blurRadius: 4)],
+ ),
+ child: Row(
+ children: [
+ Text(tipo['emoji'] as String, style: const TextStyle(fontSize: 22)),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Text(
+ tipo['label'] as String,
+ style: TextStyle(
+ fontSize: 14,
+ fontWeight: seleccionado ? FontWeight.bold : FontWeight.normal,
+ color: seleccionado ? color : Colors.black87,
+ ),
+ ),
+ ),
+ if (seleccionado)
+ Icon(Icons.check_circle, color: color, size: 20),
+ ],
+ ),
+ ),
+ );
+ })),
+
+ const SizedBox(height: 20),
+ _Label('¿En qué colonia?'),
+ const SizedBox(height: 10),
+
+ // Selector de colonia
+ Container(
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: Colors.grey.shade200),
+ ),
+ child: DropdownButtonFormField(
+ value: _coloniaSeleccionada,
+ hint: const Text('Selecciona tu colonia'),
+ decoration: const InputDecoration(
+ prefixIcon: Icon(Icons.location_city_outlined),
+ border: InputBorder.none,
+ contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
+ ),
+ items: _colonias
+ .map((c) => DropdownMenuItem(value: c, child: Text(c)))
+ .toList(),
+ onChanged: (v) => setState(() => _coloniaSeleccionada = v),
+ ),
+ ),
+
+ const SizedBox(height: 20),
+ _Label('Descripción adicional (opcional)'),
+ const SizedBox(height: 10),
+
+ Container(
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: Colors.grey.shade200),
+ ),
+ child: TextField(
+ controller: _descController,
+ maxLines: 4,
+ maxLength: 300,
+ decoration: const InputDecoration(
+ hintText: 'Describe el problema con más detalle...',
+ border: InputBorder.none,
+ contentPadding: EdgeInsets.all(16),
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 28),
+
+ // Botón enviar
+ SizedBox(
+ width: double.infinity,
+ height: 54,
+ child: ElevatedButton.icon(
+ onPressed: _enviando ? null : _enviarReporte,
+ icon: _enviando
+ ? const SizedBox(
+ width: 20, height: 20,
+ child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
+ : const Icon(Icons.send_rounded),
+ label: Text(_enviando ? 'Enviando...' : 'Enviar reporte'),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: const Color(0xFF2E7D32),
+ foregroundColor: Colors.white,
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
+ textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
+ ),
+ ),
+ ),
+
+ const SizedBox(height: 16),
+ Center(
+ child: Text(
+ 'Tu reporte es anónimo para el operador y ayuda\na mejorar el servicio de recolección.',
+ textAlign: TextAlign.center,
+ style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
+ ),
+ ),
+ const SizedBox(height: 20),
+ ],
+ ),
+ );
+ }
+
+ // ================================================================
+ // TAB 2: HISTORIAL
+ // ================================================================
+ Widget _buildHistorial() {
+ if (_cargandoHistorial) {
+ return const Center(child: CircularProgressIndicator());
+ }
+
+ if (_reportes.isEmpty) {
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.inbox_outlined, size: 72, color: Colors.grey.shade300),
+ const SizedBox(height: 16),
+ const Text('No has enviado reportes aún',
+ style: TextStyle(fontSize: 16, color: Colors.grey)),
+ const SizedBox(height: 8),
+ const Text('Usa la pestaña anterior para reportar un problema.',
+ style: TextStyle(fontSize: 13, color: Colors.grey)),
+ ],
+ ),
+ );
+ }
+
+ return RefreshIndicator(
+ onRefresh: _cargarHistorial,
+ child: ListView.builder(
+ padding: const EdgeInsets.all(16),
+ itemCount: _reportes.length,
+ itemBuilder: (context, i) => _TarjetaReporte(reporte: _reportes[i]),
+ ),
+ );
+ }
+}
+
+// ================================================================
+// WIDGETS AUXILIARES
+// ================================================================
+
+class _Label extends StatelessWidget {
+ final String text;
+ const _Label(this.text);
+
+ @override
+ Widget build(BuildContext context) => Text(
+ text,
+ style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
+ );
+}
+
+class _TarjetaReporte extends StatelessWidget {
+ final ReporteInfo reporte;
+ const _TarjetaReporte({required this.reporte});
+
+ Map get _tipoInfo {
+ return _tiposReporte.firstWhere(
+ (t) => t['valor'] == reporte.tipo,
+ orElse: () => {'label': reporte.tipo, 'emoji': '📝', 'color': 0xFF37474F},
+ );
+ }
+
+ Color get _colorEstado => reporte.estado == 'ATENDIDO'
+ ? const Color(0xFF2E7D32)
+ : const Color(0xFFE65100);
+
+ @override
+ Widget build(BuildContext context) {
+ final info = _tipoInfo;
+ final color = Color(info['color'] as int);
+
+ return Container(
+ margin: const EdgeInsets.only(bottom: 12),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(14),
+ boxShadow: [
+ BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 6)
+ ],
+ border: Border(left: BorderSide(color: color, width: 4)),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.all(14),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Text(info['emoji'] as String, style: const TextStyle(fontSize: 20)),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(info['label'] as String,
+ style: TextStyle(
+ fontWeight: FontWeight.bold, color: color, fontSize: 14)),
+ ),
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
+ decoration: BoxDecoration(
+ color: _colorEstado.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Text(
+ reporte.estado == 'ATENDIDO' ? '✅ Atendido' : '⏳ Pendiente',
+ style: TextStyle(
+ color: _colorEstado, fontSize: 11, fontWeight: FontWeight.bold),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ Icon(Icons.location_city_outlined, size: 14, color: Colors.grey.shade500),
+ const SizedBox(width: 4),
+ Text(reporte.colonia,
+ style: TextStyle(color: Colors.grey.shade600, fontSize: 13)),
+ const SizedBox(width: 16),
+ Icon(Icons.calendar_today_outlined, size: 14, color: Colors.grey.shade500),
+ const SizedBox(width: 4),
+ Text('${reporte.fecha} ${reporte.hora.substring(0, 5)}',
+ style: TextStyle(color: Colors.grey.shade600, fontSize: 13)),
+ ],
+ ),
+ if (reporte.descripcion != null && reporte.descripcion!.isNotEmpty) ...[
+ const SizedBox(height: 8),
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(10),
+ decoration: BoxDecoration(
+ color: Colors.grey.shade50,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Text(
+ reporte.descripcion!,
+ style: TextStyle(fontSize: 13, color: Colors.grey.shade700, height: 1.4),
+ ),
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/HackOnLinces_app/aplicacion_hack/lib/screens/route_list_screen.dart b/HackOnLinces_app/aplicacion_hack/lib/screens/route_list_screen.dart
new file mode 100644
index 0000000..f8ae98a
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/lib/screens/route_list_screen.dart
@@ -0,0 +1,354 @@
+import 'package:flutter/material.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+import '../services/api_service.dart';
+
+class RouteListScreen extends StatefulWidget {
+ const RouteListScreen({super.key});
+
+ @override
+ State createState() => _RouteListScreenState();
+}
+
+class _RouteListScreenState extends State {
+ final ApiService _apiService = ApiService();
+ bool _cargando = true;
+ bool _avanzando = false;
+ String? _error;
+ List _rutas = [];
+ int? _usuarioId;
+
+ @override
+ void initState() {
+ super.initState();
+ _cargarUsuarioYRutas();
+ }
+
+ Future _cargarUsuarioYRutas() async {
+ final prefs = await SharedPreferences.getInstance();
+ final usuarioId = prefs.getInt('usuario_id');
+
+ if (usuarioId == null) {
+ if (mounted) {
+ setState(() {
+ _error =
+ 'No se encontró sesión activa. Por favor inicia sesión de nuevo.';
+ _cargando = false;
+ });
+ }
+ return;
+ }
+
+ _usuarioId = usuarioId;
+ await _cargarRutas();
+ }
+
+ Future _cargarRutas() async {
+ setState(() {
+ _cargando = true;
+ _error = null;
+ });
+
+ if (_usuarioId == null) {
+ if (mounted) {
+ setState(() {
+ _error =
+ 'No se encontró sesión activa. Por favor inicia sesión de nuevo.';
+ _cargando = false;
+ });
+ }
+ return;
+ }
+
+ try {
+ final rutas = await _apiService.obtenerRutas(_usuarioId!);
+ if (mounted) {
+ setState(() {
+ _rutas = rutas;
+ });
+ }
+ } catch (e) {
+ if (mounted) {
+ setState(() {
+ _error = 'No se pudieron cargar las rutas. Verifica el backend.';
+ });
+ }
+ } finally {
+ if (mounted) {
+ setState(() {
+ _cargando = false;
+ });
+ }
+ }
+ }
+
+ Future _simularAvance(String routeId) async {
+ setState(() {
+ _avanzando = true;
+ });
+
+ if (_usuarioId == null) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content:
+ Text('No se encontró sesión activa. Inicia sesión de nuevo.'),
+ backgroundColor: Colors.redAccent,
+ ),
+ );
+ }
+ return;
+ }
+
+ try {
+ final rutaActualizada =
+ await _apiService.avanzarRuta(routeId, _usuarioId!);
+ if (mounted) {
+ setState(() {
+ final index = _rutas.indexWhere((r) => r.routeId == routeId);
+ if (index >= 0) {
+ _rutas[index] = rutaActualizada;
+ }
+ });
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(
+ 'La ruta ${rutaActualizada.routeId} avanzó al siguiente tramo.'),
+ duration: const Duration(seconds: 3),
+ ),
+ );
+ }
+ } catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Error al simular avance: $e'),
+ backgroundColor: Colors.red.shade700,
+ ),
+ );
+ }
+ } finally {
+ if (mounted) {
+ setState(() {
+ _avanzando = false;
+ });
+ }
+ }
+ }
+
+ Color _colorEstado(String status) {
+ switch (status) {
+ case 'COMPLETADO':
+ return Colors.blue.shade600;
+ case 'EN_RUTA':
+ return Colors.green.shade600;
+ default:
+ return Colors.orange.shade600;
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Rutas y estado del camión'),
+ actions: [
+ IconButton(
+ onPressed: () => Navigator.pushNamed(context, '/mapa'),
+ icon: const Icon(Icons.map_rounded),
+ tooltip: 'Ver mapa de rutas',
+ ),
+ IconButton(
+ onPressed: _cargarRutas,
+ icon: const Icon(Icons.refresh),
+ ),
+ ],
+ ),
+ body: _cargando
+ ? const Center(child: CircularProgressIndicator())
+ : _error != null
+ ? Center(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 24),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(Icons.error_outline,
+ size: 72, color: Colors.redAccent),
+ const SizedBox(height: 16),
+ Text(
+ _error!,
+ textAlign: TextAlign.center,
+ style: const TextStyle(fontSize: 16),
+ ),
+ const SizedBox(height: 20),
+ ElevatedButton(
+ onPressed: _cargarRutas,
+ child: const Text('Reintentar'),
+ ),
+ ],
+ ),
+ ),
+ )
+ : RefreshIndicator(
+ onRefresh: _cargarRutas,
+ child: ListView(
+ padding: const EdgeInsets.all(16),
+ children: [
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(18),
+ decoration: BoxDecoration(
+ color: Colors.green.shade50,
+ borderRadius: BorderRadius.circular(16),
+ border: Border.all(color: Colors.green.shade200),
+ ),
+ child: const Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Simulación de rutas',
+ style: TextStyle(
+ fontSize: 18, fontWeight: FontWeight.bold),
+ ),
+ SizedBox(height: 10),
+ Text(
+ 'Esta pantalla muestra el estado actual de cada camión y permite simular el siguiente tramo de la ruta para pruebas.',
+ style: TextStyle(fontSize: 14),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 16),
+ ..._rutas.map((ruta) {
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 14),
+ child: Card(
+ elevation: 4,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16)),
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Expanded(
+ child: Text(
+ ruta.name,
+ style: const TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold),
+ ),
+ ),
+ Chip(
+ label: Text(ruta.status),
+ backgroundColor:
+ _colorEstado(ruta.status)
+ .withValues(alpha: 0.15),
+ labelStyle: TextStyle(
+ color: _colorEstado(ruta.status),
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 10),
+ Wrap(
+ runSpacing: 6,
+ spacing: 8,
+ children: [
+ _buildTag('Ruta: ${ruta.routeId}',
+ Colors.grey.shade200),
+ _buildTag(
+ 'Posición: ${ruta.lastPositionId}',
+ Colors.blue.shade50),
+ _buildTag(
+ ruta.gpsOk
+ ? 'GPS OK'
+ : 'GPS desconectado',
+ ruta.gpsOk
+ ? Colors.green.shade50
+ : Colors.red.shade50,
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Text(
+ 'Último reporte: ${ruta.lastTimestamp}',
+ style: TextStyle(
+ color: Colors.grey.shade700,
+ fontSize: 13),
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton.icon(
+ onPressed: ruta.status == 'COMPLETADO' ||
+ _avanzando
+ ? null
+ : () => _simularAvance(ruta.routeId),
+ icon: const Icon(Icons.play_arrow),
+ label: const Text('Simular avance'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }),
+ const SizedBox(height: 16),
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(18),
+ decoration: BoxDecoration(
+ color: Colors.grey.shade100,
+ borderRadius: BorderRadius.circular(16),
+ border: Border.all(color: Colors.grey.shade300),
+ ),
+ child: const Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Información relevante',
+ style: TextStyle(
+ fontSize: 17, fontWeight: FontWeight.bold),
+ ),
+ SizedBox(height: 12),
+ Text(
+ '• Separa correctamente orgánicos, reciclables y no reciclables.',
+ style: TextStyle(fontSize: 14)),
+ SizedBox(height: 8),
+ Text(
+ '• No mezcles basura húmeda con envases secos y mantén los líquidos controlados.',
+ style: TextStyle(fontSize: 14)),
+ SizedBox(height: 8),
+ Text(
+ '• Saca tu basura cuando el camión esté cerca: así evitamos plagas y malos olores.',
+ style: TextStyle(fontSize: 14)),
+ SizedBox(height: 8),
+ Text(
+ '• Usa bolsas resistentes y cierra bien los residuos antes de ponerlos en la acera.',
+ style: TextStyle(fontSize: 14)),
+ ],
+ ),
+ ),
+ const SizedBox(height: 24),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildTag(String text, Color color) {
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
+ decoration: BoxDecoration(
+ color: color,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Text(
+ text,
+ style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
+ ),
+ );
+ }
+}
diff --git a/HackOnLinces_app/aplicacion_hack/lib/services/api_service.dart b/HackOnLinces_app/aplicacion_hack/lib/services/api_service.dart
new file mode 100644
index 0000000..d44912e
--- /dev/null
+++ b/HackOnLinces_app/aplicacion_hack/lib/services/api_service.dart
@@ -0,0 +1,483 @@
+// ================================================================
+// lib/services/api_service.dart (v2)
+// Servicio de comunicación con el backend FastAPI
+// ================================================================
+//
+// CAMBIOS v2:
+// - LoginResponse incluye 'nombre' del usuario
+// - ActualizarPassword requiere password_actual + password_nuevo
+// - Nuevos métodos: obtenerDashboard, historialPosiciones,
+// resumenRutas, estadisticasColonias
+// ================================================================
+
+import 'dart:convert';
+import 'package:http/http.dart' as http;
+
+// ----------------------------------------------------------------
+// MODELOS
+// ----------------------------------------------------------------
+
+class ETAInfo {
+ final int usuarioId;
+ final String colonia;
+ final String rutaNombre;
+ final String rutaStatus;
+ final bool gpsOk;
+ final String etaTexto;
+ final int etaMinutos;
+ final String mensajePreventivo;
+
+ ETAInfo({
+ required this.usuarioId,
+ required this.colonia,
+ required this.rutaNombre,
+ required this.rutaStatus,
+ required this.gpsOk,
+ required this.etaTexto,
+ required this.etaMinutos,
+ required this.mensajePreventivo,
+ });
+
+ factory ETAInfo.fromJson(Map json) {
+ return ETAInfo(
+ usuarioId: json['usuario_id'],
+ colonia: json['colonia'],
+ rutaNombre: json['ruta_nombre'] ?? '',
+ rutaStatus: json['ruta_status'] ?? '',
+ gpsOk: json['gps_ok'] ?? true,
+ etaTexto: json['eta_texto'],
+ etaMinutos: json['eta_minutos'],
+ mensajePreventivo: json['mensaje_preventivo'],
+ );
+ }
+}
+
+class DireccionInfo {
+ final String colonia;
+ final String direccion;
+
+ DireccionInfo({required this.colonia, required this.direccion});
+
+ factory DireccionInfo.fromJson(Map json) {
+ return DireccionInfo(colonia: json['colonia'], direccion: json['direccion']);
+ }
+}
+
+class UsuarioInfo {
+ final int usuarioId;
+ final String nombre;
+ final String email;
+ final List direcciones;
+
+ UsuarioInfo({
+ required this.usuarioId,
+ required this.nombre,
+ required this.email,
+ required this.direcciones,
+ });
+
+ factory UsuarioInfo.fromJson(Map json) {
+ return UsuarioInfo(
+ usuarioId: json['usuario_id'],
+ nombre: json['nombre'],
+ email: json['email'],
+ direcciones: List