Actualizacion del programa
@@ -15,10 +15,11 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.example.celaya_limpia"
|
applicationId = "com.example.celaya_limpia"
|
||||||
minSdk = 21
|
minSdk = flutter.minSdkVersion
|
||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
versionCode = flutter.versionCode
|
versionCode = flutter.versionCode
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
|
multiDexEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.CAMERA"/>
|
<uses-permission android:name="android.permission.CAMERA"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
@@ -27,6 +28,10 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<!-- Google Maps API Key - Reemplaza con tu clave real -->
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.geo.API_KEY"
|
||||||
|
android:value="TU_API_KEY_DE_GOOGLE_MAPS_AQUI"/>
|
||||||
<meta-data android:name="flutterEmbedding" android:value="2"/>
|
<meta-data android:name="flutterEmbedding" android:value="2"/>
|
||||||
</application>
|
</application>
|
||||||
<queries>
|
<queries>
|
||||||
|
|||||||
90
celaya_limpia/README.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# 🗑️ Celaya Limpia — Sistema Integral de Recolección de Residuos
|
||||||
|
|
||||||
|
## H. Ayuntamiento de Celaya, Guanajuato
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👥 Roles del Sistema
|
||||||
|
|
||||||
|
### 🏠 CIUDADANO
|
||||||
|
- ETA del camión recolector en tiempo real
|
||||||
|
- **Mapa visible SOLO cuando el camión está a <15 min**
|
||||||
|
- Guía de separación de residuos (sin internet)
|
||||||
|
- Clasificador de residuos con IA (cámara)
|
||||||
|
- Reporte de incidencias
|
||||||
|
|
||||||
|
### 🚛 CONDUCTOR
|
||||||
|
- Vista de su ruta asignada por día
|
||||||
|
- Mapa de su ruta específica
|
||||||
|
- Alertas de GPS desactivado
|
||||||
|
- Horario semanal asignado por el admin
|
||||||
|
|
||||||
|
### ⚙️ ADMINISTRADOR
|
||||||
|
- Mapa de TODAS las rutas simultáneamente
|
||||||
|
- Control de estado de rutas (cancelar, marcar falla, retraso)
|
||||||
|
- Gestión de reportes ciudadanos
|
||||||
|
- Asignación de rutas a conductores por día/turno
|
||||||
|
- Panel de alertas (GPS perdido, camión detenido +30min)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 Cuentas Demo
|
||||||
|
|
||||||
|
| Rol | Email | Contraseña |
|
||||||
|
|-----|-------|-----------|
|
||||||
|
| Administrador | admin@celaya.gob.mx | admin123 |
|
||||||
|
| Conductor | conductor@celaya.gob.mx | conductor123 |
|
||||||
|
| Ciudadano | Crear desde la app | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Cómo ejecutar
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter pub get
|
||||||
|
flutter run
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤖 IA de Clasificación de Residuos
|
||||||
|
|
||||||
|
1. Convierte tu modelo: `waste_clasification.h5` → `waste_model.tflite`
|
||||||
|
2. Coloca el archivo en: `assets/models/waste_model.tflite`
|
||||||
|
3. El modelo clasifica: **Orgánico (0) / Inorgánico (1)**
|
||||||
|
|
||||||
|
Script de conversión:
|
||||||
|
```python
|
||||||
|
import tensorflow as tf
|
||||||
|
model = tf.keras.models.load_model('waste_clasification.h5')
|
||||||
|
converter = tf.lite.TFLiteConverter.from_keras_model(model)
|
||||||
|
tflite_model = converter.convert()
|
||||||
|
with open('assets/models/waste_model.tflite', 'wb') as f:
|
||||||
|
f.write(tflite_model)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗺️ Mapas
|
||||||
|
|
||||||
|
Usa **OpenStreetMap** (gratuito, sin API Key)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔔 Notificaciones Simuladas
|
||||||
|
|
||||||
|
| Evento | Cuándo |
|
||||||
|
|--------|--------|
|
||||||
|
| 🚛 Ruta Iniciada | posición 1→2 |
|
||||||
|
| ⚠️ Camión Cercano | posición 4 (~15 min) |
|
||||||
|
| 🏁 Servicio Finalizado | posición 8 |
|
||||||
|
| 📡 GPS Perdido | Admin lo activa manualmente |
|
||||||
|
| ⚠️ Camión Detenido | Sin movimiento 30+ min |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Rutas disponibles
|
||||||
|
|
||||||
|
- 15 rutas con GPS real de Celaya
|
||||||
|
- Turnos: Matutino, Vespertino, Nocturno
|
||||||
|
- 40+ colonias mapeadas
|
||||||
35
celaya_limpia/SETUP_GOOGLE_MAPS.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Configuración de Google Maps
|
||||||
|
|
||||||
|
## 1. Obtener tu API Key (GRATIS para desarrollo)
|
||||||
|
|
||||||
|
1. Ve a https://console.cloud.google.com
|
||||||
|
2. Crea un proyecto nuevo o selecciona uno existente
|
||||||
|
3. Activa las siguientes APIs:
|
||||||
|
- **Maps SDK for Android**
|
||||||
|
- **Maps SDK for iOS** (si lo necesitas)
|
||||||
|
4. Ve a "Credenciales" → "Crear credencial" → "Clave de API"
|
||||||
|
5. Copia tu clave
|
||||||
|
|
||||||
|
## 2. Agregar la clave al proyecto
|
||||||
|
|
||||||
|
Abre este archivo:
|
||||||
|
```
|
||||||
|
android/app/src/main/AndroidManifest.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
Busca esta línea y reemplaza `TU_API_KEY_DE_GOOGLE_MAPS_AQUI` con tu clave real:
|
||||||
|
```xml
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.geo.API_KEY"
|
||||||
|
android:value="TU_API_KEY_DE_GOOGLE_MAPS_AQUI"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Restricciones recomendadas (seguridad)
|
||||||
|
|
||||||
|
En Google Cloud Console, restringe tu API key a:
|
||||||
|
- **Aplicaciones Android** → tu package name: `com.example.celaya_limpia`
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
- El mapa funcionará sin restricciones durante desarrollo
|
||||||
|
- La capa de tráfico en tiempo real requiere que actives "Maps SDK for Android"
|
||||||
|
- El plan gratuito incluye $200 USD mensuales (suficiente para esta app)
|
||||||
28
celaya_limpia/analysis_options.yaml
Normal file
@@ -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
|
||||||
40
celaya_limpia/android/app/build.gradle.kts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.example.celaya_limpia"
|
||||||
|
compileSdk = flutter.compileSdkVersion
|
||||||
|
ndkVersion = "28.2.13676358"
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "com.example.celaya_limpia"
|
||||||
|
minSdk = 21
|
||||||
|
targetSdk = flutter.targetSdkVersion
|
||||||
|
versionCode = flutter.versionCode
|
||||||
|
versionName = flutter.versionName
|
||||||
|
multiDexEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source = "../.."
|
||||||
|
}
|
||||||
7
celaya_limpia/android/app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
43
celaya_limpia/android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.CAMERA"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:label="Celaya Limpia"
|
||||||
|
android:name="${applicationName}"
|
||||||
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<meta-data android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
|
android:resource="@style/NormalTheme"/>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<!-- Google Maps API Key - Reemplaza con tu clave real -->
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.geo.API_KEY"
|
||||||
|
android:value="TU_API_KEY_DE_GOOGLE_MAPS_AQUI"/>
|
||||||
|
<meta-data android:name="flutterEmbedding" android:value="2"/>
|
||||||
|
</application>
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
|
<data android:mimeType="text/plain"/>
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package io.flutter.plugins;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import io.flutter.Log;
|
||||||
|
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generated file. Do not edit.
|
||||||
|
* This file is generated by the Flutter tool based on the
|
||||||
|
* plugins that support the Android platform.
|
||||||
|
*/
|
||||||
|
@Keep
|
||||||
|
public final class GeneratedPluginRegistrant {
|
||||||
|
private static final String TAG = "GeneratedPluginRegistrant";
|
||||||
|
public static void registerWith(@NonNull FlutterEngine flutterEngine) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.example.ejemplo
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity()
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="?android:colorBackground" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@android:color/white" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
18
celaya_limpia/android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
35
celaya_limpia/android/build.gradle.kts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
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)
|
||||||
|
layout.buildDirectory.value(newSubprojectBuildDir)
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(":app")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Delete>("clean") {
|
||||||
|
delete(rootProject.layout.buildDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forzar Java 17 en todos los subproyectos (plugins)
|
||||||
|
gradle.projectsEvaluated {
|
||||||
|
subprojects {
|
||||||
|
tasks.withType<JavaCompile>().configureEach {
|
||||||
|
sourceCompatibility = "17"
|
||||||
|
targetCompatibility = "17"
|
||||||
|
}
|
||||||
|
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
celaya_limpia/android/gradle.properties
Normal file
@@ -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
|
||||||
BIN
celaya_limpia/android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
5
celaya_limpia/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -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
|
||||||
160
celaya_limpia/android/gradlew
vendored
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS=""
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn ( ) {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die ( ) {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin, switch paths to Windows format before running java
|
||||||
|
if $cygwin ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
(0) set -- ;;
|
||||||
|
(1) set -- "$args0" ;;
|
||||||
|
(2) set -- "$args0" "$args1" ;;
|
||||||
|
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
||||||
|
function splitJvmOpts() {
|
||||||
|
JVM_OPTS=("$@")
|
||||||
|
}
|
||||||
|
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||||
|
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
||||||
90
celaya_limpia/android/gradlew.bat
vendored
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS=
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:init
|
||||||
|
@rem Get command-line arguments, handling Windowz variants
|
||||||
|
|
||||||
|
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||||
|
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||||
|
|
||||||
|
:win9xME_args
|
||||||
|
@rem Slurp the command line arguments.
|
||||||
|
set CMD_LINE_ARGS=
|
||||||
|
set _SKIP=2
|
||||||
|
|
||||||
|
:win9xME_args_slurp
|
||||||
|
if "x%~1" == "x" goto execute
|
||||||
|
|
||||||
|
set CMD_LINE_ARGS=%*
|
||||||
|
goto execute
|
||||||
|
|
||||||
|
:4NT_args
|
||||||
|
@rem Get arguments from the 4NT Shell from JP Software
|
||||||
|
set CMD_LINE_ARGS=%$
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
2
celaya_limpia/android/local.properties
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
sdk.dir=C:\\Users\\benga\\AppData\\Local\\Android\\sdk
|
||||||
|
flutter.sdk=C:\\flutter
|
||||||
26
celaya_limpia/android/settings.gradle.kts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
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
|
||||||
|
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include(":app")
|
||||||
1
celaya_limpia/assets/models/README.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
placeholder - add waste_model.tflite here
|
||||||
2
celaya_limpia/assets/models/labels.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Organico
|
||||||
|
Inorganico
|
||||||
24
celaya_limpia/ios/Flutter/AppFrameworkInfo.plist
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>io.flutter.flutter.app</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>FMWK</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
1
celaya_limpia/ios/Flutter/Debug.xcconfig
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#include "Generated.xcconfig"
|
||||||
15
celaya_limpia/ios/Flutter/Generated.xcconfig
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// This is a generated file; do not edit or check into version control.
|
||||||
|
FLUTTER_ROOT=C:\flutter
|
||||||
|
FLUTTER_APPLICATION_PATH=C:\Users\benga\OneDrive\Documentos\app\ejemplo
|
||||||
|
FLUTTER_FRAMEWORK_SWIFT_PACKAGE_PATH=C:\Users\benga\OneDrive\Documentos\app\ejemplo\ios\Flutter\ephemeral\Packages\.packages\FlutterFramework
|
||||||
|
COCOAPODS_PARALLEL_CODE_SIGN=true
|
||||||
|
FLUTTER_TARGET=lib\main.dart
|
||||||
|
FLUTTER_BUILD_DIR=build
|
||||||
|
FLUTTER_BUILD_NAME=1.0.0
|
||||||
|
FLUTTER_BUILD_NUMBER=1
|
||||||
|
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386
|
||||||
|
EXCLUDED_ARCHS[sdk=iphoneos*]=armv7
|
||||||
|
DART_OBFUSCATION=false
|
||||||
|
TRACK_WIDGET_CREATION=true
|
||||||
|
TREE_SHAKE_ICONS=false
|
||||||
|
PACKAGE_CONFIG=.dart_tool/package_config.json
|
||||||
1
celaya_limpia/ios/Flutter/Release.xcconfig
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#include "Generated.xcconfig"
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// swift-tools-version: 5.9
|
||||||
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "FlutterGeneratedPluginSwiftPackage",
|
||||||
|
platforms: [
|
||||||
|
.iOS("13.0")
|
||||||
|
],
|
||||||
|
products: [
|
||||||
|
.library(name: "FlutterGeneratedPluginSwiftPackage", type: .static, targets: ["FlutterGeneratedPluginSwiftPackage"])
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
.target(
|
||||||
|
name: "FlutterGeneratedPluginSwiftPackage"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
32
celaya_limpia/ios/Flutter/ephemeral/flutter_lldb_helper.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#
|
||||||
|
# Generated file, do not edit.
|
||||||
|
#
|
||||||
|
|
||||||
|
import lldb
|
||||||
|
|
||||||
|
def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict):
|
||||||
|
"""Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages."""
|
||||||
|
base = frame.register["x0"].GetValueAsAddress()
|
||||||
|
page_len = frame.register["x1"].GetValueAsUnsigned()
|
||||||
|
|
||||||
|
# Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the
|
||||||
|
# first page to see if handled it correctly. This makes diagnosing
|
||||||
|
# misconfiguration (e.g. missing breakpoint) easier.
|
||||||
|
data = bytearray(page_len)
|
||||||
|
data[0:8] = b'IHELPED!'
|
||||||
|
|
||||||
|
error = lldb.SBError()
|
||||||
|
frame.GetThread().GetProcess().WriteMemory(base, data, error)
|
||||||
|
if not error.Success():
|
||||||
|
print(f'Failed to write into {base}[+{page_len}]', error)
|
||||||
|
return
|
||||||
|
|
||||||
|
def __lldb_init_module(debugger: lldb.SBDebugger, _):
|
||||||
|
target = debugger.GetDummyTarget()
|
||||||
|
# Caveat: must use BreakpointCreateByRegEx here and not
|
||||||
|
# BreakpointCreateByName. For some reasons callback function does not
|
||||||
|
# get carried over from dummy target for the later.
|
||||||
|
bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$")
|
||||||
|
bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__))
|
||||||
|
bp.SetAutoContinue(True)
|
||||||
|
print("-- LLDB integration loaded --")
|
||||||
5
celaya_limpia/ios/Flutter/ephemeral/flutter_lldbinit
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#
|
||||||
|
# Generated file, do not edit.
|
||||||
|
#
|
||||||
|
|
||||||
|
command script import --relative-to-command-file flutter_lldb_helper.py
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
FLUTTER_ROOT=C:\flutter
|
||||||
|
FLUTTER_APPLICATION_PATH=C:\Users\benga\OneDrive\Documentos\app\ejemplo
|
||||||
|
FLUTTER_FRAMEWORK_SWIFT_PACKAGE_PATH=C:\Users\benga\OneDrive\Documentos\app\ejemplo\ios\Flutter\ephemeral\Packages\.packages\FlutterFramework
|
||||||
|
COCOAPODS_PARALLEL_CODE_SIGN=true
|
||||||
|
FLUTTER_TARGET=lib\main.dart
|
||||||
|
FLUTTER_BUILD_DIR=build
|
||||||
|
FLUTTER_BUILD_NAME=1.0.0
|
||||||
|
FLUTTER_BUILD_NUMBER=1
|
||||||
|
DART_OBFUSCATION=false
|
||||||
|
TRACK_WIDGET_CREATION=true
|
||||||
|
TREE_SHAKE_ICONS=false
|
||||||
|
PACKAGE_CONFIG=.dart_tool/package_config.json
|
||||||
14
celaya_limpia/ios/Flutter/flutter_export_environment.sh
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# This is a generated file; do not edit or check into version control.
|
||||||
|
export "FLUTTER_ROOT=C:\flutter"
|
||||||
|
export "FLUTTER_APPLICATION_PATH=C:\Users\benga\OneDrive\Documentos\app\ejemplo"
|
||||||
|
export "FLUTTER_FRAMEWORK_SWIFT_PACKAGE_PATH=C:\Users\benga\OneDrive\Documentos\app\ejemplo\ios\Flutter\ephemeral\Packages\.packages\FlutterFramework"
|
||||||
|
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
|
||||||
|
export "FLUTTER_TARGET=lib\main.dart"
|
||||||
|
export "FLUTTER_BUILD_DIR=build"
|
||||||
|
export "FLUTTER_BUILD_NAME=1.0.0"
|
||||||
|
export "FLUTTER_BUILD_NUMBER=1"
|
||||||
|
export "DART_OBFUSCATION=false"
|
||||||
|
export "TRACK_WIDGET_CREATION=true"
|
||||||
|
export "TREE_SHAKE_ICONS=false"
|
||||||
|
export "PACKAGE_CONFIG=.dart_tool/package_config.json"
|
||||||
644
celaya_limpia/ios/Runner.xcodeproj/project.pbxproj
Normal file
@@ -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 = "<group>"; };
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
|
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 = "<group>"; };
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||||
|
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 = "<group>"; };
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
/* 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 = "<group>";
|
||||||
|
};
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Flutter;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146E51CF9000F007C117D = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */,
|
||||||
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146EF1CF9000F007C117D /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
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 = "<group>";
|
||||||
|
};
|
||||||
|
/* 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 = "<group>";
|
||||||
|
};
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
97C147001CF9000F007C117D /* Base */,
|
||||||
|
);
|
||||||
|
name = LaunchScreen.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* 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.ejemplo;
|
||||||
|
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.ejemplo.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.ejemplo.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.ejemplo.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.ejemplo;
|
||||||
|
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.ejemplo;
|
||||||
|
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 */;
|
||||||
|
}
|
||||||
7
celaya_limpia/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1510"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<PreActions>
|
||||||
|
<ExecutionAction
|
||||||
|
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||||
|
<ActionContent
|
||||||
|
title = "Run Prepare Flutter Framework Script"
|
||||||
|
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||||
|
<EnvironmentBuildable>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</EnvironmentBuildable>
|
||||||
|
</ActionContent>
|
||||||
|
</ExecutionAction>
|
||||||
|
</PreActions>
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||||
|
BuildableName = "RunnerTests.xctest"
|
||||||
|
BlueprintName = "RunnerTests"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
enableGPUValidationMode = "1"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Profile"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
7
celaya_limpia/ios/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:Runner.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
16
celaya_limpia/ios/Runner/AppDelegate.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
23
celaya_limpia/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
vendored
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
celaya_limpia/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
celaya_limpia/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
celaya_limpia/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
5
celaya_limpia/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
vendored
Normal file
@@ -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.
|
||||||
37
celaya_limpia/ios/Runner/Base.lproj/LaunchScreen.storyboard
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="EHf-IW-A2E">
|
||||||
|
<objects>
|
||||||
|
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="53" y="375"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
<resources>
|
||||||
|
<image name="LaunchImage" width="168" height="185"/>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
26
celaya_limpia/ios/Runner/Base.lproj/Main.storyboard
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--Flutter View Controller-->
|
||||||
|
<scene sceneID="tne-QT-ifu">
|
||||||
|
<objects>
|
||||||
|
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
||||||
19
celaya_limpia/ios/Runner/GeneratedPluginRegistrant.h
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
#ifndef GeneratedPluginRegistrant_h
|
||||||
|
#define GeneratedPluginRegistrant_h
|
||||||
|
|
||||||
|
#import <Flutter/Flutter.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@interface GeneratedPluginRegistrant : NSObject
|
||||||
|
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry;
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
#endif /* GeneratedPluginRegistrant_h */
|
||||||
14
celaya_limpia/ios/Runner/GeneratedPluginRegistrant.m
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
#import "GeneratedPluginRegistrant.h"
|
||||||
|
|
||||||
|
@implementation GeneratedPluginRegistrant
|
||||||
|
|
||||||
|
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
70
celaya_limpia/ios/Runner/Info.plist
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true/>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Ejemplo</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>ejemplo</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<false/>
|
||||||
|
<key>UISceneConfigurations</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIWindowSceneSessionRoleApplication</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UISceneClassName</key>
|
||||||
|
<string>UIWindowScene</string>
|
||||||
|
<key>UISceneConfigurationName</key>
|
||||||
|
<string>flutter</string>
|
||||||
|
<key>UISceneDelegateClassName</key>
|
||||||
|
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||||
|
<key>UISceneStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIMainStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
1
celaya_limpia/ios/Runner/Runner-Bridging-Header.h
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#import "GeneratedPluginRegistrant.h"
|
||||||
6
celaya_limpia/ios/Runner/SceneDelegate.swift
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SceneDelegate: FlutterSceneDelegate {
|
||||||
|
|
||||||
|
}
|
||||||
12
celaya_limpia/ios/RunnerTests/RunnerTests.swift
Normal file
@@ -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.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
73
celaya_limpia/lib/core/app_colors.dart
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AppColors {
|
||||||
|
static const Color guindaPrimary = Color(0xFF6D1E3A);
|
||||||
|
static const Color guindaDark = Color(0xFF4A1228);
|
||||||
|
static const Color guindaLight = Color(0xFF9B3D5C);
|
||||||
|
static const Color dorado = Color(0xFFC9A84C);
|
||||||
|
static const Color blanco = Color(0xFFFFFFFF);
|
||||||
|
static const Color grisFondo = Color(0xFFF5F5F5);
|
||||||
|
static const Color grisTexto = Color(0xFF757575);
|
||||||
|
static const Color negroTexto = Color(0xFF212121);
|
||||||
|
static const Color verdeExito = Color(0xFF2E7D32);
|
||||||
|
static const Color rojoError = Color(0xFFC62828);
|
||||||
|
static const Color naranjaAlerta = Color(0xFFE65100);
|
||||||
|
static const Color azulInfo = Color(0xFF1565C0);
|
||||||
|
static const Color moradoConductor= Color(0xFF4A148C);
|
||||||
|
static const Color verdeAdmin = Color(0xFF1B5E20);
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppRoles {
|
||||||
|
static const String ciudadano = 'CIUDADANO';
|
||||||
|
static const String conductor = 'CONDUCTOR';
|
||||||
|
static const String administrador = 'ADMINISTRADOR';
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppTurnos {
|
||||||
|
static const String matutino = 'MATUTINO';
|
||||||
|
static const String vespertino= 'VESPERTINO';
|
||||||
|
static const String nocturno = 'NOCTURNO';
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppDias {
|
||||||
|
static const List<String> todos = [
|
||||||
|
'LUNES','MARTES','MIERCOLES','JUEVES','VIERNES','SABADO','DOMINGO'
|
||||||
|
];
|
||||||
|
static String label(String dia) {
|
||||||
|
const m = {
|
||||||
|
'LUNES':'Lunes','MARTES':'Martes','MIERCOLES':'Miércoles',
|
||||||
|
'JUEVES':'Jueves','VIERNES':'Viernes','SABADO':'Sábado','DOMINGO':'Domingo',
|
||||||
|
};
|
||||||
|
return m[dia] ?? dia;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RouteStatus {
|
||||||
|
static const String enRuta = 'EN_RUTA';
|
||||||
|
static const String cancelada = 'CANCELADA';
|
||||||
|
static const String retrasada = 'RETRASADA';
|
||||||
|
static const String fallaMecanica= 'FALLA_MECANICA';
|
||||||
|
static const String finalizada = 'FINALIZADA';
|
||||||
|
|
||||||
|
static Color color(String status) {
|
||||||
|
switch (status) {
|
||||||
|
case enRuta: return AppColors.verdeExito;
|
||||||
|
case cancelada: return AppColors.rojoError;
|
||||||
|
case retrasada: return AppColors.naranjaAlerta;
|
||||||
|
case fallaMecanica: return Colors.red.shade900;
|
||||||
|
case finalizada: return AppColors.grisTexto;
|
||||||
|
default: return AppColors.grisTexto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String label(String status) {
|
||||||
|
switch (status) {
|
||||||
|
case enRuta: return '🚛 En Ruta';
|
||||||
|
case cancelada: return '❌ Cancelada';
|
||||||
|
case retrasada: return '⏱️ Retrasada';
|
||||||
|
case fallaMecanica: return '🔧 Falla Mecánica';
|
||||||
|
case finalizada: return '✅ Finalizada';
|
||||||
|
default: return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
243
celaya_limpia/lib/data/celaya_colonias.dart
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
// 240 colonias oficiales de Celaya, Guanajuato
|
||||||
|
const List<String> celayaColonias = [
|
||||||
|
'10 de Abril',
|
||||||
|
'10 de mayo',
|
||||||
|
'15 de Mayo',
|
||||||
|
'3 Guerras',
|
||||||
|
'Alameda',
|
||||||
|
'Álamos',
|
||||||
|
'Álamos Oriente',
|
||||||
|
'Alfredo Vázquez Bonfil',
|
||||||
|
'Américas del Bajío',
|
||||||
|
'Arboledas de Camargo',
|
||||||
|
'Arboledas del Campestre',
|
||||||
|
'Arcada Alameda',
|
||||||
|
'Baalam Residencial',
|
||||||
|
'Benito Juárez',
|
||||||
|
'Bosques de la Alameda',
|
||||||
|
'Bosques del Sol',
|
||||||
|
'Brisas del Carmen',
|
||||||
|
'Bugambilias',
|
||||||
|
'Calesa',
|
||||||
|
'Camargo',
|
||||||
|
'Campestre Celaya',
|
||||||
|
'Canal de Camargo',
|
||||||
|
'Canal de Labradores',
|
||||||
|
'Capitales de Europa',
|
||||||
|
'Celaya Centro',
|
||||||
|
'Ciudadela',
|
||||||
|
'Ciudad Industrial',
|
||||||
|
'Claustros de Arboledas',
|
||||||
|
'Conjunto Habitacional Girasoles',
|
||||||
|
'Cuauhtémoc',
|
||||||
|
'Del Bosque',
|
||||||
|
'Del Parque',
|
||||||
|
'Del Valle',
|
||||||
|
'Don Gu',
|
||||||
|
'Dos Plazas',
|
||||||
|
'Ejidal',
|
||||||
|
'El Atrio',
|
||||||
|
'El Campanario',
|
||||||
|
'El Campanario Residencial',
|
||||||
|
'El Cantar',
|
||||||
|
'El Dorado',
|
||||||
|
'El Haba',
|
||||||
|
'El Junco Residencial',
|
||||||
|
'El Olivar',
|
||||||
|
'El Panamericano',
|
||||||
|
'El Paraíso de los Ángeles',
|
||||||
|
'El Vergel',
|
||||||
|
'Emeteria Valencia',
|
||||||
|
'Emiliano Zapata',
|
||||||
|
'Emiliano Zapata Sur',
|
||||||
|
'Enrique Colunga',
|
||||||
|
'Esmeralda',
|
||||||
|
'Exelaris',
|
||||||
|
'Felipe Ángeles',
|
||||||
|
'Floresta del Sur',
|
||||||
|
'FOVISSSTE',
|
||||||
|
'Galaxias del Parque',
|
||||||
|
'Geo Villas Los Sauces',
|
||||||
|
'Gobernadores',
|
||||||
|
'Granada',
|
||||||
|
'Gran Clase',
|
||||||
|
'Guadalupe',
|
||||||
|
'Guanajuato',
|
||||||
|
'Hacienda del Bosque',
|
||||||
|
'Hacienda del Sol',
|
||||||
|
'Hidalgo',
|
||||||
|
'Imperial',
|
||||||
|
'Independencia',
|
||||||
|
'Industriales',
|
||||||
|
'Jacarandas',
|
||||||
|
'Jardines de Celaya 1a Secc',
|
||||||
|
'Jardines de Celaya 2a Secc',
|
||||||
|
'Jardines de Celaya 3a Secc',
|
||||||
|
'Jardines del Centro',
|
||||||
|
'Jardines del Sur',
|
||||||
|
'José Suárez Irigoyen',
|
||||||
|
'Juan Pablo II',
|
||||||
|
'Karina',
|
||||||
|
'La Campiña',
|
||||||
|
'La Capilla',
|
||||||
|
'La Cruz',
|
||||||
|
'La Escondida',
|
||||||
|
'La Favorita',
|
||||||
|
'La Fundación',
|
||||||
|
'La Herradura',
|
||||||
|
'La Joya',
|
||||||
|
'La Misión',
|
||||||
|
'La Purísima',
|
||||||
|
'Las Alamedas',
|
||||||
|
'Las Américas',
|
||||||
|
'Las Arboledas',
|
||||||
|
'Las Arenas',
|
||||||
|
'Las Aves',
|
||||||
|
'Las Brisas',
|
||||||
|
'Las Carretas',
|
||||||
|
'Las Casas',
|
||||||
|
'Las Delicias',
|
||||||
|
'Las Flores',
|
||||||
|
'Las Fuentes',
|
||||||
|
'Las Insurgentes',
|
||||||
|
'La Soledad',
|
||||||
|
'Latinoamericana',
|
||||||
|
'La Trinidad',
|
||||||
|
'Lázaro Cárdenas',
|
||||||
|
'Lindavista',
|
||||||
|
'López Portillo',
|
||||||
|
'Los Ángeles',
|
||||||
|
'Los Frailes',
|
||||||
|
'Los Impresionistas',
|
||||||
|
'Los Lagos',
|
||||||
|
'Los Laureles',
|
||||||
|
'Los Naranjos',
|
||||||
|
'Los Olivos Residencial',
|
||||||
|
'Los Pinos',
|
||||||
|
'Los Pirules',
|
||||||
|
'Los Pirules Don Gu',
|
||||||
|
'Los Portones',
|
||||||
|
'Los Santos',
|
||||||
|
'Los Sauces',
|
||||||
|
'Los Tules',
|
||||||
|
'Los Veintes',
|
||||||
|
'Magno Residencial',
|
||||||
|
'Mediterráneo',
|
||||||
|
'México',
|
||||||
|
'Miguel Alemán',
|
||||||
|
'Misión de La Esperanza',
|
||||||
|
'Misión Santa Fe',
|
||||||
|
'Moctezuma',
|
||||||
|
'Monte Blanco',
|
||||||
|
'Nat Tha Hi',
|
||||||
|
'Nueva Santa María',
|
||||||
|
'Nueva Terraza',
|
||||||
|
'Nuevo Celaya',
|
||||||
|
'Nuevo Tecnológico',
|
||||||
|
'Obrero Mundial',
|
||||||
|
'Oro',
|
||||||
|
'Palas Atenea',
|
||||||
|
'Palma Real',
|
||||||
|
'Parque Central',
|
||||||
|
'Parque Verde',
|
||||||
|
'Pedregal del Junco',
|
||||||
|
'Porta Maggiore',
|
||||||
|
'Portones de la Hacienda',
|
||||||
|
'Praderas del Bosque',
|
||||||
|
'Praderas de Santa Julia',
|
||||||
|
'Praderas de Santa Lucía',
|
||||||
|
'Prados el Naranjal',
|
||||||
|
'Privada Ciruelo',
|
||||||
|
'Privada del Pedregal',
|
||||||
|
'Privada del Real',
|
||||||
|
'Privada el Sauz',
|
||||||
|
'Progreso Solidaridad',
|
||||||
|
'Providencia',
|
||||||
|
'Puerta Grande',
|
||||||
|
'Puertas del Sol',
|
||||||
|
'Puertas de Santa María',
|
||||||
|
'Puesta del Sol',
|
||||||
|
'Punta Norte',
|
||||||
|
'Quinta Santa María',
|
||||||
|
'Raquet Club Cross',
|
||||||
|
'Real de Celaya',
|
||||||
|
'Real de San Antonio',
|
||||||
|
'Recursos Hidráulicos',
|
||||||
|
'Reforma',
|
||||||
|
'Reforma',
|
||||||
|
'Residencial Las Margaritas',
|
||||||
|
'Residencial Las Praderas',
|
||||||
|
'Residencial Paraíso',
|
||||||
|
'Residencial San Pablo',
|
||||||
|
'Residencial Santiago',
|
||||||
|
'Residencial Tecnológico',
|
||||||
|
'Residencial Xochipilli',
|
||||||
|
'Resurrección',
|
||||||
|
'Revolución',
|
||||||
|
'Rinconada del Bosque',
|
||||||
|
'Rinconada Laureles',
|
||||||
|
'Rinconada Los Álamos',
|
||||||
|
'Rinconada San Jorge',
|
||||||
|
'Rincón de Cantarranas',
|
||||||
|
'Riveras del Campestre',
|
||||||
|
'Rosalinda',
|
||||||
|
'San Andrés',
|
||||||
|
'San Antonio',
|
||||||
|
'San Antonio',
|
||||||
|
'San Francisco',
|
||||||
|
'San Gabriel',
|
||||||
|
'San José de Torres',
|
||||||
|
'San Juan',
|
||||||
|
'San Juan de Dios',
|
||||||
|
'San Juanico',
|
||||||
|
'San Juanico 1a Secc',
|
||||||
|
'San Juanico 2a Secc',
|
||||||
|
'San Martín de Camargo',
|
||||||
|
'San Miguel',
|
||||||
|
'San Rafael',
|
||||||
|
'San Román',
|
||||||
|
'Santa Anita',
|
||||||
|
'Santa Bárbara',
|
||||||
|
'Santa Cecilia',
|
||||||
|
'Santa Fe de los Naranjos',
|
||||||
|
'Santa Isabel',
|
||||||
|
'Santa María',
|
||||||
|
'Santa María',
|
||||||
|
'Santa Rita',
|
||||||
|
'Santa Teresa',
|
||||||
|
'Santiaguito',
|
||||||
|
'Suiza',
|
||||||
|
'Tahi',
|
||||||
|
'Tierras Negras',
|
||||||
|
'Tierra y Libertad',
|
||||||
|
'Tres Lunas',
|
||||||
|
'Valle de La Primavera',
|
||||||
|
'Valle de los Naranjos III Sección',
|
||||||
|
'Valle de los Naranjos II Sección',
|
||||||
|
'Valle del Real',
|
||||||
|
'Valle Hermoso',
|
||||||
|
'Valle Naranjos',
|
||||||
|
'Ventanales de Santa María',
|
||||||
|
'Villa Arbolada',
|
||||||
|
'Villa de Celaya',
|
||||||
|
'Villa de los Álamos',
|
||||||
|
'Villa de los Reyes',
|
||||||
|
'Villa Jardín',
|
||||||
|
'Villas de Benavente',
|
||||||
|
'Villas de Benavente II',
|
||||||
|
'Villas de La Esperanza',
|
||||||
|
'Villas de La Hacienda',
|
||||||
|
'Villas del Bajío',
|
||||||
|
'Villas del Palmar',
|
||||||
|
'Villas del Paraíso',
|
||||||
|
'Villas del Rocío',
|
||||||
|
'Villas del Romeral',
|
||||||
|
'Villas del Tenis',
|
||||||
|
'Villas Reales',
|
||||||
|
'Villas Vicenza',
|
||||||
|
'Viñas de La Herradura',
|
||||||
|
'Virgen del Refugio',
|
||||||
|
'Zempoala',
|
||||||
|
'Zona de Oro',
|
||||||
|
'Zona de Oro del Bajío',
|
||||||
|
];
|
||||||
51
celaya_limpia/lib/data/colonies_data.dart
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import '../models/route_model.dart';
|
||||||
|
|
||||||
|
final List<ColonyModel> coloniesData = [
|
||||||
|
ColonyModel(colonia:'Zona Centro',routeId:'RUTA-01',horarioEstimado:'Matutino (06:30-07:15)'),
|
||||||
|
ColonyModel(colonia:'Las Arboledas',routeId:'RUTA-01',horarioEstimado:'Matutino (07:00-07:30)'),
|
||||||
|
ColonyModel(colonia:'Centro Histórico',routeId:'RUTA-01',horarioEstimado:'Matutino (06:20-07:00)'),
|
||||||
|
ColonyModel(colonia:'Barrio de Santiago',routeId:'RUTA-01',horarioEstimado:'Matutino (06:30-07:10)'),
|
||||||
|
ColonyModel(colonia:'Col. Obrera',routeId:'RUTA-01',horarioEstimado:'Matutino (06:50-07:25)'),
|
||||||
|
ColonyModel(colonia:'Av. Tecnológico',routeId:'RUTA-02',horarioEstimado:'Matutino (06:20-07:00)'),
|
||||||
|
ColonyModel(colonia:'Col. Magisterial',routeId:'RUTA-02',horarioEstimado:'Matutino (06:40-07:15)'),
|
||||||
|
ColonyModel(colonia:'Fracc. Las Américas',routeId:'RUTA-02',horarioEstimado:'Matutino (06:55-07:30)'),
|
||||||
|
ColonyModel(colonia:'Col. Constitución',routeId:'RUTA-02',horarioEstimado:'Matutino (06:30-07:05)'),
|
||||||
|
ColonyModel(colonia:'San Juanico',routeId:'RUTA-03',horarioEstimado:'Vespertino (14:45-15:15)'),
|
||||||
|
ColonyModel(colonia:'Col. Los Álamos',routeId:'RUTA-03',horarioEstimado:'Vespertino (14:30-15:00)'),
|
||||||
|
ColonyModel(colonia:'Fracc. El Dorado',routeId:'RUTA-03',horarioEstimado:'Vespertino (15:00-15:30)'),
|
||||||
|
ColonyModel(colonia:'Los Olivos',routeId:'RUTA-04',horarioEstimado:'Matutino (07:00-07:40)'),
|
||||||
|
ColonyModel(colonia:'Col. Revolución',routeId:'RUTA-04',horarioEstimado:'Matutino (06:35-07:10)'),
|
||||||
|
ColonyModel(colonia:'Col. Ladrillera',routeId:'RUTA-04',horarioEstimado:'Matutino (06:50-07:25)'),
|
||||||
|
ColonyModel(colonia:'Rancho Seco',routeId:'RUTA-05',horarioEstimado:'Vespertino (15:00-15:35)'),
|
||||||
|
ColonyModel(colonia:'Col. El Potrero',routeId:'RUTA-05',horarioEstimado:'Vespertino (14:45-15:20)'),
|
||||||
|
ColonyModel(colonia:'Col. Los Sauces',routeId:'RUTA-05',horarioEstimado:'Vespertino (15:15-15:50)'),
|
||||||
|
ColonyModel(colonia:'Rumbos de Roque',routeId:'RUTA-06',horarioEstimado:'Matutino (06:30-07:10)'),
|
||||||
|
ColonyModel(colonia:'Col. Vista Hermosa',routeId:'RUTA-06',horarioEstimado:'Matutino (06:45-07:20)'),
|
||||||
|
ColonyModel(colonia:'Ciudad Industrial',routeId:'RUTA-07',horarioEstimado:'Matutino (06:30-07:10)'),
|
||||||
|
ColonyModel(colonia:'Parque Industrial',routeId:'RUTA-07',horarioEstimado:'Matutino (06:50-07:25)'),
|
||||||
|
ColonyModel(colonia:'Universidad Latina',routeId:'RUTA-08',horarioEstimado:'Nocturno (22:30-23:00)'),
|
||||||
|
ColonyModel(colonia:'Col. Del Moral',routeId:'RUTA-08',horarioEstimado:'Nocturno (22:00-22:30)'),
|
||||||
|
ColonyModel(colonia:'Hospital General',routeId:'RUTA-09',horarioEstimado:'Matutino (06:20-07:00)'),
|
||||||
|
ColonyModel(colonia:'Col. Peñuelas',routeId:'RUTA-09',horarioEstimado:'Matutino (06:50-07:20)'),
|
||||||
|
ColonyModel(colonia:'UG Sur',routeId:'RUTA-10',horarioEstimado:'Nocturno (21:30-22:00)'),
|
||||||
|
ColonyModel(colonia:'Eje Juan Pablo II',routeId:'RUTA-10',horarioEstimado:'Nocturno (21:00-21:30)'),
|
||||||
|
ColonyModel(colonia:'Torres Landa',routeId:'RUTA-11',horarioEstimado:'Matutino (06:45-07:15)'),
|
||||||
|
ColonyModel(colonia:'Zona de Oro',routeId:'RUTA-11',horarioEstimado:'Matutino (06:30-07:00)'),
|
||||||
|
ColonyModel(colonia:'Las Insurgentes',routeId:'RUTA-12',horarioEstimado:'Matutino (06:35-07:10)'),
|
||||||
|
ColonyModel(colonia:'Col. Independencia',routeId:'RUTA-12',horarioEstimado:'Matutino (06:50-07:20)'),
|
||||||
|
ColonyModel(colonia:'Trojes',routeId:'RUTA-13',horarioEstimado:'Matutino (06:40-07:10)'),
|
||||||
|
ColonyModel(colonia:'Irrigación',routeId:'RUTA-13',horarioEstimado:'Matutino (06:55-07:25)'),
|
||||||
|
ColonyModel(colonia:'Col. Benito Juárez',routeId:'RUTA-13',horarioEstimado:'Matutino (06:30-07:00)'),
|
||||||
|
ColonyModel(colonia:'La Toscana',routeId:'RUTA-14',horarioEstimado:'Vespertino (15:00-15:35)'),
|
||||||
|
ColonyModel(colonia:'Fracc. La Laborcita',routeId:'RUTA-14',horarioEstimado:'Vespertino (14:45-15:20)'),
|
||||||
|
ColonyModel(colonia:'San José de Celaya',routeId:'RUTA-15',horarioEstimado:'Nocturno (22:45-23:20)'),
|
||||||
|
ColonyModel(colonia:'Col. Camino Real',routeId:'RUTA-15',horarioEstimado:'Nocturno (22:30-23:00)'),
|
||||||
|
ColonyModel(colonia:'Col. Jardín',routeId:'RUTA-15',horarioEstimado:'Nocturno (23:00-23:30)'),
|
||||||
|
];
|
||||||
|
|
||||||
|
ColonyModel? getColonyByName(String name) {
|
||||||
|
try { return coloniesData.firstWhere((c) => c.colonia.toLowerCase() == name.toLowerCase()); }
|
||||||
|
catch (_) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> get colonyNames => coloniesData.map((c) => c.colonia).toList()..sort();
|
||||||
159
celaya_limpia/lib/data/routes_data.dart
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import '../models/route_model.dart';
|
||||||
|
|
||||||
|
final List<RouteModel> routesData = [
|
||||||
|
RouteModel(routeId:'RUTA-01',name:'Zona Centro - Las Arboledas',truckId:101,status:'EN_RUTA',turno:'MATUTINO',positions:[
|
||||||
|
RoutePosition(positionId:1,lat:20.5111,lng:-100.9037,speed:0,timestamp:'06:00'),
|
||||||
|
RoutePosition(positionId:2,lat:20.5185,lng:-100.8450,speed:45,timestamp:'06:12'),
|
||||||
|
RoutePosition(positionId:3,lat:20.5215,lng:-100.8142,speed:22,timestamp:'06:25'),
|
||||||
|
RoutePosition(positionId:4,lat:20.5212,lng:-100.8175,speed:15,timestamp:'06:38'),
|
||||||
|
RoutePosition(positionId:5,lat:20.5210,lng:-100.8210,speed:0,timestamp:'06:50'),
|
||||||
|
RoutePosition(positionId:6,lat:20.5235,lng:-100.8212,speed:18,timestamp:'07:05'),
|
||||||
|
RoutePosition(positionId:7,lat:20.5260,lng:-100.8215,speed:20,timestamp:'07:18'),
|
||||||
|
RoutePosition(positionId:8,lat:20.5111,lng:-100.9037,speed:40,timestamp:'07:40'),
|
||||||
|
]),
|
||||||
|
RouteModel(routeId:'RUTA-02',name:'Sector Norte - Av. Tecnológico',truckId:102,status:'EN_RUTA',turno:'MATUTINO',positions:[
|
||||||
|
RoutePosition(positionId:1,lat:20.5111,lng:-100.9037,speed:0,timestamp:'06:05'),
|
||||||
|
RoutePosition(positionId:2,lat:20.5280,lng:-100.8135,speed:38,timestamp:'06:18'),
|
||||||
|
RoutePosition(positionId:3,lat:20.5410,lng:-100.8130,speed:25,timestamp:'06:30'),
|
||||||
|
RoutePosition(positionId:4,lat:20.5445,lng:-100.8132,speed:12,timestamp:'06:45'),
|
||||||
|
RoutePosition(positionId:5,lat:20.5480,lng:-100.8135,speed:0,timestamp:'06:58'),
|
||||||
|
RoutePosition(positionId:6,lat:20.5515,lng:-100.8138,speed:15,timestamp:'07:10'),
|
||||||
|
RoutePosition(positionId:7,lat:20.5540,lng:-100.8110,speed:22,timestamp:'07:25'),
|
||||||
|
RoutePosition(positionId:8,lat:20.5111,lng:-100.9037,speed:45,timestamp:'07:50'),
|
||||||
|
]),
|
||||||
|
RouteModel(routeId:'RUTA-03',name:'Sector Poniente - San Juanico',truckId:103,status:'EN_RUTA',turno:'VESPERTINO',positions:[
|
||||||
|
RoutePosition(positionId:1,lat:20.5111,lng:-100.9037,speed:0,timestamp:'14:00'),
|
||||||
|
RoutePosition(positionId:2,lat:20.5250,lng:-100.8510,speed:42,timestamp:'14:15'),
|
||||||
|
RoutePosition(positionId:3,lat:20.5290,lng:-100.8320,speed:20,timestamp:'14:30'),
|
||||||
|
RoutePosition(positionId:4,lat:20.5315,lng:-100.8355,speed:15,timestamp:'14:45'),
|
||||||
|
RoutePosition(positionId:5,lat:20.5340,lng:-100.8390,speed:0,timestamp:'15:00'),
|
||||||
|
RoutePosition(positionId:6,lat:20.5362,lng:-100.8425,speed:10,timestamp:'15:15'),
|
||||||
|
RoutePosition(positionId:7,lat:20.5330,lng:-100.8430,speed:18,timestamp:'15:28'),
|
||||||
|
RoutePosition(positionId:8,lat:20.5111,lng:-100.9037,speed:35,timestamp:'15:45'),
|
||||||
|
]),
|
||||||
|
RouteModel(routeId:'RUTA-04',name:'Oriente - Los Olivos',truckId:104,status:'EN_RUTA',turno:'MATUTINO',positions:[
|
||||||
|
RoutePosition(positionId:1,lat:20.5111,lng:-100.9037,speed:0,timestamp:'06:15'),
|
||||||
|
RoutePosition(positionId:2,lat:20.5260,lng:-100.8010,speed:45,timestamp:'06:30'),
|
||||||
|
RoutePosition(positionId:3,lat:20.5295,lng:-100.7890,speed:24,timestamp:'06:45'),
|
||||||
|
RoutePosition(positionId:4,lat:20.5320,lng:-100.7850,speed:12,timestamp:'06:58'),
|
||||||
|
RoutePosition(positionId:5,lat:20.5350,lng:-100.7790,speed:0,timestamp:'07:12'),
|
||||||
|
RoutePosition(positionId:6,lat:20.5310,lng:-100.7760,speed:15,timestamp:'07:25'),
|
||||||
|
RoutePosition(positionId:7,lat:20.5270,lng:-100.7820,speed:26,timestamp:'07:38'),
|
||||||
|
RoutePosition(positionId:8,lat:20.5111,lng:-100.9037,speed:48,timestamp:'07:58'),
|
||||||
|
]),
|
||||||
|
RouteModel(routeId:'RUTA-05',name:'Sector Sur - Rancho Seco',truckId:105,status:'EN_RUTA',turno:'VESPERTINO',positions:[
|
||||||
|
RoutePosition(positionId:1,lat:20.5111,lng:-100.9037,speed:0,timestamp:'14:20'),
|
||||||
|
RoutePosition(positionId:2,lat:20.5050,lng:-100.8620,speed:35,timestamp:'14:32'),
|
||||||
|
RoutePosition(positionId:3,lat:20.5020,lng:-100.8350,speed:22,timestamp:'14:45'),
|
||||||
|
RoutePosition(positionId:4,lat:20.4995,lng:-100.8210,speed:14,timestamp:'14:58'),
|
||||||
|
RoutePosition(positionId:5,lat:20.4970,lng:-100.8150,speed:0,timestamp:'15:10'),
|
||||||
|
RoutePosition(positionId:6,lat:20.5010,lng:-100.8120,speed:16,timestamp:'15:22'),
|
||||||
|
RoutePosition(positionId:7,lat:20.5060,lng:-100.8160,speed:25,timestamp:'15:35'),
|
||||||
|
RoutePosition(positionId:8,lat:20.5111,lng:-100.9037,speed:40,timestamp:'15:55'),
|
||||||
|
]),
|
||||||
|
RouteModel(routeId:'RUTA-06',name:'Norte Extremo - Rumbos de Roque',truckId:106,status:'EN_RUTA',turno:'MATUTINO',positions:[
|
||||||
|
RoutePosition(positionId:1,lat:20.5111,lng:-100.9037,speed:0,timestamp:'06:00'),
|
||||||
|
RoutePosition(positionId:2,lat:20.5380,lng:-100.8380,speed:40,timestamp:'06:15'),
|
||||||
|
RoutePosition(positionId:3,lat:20.5610,lng:-100.8370,speed:30,timestamp:'06:30'),
|
||||||
|
RoutePosition(positionId:4,lat:20.5750,lng:-100.8360,speed:15,timestamp:'06:45'),
|
||||||
|
RoutePosition(positionId:5,lat:20.5820,lng:-100.8350,speed:0,timestamp:'07:00'),
|
||||||
|
RoutePosition(positionId:6,lat:20.5780,lng:-100.8310,speed:20,timestamp:'07:15'),
|
||||||
|
RoutePosition(positionId:7,lat:20.5650,lng:-100.8320,speed:28,timestamp:'07:30'),
|
||||||
|
RoutePosition(positionId:8,lat:20.5111,lng:-100.9037,speed:45,timestamp:'07:55'),
|
||||||
|
]),
|
||||||
|
RouteModel(routeId:'RUTA-07',name:'Nororiente - Ciudad Industrial',truckId:107,status:'EN_RUTA',turno:'MATUTINO',positions:[
|
||||||
|
RoutePosition(positionId:1,lat:20.5111,lng:-100.9037,speed:0,timestamp:'06:10'),
|
||||||
|
RoutePosition(positionId:2,lat:20.5350,lng:-100.8050,speed:44,timestamp:'06:24'),
|
||||||
|
RoutePosition(positionId:3,lat:20.5450,lng:-100.7950,speed:25,timestamp:'06:38'),
|
||||||
|
RoutePosition(positionId:4,lat:20.5480,lng:-100.7850,speed:18,timestamp:'06:52'),
|
||||||
|
RoutePosition(positionId:5,lat:20.5510,lng:-100.7750,speed:0,timestamp:'07:05'),
|
||||||
|
RoutePosition(positionId:6,lat:20.5460,lng:-100.7720,speed:12,timestamp:'07:18'),
|
||||||
|
RoutePosition(positionId:7,lat:20.5390,lng:-100.7820,speed:30,timestamp:'07:30'),
|
||||||
|
RoutePosition(positionId:8,lat:20.5111,lng:-100.9037,speed:42,timestamp:'07:52'),
|
||||||
|
]),
|
||||||
|
RouteModel(routeId:'RUTA-08',name:'Suroriente - Universidad Latina',truckId:108,status:'EN_RUTA',turno:'NOCTURNO',positions:[
|
||||||
|
RoutePosition(positionId:1,lat:20.5111,lng:-100.9037,speed:0,timestamp:'22:00'),
|
||||||
|
RoutePosition(positionId:2,lat:20.5180,lng:-100.8310,speed:38,timestamp:'22:15'),
|
||||||
|
RoutePosition(positionId:3,lat:20.5245,lng:-100.7980,speed:30,timestamp:'22:30'),
|
||||||
|
RoutePosition(positionId:4,lat:20.5210,lng:-100.7995,speed:14,timestamp:'22:45'),
|
||||||
|
RoutePosition(positionId:5,lat:20.5175,lng:-100.8010,speed:0,timestamp:'23:00'),
|
||||||
|
RoutePosition(positionId:6,lat:20.5140,lng:-100.8030,speed:18,timestamp:'23:15'),
|
||||||
|
RoutePosition(positionId:7,lat:20.5110,lng:-100.8055,speed:22,timestamp:'23:30'),
|
||||||
|
RoutePosition(positionId:8,lat:20.5111,lng:-100.9037,speed:40,timestamp:'23:50'),
|
||||||
|
]),
|
||||||
|
RouteModel(routeId:'RUTA-09',name:'Poniente - Hospital General',truckId:109,status:'EN_RUTA',turno:'MATUTINO',positions:[
|
||||||
|
RoutePosition(positionId:1,lat:20.5111,lng:-100.9037,speed:0,timestamp:'06:02'),
|
||||||
|
RoutePosition(positionId:2,lat:20.5210,lng:-100.8650,speed:45,timestamp:'06:12'),
|
||||||
|
RoutePosition(positionId:3,lat:20.5260,lng:-100.8520,speed:26,timestamp:'06:24'),
|
||||||
|
RoutePosition(positionId:4,lat:20.5275,lng:-100.8490,speed:12,timestamp:'06:36'),
|
||||||
|
RoutePosition(positionId:5,lat:20.5285,lng:-100.8460,speed:0,timestamp:'06:48'),
|
||||||
|
RoutePosition(positionId:6,lat:20.5250,lng:-100.8470,speed:15,timestamp:'07:00'),
|
||||||
|
RoutePosition(positionId:7,lat:20.5220,lng:-100.8550,speed:32,timestamp:'07:12'),
|
||||||
|
RoutePosition(positionId:8,lat:20.5111,lng:-100.9037,speed:44,timestamp:'07:30'),
|
||||||
|
]),
|
||||||
|
RouteModel(routeId:'RUTA-10',name:'Eje Juan Pablo II - UG Sur',truckId:110,status:'EN_RUTA',turno:'NOCTURNO',positions:[
|
||||||
|
RoutePosition(positionId:1,lat:20.5111,lng:-100.9037,speed:0,timestamp:'21:00'),
|
||||||
|
RoutePosition(positionId:2,lat:20.5015,lng:-100.8520,speed:40,timestamp:'21:15'),
|
||||||
|
RoutePosition(positionId:3,lat:20.4990,lng:-100.8390,speed:28,timestamp:'21:30'),
|
||||||
|
RoutePosition(positionId:4,lat:20.4950,lng:-100.8320,speed:18,timestamp:'21:45'),
|
||||||
|
RoutePosition(positionId:5,lat:20.4920,lng:-100.8280,speed:0,timestamp:'22:00'),
|
||||||
|
RoutePosition(positionId:6,lat:20.4945,lng:-100.8240,speed:14,timestamp:'22:15'),
|
||||||
|
RoutePosition(positionId:7,lat:20.4980,lng:-100.8300,speed:30,timestamp:'22:30'),
|
||||||
|
RoutePosition(positionId:8,lat:20.5111,lng:-100.9037,speed:38,timestamp:'22:50'),
|
||||||
|
]),
|
||||||
|
RouteModel(routeId:'RUTA-11',name:'Zona de Oro - Torres Landa',truckId:111,status:'EN_RUTA',turno:'MATUTINO',positions:[
|
||||||
|
RoutePosition(positionId:1,lat:20.5111,lng:-100.9037,speed:0,timestamp:'06:04'),
|
||||||
|
RoutePosition(positionId:2,lat:20.5240,lng:-100.8350,speed:36,timestamp:'06:16'),
|
||||||
|
RoutePosition(positionId:3,lat:20.5280,lng:-100.8250,speed:22,timestamp:'06:29'),
|
||||||
|
RoutePosition(positionId:4,lat:20.5295,lng:-100.8210,speed:10,timestamp:'06:42'),
|
||||||
|
RoutePosition(positionId:5,lat:20.5310,lng:-100.8170,speed:0,timestamp:'06:55'),
|
||||||
|
RoutePosition(positionId:6,lat:20.5290,lng:-100.8140,speed:16,timestamp:'07:08'),
|
||||||
|
RoutePosition(positionId:7,lat:20.5260,lng:-100.8220,speed:28,timestamp:'07:21'),
|
||||||
|
RoutePosition(positionId:8,lat:20.5111,lng:-100.9037,speed:42,timestamp:'07:42'),
|
||||||
|
]),
|
||||||
|
RouteModel(routeId:'RUTA-12',name:'Nororiente - Las Insurgentes',truckId:112,status:'EN_RUTA',turno:'MATUTINO',positions:[
|
||||||
|
RoutePosition(positionId:1,lat:20.5111,lng:-100.9037,speed:0,timestamp:'06:08'),
|
||||||
|
RoutePosition(positionId:2,lat:20.5280,lng:-100.8080,speed:40,timestamp:'06:22'),
|
||||||
|
RoutePosition(positionId:3,lat:20.5320,lng:-100.7980,speed:24,timestamp:'06:35'),
|
||||||
|
RoutePosition(positionId:4,lat:20.5340,lng:-100.7940,speed:15,timestamp:'06:48'),
|
||||||
|
RoutePosition(positionId:5,lat:20.5360,lng:-100.7900,speed:0,timestamp:'07:00'),
|
||||||
|
RoutePosition(positionId:6,lat:20.5310,lng:-100.7920,speed:12,timestamp:'07:12'),
|
||||||
|
RoutePosition(positionId:7,lat:20.5270,lng:-100.8020,speed:26,timestamp:'07:25'),
|
||||||
|
RoutePosition(positionId:8,lat:20.5111,lng:-100.9037,speed:44,timestamp:'07:48'),
|
||||||
|
]),
|
||||||
|
RouteModel(routeId:'RUTA-13',name:'Sector Norte - Trojes e Irrigación',truckId:113,status:'EN_RUTA',turno:'MATUTINO',positions:[
|
||||||
|
RoutePosition(positionId:1,lat:20.5111,lng:-100.9037,speed:0,timestamp:'06:12'),
|
||||||
|
RoutePosition(positionId:2,lat:20.5360,lng:-100.8190,speed:35,timestamp:'06:26'),
|
||||||
|
RoutePosition(positionId:3,lat:20.5420,lng:-100.8080,speed:28,timestamp:'06:40'),
|
||||||
|
RoutePosition(positionId:4,lat:20.5440,lng:-100.8040,speed:14,timestamp:'06:54'),
|
||||||
|
RoutePosition(positionId:5,lat:20.5460,lng:-100.8000,speed:0,timestamp:'07:06'),
|
||||||
|
RoutePosition(positionId:6,lat:20.5410,lng:-100.8020,speed:18,timestamp:'07:18'),
|
||||||
|
RoutePosition(positionId:7,lat:20.5370,lng:-100.8120,speed:25,timestamp:'07:30'),
|
||||||
|
RoutePosition(positionId:8,lat:20.5111,lng:-100.9037,speed:39,timestamp:'07:54'),
|
||||||
|
]),
|
||||||
|
RouteModel(routeId:'RUTA-14',name:'Sur Poniente - La Toscana',truckId:114,status:'EN_RUTA',turno:'VESPERTINO',positions:[
|
||||||
|
RoutePosition(positionId:1,lat:20.5111,lng:-100.9037,speed:0,timestamp:'14:16'),
|
||||||
|
RoutePosition(positionId:2,lat:20.5150,lng:-100.8580,speed:42,timestamp:'14:28'),
|
||||||
|
RoutePosition(positionId:3,lat:20.5140,lng:-100.8390,speed:26,timestamp:'14:41'),
|
||||||
|
RoutePosition(positionId:4,lat:20.5125,lng:-100.8310,speed:16,timestamp:'14:54'),
|
||||||
|
RoutePosition(positionId:5,lat:20.5110,lng:-100.8250,speed:0,timestamp:'15:06'),
|
||||||
|
RoutePosition(positionId:6,lat:20.5135,lng:-100.8280,speed:12,timestamp:'15:18'),
|
||||||
|
RoutePosition(positionId:7,lat:20.5160,lng:-100.8420,speed:32,timestamp:'15:30'),
|
||||||
|
RoutePosition(positionId:8,lat:20.5111,lng:-100.9037,speed:45,timestamp:'15:51'),
|
||||||
|
]),
|
||||||
|
RouteModel(routeId:'RUTA-15',name:'Norponiente - San José de Celaya',truckId:115,status:'EN_RUTA',turno:'NOCTURNO',positions:[
|
||||||
|
RoutePosition(positionId:1,lat:20.5111,lng:-100.9037,speed:0,timestamp:'22:30'),
|
||||||
|
RoutePosition(positionId:2,lat:20.5320,lng:-100.8590,speed:38,timestamp:'22:45'),
|
||||||
|
RoutePosition(positionId:3,lat:20.5390,lng:-100.8480,speed:24,timestamp:'23:00'),
|
||||||
|
RoutePosition(positionId:4,lat:20.5420,lng:-100.8440,speed:15,timestamp:'23:15'),
|
||||||
|
RoutePosition(positionId:5,lat:20.5450,lng:-100.8410,speed:0,timestamp:'23:30'),
|
||||||
|
RoutePosition(positionId:6,lat:20.5410,lng:-100.8430,speed:14,timestamp:'23:45'),
|
||||||
|
RoutePosition(positionId:7,lat:20.5360,lng:-100.8520,speed:28,timestamp:'00:00'),
|
||||||
|
RoutePosition(positionId:8,lat:20.5111,lng:-100.9037,speed:41,timestamp:'00:20'),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
|
RouteModel? getRouteById(String id) {
|
||||||
|
try { return routesData.firstWhere((r) => r.routeId == id); }
|
||||||
|
catch (_) { return null; }
|
||||||
|
}
|
||||||
388
celaya_limpia/lib/database/db_helper.dart
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
import '../models/models.dart';
|
||||||
|
|
||||||
|
class DbHelper {
|
||||||
|
static Database? _db;
|
||||||
|
|
||||||
|
static Future<Database> get database async {
|
||||||
|
_db ??= await _initDb();
|
||||||
|
return _db!;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Database> _initDb() async {
|
||||||
|
final path = join(await getDatabasesPath(), 'celaya_v3.db');
|
||||||
|
return openDatabase(path, version: 1, onCreate: _onCreate);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> _onCreate(Database db, int v) async {
|
||||||
|
await db.execute('''CREATE TABLE users(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
nombre TEXT NOT NULL, email TEXT UNIQUE NOT NULL,
|
||||||
|
password TEXT NOT NULL, rol TEXT NOT NULL)''');
|
||||||
|
|
||||||
|
await db.execute('''CREATE TABLE domicilios(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL, alias TEXT DEFAULT 'Casa',
|
||||||
|
calle TEXT NOT NULL, colonia TEXT NOT NULL,
|
||||||
|
route_id TEXT NOT NULL, horario_estimado TEXT NOT NULL,
|
||||||
|
is_primary INTEGER DEFAULT 0)''');
|
||||||
|
|
||||||
|
await db.execute('''CREATE TABLE route_definitions(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
route_id TEXT UNIQUE NOT NULL, nombre TEXT NOT NULL,
|
||||||
|
dias TEXT NOT NULL, hora_inicio TEXT NOT NULL,
|
||||||
|
hora_fin TEXT NOT NULL, turno TEXT NOT NULL,
|
||||||
|
colonias TEXT NOT NULL, activa INTEGER DEFAULT 1)''');
|
||||||
|
|
||||||
|
await db.execute('''CREATE TABLE route_status(
|
||||||
|
route_id TEXT PRIMARY KEY, status TEXT NOT NULL,
|
||||||
|
mensaje TEXT, updated_at TEXT)''');
|
||||||
|
|
||||||
|
await db.execute('''CREATE TABLE asignaciones(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
conductor_id INTEGER NOT NULL, route_id TEXT NOT NULL,
|
||||||
|
dia_semana TEXT NOT NULL, turno TEXT NOT NULL)''');
|
||||||
|
|
||||||
|
await db.execute('''CREATE TABLE alertas(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
tipo TEXT NOT NULL, route_id TEXT NOT NULL,
|
||||||
|
mensaje TEXT NOT NULL, fecha TEXT NOT NULL,
|
||||||
|
resuelta INTEGER DEFAULT 0)''');
|
||||||
|
|
||||||
|
await db.execute('''CREATE TABLE reportes(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL, tipo TEXT NOT NULL,
|
||||||
|
descripcion TEXT NOT NULL, colonia TEXT NOT NULL,
|
||||||
|
route_id TEXT, fecha TEXT NOT NULL,
|
||||||
|
estado TEXT DEFAULT 'PENDIENTE', calificacion INTEGER DEFAULT 5,
|
||||||
|
foto_path TEXT)''');
|
||||||
|
|
||||||
|
await db.execute('''CREATE TABLE reviews(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL, colonia TEXT NOT NULL,
|
||||||
|
route_id TEXT NOT NULL, estrellas INTEGER NOT NULL,
|
||||||
|
comentario TEXT NOT NULL, fecha TEXT NOT NULL,
|
||||||
|
nombre_usuario TEXT DEFAULT 'Ciudadano')''');
|
||||||
|
|
||||||
|
await db.execute('''CREATE TABLE notification_history(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER, route_id TEXT NOT NULL,
|
||||||
|
event_type TEXT NOT NULL, title TEXT NOT NULL,
|
||||||
|
body TEXT NOT NULL, fecha TEXT NOT NULL,
|
||||||
|
leida INTEGER DEFAULT 0)''');
|
||||||
|
|
||||||
|
await db.execute('''CREATE TABLE user_meta(
|
||||||
|
user_id INTEGER PRIMARY KEY, activo INTEGER DEFAULT 1,
|
||||||
|
notas TEXT)''');
|
||||||
|
|
||||||
|
await db.insert('users', {'nombre':'Administrador','email':'admin@celaya.gob.mx',
|
||||||
|
'password':'admin123','rol':'ADMINISTRADOR'});
|
||||||
|
final conductorId = await db.insert('users', {'nombre':'Juan Conductor',
|
||||||
|
'email':'conductor@celaya.gob.mx','password':'conductor123','rol':'CONDUCTOR'});
|
||||||
|
await db.insert('user_meta', {'user_id': conductorId, 'activo': 1});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── USERS ────────────────────────────────────────────────────────────────
|
||||||
|
static Future<int> insertUser(UserModel u) async =>
|
||||||
|
(await database).insert('users', u.toMap(), conflictAlgorithm: ConflictAlgorithm.abort);
|
||||||
|
|
||||||
|
static Future<UserModel?> getUserByEmail(String email) async {
|
||||||
|
final res = await (await database).query('users', where:'email=?', whereArgs:[email]);
|
||||||
|
return res.isEmpty ? null : UserModel.fromMap(res.first);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<UserModel?> getUserById(int id) async {
|
||||||
|
final res = await (await database).query('users', where:'id=?', whereArgs:[id]);
|
||||||
|
return res.isEmpty ? null : UserModel.fromMap(res.first);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<UserModel>> getUsersByRol(String rol) async {
|
||||||
|
final res = await (await database).query('users', where:'rol=?', whereArgs:[rol]);
|
||||||
|
return res.map((m) => UserModel.fromMap(m)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DOMICILIOS ───────────────────────────────────────────────────────────
|
||||||
|
static Future<int> insertDomicilio(DomicilioModel d) async {
|
||||||
|
final db = await database;
|
||||||
|
final existing = await db.query('domicilios', where:'user_id=?', whereArgs:[d.userId]);
|
||||||
|
final isPrimary = existing.isEmpty ? 1 : (d.isPrimary ? 1 : 0);
|
||||||
|
return db.insert('domicilios', {...d.toMap(), 'is_primary': isPrimary});
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<DomicilioModel>> getDomiciliosByUser(int userId) async {
|
||||||
|
final res = await (await database).query('domicilios',
|
||||||
|
where:'user_id=?', whereArgs:[userId], orderBy:'is_primary DESC, id ASC');
|
||||||
|
return res.map((m) => DomicilioModel.fromMap(m)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<DomicilioModel?> getPrimaryDomicilio(int userId) async {
|
||||||
|
final db = await database;
|
||||||
|
var res = await db.query('domicilios',
|
||||||
|
where:'user_id=? AND is_primary=1', whereArgs:[userId]);
|
||||||
|
if (res.isEmpty) {
|
||||||
|
res = await db.query('domicilios', where:'user_id=?', whereArgs:[userId], limit:1);
|
||||||
|
}
|
||||||
|
return res.isEmpty ? null : DomicilioModel.fromMap(res.first);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> setPrimaryDomicilio(int domId, int userId) async {
|
||||||
|
final db = await database;
|
||||||
|
await db.update('domicilios', {'is_primary':0}, where:'user_id=?', whereArgs:[userId]);
|
||||||
|
await db.update('domicilios', {'is_primary':1}, where:'id=?', whereArgs:[domId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> deleteDomicilio(int id) async =>
|
||||||
|
(await database).delete('domicilios', where:'id=?', whereArgs:[id]);
|
||||||
|
|
||||||
|
static Future<List<DomicilioModel>> getDomiciliosByRoute(String routeId) async {
|
||||||
|
final res = await (await database).query('domicilios',
|
||||||
|
where:'route_id=?', whereArgs:[routeId]);
|
||||||
|
return res.map((m) => DomicilioModel.fromMap(m)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ROUTE DEFINITIONS ────────────────────────────────────────────────────
|
||||||
|
static Future<int> insertRouteDefinition(RouteDefinitionModel r) async =>
|
||||||
|
(await database).insert('route_definitions', r.toMap(),
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace);
|
||||||
|
|
||||||
|
static Future<List<RouteDefinitionModel>> getAllRouteDefinitions() async {
|
||||||
|
final res = await (await database).query('route_definitions', orderBy:'route_id ASC');
|
||||||
|
return res.map((m) => RouteDefinitionModel.fromMap(m)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<RouteDefinitionModel?> getRouteDefinitionById(String routeId) async {
|
||||||
|
final res = await (await database).query('route_definitions',
|
||||||
|
where:'route_id=?', whereArgs:[routeId]);
|
||||||
|
return res.isEmpty ? null : RouteDefinitionModel.fromMap(res.first);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> updateRouteDefinition(RouteDefinitionModel r) async =>
|
||||||
|
(await database).update('route_definitions', r.toMap(),
|
||||||
|
where:'route_id=?', whereArgs:[r.routeId]);
|
||||||
|
|
||||||
|
// ── ROUTE STATUS ─────────────────────────────────────────────────────────
|
||||||
|
static Future<void> upsertRouteStatus(RouteStatusModel s) async =>
|
||||||
|
(await database).insert('route_status', s.toMap(),
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace);
|
||||||
|
|
||||||
|
static Future<RouteStatusModel?> getRouteStatus(String routeId) async {
|
||||||
|
final res = await (await database).query('route_status',
|
||||||
|
where:'route_id=?', whereArgs:[routeId]);
|
||||||
|
return res.isEmpty ? null : RouteStatusModel.fromMap(res.first);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<RouteStatusModel>> getAllRouteStatuses() async {
|
||||||
|
final res = await (await database).query('route_status');
|
||||||
|
return res.map((m) => RouteStatusModel.fromMap(m)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ASIGNACIONES ─────────────────────────────────────────────────────────
|
||||||
|
static Future<void> upsertAsignacion(AssignmentModel a) async {
|
||||||
|
final db = await database;
|
||||||
|
final ex = await db.query('asignaciones',
|
||||||
|
where:'conductor_id=? AND dia_semana=?', whereArgs:[a.conductorId, a.diaSemana]);
|
||||||
|
if (ex.isEmpty) {
|
||||||
|
await db.insert('asignaciones', a.toMap());
|
||||||
|
} else {
|
||||||
|
await db.update('asignaciones', {'route_id':a.routeId,'turno':a.turno},
|
||||||
|
where:'conductor_id=? AND dia_semana=?', whereArgs:[a.conductorId, a.diaSemana]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<AssignmentModel>> getAsignacionesByConductor(int id) async {
|
||||||
|
final res = await (await database).query('asignaciones',
|
||||||
|
where:'conductor_id=?', whereArgs:[id]);
|
||||||
|
return res.map((m) => AssignmentModel.fromMap(m)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<AssignmentModel>> getAllAsignaciones() async {
|
||||||
|
final res = await (await database).query('asignaciones');
|
||||||
|
return res.map((m) => AssignmentModel.fromMap(m)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ALERTAS ──────────────────────────────────────────────────────────────
|
||||||
|
static Future<int> insertAlerta(AlertaModel a) async =>
|
||||||
|
(await database).insert('alertas', a.toMap());
|
||||||
|
|
||||||
|
static Future<List<AlertaModel>> getAlertas({bool soloNoResueltas=false}) async {
|
||||||
|
final db = await database;
|
||||||
|
final res = soloNoResueltas
|
||||||
|
? await db.query('alertas', where:'resuelta=0', orderBy:'fecha DESC')
|
||||||
|
: await db.query('alertas', orderBy:'fecha DESC');
|
||||||
|
return res.map((m) => AlertaModel.fromMap(m)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<AlertaModel>> getIncidentesConductor() async {
|
||||||
|
final res = await (await database).query('alertas',
|
||||||
|
where:"tipo LIKE 'INCIDENTE_%'", orderBy:'fecha DESC');
|
||||||
|
return res.map((m) => AlertaModel.fromMap(m)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> resolverAlerta(int id) async =>
|
||||||
|
(await database).update('alertas', {'resuelta':1}, where:'id=?', whereArgs:[id]);
|
||||||
|
|
||||||
|
// ── REPORTES ─────────────────────────────────────────────────────────────
|
||||||
|
static Future<int> insertReporte(ReporteModel r) async =>
|
||||||
|
(await database).insert('reportes', r.toMap());
|
||||||
|
|
||||||
|
static Future<List<ReporteModel>> getAllReportes() async {
|
||||||
|
final res = await (await database).query('reportes', orderBy:'fecha DESC');
|
||||||
|
return res.map((m) => ReporteModel.fromMap(m)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<ReporteModel>> getReportesByUser(int userId) async {
|
||||||
|
final res = await (await database).query('reportes',
|
||||||
|
where:'user_id=?', whereArgs:[userId], orderBy:'fecha DESC');
|
||||||
|
return res.map((m) => ReporteModel.fromMap(m)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<Map<String, dynamic>>> getReportesConUsuario() async {
|
||||||
|
final db = await database;
|
||||||
|
return db.rawQuery('''
|
||||||
|
SELECT r.*, u.nombre as user_nombre, u.email as user_email
|
||||||
|
FROM reportes r LEFT JOIN users u ON r.user_id = u.id
|
||||||
|
ORDER BY r.fecha DESC''');
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> updateReporteEstado(int id, String estado) async =>
|
||||||
|
(await database).update('reportes', {'estado':estado}, where:'id=?', whereArgs:[id]);
|
||||||
|
|
||||||
|
// ── REVIEWS ──────────────────────────────────────────────────────────────
|
||||||
|
static Future<int> insertReview(ReviewModel r) async =>
|
||||||
|
(await database).insert('reviews', r.toMap());
|
||||||
|
|
||||||
|
static Future<List<ReviewModel>> getAllReviews() async {
|
||||||
|
final res = await (await database).query('reviews', orderBy:'fecha DESC');
|
||||||
|
return res.map((m) => ReviewModel.fromMap(m)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> hasReviewedRoute(int userId, String routeId) async {
|
||||||
|
final today = DateTime.now().toIso8601String().substring(0, 10);
|
||||||
|
final res = await (await database).query('reviews',
|
||||||
|
where:"user_id=? AND route_id=? AND fecha LIKE '$today%'",
|
||||||
|
whereArgs:[userId, routeId]);
|
||||||
|
return res.isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<Map<String, dynamic>>> getReviewSummaryByColonia() async {
|
||||||
|
final db = await database;
|
||||||
|
return db.rawQuery('''
|
||||||
|
SELECT colonia, route_id,
|
||||||
|
AVG(estrellas) as promedio,
|
||||||
|
COUNT(*) as total,
|
||||||
|
MIN(estrellas) as min_est,
|
||||||
|
MAX(estrellas) as max_est
|
||||||
|
FROM reviews GROUP BY colonia ORDER BY promedio ASC''');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── NOTIFICATION HISTORY ─────────────────────────────────────────────────
|
||||||
|
static Future<int> insertNotifHistory({
|
||||||
|
int? userId, required String routeId, required String eventType,
|
||||||
|
required String title, required String body,
|
||||||
|
}) async => (await database).insert('notification_history', {
|
||||||
|
'user_id': userId, 'route_id': routeId, 'event_type': eventType,
|
||||||
|
'title': title, 'body': body,
|
||||||
|
'fecha': DateTime.now().toIso8601String(), 'leida': 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
static Future<List<Map<String, dynamic>>> getNotifHistory(int userId) async =>
|
||||||
|
(await database).query('notification_history',
|
||||||
|
where: 'user_id IS NULL OR user_id = ?', whereArgs: [userId],
|
||||||
|
orderBy: 'fecha DESC', limit: 50);
|
||||||
|
|
||||||
|
static Future<int> countUnreadNotifs(int userId) async {
|
||||||
|
final res = await (await database).rawQuery(
|
||||||
|
'SELECT COUNT(*) as c FROM notification_history WHERE (user_id IS NULL OR user_id=?) AND leida=0',
|
||||||
|
[userId]);
|
||||||
|
return (res.first['c'] as int? ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> markAllNotifsRead(int userId) async =>
|
||||||
|
(await database).update('notification_history', {'leida': 1},
|
||||||
|
where: 'user_id IS NULL OR user_id = ?', whereArgs: [userId]);
|
||||||
|
|
||||||
|
// ── CONDUCTORES CON METADATA ─────────────────────────────────────────────
|
||||||
|
static Future<List<Map<String, dynamic>>> getConductoresConMeta() async {
|
||||||
|
final db = await database;
|
||||||
|
return db.rawQuery('''
|
||||||
|
SELECT u.*, COALESCE(m.activo, 1) as activo, m.notas,
|
||||||
|
(SELECT COUNT(*) FROM alertas a
|
||||||
|
WHERE a.tipo LIKE 'INCIDENTE_%'
|
||||||
|
AND a.route_id IN (
|
||||||
|
SELECT route_id FROM asignaciones WHERE conductor_id = u.id
|
||||||
|
)) as total_incidentes
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN user_meta m ON m.user_id = u.id
|
||||||
|
WHERE u.rol = 'CONDUCTOR'
|
||||||
|
ORDER BY u.nombre ASC''');
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> updateConductorMeta(int userId, bool activo, String notas) async {
|
||||||
|
final db = await database;
|
||||||
|
final ex = await db.query('user_meta', where:'user_id=?', whereArgs:[userId]);
|
||||||
|
if (ex.isEmpty) {
|
||||||
|
await db.insert('user_meta', {'user_id':userId,'activo':activo?1:0,'notas':notas});
|
||||||
|
} else {
|
||||||
|
await db.update('user_meta', {'activo':activo?1:0,'notas':notas},
|
||||||
|
where:'user_id=?', whereArgs:[userId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<int> insertConductor(String nombre, String email, String password) async {
|
||||||
|
final db = await database;
|
||||||
|
final uid = await db.insert('users',
|
||||||
|
{'nombre':nombre,'email':email,'password':password,'rol':'CONDUCTOR'},
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.abort);
|
||||||
|
await db.insert('user_meta', {'user_id':uid,'activo':1});
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> updateConductor(int id, String nombre, String email) async =>
|
||||||
|
(await database).update('users', {'nombre':nombre,'email':email},
|
||||||
|
where:'id=?', whereArgs:[id]);
|
||||||
|
|
||||||
|
// ── ESTADÍSTICAS ─────────────────────────────────────────────────────────
|
||||||
|
static Future<Map<String, dynamic>> getAdminStats() async {
|
||||||
|
final db = await database;
|
||||||
|
final totalReportes = (await db.rawQuery('SELECT COUNT(*) as c FROM reportes')).first['c'];
|
||||||
|
final totalReviews = (await db.rawQuery('SELECT COUNT(*) as c FROM reviews')).first['c'];
|
||||||
|
final avgRating = (await db.rawQuery('SELECT AVG(estrellas) as a FROM reviews')).first['a'];
|
||||||
|
final totalAlertas = (await db.rawQuery('SELECT COUNT(*) as c FROM alertas WHERE resuelta=0')).first['c'];
|
||||||
|
final totalConductores = (await db.rawQuery(
|
||||||
|
"SELECT COUNT(*) as c FROM users WHERE rol='CONDUCTOR'")).first['c'];
|
||||||
|
return {
|
||||||
|
'total_reportes': totalReportes ?? 0,
|
||||||
|
'total_reviews': totalReviews ?? 0,
|
||||||
|
'avg_rating': (avgRating as num?)?.toDouble() ?? 0.0,
|
||||||
|
'alertas_activas': totalAlertas ?? 0,
|
||||||
|
'total_conductores': totalConductores ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<Map<String, dynamic>>> getReportesByColonia() async {
|
||||||
|
final db = await database;
|
||||||
|
return db.rawQuery('''
|
||||||
|
SELECT colonia, COUNT(*) as total,
|
||||||
|
SUM(CASE WHEN estado='RESUELTO' THEN 1 ELSE 0 END) as resueltos
|
||||||
|
FROM reportes GROUP BY colonia ORDER BY total DESC LIMIT 10''');
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<Map<String, dynamic>>> getIncidentesByRoute() async {
|
||||||
|
final db = await database;
|
||||||
|
return db.rawQuery('''
|
||||||
|
SELECT route_id, COUNT(*) as total
|
||||||
|
FROM alertas WHERE tipo LIKE 'INCIDENTE_%'
|
||||||
|
GROUP BY route_id ORDER BY total DESC LIMIT 10''');
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<Map<String, dynamic>>> getRatingByWeek() async {
|
||||||
|
final db = await database;
|
||||||
|
return db.rawQuery('''
|
||||||
|
SELECT strftime('%W', fecha) as semana,
|
||||||
|
AVG(estrellas) as promedio,
|
||||||
|
COUNT(*) as total
|
||||||
|
FROM reviews
|
||||||
|
GROUP BY semana ORDER BY semana DESC LIMIT 8''');
|
||||||
|
}
|
||||||
|
}
|
||||||
82
celaya_limpia/lib/main.dart
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'core/app_colors.dart';
|
||||||
|
import 'services/auth_service.dart';
|
||||||
|
import 'services/route_simulator_service.dart';
|
||||||
|
import 'services/theme_service.dart';
|
||||||
|
import 'screens/splash_screen.dart';
|
||||||
|
import 'screens/login_screen.dart';
|
||||||
|
import 'screens/register_screen.dart';
|
||||||
|
import 'screens/citizen/citizen_home_screen.dart';
|
||||||
|
import 'screens/driver/driver_home_screen.dart';
|
||||||
|
import 'screens/admin/admin_dashboard_screen.dart';
|
||||||
|
import 'screens/onboarding/onboarding_screen.dart';
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final onboardingDone = prefs.getBool('onboarding_done') ?? false;
|
||||||
|
runApp(CelayaLimpiaApp(onboardingDone: onboardingDone));
|
||||||
|
}
|
||||||
|
|
||||||
|
class CelayaLimpiaApp extends StatelessWidget {
|
||||||
|
final bool onboardingDone;
|
||||||
|
const CelayaLimpiaApp({super.key, required this.onboardingDone});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MultiProvider(
|
||||||
|
providers: [
|
||||||
|
ChangeNotifierProvider(create: (_) => AuthService()),
|
||||||
|
ChangeNotifierProvider(create: (_) => RouteSimulatorService()),
|
||||||
|
ChangeNotifierProvider(create: (_) => ThemeService()),
|
||||||
|
],
|
||||||
|
child: Consumer<ThemeService>(
|
||||||
|
builder: (_, themeService, __) => MaterialApp(
|
||||||
|
title: 'Celaya Limpia',
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
themeMode: themeService.themeMode,
|
||||||
|
theme: _lightTheme(),
|
||||||
|
darkTheme: _darkTheme(),
|
||||||
|
initialRoute: onboardingDone ? '/splash' : '/onboarding',
|
||||||
|
routes: {
|
||||||
|
'/onboarding': (_) => const OnboardingScreen(),
|
||||||
|
'/splash': (_) => const SplashScreen(),
|
||||||
|
'/login': (_) => const LoginScreen(),
|
||||||
|
'/register': (_) => const RegisterScreen(),
|
||||||
|
'/home': (_) => const CitizenHomeScreen(),
|
||||||
|
'/driver': (_) => const DriverHomeScreen(),
|
||||||
|
'/admin': (_) => const AdminDashboardScreen(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeData _lightTheme() => ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
brightness: Brightness.light,
|
||||||
|
colorScheme: ColorScheme.fromSeed(seedColor: AppColors.guindaPrimary,
|
||||||
|
primary: AppColors.guindaPrimary, secondary: AppColors.dorado),
|
||||||
|
scaffoldBackgroundColor: AppColors.grisFondo,
|
||||||
|
inputDecorationTheme: const InputDecorationTheme(
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: AppColors.guindaPrimary, width: 2)),
|
||||||
|
labelStyle: TextStyle(color: AppColors.guindaPrimary)),
|
||||||
|
);
|
||||||
|
|
||||||
|
ThemeData _darkTheme() => ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
colorScheme: ColorScheme.fromSeed(seedColor: AppColors.guindaLight,
|
||||||
|
brightness: Brightness.dark, primary: AppColors.guindaLight,
|
||||||
|
secondary: AppColors.dorado),
|
||||||
|
scaffoldBackgroundColor: const Color(0xFF121212),
|
||||||
|
cardColor: const Color(0xFF1E1E1E),
|
||||||
|
inputDecorationTheme: const InputDecorationTheme(
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: AppColors.guindaLight, width: 2)),
|
||||||
|
labelStyle: TextStyle(color: AppColors.guindaLight)),
|
||||||
|
);
|
||||||
|
}
|
||||||
186
celaya_limpia/lib/models/models.dart
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
// ── USER ──────────────────────────────────────────────────────────────────
|
||||||
|
class UserModel {
|
||||||
|
final int? id;
|
||||||
|
final String nombre;
|
||||||
|
final String email;
|
||||||
|
final String password;
|
||||||
|
final String rol;
|
||||||
|
|
||||||
|
UserModel({this.id, required this.nombre, required this.email,
|
||||||
|
required this.password, required this.rol});
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() =>
|
||||||
|
{'id':id,'nombre':nombre,'email':email,'password':password,'rol':rol};
|
||||||
|
|
||||||
|
factory UserModel.fromMap(Map<String, dynamic> m) => UserModel(
|
||||||
|
id:m['id'], nombre:m['nombre'], email:m['email'],
|
||||||
|
password:m['password'], rol:m['rol']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DOMICILIO (User → Domicilio → Zona → Ruta) ────────────────────────────
|
||||||
|
class DomicilioModel {
|
||||||
|
final int? id;
|
||||||
|
final int userId;
|
||||||
|
final String alias; // "Casa", "Trabajo", etc.
|
||||||
|
final String calle;
|
||||||
|
final String colonia; // Zona de cobertura
|
||||||
|
final String routeId; // Ruta asignada
|
||||||
|
final String horarioEstimado;
|
||||||
|
final bool isPrimary;
|
||||||
|
|
||||||
|
DomicilioModel({this.id, required this.userId, this.alias = 'Casa',
|
||||||
|
required this.calle, required this.colonia, required this.routeId,
|
||||||
|
required this.horarioEstimado, this.isPrimary = true});
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() => {'id':id,'user_id':userId,'alias':alias,
|
||||||
|
'calle':calle,'colonia':colonia,'route_id':routeId,
|
||||||
|
'horario_estimado':horarioEstimado,'is_primary':isPrimary?1:0};
|
||||||
|
|
||||||
|
factory DomicilioModel.fromMap(Map<String, dynamic> m) => DomicilioModel(
|
||||||
|
id:m['id'], userId:m['user_id'], alias:m['alias']??'Casa',
|
||||||
|
calle:m['calle'], colonia:m['colonia'], routeId:m['route_id'],
|
||||||
|
horarioEstimado:m['horario_estimado'], isPrimary:m['is_primary']==1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── RUTA DINÁMICA (creada por admin) ──────────────────────────────────────
|
||||||
|
class RouteDefinitionModel {
|
||||||
|
final int? id;
|
||||||
|
final String routeId;
|
||||||
|
final String nombre;
|
||||||
|
final List<String> dias; // ['LUNES','MIERCOLES','VIERNES']
|
||||||
|
final String horaInicio; // '06:00'
|
||||||
|
final String horaFin; // '08:00'
|
||||||
|
final String turno; // MATUTINO|VESPERTINO|NOCTURNO
|
||||||
|
final List<String> colonias; // colonias que cubre
|
||||||
|
final bool activa;
|
||||||
|
|
||||||
|
RouteDefinitionModel({this.id, required this.routeId, required this.nombre,
|
||||||
|
required this.dias, required this.horaInicio, required this.horaFin,
|
||||||
|
required this.turno, required this.colonias, this.activa = true});
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() => {
|
||||||
|
'id':id,'route_id':routeId,'nombre':nombre,
|
||||||
|
'dias':dias.join(','),'hora_inicio':horaInicio,'hora_fin':horaFin,
|
||||||
|
'turno':turno,'colonias':colonias.join('|'),'activa':activa?1:0,
|
||||||
|
};
|
||||||
|
|
||||||
|
factory RouteDefinitionModel.fromMap(Map<String, dynamic> m) =>
|
||||||
|
RouteDefinitionModel(
|
||||||
|
id:m['id'], routeId:m['route_id'], nombre:m['nombre'],
|
||||||
|
dias:(m['dias']??'').toString().split(',').where((s)=>s.isNotEmpty).toList(),
|
||||||
|
horaInicio:m['hora_inicio']??'06:00', horaFin:m['hora_fin']??'08:00',
|
||||||
|
turno:m['turno']??'MATUTINO',
|
||||||
|
colonias:(m['colonias']??'').toString().split('|').where((s)=>s.isNotEmpty).toList(),
|
||||||
|
activa:m['activa']==1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── RESEÑA DEL SERVICIO ───────────────────────────────────────────────────
|
||||||
|
class ReviewModel {
|
||||||
|
final int? id;
|
||||||
|
final int userId;
|
||||||
|
final String colonia;
|
||||||
|
final String routeId;
|
||||||
|
final int estrellas; // 1-5
|
||||||
|
final String comentario;
|
||||||
|
final String fecha;
|
||||||
|
final String nombreUsuario;
|
||||||
|
|
||||||
|
ReviewModel({this.id, required this.userId, required this.colonia,
|
||||||
|
required this.routeId, required this.estrellas, required this.comentario,
|
||||||
|
required this.fecha, this.nombreUsuario = ''});
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() => {'id':id,'user_id':userId,'colonia':colonia,
|
||||||
|
'route_id':routeId,'estrellas':estrellas,'comentario':comentario,
|
||||||
|
'fecha':fecha,'nombre_usuario':nombreUsuario};
|
||||||
|
|
||||||
|
factory ReviewModel.fromMap(Map<String, dynamic> m) => ReviewModel(
|
||||||
|
id:m['id'], userId:m['user_id'], colonia:m['colonia'],
|
||||||
|
routeId:m['route_id'], estrellas:m['estrellas'],
|
||||||
|
comentario:m['comentario']??'', fecha:m['fecha'],
|
||||||
|
nombreUsuario:m['nombre_usuario']??'Ciudadano');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ASSIGNMENT ────────────────────────────────────────────────────────────
|
||||||
|
class AssignmentModel {
|
||||||
|
final int? id;
|
||||||
|
final int conductorId;
|
||||||
|
final String routeId;
|
||||||
|
final String diaSemana;
|
||||||
|
final String turno;
|
||||||
|
|
||||||
|
AssignmentModel({this.id, required this.conductorId, required this.routeId,
|
||||||
|
required this.diaSemana, required this.turno});
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() => {'id':id,'conductor_id':conductorId,
|
||||||
|
'route_id':routeId,'dia_semana':diaSemana,'turno':turno};
|
||||||
|
|
||||||
|
factory AssignmentModel.fromMap(Map<String, dynamic> m) => AssignmentModel(
|
||||||
|
id:m['id'], conductorId:m['conductor_id'], routeId:m['route_id'],
|
||||||
|
diaSemana:m['dia_semana'], turno:m['turno']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ROUTE STATUS ──────────────────────────────────────────────────────────
|
||||||
|
class RouteStatusModel {
|
||||||
|
final String routeId;
|
||||||
|
final String status;
|
||||||
|
final String? mensaje;
|
||||||
|
final String updatedAt;
|
||||||
|
|
||||||
|
RouteStatusModel({required this.routeId, required this.status,
|
||||||
|
this.mensaje, required this.updatedAt});
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() => {'route_id':routeId,'status':status,
|
||||||
|
'mensaje':mensaje,'updated_at':updatedAt};
|
||||||
|
|
||||||
|
factory RouteStatusModel.fromMap(Map<String, dynamic> m) => RouteStatusModel(
|
||||||
|
routeId:m['route_id'], status:m['status'],
|
||||||
|
mensaje:m['mensaje'], updatedAt:m['updated_at']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ALERTA ────────────────────────────────────────────────────────────────
|
||||||
|
class AlertaModel {
|
||||||
|
final int? id;
|
||||||
|
final String tipo;
|
||||||
|
final String routeId;
|
||||||
|
final String mensaje;
|
||||||
|
final String fecha;
|
||||||
|
final bool resuelta;
|
||||||
|
|
||||||
|
AlertaModel({this.id, required this.tipo, required this.routeId,
|
||||||
|
required this.mensaje, required this.fecha, this.resuelta=false});
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() => {'id':id,'tipo':tipo,'route_id':routeId,
|
||||||
|
'mensaje':mensaje,'fecha':fecha,'resuelta':resuelta?1:0};
|
||||||
|
|
||||||
|
factory AlertaModel.fromMap(Map<String, dynamic> m) => AlertaModel(
|
||||||
|
id:m['id'], tipo:m['tipo'], routeId:m['route_id'],
|
||||||
|
mensaje:m['mensaje'], fecha:m['fecha'], resuelta:m['resuelta']==1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── REPORTE ───────────────────────────────────────────────────────────────
|
||||||
|
class ReporteModel {
|
||||||
|
final int? id;
|
||||||
|
final int userId;
|
||||||
|
final String tipo;
|
||||||
|
final String descripcion;
|
||||||
|
final String colonia;
|
||||||
|
final String routeId;
|
||||||
|
final String fecha;
|
||||||
|
final String estado;
|
||||||
|
final int calificacion;
|
||||||
|
|
||||||
|
ReporteModel({this.id, required this.userId, required this.tipo,
|
||||||
|
required this.descripcion, required this.colonia, required this.routeId,
|
||||||
|
required this.fecha, this.estado='PENDIENTE', this.calificacion=5});
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() => {'id':id,'user_id':userId,'tipo':tipo,
|
||||||
|
'descripcion':descripcion,'colonia':colonia,'route_id':routeId,
|
||||||
|
'fecha':fecha,'estado':estado,'calificacion':calificacion};
|
||||||
|
|
||||||
|
factory ReporteModel.fromMap(Map<String, dynamic> m) => ReporteModel(
|
||||||
|
id:m['id'], userId:m['user_id'], tipo:m['tipo'],
|
||||||
|
descripcion:m['descripcion'], colonia:m['colonia'],
|
||||||
|
routeId:m['route_id']??'', fecha:m['fecha'],
|
||||||
|
estado:m['estado'], calificacion:m['calificacion']??5);
|
||||||
|
}
|
||||||
39
celaya_limpia/lib/models/route_model.dart
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
|
class RoutePosition {
|
||||||
|
final int positionId;
|
||||||
|
final double lat;
|
||||||
|
final double lng;
|
||||||
|
final int speed;
|
||||||
|
final String timestamp;
|
||||||
|
|
||||||
|
RoutePosition({required this.positionId, required this.lat,
|
||||||
|
required this.lng, required this.speed, required this.timestamp});
|
||||||
|
|
||||||
|
LatLng get latLng => LatLng(lat, lng);
|
||||||
|
}
|
||||||
|
|
||||||
|
class RouteModel {
|
||||||
|
final String routeId;
|
||||||
|
final String name;
|
||||||
|
final int truckId;
|
||||||
|
String status;
|
||||||
|
final List<RoutePosition> positions;
|
||||||
|
final String turno;
|
||||||
|
|
||||||
|
RouteModel({required this.routeId, required this.name,
|
||||||
|
required this.truckId, required this.status,
|
||||||
|
required this.positions, this.turno = 'MATUTINO'});
|
||||||
|
|
||||||
|
List<LatLng> get polylinePoints =>
|
||||||
|
positions.map((p) => LatLng(p.lat, p.lng)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ColonyModel {
|
||||||
|
final String colonia;
|
||||||
|
final String routeId;
|
||||||
|
final String horarioEstimado;
|
||||||
|
|
||||||
|
ColonyModel({required this.colonia, required this.routeId,
|
||||||
|
required this.horarioEstimado});
|
||||||
|
}
|
||||||
1070
celaya_limpia/lib/screens/admin/admin_dashboard_screen.dart
Normal file
262
celaya_limpia/lib/screens/admin/admin_stats_screen.dart
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
|
import '../../core/app_colors.dart';
|
||||||
|
import '../../database/db_helper.dart';
|
||||||
|
|
||||||
|
class AdminStatsScreen extends StatefulWidget {
|
||||||
|
const AdminStatsScreen({super.key});
|
||||||
|
@override State<AdminStatsScreen> createState() => _AdminStatsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AdminStatsScreenState extends State<AdminStatsScreen> {
|
||||||
|
Map<String, dynamic> _stats = {};
|
||||||
|
List<Map<String, dynamic>> _byColonia = [];
|
||||||
|
List<Map<String, dynamic>> _byRoute = [];
|
||||||
|
List<Map<String, dynamic>> _byWeek = [];
|
||||||
|
bool _loading = true;
|
||||||
|
|
||||||
|
@override void initState() { super.initState(); _load(); }
|
||||||
|
|
||||||
|
Future<void> _load() async {
|
||||||
|
final s = await DbHelper.getAdminStats();
|
||||||
|
final bc = await DbHelper.getReportesByColonia();
|
||||||
|
final br = await DbHelper.getIncidentesByRoute();
|
||||||
|
final bw = await DbHelper.getRatingByWeek();
|
||||||
|
if (mounted) setState(() {
|
||||||
|
_stats = s; _byColonia = bc; _byRoute = br; _byWeek = bw; _loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
backgroundColor: AppColors.grisFondo,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
|
||||||
|
title: const Text('Dashboard de Estadisticas'),
|
||||||
|
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||||
|
child: Container(height: 4, color: AppColors.dorado)),
|
||||||
|
actions: [IconButton(icon: const Icon(Icons.refresh), onPressed: _load)],
|
||||||
|
),
|
||||||
|
body: _loading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: SingleChildScrollView(padding: const EdgeInsets.all(14), child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
|
||||||
|
// KPIs
|
||||||
|
Row(children: [
|
||||||
|
_KpiCard('Reportes', '${_stats['total_reportes']}',
|
||||||
|
Icons.report, AppColors.naranjaAlerta),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_KpiCard('Calificacion Prom.',
|
||||||
|
(_stats['avg_rating'] as double? ?? 0).toStringAsFixed(1),
|
||||||
|
Icons.star, Colors.amber),
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(children: [
|
||||||
|
_KpiCard('Alertas Activas', '${_stats['alertas_activas']}',
|
||||||
|
Icons.warning, AppColors.rojoError),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_KpiCard('Conductores', '${_stats['total_conductores']}',
|
||||||
|
Icons.person, AppColors.moradoConductor),
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Calificacion por semana (línea)
|
||||||
|
if (_byWeek.isNotEmpty) ...[
|
||||||
|
_SectionTitle('Calificacion promedio semanal'),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Card(child: Padding(padding: const EdgeInsets.all(16),
|
||||||
|
child: SizedBox(height: 180,
|
||||||
|
child: LineChart(LineChartData(
|
||||||
|
minY: 1, maxY: 5,
|
||||||
|
titlesData: FlTitlesData(
|
||||||
|
leftTitles: AxisTitles(sideTitles: SideTitles(
|
||||||
|
showTitles: true, interval: 1,
|
||||||
|
getTitlesWidget: (v,_) => Text(v.toInt().toString(),
|
||||||
|
style: const TextStyle(fontSize: 10)))),
|
||||||
|
bottomTitles: AxisTitles(sideTitles: SideTitles(
|
||||||
|
showTitles: true,
|
||||||
|
getTitlesWidget: (v, _) {
|
||||||
|
final idx = v.toInt();
|
||||||
|
if (idx < 0 || idx >= _byWeek.length) return const SizedBox();
|
||||||
|
return Text('S${_byWeek.length - idx}',
|
||||||
|
style: const TextStyle(fontSize: 9));
|
||||||
|
})),
|
||||||
|
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
|
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
|
),
|
||||||
|
gridData: FlGridData(drawHorizontalLine: true, horizontalInterval: 1),
|
||||||
|
borderData: FlBorderData(show: true,
|
||||||
|
border: Border.all(color: Colors.grey.shade300)),
|
||||||
|
lineBarsData: [LineChartBarData(
|
||||||
|
spots: _byWeek.reversed.toList().asMap().entries.map((e) =>
|
||||||
|
FlSpot(e.key.toDouble(),
|
||||||
|
(e.value['promedio'] as num? ?? 0).toDouble().clamp(1.0, 5.0))).toList(),
|
||||||
|
isCurved: true,
|
||||||
|
color: AppColors.verdeAdmin,
|
||||||
|
barWidth: 3,
|
||||||
|
belowBarData: BarAreaData(show: true,
|
||||||
|
color: AppColors.verdeAdmin.withOpacity(0.1)),
|
||||||
|
dotData: const FlDotData(show: true),
|
||||||
|
)],
|
||||||
|
))))),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Reportes por colonia (barras horizontales)
|
||||||
|
if (_byColonia.isNotEmpty) ...[
|
||||||
|
_SectionTitle('Reportes por colonia (Top 10)'),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Card(child: Padding(padding: const EdgeInsets.all(16),
|
||||||
|
child: SizedBox(height: 240,
|
||||||
|
child: BarChart(BarChartData(
|
||||||
|
alignment: BarChartAlignment.spaceAround,
|
||||||
|
maxY: (_byColonia.map((c) => (c['total'] as int? ?? 0).toDouble())
|
||||||
|
.reduce((a,b)=>a>b?a:b) * 1.2),
|
||||||
|
titlesData: FlTitlesData(
|
||||||
|
bottomTitles: AxisTitles(sideTitles: SideTitles(
|
||||||
|
showTitles: true, reservedSize: 32,
|
||||||
|
getTitlesWidget: (v, _) {
|
||||||
|
final i = v.toInt();
|
||||||
|
if (i < 0 || i >= _byColonia.length) return const SizedBox();
|
||||||
|
final name = (_byColonia[i]['colonia'] as String? ?? '');
|
||||||
|
return Transform.rotate(angle: -0.5,
|
||||||
|
child: Text(name.length > 8 ? '${name.substring(0,8)}.' : name,
|
||||||
|
style: const TextStyle(fontSize: 8)));
|
||||||
|
})),
|
||||||
|
leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: true,
|
||||||
|
getTitlesWidget: (v,_) => Text(v.toInt().toString(),
|
||||||
|
style: const TextStyle(fontSize: 9)))),
|
||||||
|
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
|
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
|
),
|
||||||
|
barGroups: _byColonia.asMap().entries.map((e) => BarChartGroupData(
|
||||||
|
x: e.key,
|
||||||
|
barRods: [
|
||||||
|
BarChartRodData(
|
||||||
|
toY: (e.value['total'] as int? ?? 0).toDouble(),
|
||||||
|
color: AppColors.guindaPrimary,
|
||||||
|
width: 16, borderRadius: BorderRadius.circular(4)),
|
||||||
|
BarChartRodData(
|
||||||
|
toY: (e.value['resueltos'] as int? ?? 0).toDouble(),
|
||||||
|
color: AppColors.verdeExito,
|
||||||
|
width: 16, borderRadius: BorderRadius.circular(4)),
|
||||||
|
],
|
||||||
|
)).toList(),
|
||||||
|
gridData: const FlGridData(drawHorizontalLine: true),
|
||||||
|
borderData: FlBorderData(show: true,
|
||||||
|
border: Border.all(color: Colors.grey.shade300)),
|
||||||
|
))))),
|
||||||
|
Padding(padding: const EdgeInsets.symmetric(vertical: 6),
|
||||||
|
child: Row(children: [
|
||||||
|
_Legend(AppColors.guindaPrimary, 'Total reportes'),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
_Legend(AppColors.verdeExito, 'Resueltos'),
|
||||||
|
])),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Rutas con más incidentes
|
||||||
|
if (_byRoute.isNotEmpty) ...[
|
||||||
|
_SectionTitle('Rutas con mas incidentes'),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Card(child: Padding(padding: const EdgeInsets.all(16),
|
||||||
|
child: SizedBox(height: 200,
|
||||||
|
child: BarChart(BarChartData(
|
||||||
|
alignment: BarChartAlignment.spaceAround,
|
||||||
|
maxY: (_byRoute.map((r) => (r['total'] as int? ?? 0).toDouble())
|
||||||
|
.reduce((a,b)=>a>b?a:b) * 1.3),
|
||||||
|
titlesData: FlTitlesData(
|
||||||
|
bottomTitles: AxisTitles(sideTitles: SideTitles(
|
||||||
|
showTitles: true, reservedSize: 28,
|
||||||
|
getTitlesWidget: (v, _) {
|
||||||
|
final i = v.toInt();
|
||||||
|
if (i < 0 || i >= _byRoute.length) return const SizedBox();
|
||||||
|
return Text((_byRoute[i]['route_id'] as String? ?? '').replaceAll('RUTA-','R'),
|
||||||
|
style: const TextStyle(fontSize: 9));
|
||||||
|
})),
|
||||||
|
leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: true,
|
||||||
|
getTitlesWidget: (v,_) => Text(v.toInt().toString(),
|
||||||
|
style: const TextStyle(fontSize: 9)))),
|
||||||
|
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
|
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||||
|
),
|
||||||
|
barGroups: _byRoute.asMap().entries.map((e) => BarChartGroupData(
|
||||||
|
x: e.key,
|
||||||
|
barRods: [BarChartRodData(
|
||||||
|
toY: (e.value['total'] as int? ?? 0).toDouble(),
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [AppColors.naranjaAlerta, AppColors.rojoError],
|
||||||
|
begin: Alignment.bottomCenter, end: Alignment.topCenter),
|
||||||
|
width: 20, borderRadius: BorderRadius.circular(4))],
|
||||||
|
)).toList(),
|
||||||
|
gridData: const FlGridData(drawHorizontalLine: true),
|
||||||
|
borderData: FlBorderData(show: true,
|
||||||
|
border: Border.all(color: Colors.grey.shade300)),
|
||||||
|
))))),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Colonias más problemáticas (lista)
|
||||||
|
_SectionTitle('Colonias mas problematicas'),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Card(child: Column(children: [
|
||||||
|
..._byColonia.take(5).map((c) {
|
||||||
|
final total = (c['total'] as int? ?? 0);
|
||||||
|
final resueltos = (c['resueltos'] as int? ?? 0);
|
||||||
|
final pct = total > 0 ? resueltos / total : 0.0;
|
||||||
|
return ListTile(dense: true,
|
||||||
|
leading: CircleAvatar(radius: 16,
|
||||||
|
backgroundColor: total > 3 ? AppColors.rojoError.withOpacity(0.15)
|
||||||
|
: AppColors.naranjaAlerta.withOpacity(0.15),
|
||||||
|
child: Text('$total', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold,
|
||||||
|
color: total > 3 ? AppColors.rojoError : AppColors.naranjaAlerta))),
|
||||||
|
title: Text(c['colonia'] as String? ?? '',
|
||||||
|
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600)),
|
||||||
|
subtitle: LinearProgressIndicator(value: pct,
|
||||||
|
backgroundColor: Colors.grey.shade200,
|
||||||
|
valueColor: const AlwaysStoppedAnimation<Color>(AppColors.verdeExito)),
|
||||||
|
trailing: Text('${(pct*100).toInt()}% resuelto',
|
||||||
|
style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
])),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
])),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _KpiCard extends StatelessWidget {
|
||||||
|
final String label, value; final IconData icon; final Color color;
|
||||||
|
const _KpiCard(this.label, this.value, this.icon, this.color);
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Expanded(child: Card(child: Padding(
|
||||||
|
padding: const EdgeInsets.all(14), child: Row(children: [
|
||||||
|
CircleAvatar(radius: 22, backgroundColor: color.withOpacity(0.12),
|
||||||
|
child: Icon(icon, color: color, size: 22)),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text(value, style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: color)),
|
||||||
|
Text(label, style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)),
|
||||||
|
]),
|
||||||
|
]))));
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SectionTitle extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
const _SectionTitle(this.title);
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Text(title,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15, color: AppColors.verdeAdmin));
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Legend extends StatelessWidget {
|
||||||
|
final Color color; final String label;
|
||||||
|
const _Legend(this.color, this.label);
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
Container(width: 12, height: 12, decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(2))),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(label, style: const TextStyle(fontSize: 11, color: AppColors.grisTexto)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
272
celaya_limpia/lib/screens/admin/create_route_screen.dart
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../core/app_colors.dart';
|
||||||
|
import '../../data/celaya_colonias.dart';
|
||||||
|
import '../../database/db_helper.dart';
|
||||||
|
import '../../models/models.dart';
|
||||||
|
|
||||||
|
class CreateRouteScreen extends StatefulWidget {
|
||||||
|
final RouteDefinitionModel? editing;
|
||||||
|
const CreateRouteScreen({super.key, this.editing});
|
||||||
|
@override State<CreateRouteScreen> createState() => _CreateRouteScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreateRouteScreenState extends State<CreateRouteScreen> {
|
||||||
|
final _nombreCtrl = TextEditingController();
|
||||||
|
final _routeIdCtrl = TextEditingController();
|
||||||
|
String _turno = 'MATUTINO';
|
||||||
|
String _horaInicio = '06:00';
|
||||||
|
String _horaFin = '08:00';
|
||||||
|
List<String> _diasSeleccionados = [];
|
||||||
|
List<String> _coloniasSeleccionadas = [];
|
||||||
|
String _searchColonia = '';
|
||||||
|
bool _loading = false;
|
||||||
|
|
||||||
|
static const _diasGrupoA = ['LUNES', 'MIERCOLES', 'VIERNES'];
|
||||||
|
static const _diasGrupoB = ['MARTES', 'JUEVES', 'SABADO'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.editing != null) {
|
||||||
|
final e = widget.editing!;
|
||||||
|
_nombreCtrl.text = e.nombre;
|
||||||
|
_routeIdCtrl.text = e.routeId;
|
||||||
|
_turno = e.turno;
|
||||||
|
_horaInicio = e.horaInicio;
|
||||||
|
_horaFin = e.horaFin;
|
||||||
|
_diasSeleccionados = List.from(e.dias);
|
||||||
|
_coloniasSeleccionadas = List.from(e.colonias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> get _filteredColonias => _searchColonia.isEmpty
|
||||||
|
? celayaColonias
|
||||||
|
: celayaColonias.where((c) =>
|
||||||
|
c.toLowerCase().contains(_searchColonia.toLowerCase())).toList();
|
||||||
|
|
||||||
|
Future<void> _guardar() async {
|
||||||
|
if (_nombreCtrl.text.trim().isEmpty) {
|
||||||
|
_snack('Ingresa un nombre para la ruta', isError: true); return; }
|
||||||
|
if (_routeIdCtrl.text.trim().isEmpty) {
|
||||||
|
_snack('Ingresa el ID de la ruta (ej. RUTA-16)', isError: true); return; }
|
||||||
|
if (_diasSeleccionados.isEmpty) {
|
||||||
|
_snack('Selecciona al menos un día', isError: true); return; }
|
||||||
|
if (_coloniasSeleccionadas.isEmpty) {
|
||||||
|
_snack('Selecciona al menos una colonia', isError: true); return; }
|
||||||
|
|
||||||
|
setState(() => _loading = true);
|
||||||
|
final route = RouteDefinitionModel(
|
||||||
|
id: widget.editing?.id,
|
||||||
|
routeId: _routeIdCtrl.text.trim().toUpperCase(),
|
||||||
|
nombre: _nombreCtrl.text.trim(),
|
||||||
|
dias: _diasSeleccionados,
|
||||||
|
horaInicio: _horaInicio,
|
||||||
|
horaFin: _horaFin,
|
||||||
|
turno: _turno,
|
||||||
|
colonias: _coloniasSeleccionadas,
|
||||||
|
);
|
||||||
|
await DbHelper.insertRouteDefinition(route);
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _loading = false);
|
||||||
|
_snack('Ruta guardada correctamente');
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _snack(String msg, {bool isError = false}) =>
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
|
content: Text(msg),
|
||||||
|
backgroundColor: isError ? AppColors.rojoError : AppColors.verdeExito));
|
||||||
|
|
||||||
|
Future<TimeOfDay?> _pickTime(String current) async {
|
||||||
|
final parts = current.split(':');
|
||||||
|
return showTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: TimeOfDay(hour: int.parse(parts[0]), minute: int.parse(parts[1])),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _timeLabel(TimeOfDay t) =>
|
||||||
|
'${t.hour.toString().padLeft(2,'0')}:${t.minute.toString().padLeft(2,'0')}';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: AppColors.grisFondo,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
|
||||||
|
title: Text(widget.editing != null ? 'Editar Ruta' : 'Nueva Ruta'),
|
||||||
|
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||||
|
child: Container(height: 4, color: AppColors.dorado)),
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
|
||||||
|
// Info básica
|
||||||
|
_section('Información de la ruta'),
|
||||||
|
_field(_routeIdCtrl, 'ID de Ruta (ej. RUTA-16)', Icons.tag),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_field(_nombreCtrl, 'Nombre descriptivo', Icons.route),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Turno
|
||||||
|
_section('Turno de operación'),
|
||||||
|
Row(children: ['MATUTINO','VESPERTINO','NOCTURNO'].map((t) =>
|
||||||
|
Expanded(child: RadioListTile<String>(dense: true, value: t,
|
||||||
|
groupValue: _turno,
|
||||||
|
title: Text(_turnoLabel(t), style: const TextStyle(fontSize: 12)),
|
||||||
|
activeColor: AppColors.verdeAdmin,
|
||||||
|
onChanged: (v) => setState(() => _turno = v!)))
|
||||||
|
).toList()),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Horario
|
||||||
|
_section('Horario de servicio'),
|
||||||
|
Row(children: [
|
||||||
|
Expanded(child: _timeButton('Hora inicio', _horaInicio, () async {
|
||||||
|
final t = await _pickTime(_horaInicio);
|
||||||
|
if (t != null) setState(() => _horaInicio = _timeLabel(t));
|
||||||
|
})),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(child: _timeButton('Hora fin', _horaFin, () async {
|
||||||
|
final t = await _pickTime(_horaFin);
|
||||||
|
if (t != null) setState(() => _horaFin = _timeLabel(t));
|
||||||
|
})),
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Días
|
||||||
|
_section('Días de operación'),
|
||||||
|
Container(padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(color: Colors.blue.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.blue.shade200)),
|
||||||
|
child: const Text(
|
||||||
|
'📅 Selecciona Grupo A (L/M/V) o Grupo B (M/J/S), o días individuales.',
|
||||||
|
style: TextStyle(fontSize: 12, color: AppColors.azulInfo)),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(children: [
|
||||||
|
Expanded(child: OutlinedButton(
|
||||||
|
onPressed: () => setState(() => _diasSeleccionados = List.from(_diasGrupoA)),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: AppColors.verdeAdmin,
|
||||||
|
side: const BorderSide(color: AppColors.verdeAdmin)),
|
||||||
|
child: const Text('Grupo A\nL/M/V', textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 11)))),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(child: OutlinedButton(
|
||||||
|
onPressed: () => setState(() => _diasSeleccionados = List.from(_diasGrupoB)),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: AppColors.moradoConductor,
|
||||||
|
side: const BorderSide(color: AppColors.moradoConductor)),
|
||||||
|
child: const Text('Grupo B\nM/J/S', textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 11)))),
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Wrap(spacing: 6, runSpacing: 6, children: AppDias.todos.map((dia) {
|
||||||
|
final sel = _diasSeleccionados.contains(dia);
|
||||||
|
return FilterChip(
|
||||||
|
label: Text(AppDias.label(dia), style: TextStyle(fontSize: 11,
|
||||||
|
color: sel ? Colors.white : AppColors.negroTexto)),
|
||||||
|
selected: sel,
|
||||||
|
selectedColor: AppColors.verdeAdmin,
|
||||||
|
checkmarkColor: Colors.white,
|
||||||
|
onSelected: (v) => setState(() {
|
||||||
|
if (v) _diasSeleccionados.add(dia);
|
||||||
|
else _diasSeleccionados.remove(dia);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}).toList()),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Colonias
|
||||||
|
_section('Colonias que cubre (${_coloniasSeleccionadas.length} seleccionadas)'),
|
||||||
|
TextField(
|
||||||
|
onChanged: (v) => setState(() => _searchColonia = v),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'Buscar colonia de Celaya...',
|
||||||
|
prefixIcon: Icon(Icons.search), border: OutlineInputBorder(),
|
||||||
|
filled: true, fillColor: Colors.white, isDense: true),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Container(height: 220,
|
||||||
|
decoration: BoxDecoration(color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.grey.shade300)),
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: _filteredColonias.length,
|
||||||
|
itemBuilder: (_, i) {
|
||||||
|
final c = _filteredColonias[i];
|
||||||
|
final sel = _coloniasSeleccionadas.contains(c);
|
||||||
|
return CheckboxListTile(dense: true,
|
||||||
|
title: Text(c, style: const TextStyle(fontSize: 12)),
|
||||||
|
value: sel,
|
||||||
|
activeColor: AppColors.verdeAdmin,
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
onChanged: (v) => setState(() {
|
||||||
|
if (v == true) _coloniasSeleccionadas.add(c);
|
||||||
|
else _coloniasSeleccionadas.remove(c);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_coloniasSeleccionadas.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Wrap(spacing: 4, runSpacing: 4, children: _coloniasSeleccionadas.map((c) =>
|
||||||
|
Chip(label: Text(c, style: const TextStyle(fontSize: 10)),
|
||||||
|
backgroundColor: AppColors.verdeAdmin.withOpacity(0.1),
|
||||||
|
deleteIconColor: AppColors.verdeAdmin,
|
||||||
|
onDeleted: () => setState(() => _coloniasSeleccionadas.remove(c)))).toList()),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
SizedBox(width: double.infinity, height: 50,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: _loading ? null : _guardar,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||||
|
icon: _loading
|
||||||
|
? const SizedBox(width: 18, height: 18,
|
||||||
|
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||||
|
: const Icon(Icons.save),
|
||||||
|
label: const Text('GUARDAR RUTA', style: TextStyle(fontWeight: FontWeight.bold)))),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _section(String title) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: Text(title, style: const TextStyle(fontWeight: FontWeight.bold,
|
||||||
|
color: AppColors.verdeAdmin, fontSize: 15)));
|
||||||
|
|
||||||
|
Widget _field(TextEditingController ctrl, String label, IconData icon) =>
|
||||||
|
TextField(controller: ctrl,
|
||||||
|
decoration: InputDecoration(labelText: label,
|
||||||
|
prefixIcon: Icon(icon, color: AppColors.verdeAdmin),
|
||||||
|
border: const OutlineInputBorder(), filled: true, fillColor: Colors.white));
|
||||||
|
|
||||||
|
Widget _timeButton(String label, String value, VoidCallback onTap) =>
|
||||||
|
InkWell(onTap: onTap,
|
||||||
|
child: Container(padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.grey.shade400)),
|
||||||
|
child: Row(children: [
|
||||||
|
const Icon(Icons.access_time, color: AppColors.verdeAdmin, size: 18),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text(label, style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)),
|
||||||
|
Text(value, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||||
|
]),
|
||||||
|
])));
|
||||||
|
|
||||||
|
String _turnoLabel(String t) => t == 'MATUTINO' ? '🌄 Matutino'
|
||||||
|
: t == 'VESPERTINO' ? '🌅 Vespertino' : '🌙 Nocturno';
|
||||||
|
|
||||||
|
@override void dispose() { _nombreCtrl.dispose(); _routeIdCtrl.dispose(); super.dispose(); }
|
||||||
|
}
|
||||||
244
celaya_limpia/lib/screens/admin/export_pdf_screen.dart
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:pdf/pdf.dart';
|
||||||
|
import 'package:pdf/widgets.dart' as pw;
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import '../../core/app_colors.dart';
|
||||||
|
import '../../database/db_helper.dart';
|
||||||
|
|
||||||
|
class ExportPdfScreen extends StatefulWidget {
|
||||||
|
const ExportPdfScreen({super.key});
|
||||||
|
@override State<ExportPdfScreen> createState() => _ExportPdfScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ExportPdfScreenState extends State<ExportPdfScreen> {
|
||||||
|
bool _generating = false;
|
||||||
|
String? _lastPath;
|
||||||
|
|
||||||
|
pw.TableRow _pdfRow(String label, String value, {bool isHeader = false}) =>
|
||||||
|
pw.TableRow(
|
||||||
|
decoration: isHeader ? pw.BoxDecoration(color: PdfColors.grey100) : null,
|
||||||
|
children: [
|
||||||
|
pw.Padding(padding: const pw.EdgeInsets.all(6),
|
||||||
|
child: pw.Text(label, style: pw.TextStyle(
|
||||||
|
fontSize: 10, fontWeight: isHeader ? pw.FontWeight.bold : null))),
|
||||||
|
pw.Padding(padding: const pw.EdgeInsets.all(6),
|
||||||
|
child: pw.Text(value, style: pw.TextStyle(
|
||||||
|
fontSize: 10, fontWeight: pw.FontWeight.bold))),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Future<void> _generatePdf() async {
|
||||||
|
setState(() => _generating = true);
|
||||||
|
try {
|
||||||
|
final stats = await DbHelper.getAdminStats();
|
||||||
|
final colonias = await DbHelper.getReportesByColonia();
|
||||||
|
final incidentes = await DbHelper.getIncidentesByRoute();
|
||||||
|
final reviews = await DbHelper.getReviewSummaryByColonia();
|
||||||
|
final now = DateTime.now();
|
||||||
|
const meses = ['','Enero','Febrero','Marzo','Abril','Mayo','Junio',
|
||||||
|
'Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre'];
|
||||||
|
|
||||||
|
final pdf = pw.Document();
|
||||||
|
pdf.addPage(pw.MultiPage(
|
||||||
|
pageFormat: PdfPageFormat.a4,
|
||||||
|
margin: const pw.EdgeInsets.all(32),
|
||||||
|
header: (ctx) => pw.Column(children: [
|
||||||
|
pw.Row(mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [
|
||||||
|
pw.Column(crossAxisAlignment: pw.CrossAxisAlignment.start, children: [
|
||||||
|
pw.Text('H. AYUNTAMIENTO DE CELAYA', style: pw.TextStyle(
|
||||||
|
fontSize: 14, fontWeight: pw.FontWeight.bold,
|
||||||
|
color: PdfColor.fromHex('6D1E3A'))),
|
||||||
|
pw.Text('Direccion de Servicios Publicos',
|
||||||
|
style: const pw.TextStyle(fontSize: 11, color: PdfColors.grey700)),
|
||||||
|
pw.Text('Sistema de Recoleccion de Residuos',
|
||||||
|
style: const pw.TextStyle(fontSize: 10, color: PdfColors.grey600)),
|
||||||
|
]),
|
||||||
|
pw.Column(crossAxisAlignment: pw.CrossAxisAlignment.end, children: [
|
||||||
|
pw.Text('REPORTE MENSUAL', style: pw.TextStyle(
|
||||||
|
fontSize: 12, fontWeight: pw.FontWeight.bold,
|
||||||
|
color: PdfColor.fromHex('6D1E3A'))),
|
||||||
|
pw.Text('${meses[now.month]} ${now.year}',
|
||||||
|
style: const pw.TextStyle(fontSize: 11)),
|
||||||
|
pw.Text('Generado: ${now.day}/${now.month}/${now.year}',
|
||||||
|
style: const pw.TextStyle(fontSize: 9, color: PdfColors.grey600)),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
pw.Divider(color: PdfColor.fromHex('C9A84C'), thickness: 2),
|
||||||
|
pw.SizedBox(height: 8),
|
||||||
|
]),
|
||||||
|
build: (ctx) => [
|
||||||
|
pw.Text('RESUMEN EJECUTIVO', style: pw.TextStyle(
|
||||||
|
fontSize: 13, fontWeight: pw.FontWeight.bold,
|
||||||
|
color: PdfColor.fromHex('6D1E3A'))),
|
||||||
|
pw.SizedBox(height: 8),
|
||||||
|
pw.Table(
|
||||||
|
border: pw.TableBorder.all(color: PdfColors.grey300),
|
||||||
|
columnWidths: {0: const pw.FlexColumnWidth(2), 1: const pw.FlexColumnWidth(1)},
|
||||||
|
children: [
|
||||||
|
_pdfRow('Total de reportes ciudadanos', '${stats["total_reportes"]}', isHeader: true),
|
||||||
|
_pdfRow('Total de resenas recibidas', '${stats["total_reviews"]}'),
|
||||||
|
_pdfRow('Calificacion promedio',
|
||||||
|
'${(stats["avg_rating"] as double? ?? 0).toStringAsFixed(2)} / 5.0'),
|
||||||
|
_pdfRow('Alertas activas', '${stats["alertas_activas"]}'),
|
||||||
|
_pdfRow('Conductores', '${stats["total_conductores"]}'),
|
||||||
|
]),
|
||||||
|
pw.SizedBox(height: 20),
|
||||||
|
|
||||||
|
if (colonias.isNotEmpty) ...[
|
||||||
|
pw.Text('REPORTES POR COLONIA', style: pw.TextStyle(
|
||||||
|
fontSize: 13, fontWeight: pw.FontWeight.bold,
|
||||||
|
color: PdfColor.fromHex('6D1E3A'))),
|
||||||
|
pw.SizedBox(height: 8),
|
||||||
|
pw.Table(
|
||||||
|
border: pw.TableBorder.all(color: PdfColors.grey300),
|
||||||
|
columnWidths: {
|
||||||
|
0: const pw.FlexColumnWidth(3), 1: const pw.FlexColumnWidth(1),
|
||||||
|
2: const pw.FlexColumnWidth(1), 3: const pw.FlexColumnWidth(1),
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
pw.TableRow(
|
||||||
|
decoration: pw.BoxDecoration(color: PdfColor.fromHex('6D1E3A')),
|
||||||
|
children: ['Colonia','Total','Resueltos','Pendientes'].map((h) =>
|
||||||
|
pw.Padding(padding: const pw.EdgeInsets.all(6),
|
||||||
|
child: pw.Text(h, style: pw.TextStyle(color: PdfColors.white,
|
||||||
|
fontWeight: pw.FontWeight.bold, fontSize: 10)))).toList()),
|
||||||
|
...colonias.map((c) {
|
||||||
|
final total = c['total'] as int? ?? 0;
|
||||||
|
final res = c['resueltos'] as int? ?? 0;
|
||||||
|
return pw.TableRow(children: [
|
||||||
|
c['colonia'] as String? ?? '', '$total', '$res', '${total - res}',
|
||||||
|
].map((v) => pw.Padding(padding: const pw.EdgeInsets.all(5),
|
||||||
|
child: pw.Text(v, style: const pw.TextStyle(fontSize: 9)))).toList());
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
pw.SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
|
||||||
|
if (incidentes.isNotEmpty) ...[
|
||||||
|
pw.Text('INCIDENTES POR RUTA', style: pw.TextStyle(
|
||||||
|
fontSize: 13, fontWeight: pw.FontWeight.bold,
|
||||||
|
color: PdfColor.fromHex('6D1E3A'))),
|
||||||
|
pw.SizedBox(height: 8),
|
||||||
|
pw.Table(
|
||||||
|
border: pw.TableBorder.all(color: PdfColors.grey300),
|
||||||
|
columnWidths: {0: const pw.FlexColumnWidth(2), 1: const pw.FlexColumnWidth(1)},
|
||||||
|
children: [
|
||||||
|
pw.TableRow(decoration: pw.BoxDecoration(color: PdfColor.fromHex('6D1E3A')),
|
||||||
|
children: ['Ruta','Incidentes'].map((h) => pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.all(6),
|
||||||
|
child: pw.Text(h, style: pw.TextStyle(color: PdfColors.white,
|
||||||
|
fontWeight: pw.FontWeight.bold, fontSize: 10)))).toList()),
|
||||||
|
...incidentes.map((r) => pw.TableRow(children: [
|
||||||
|
r['route_id'] as String? ?? '', '${r["total"]}',
|
||||||
|
].map((v) => pw.Padding(padding: const pw.EdgeInsets.all(5),
|
||||||
|
child: pw.Text(v, style: const pw.TextStyle(fontSize: 9)))).toList())),
|
||||||
|
]),
|
||||||
|
pw.SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
|
||||||
|
if (reviews.isNotEmpty) ...[
|
||||||
|
pw.Text('CALIFICACIONES POR COLONIA', style: pw.TextStyle(
|
||||||
|
fontSize: 13, fontWeight: pw.FontWeight.bold,
|
||||||
|
color: PdfColor.fromHex('6D1E3A'))),
|
||||||
|
pw.SizedBox(height: 8),
|
||||||
|
pw.Table(
|
||||||
|
border: pw.TableBorder.all(color: PdfColors.grey300),
|
||||||
|
columnWidths: {0: const pw.FlexColumnWidth(3),
|
||||||
|
1: const pw.FlexColumnWidth(1), 2: const pw.FlexColumnWidth(1)},
|
||||||
|
children: [
|
||||||
|
pw.TableRow(decoration: pw.BoxDecoration(color: PdfColor.fromHex('6D1E3A')),
|
||||||
|
children: ['Colonia','Promedio','Total'].map((h) => pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.all(6),
|
||||||
|
child: pw.Text(h, style: pw.TextStyle(color: PdfColors.white,
|
||||||
|
fontWeight: pw.FontWeight.bold, fontSize: 10)))).toList()),
|
||||||
|
...reviews.map((r) => pw.TableRow(children: [
|
||||||
|
r['colonia'] as String? ?? '',
|
||||||
|
'${(r["promedio"] as num? ?? 0).toStringAsFixed(1)}/5',
|
||||||
|
'${r["total"]}',
|
||||||
|
].map((v) => pw.Padding(padding: const pw.EdgeInsets.all(5),
|
||||||
|
child: pw.Text(v, style: const pw.TextStyle(fontSize: 9)))).toList())),
|
||||||
|
]),
|
||||||
|
pw.SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
|
||||||
|
pw.Divider(color: PdfColor.fromHex('C9A84C')),
|
||||||
|
pw.Text('Celaya Limpia - H. Ayuntamiento de Celaya, Gto. - ${now.year}',
|
||||||
|
style: const pw.TextStyle(fontSize: 8, color: PdfColors.grey500),
|
||||||
|
textAlign: pw.TextAlign.center),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
|
||||||
|
// Guardar en directorio temporal y compartir con share_plus
|
||||||
|
final bytes = await pdf.save();
|
||||||
|
final dir = await getTemporaryDirectory();
|
||||||
|
final file = File('${dir.path}/reporte_celaya_${now.month}_${now.year}.pdf');
|
||||||
|
await file.writeAsBytes(bytes);
|
||||||
|
|
||||||
|
setState(() => _lastPath = file.path);
|
||||||
|
|
||||||
|
await Share.shareXFiles(
|
||||||
|
[XFile(file.path, mimeType: 'application/pdf')],
|
||||||
|
subject: 'Reporte Mensual Celaya Limpia - ${meses[now.month]} ${now.year}',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Error al generar PDF: $e'),
|
||||||
|
backgroundColor: AppColors.rojoError));
|
||||||
|
}
|
||||||
|
if (mounted) setState(() => _generating = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
backgroundColor: AppColors.grisFondo,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
|
||||||
|
title: const Text('Exportar Reporte PDF'),
|
||||||
|
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||||
|
child: Container(height: 4, color: AppColors.dorado))),
|
||||||
|
body: Center(child: Padding(padding: const EdgeInsets.all(32), child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||||
|
Container(width: 100, height: 100,
|
||||||
|
decoration: BoxDecoration(color: AppColors.verdeAdmin.withOpacity(0.1),
|
||||||
|
shape: BoxShape.circle),
|
||||||
|
child: const Icon(Icons.picture_as_pdf, size: 52, color: AppColors.verdeAdmin)),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const Text('Reporte Mensual', style: TextStyle(fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold, color: AppColors.verdeAdmin)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text('Genera un PDF con el resumen completo:\nreportes, incidentes y calificaciones.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: AppColors.grisTexto)),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
SizedBox(width: double.infinity, height: 52,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: _generating ? null : _generatePdf,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
|
||||||
|
icon: _generating
|
||||||
|
? const SizedBox(width: 20, height: 20,
|
||||||
|
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||||
|
: const Icon(Icons.download),
|
||||||
|
label: Text(_generating ? 'Generando...' : 'Generar y Compartir PDF',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold)))),
|
||||||
|
if (_lastPath != null) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Container(padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(color: AppColors.verdeExito.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: AppColors.verdeExito.withOpacity(0.3))),
|
||||||
|
child: Row(children: [
|
||||||
|
const Icon(Icons.check_circle, color: AppColors.verdeExito, size: 18),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Expanded(child: Text('PDF generado correctamente',
|
||||||
|
style: TextStyle(color: AppColors.verdeExito, fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 13))),
|
||||||
|
TextButton(onPressed: _generatePdf,
|
||||||
|
child: const Text('Compartir de nuevo',
|
||||||
|
style: TextStyle(fontSize: 11, color: AppColors.verdeAdmin))),
|
||||||
|
])),
|
||||||
|
],
|
||||||
|
]))));
|
||||||
|
}
|
||||||
179
celaya_limpia/lib/screens/admin/manage_conductors_screen.dart
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../core/app_colors.dart';
|
||||||
|
import '../../database/db_helper.dart';
|
||||||
|
|
||||||
|
class ManageConductorsScreen extends StatefulWidget {
|
||||||
|
const ManageConductorsScreen({super.key});
|
||||||
|
@override State<ManageConductorsScreen> createState() => _ManageConductorsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ManageConductorsScreenState extends State<ManageConductorsScreen> {
|
||||||
|
List<Map<String, dynamic>> _conductores = [];
|
||||||
|
bool _loading = true;
|
||||||
|
|
||||||
|
@override void initState() { super.initState(); _load(); }
|
||||||
|
|
||||||
|
Future<void> _load() async {
|
||||||
|
final c = await DbHelper.getConductoresConMeta();
|
||||||
|
if (mounted) setState(() { _conductores = c; _loading = false; });
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showFormDialog({Map<String, dynamic>? existing}) async {
|
||||||
|
final nombreCtrl = TextEditingController(text: existing?['nombre'] ?? '');
|
||||||
|
final emailCtrl = TextEditingController(text: existing?['email'] ?? '');
|
||||||
|
final passCtrl = TextEditingController();
|
||||||
|
final notasCtrl = TextEditingController(text: existing?['notas'] ?? '');
|
||||||
|
bool activo = (existing?['activo'] as int? ?? 1) == 1;
|
||||||
|
bool obscure = true;
|
||||||
|
|
||||||
|
await showDialog(context: context, builder: (ctx) => StatefulBuilder(
|
||||||
|
builder: (ctx, setSt) => AlertDialog(
|
||||||
|
title: Text(existing == null ? 'Nuevo Conductor' : 'Editar Conductor'),
|
||||||
|
content: SingleChildScrollView(child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
TextField(controller: nombreCtrl,
|
||||||
|
decoration: const InputDecoration(labelText: 'Nombre completo',
|
||||||
|
prefixIcon: Icon(Icons.person_outline), border: OutlineInputBorder())),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
TextField(controller: emailCtrl, keyboardType: TextInputType.emailAddress,
|
||||||
|
decoration: const InputDecoration(labelText: 'Correo electronico',
|
||||||
|
prefixIcon: Icon(Icons.email_outlined), border: OutlineInputBorder())),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
if (existing == null)
|
||||||
|
TextField(controller: passCtrl, obscureText: obscure,
|
||||||
|
decoration: InputDecoration(labelText: 'Contrasena',
|
||||||
|
prefixIcon: const Icon(Icons.lock_outline), border: const OutlineInputBorder(),
|
||||||
|
suffixIcon: IconButton(icon: Icon(obscure ? Icons.visibility_off : Icons.visibility),
|
||||||
|
onPressed: () => setSt(() => obscure = !obscure)))),
|
||||||
|
if (existing == null) const SizedBox(height: 10),
|
||||||
|
TextField(controller: notasCtrl, maxLines: 2,
|
||||||
|
decoration: const InputDecoration(labelText: 'Notas internas (opcional)',
|
||||||
|
border: OutlineInputBorder())),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
if (existing != null)
|
||||||
|
SwitchListTile(value: activo, dense: true,
|
||||||
|
title: Text(activo ? 'Conductor Activo' : 'Conductor Inactivo',
|
||||||
|
style: TextStyle(color: activo ? AppColors.verdeAdmin : AppColors.rojoError,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
|
activeColor: AppColors.verdeAdmin,
|
||||||
|
onChanged: (v) => setSt(() => activo = v)),
|
||||||
|
])),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Cancelar')),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: AppColors.verdeAdmin,
|
||||||
|
foregroundColor: Colors.white),
|
||||||
|
onPressed: () async {
|
||||||
|
if (nombreCtrl.text.trim().isEmpty || emailCtrl.text.trim().isEmpty) return;
|
||||||
|
if (existing == null) {
|
||||||
|
if (passCtrl.text.length < 6) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||||
|
content: Text('La contrasena debe tener al menos 6 caracteres'),
|
||||||
|
backgroundColor: AppColors.rojoError));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await DbHelper.insertConductor(nombreCtrl.text.trim(),
|
||||||
|
emailCtrl.text.trim().toLowerCase(), passCtrl.text);
|
||||||
|
} else {
|
||||||
|
await DbHelper.updateConductor(existing['id'], nombreCtrl.text.trim(),
|
||||||
|
emailCtrl.text.trim().toLowerCase());
|
||||||
|
await DbHelper.updateConductorMeta(existing['id'], activo, notasCtrl.text.trim());
|
||||||
|
}
|
||||||
|
if (ctx.mounted) Navigator.pop(ctx);
|
||||||
|
await _load();
|
||||||
|
},
|
||||||
|
child: Text(existing == null ? 'Crear' : 'Guardar')),
|
||||||
|
])));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
backgroundColor: AppColors.grisFondo,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
|
||||||
|
title: Text('Conductores (${_conductores.length})'),
|
||||||
|
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||||
|
child: Container(height: 4, color: AppColors.dorado)),
|
||||||
|
actions: [
|
||||||
|
IconButton(icon: const Icon(Icons.refresh), onPressed: _load),
|
||||||
|
IconButton(icon: const Icon(Icons.add_circle_outline),
|
||||||
|
tooltip: 'Nuevo conductor',
|
||||||
|
onPressed: () => _showFormDialog()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: _loading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: _conductores.isEmpty
|
||||||
|
? Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||||
|
const Icon(Icons.person_off, color: AppColors.grisTexto, size: 48),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text('Sin conductores registrados',
|
||||||
|
style: TextStyle(color: AppColors.grisTexto)),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: AppColors.verdeAdmin,
|
||||||
|
foregroundColor: Colors.white),
|
||||||
|
onPressed: () => _showFormDialog(),
|
||||||
|
icon: const Icon(Icons.add), label: const Text('Agregar primer conductor')),
|
||||||
|
]))
|
||||||
|
: ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
itemCount: _conductores.length,
|
||||||
|
itemBuilder: (_, i) {
|
||||||
|
final c = _conductores[i];
|
||||||
|
final activo = (c['activo'] as int? ?? 1) == 1;
|
||||||
|
final incidentes = c['total_incidentes'] as int? ?? 0;
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 10),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10),
|
||||||
|
side: BorderSide(color: activo
|
||||||
|
? AppColors.verdeAdmin.withOpacity(0.3)
|
||||||
|
: AppColors.rojoError.withOpacity(0.3))),
|
||||||
|
child: Padding(padding: const EdgeInsets.all(14), child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Row(children: [
|
||||||
|
CircleAvatar(radius: 22,
|
||||||
|
backgroundColor: activo
|
||||||
|
? AppColors.verdeAdmin.withOpacity(0.15)
|
||||||
|
: Colors.grey.shade200,
|
||||||
|
child: Icon(Icons.person,
|
||||||
|
color: activo ? AppColors.verdeAdmin : AppColors.grisTexto, size: 24)),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text(c['nombre'] ?? '', style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, fontSize: 14)),
|
||||||
|
Text(c['email'] ?? '', style: const TextStyle(
|
||||||
|
color: AppColors.grisTexto, fontSize: 12)),
|
||||||
|
])),
|
||||||
|
Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: activo ? AppColors.verdeAdmin.withOpacity(0.1)
|
||||||
|
: AppColors.rojoError.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(10)),
|
||||||
|
child: Text(activo ? 'Activo' : 'Inactivo',
|
||||||
|
style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold,
|
||||||
|
color: activo ? AppColors.verdeAdmin : AppColors.rojoError))),
|
||||||
|
IconButton(icon: const Icon(Icons.edit_outlined, size: 18),
|
||||||
|
onPressed: () => _showFormDialog(existing: c)),
|
||||||
|
]),
|
||||||
|
if (incidentes > 0 || (c['notas'] as String?)?.isNotEmpty == true) ...[
|
||||||
|
const Divider(height: 16),
|
||||||
|
if (incidentes > 0)
|
||||||
|
Row(children: [
|
||||||
|
Icon(Icons.warning_amber, size: 14,
|
||||||
|
color: incidentes > 3 ? AppColors.rojoError : AppColors.naranjaAlerta),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text('$incidentes incidente${incidentes != 1 ? 's' : ''} historico${incidentes != 1 ? 's' : ''}',
|
||||||
|
style: TextStyle(fontSize: 12,
|
||||||
|
color: incidentes > 3 ? AppColors.rojoError : AppColors.naranjaAlerta)),
|
||||||
|
]),
|
||||||
|
if ((c['notas'] as String?)?.isNotEmpty == true) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text('Notas: ${c['notas']}',
|
||||||
|
style: const TextStyle(fontSize: 11, color: AppColors.grisTexto,
|
||||||
|
fontStyle: FontStyle.italic)),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
])));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
212
celaya_limpia/lib/screens/citizen/add_domicilio_screen.dart
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../../core/app_colors.dart';
|
||||||
|
import '../../data/celaya_colonias.dart';
|
||||||
|
import '../../data/colonies_data.dart';
|
||||||
|
import '../../database/db_helper.dart';
|
||||||
|
import '../../models/models.dart';
|
||||||
|
import '../../models/route_model.dart';
|
||||||
|
import '../../services/auth_service.dart';
|
||||||
|
|
||||||
|
class AddDomicilioScreen extends StatefulWidget {
|
||||||
|
final DomicilioModel? editing;
|
||||||
|
const AddDomicilioScreen({super.key, this.editing});
|
||||||
|
@override State<AddDomicilioScreen> createState() => _AddDomicilioScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AddDomicilioScreenState extends State<AddDomicilioScreen> {
|
||||||
|
final _calleCtrl = TextEditingController();
|
||||||
|
final _aliasCtrl = TextEditingController(text: 'Casa');
|
||||||
|
String? _coloniaSeleccionada;
|
||||||
|
ColonyModel? _coloniaData;
|
||||||
|
bool _loading = false;
|
||||||
|
String _searchQuery = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.editing != null) {
|
||||||
|
_calleCtrl.text = widget.editing!.calle;
|
||||||
|
_aliasCtrl.text = widget.editing!.alias;
|
||||||
|
_coloniaSeleccionada = widget.editing!.colonia;
|
||||||
|
_coloniaData = getColonyByName(widget.editing!.colonia);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> get _filteredColonias {
|
||||||
|
if (_searchQuery.isEmpty) return celayaColonias;
|
||||||
|
return celayaColonias
|
||||||
|
.where((c) => c.toLowerCase().contains(_searchQuery.toLowerCase()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _guardar() async {
|
||||||
|
if (_calleCtrl.text.trim().isEmpty || _coloniaSeleccionada == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||||
|
content: Text('Por favor completa todos los campos'),
|
||||||
|
backgroundColor: AppColors.rojoError));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() => _loading = true);
|
||||||
|
|
||||||
|
final auth = context.read<AuthService>();
|
||||||
|
|
||||||
|
// 1. Buscar primero en colonies_data (rutas predefinidas)
|
||||||
|
final staticData = getColonyByName(_coloniaSeleccionada!);
|
||||||
|
String routeId = staticData?.routeId ?? '';
|
||||||
|
String horario = staticData?.horarioEstimado ?? '';
|
||||||
|
|
||||||
|
// 2. Si no hay match estático, buscar en route_definitions del admin
|
||||||
|
if (routeId.isEmpty) {
|
||||||
|
final routeDefs = await DbHelper.getAllRouteDefinitions();
|
||||||
|
for (final rd in routeDefs) {
|
||||||
|
if (rd.colonias.any((c) =>
|
||||||
|
c.toLowerCase() == _coloniaSeleccionada!.toLowerCase())) {
|
||||||
|
routeId = rd.routeId;
|
||||||
|
horario = '${_turnoLabel(rd.turno)} (${rd.horaInicio}–${rd.horaFin})';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fallback si no se encontró
|
||||||
|
if (routeId.isEmpty) {
|
||||||
|
routeId = 'RUTA-01';
|
||||||
|
horario = 'Matutino (06:00–08:00)';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.editing != null) {
|
||||||
|
await DbHelper.deleteDomicilio(widget.editing!.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
final dom = DomicilioModel(
|
||||||
|
userId: auth.currentUser!.id!,
|
||||||
|
alias: _aliasCtrl.text.trim(),
|
||||||
|
calle: _calleCtrl.text.trim(),
|
||||||
|
colonia: _coloniaSeleccionada!,
|
||||||
|
routeId: routeId,
|
||||||
|
horarioEstimado: horario,
|
||||||
|
);
|
||||||
|
await DbHelper.insertDomicilio(dom);
|
||||||
|
await auth.reloadDomicilios();
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _loading = false);
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _turnoLabel(String t) =>
|
||||||
|
t == 'MATUTINO' ? 'Matutino' : t == 'VESPERTINO' ? 'Vespertino' : 'Nocturno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: AppColors.grisFondo,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||||
|
title: Text(widget.editing != null ? 'Editar Domicilio' : 'Agregar Domicilio'),
|
||||||
|
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||||
|
child: Container(height: 4, color: AppColors.dorado)),
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
// Alias
|
||||||
|
TextField(
|
||||||
|
controller: _aliasCtrl,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Alias (ej. Casa, Trabajo, Familia)',
|
||||||
|
prefixIcon: Icon(Icons.label_outline, color: AppColors.guindaPrimary),
|
||||||
|
border: OutlineInputBorder(), filled: true, fillColor: Colors.white),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
// Calle
|
||||||
|
TextField(
|
||||||
|
controller: _calleCtrl,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Calle y número',
|
||||||
|
prefixIcon: Icon(Icons.signpost_outlined, color: AppColors.guindaPrimary),
|
||||||
|
border: OutlineInputBorder(), filled: true, fillColor: Colors.white),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Text('Colonia', style: TextStyle(fontWeight: FontWeight.bold,
|
||||||
|
color: AppColors.guindaPrimary, fontSize: 15)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
// Buscador de colonias
|
||||||
|
TextField(
|
||||||
|
onChanged: (v) => setState(() => _searchQuery = v),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'Buscar colonia...',
|
||||||
|
prefixIcon: Icon(Icons.search),
|
||||||
|
border: OutlineInputBorder(), filled: true, fillColor: Colors.white,
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
// Lista de colonias
|
||||||
|
Container(
|
||||||
|
height: 240,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white, borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.grey.shade300)),
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: _filteredColonias.length,
|
||||||
|
itemBuilder: (_, i) {
|
||||||
|
final c = _filteredColonias[i];
|
||||||
|
final isSelected = c == _coloniaSeleccionada;
|
||||||
|
return ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text(c, style: TextStyle(fontSize: 13,
|
||||||
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||||
|
color: isSelected ? AppColors.guindaPrimary : AppColors.negroTexto)),
|
||||||
|
trailing: isSelected
|
||||||
|
? const Icon(Icons.check_circle, color: AppColors.guindaPrimary, size: 18)
|
||||||
|
: null,
|
||||||
|
tileColor: isSelected ? AppColors.guindaPrimary.withOpacity(0.08) : null,
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_coloniaSeleccionada = c;
|
||||||
|
_coloniaData = getColonyByName(c);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_coloniaData != null) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.guindaPrimary.withOpacity(0.08),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: AppColors.guindaPrimary.withOpacity(0.3))),
|
||||||
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text('Ruta asignada: ${_coloniaData!.routeId}',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold,
|
||||||
|
color: AppColors.guindaPrimary, fontSize: 13)),
|
||||||
|
Text('Horario: ${_coloniaData!.horarioEstimado}',
|
||||||
|
style: const TextStyle(color: AppColors.grisTexto, fontSize: 12)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
SizedBox(width: double.infinity, height: 50,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: _loading ? null : _guardar,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||||
|
icon: _loading
|
||||||
|
? const SizedBox(width: 18, height: 18,
|
||||||
|
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||||
|
: const Icon(Icons.save),
|
||||||
|
label: Text(widget.editing != null ? 'ACTUALIZAR' : 'GUARDAR DOMICILIO',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold)))),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override void dispose() { _calleCtrl.dispose(); _aliasCtrl.dispose(); super.dispose(); }
|
||||||
|
}
|
||||||
175
celaya_limpia/lib/screens/citizen/ai_camera_screen.dart
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:camera/camera.dart';
|
||||||
|
import 'package:tflite_flutter/tflite_flutter.dart';
|
||||||
|
import 'package:image/image.dart' as img;
|
||||||
|
import '../../core/app_colors.dart';
|
||||||
|
|
||||||
|
List<CameraDescription> _cameras = [];
|
||||||
|
|
||||||
|
class AiCameraScreen extends StatefulWidget {
|
||||||
|
const AiCameraScreen({super.key});
|
||||||
|
@override State<AiCameraScreen> createState() => _AiCameraScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AiCameraScreenState extends State<AiCameraScreen> {
|
||||||
|
CameraController? _cam;
|
||||||
|
Interpreter? _interpreter;
|
||||||
|
bool _processing = false;
|
||||||
|
String _result = 'Apunta a un residuo y toca el botón';
|
||||||
|
String _confidence = '';
|
||||||
|
bool _modelLoaded = false;
|
||||||
|
|
||||||
|
// 0=Orgánico, 1=Inorgánico (según waste_classification_model)
|
||||||
|
final _labels = ['Residuo Organico', 'Residuo Inorganico'];
|
||||||
|
final _labelColors = [AppColors.verdeExito, AppColors.naranjaAlerta];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_init();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _init() async {
|
||||||
|
try {
|
||||||
|
_cameras = await availableCameras();
|
||||||
|
} catch (_) {}
|
||||||
|
await _initCamera();
|
||||||
|
await _loadModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initCamera() async {
|
||||||
|
if (_cameras.isEmpty) return;
|
||||||
|
_cam = CameraController(_cameras[0], ResolutionPreset.medium, enableAudio: false);
|
||||||
|
try {
|
||||||
|
await _cam!.initialize();
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadModel() async {
|
||||||
|
try {
|
||||||
|
_interpreter = await Interpreter.fromAsset('assets/models/waste_model.tflite');
|
||||||
|
setState(() => _modelLoaded = true);
|
||||||
|
} catch (e) {
|
||||||
|
setState(() => _result = 'Modelo no encontrado.\nAgrega waste_model.tflite a assets/models/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _classify() async {
|
||||||
|
if (_cam == null || !_cam!.value.isInitialized || _processing || !_modelLoaded) return;
|
||||||
|
setState(() { _processing = true; _result = 'Analizando...'; _confidence = ''; });
|
||||||
|
try {
|
||||||
|
final pic = await _cam!.takePicture();
|
||||||
|
final raw = await File(pic.path).readAsBytes();
|
||||||
|
img.Image? decoded = img.decodeImage(raw);
|
||||||
|
if (decoded == null) throw Exception('No se pudo decodificar');
|
||||||
|
final resized = img.copyResize(decoded, width: 150, height: 150);
|
||||||
|
|
||||||
|
var input = List.generate(1, (_) =>
|
||||||
|
List.generate(150, (_) => List.generate(150, (_) => List.generate(3, (_) => 0.0))));
|
||||||
|
|
||||||
|
for (int y = 0; y < 150; y++) {
|
||||||
|
for (int x = 0; x < 150; x++) {
|
||||||
|
final px = resized.getPixel(x, y);
|
||||||
|
input[0][y][x][0] = px.r / 255.0;
|
||||||
|
input[0][y][x][1] = px.g / 255.0;
|
||||||
|
input[0][y][x][2] = px.b / 255.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var output = List.filled(2, 0.0).reshape([1, 2]);
|
||||||
|
_interpreter!.run(input, output);
|
||||||
|
|
||||||
|
final pred = List<double>.from(output[0]);
|
||||||
|
final maxIdx = pred[0] > pred[1] ? 0 : 1;
|
||||||
|
final conf = pred[maxIdx] * 100;
|
||||||
|
|
||||||
|
await File(pic.path).delete();
|
||||||
|
setState(() {
|
||||||
|
_result = _labels[maxIdx];
|
||||||
|
_confidence = 'Confianza: ${conf.toStringAsFixed(1)}%';
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() => _result = 'Error en análisis');
|
||||||
|
} finally {
|
||||||
|
setState(() => _processing = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_cam?.dispose();
|
||||||
|
_interpreter?.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final resultColor = _result.contains('Orgánico') ? AppColors.verdeExito
|
||||||
|
: _result.contains('Inorgánico') ? AppColors.naranjaAlerta
|
||||||
|
: AppColors.guindaPrimary;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||||
|
title: const Text('Clasificador IA de Residuos'),
|
||||||
|
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||||
|
child: Container(height: 4, color: AppColors.dorado)),
|
||||||
|
),
|
||||||
|
body: Column(children: [
|
||||||
|
// Visor cámara
|
||||||
|
Expanded(flex: 4,
|
||||||
|
child: Container(margin: const EdgeInsets.all(14),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: AppColors.guindaPrimary, width: 3)),
|
||||||
|
child: _cam != null && _cam!.value.isInitialized
|
||||||
|
? CameraPreview(_cam!)
|
||||||
|
: const Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||||
|
Icon(Icons.camera_alt, color: Colors.white54, size: 48),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Text('Iniciando cámara...', style: TextStyle(color: Colors.white54)),
|
||||||
|
])),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Panel resultado
|
||||||
|
Expanded(flex: 2,
|
||||||
|
child: Container(width: double.infinity,
|
||||||
|
decoration: BoxDecoration(color: AppColors.guindaPrimary.withOpacity(0.06),
|
||||||
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(28))),
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||||
|
Text(_result, textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: resultColor)),
|
||||||
|
if (_confidence.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(_confidence, style: const TextStyle(fontSize: 16, color: Colors.black54, fontWeight: FontWeight.w500)),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (!_modelLoaded)
|
||||||
|
Container(padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(color: Colors.orange.shade50, borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.orange.shade300)),
|
||||||
|
child: const Text('ℹ️ Para usar la IA, coloca waste_model.tflite en assets/models/',
|
||||||
|
textAlign: TextAlign.center, style: TextStyle(fontSize: 11))),
|
||||||
|
if (_modelLoaded)
|
||||||
|
SizedBox(width: double.infinity, height: 50,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: _processing ? null : _classify,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14))),
|
||||||
|
icon: _processing
|
||||||
|
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||||
|
: const Icon(Icons.center_focus_strong),
|
||||||
|
label: Text(_processing ? 'Procesando...' : 'Escanear Residuo',
|
||||||
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||||
|
)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
270
celaya_limpia/lib/screens/citizen/chatbot_screen.dart
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../core/app_colors.dart';
|
||||||
|
|
||||||
|
// ── Árbol de respuestas predefinidas ──────────────────────────────────────
|
||||||
|
class _ChatNode {
|
||||||
|
final String text;
|
||||||
|
final List<_ChatOption> options;
|
||||||
|
final bool isAnswer;
|
||||||
|
const _ChatNode(this.text, this.options, {this.isAnswer = false});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatOption {
|
||||||
|
final String label;
|
||||||
|
final _ChatNode next;
|
||||||
|
const _ChatOption(this.label, this.next);
|
||||||
|
}
|
||||||
|
|
||||||
|
final _chatTree = _ChatNode('Hola, soy el asistente de Celaya Limpia. ¿En que te puedo ayudar?', [
|
||||||
|
_ChatOption('Separacion de residuos', _ChatNode('¿Que quieres saber sobre separacion?', [
|
||||||
|
_ChatOption('Como separo mi basura', _ChatNode(
|
||||||
|
'Separa tus residuos en 3 grupos:\n\n'
|
||||||
|
'ORGANICOS (bolsa verde):\nRestos de comida, cascara de huevo, pasto, hojas.\n\n'
|
||||||
|
'INORGANICOS reciclables (bolsa azul):\nPET, latas, carton limpio, vidrio.\n\n'
|
||||||
|
'NO reciclables (bolsa negra):\nPanales, papel sanitario, colillas, chicles.',
|
||||||
|
[], isAnswer: true)),
|
||||||
|
_ChatOption('Que NO debo mezclar', _ChatNode(
|
||||||
|
'NUNCA mezcles:\n\n'
|
||||||
|
'- Pilas o baterias con basura comun\n'
|
||||||
|
'- Aceite de cocina (contamina el agua)\n'
|
||||||
|
'- Medicamentos vencidos\n'
|
||||||
|
'- Jeringas o material medico\n'
|
||||||
|
'- Electronicos con basura doméstica\n\n'
|
||||||
|
'Estos requieren manejo especial.',
|
||||||
|
[], isAnswer: true)),
|
||||||
|
_ChatOption('Que hago con el aceite', _ChatNode(
|
||||||
|
'El aceite de cocina usado NO va a la basura ni al drenaje.\n\n'
|
||||||
|
'1. Dejalo enfriar completamente\n'
|
||||||
|
'2. Guardalo en botella de PET cerrada\n'
|
||||||
|
'3. Llevalo a los puntos de acopio del Ayuntamiento de Celaya\n\n'
|
||||||
|
'El aceite reciclado se convierte en biodiesel.',
|
||||||
|
[], isAnswer: true)),
|
||||||
|
])),
|
||||||
|
_ChatOption('Residuos especiales', _ChatNode('¿Que tipo de residuo especial tienes?', [
|
||||||
|
_ChatOption('Donde dejo electronicos', _ChatNode(
|
||||||
|
'Los aparatos electronicos (celulares, computadoras, focos ahorradores) '
|
||||||
|
'son residuos RAEE.\n\n'
|
||||||
|
'Puntos de acopio en Celaya:\n'
|
||||||
|
'- Tiendas de electronica\n'
|
||||||
|
'- Centros comerciales con contenedores especiales\n'
|
||||||
|
'- Eventos de recoleccion del municipio\n\n'
|
||||||
|
'NUNCA los tires a la basura comun.',
|
||||||
|
[], isAnswer: true)),
|
||||||
|
_ChatOption('Que hago con medicamentos', _ChatNode(
|
||||||
|
'Los medicamentos vencidos son residuos peligrosos.\n\n'
|
||||||
|
'- Llevalos a farmacias que tengan programa de devolucion\n'
|
||||||
|
'- Algunos hospitales los reciben\n'
|
||||||
|
'- Nunca los tires al drenaje ni a la basura comun\n\n'
|
||||||
|
'Contaminar el agua con medicamentos afecta a toda la comunidad.',
|
||||||
|
[], isAnswer: true)),
|
||||||
|
_ChatOption('Que hago con pilas y baterias', _ChatNode(
|
||||||
|
'Las pilas y baterias contienen metales pesados toxicos.\n\n'
|
||||||
|
'Depositalas en:\n'
|
||||||
|
'- Supermercados (contenedores naranjas)\n'
|
||||||
|
'- Tiendas de electronica\n'
|
||||||
|
'- Oficinas del Ayuntamiento de Celaya\n\n'
|
||||||
|
'1 pila puede contaminar 600,000 litros de agua.',
|
||||||
|
[], isAnswer: true)),
|
||||||
|
])),
|
||||||
|
_ChatOption('Sobre el servicio de recoleccion', _ChatNode('¿Que necesitas saber?', [
|
||||||
|
_ChatOption('Cuando debo sacar la basura', _ChatNode(
|
||||||
|
'Celaya Limpia te notificara:\n\n'
|
||||||
|
'1. Cuando el camion salga del relleno sanitario\n'
|
||||||
|
'2. Cuando este a 30 minutos\n'
|
||||||
|
'3. A 15 minutos: este es el momento de sacar tus bolsas\n\n'
|
||||||
|
'NO saques la basura antes del aviso de 15 minutos. '
|
||||||
|
'Atrae fauna nociva y obstruye la acera.',
|
||||||
|
[], isAnswer: true)),
|
||||||
|
_ChatOption('El camion no paso', _ChatNode(
|
||||||
|
'Si el camion no paso en tu horario habitual:\n\n'
|
||||||
|
'1. Revisa las alertas en la app (puede haber un retraso o incidente)\n'
|
||||||
|
'2. Guarda tu basura bien cerrada\n'
|
||||||
|
'3. Reporta la incidencia desde la seccion "Reportar"\n\n'
|
||||||
|
'El administrador revisara tu reporte y te mantendra informado.',
|
||||||
|
[], isAnswer: true)),
|
||||||
|
_ChatOption('Como califico el servicio', _ChatNode(
|
||||||
|
'Despues de que el camion pase por tu zona, '
|
||||||
|
'la app te mostrara una notificacion para calificar.\n\n'
|
||||||
|
'Puedes dar de 1 a 5 estrellas y dejar un comentario.\n\n'
|
||||||
|
'Tus calificaciones ayudan al Ayuntamiento a identificar '
|
||||||
|
'colonias con problemas y mejorar el servicio.',
|
||||||
|
[], isAnswer: true)),
|
||||||
|
])),
|
||||||
|
_ChatOption('Denuncia o emergencia', _ChatNode(
|
||||||
|
'Para situaciones urgentes:\n\n'
|
||||||
|
'- Reporte de incidencias: usa la seccion "Reportar" en la app\n'
|
||||||
|
'- Emergencias: llama al 911\n'
|
||||||
|
'- Ayuntamiento de Celaya: (461) 614-8000\n'
|
||||||
|
'- SEMARNAT Guanajuato: (477) 717-2600\n\n'
|
||||||
|
'Para basura clandestina o tiraderos ilegales, reportalo al municipio.',
|
||||||
|
[], isAnswer: true)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ── Pantalla del chatbot ──────────────────────────────────────────────────
|
||||||
|
class ChatbotScreen extends StatefulWidget {
|
||||||
|
const ChatbotScreen({super.key});
|
||||||
|
@override State<ChatbotScreen> createState() => _ChatbotScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatbotScreenState extends State<ChatbotScreen> {
|
||||||
|
final List<_Message> _messages = [];
|
||||||
|
_ChatNode _current = _chatTree;
|
||||||
|
final _scroll = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Mensaje inicial
|
||||||
|
_messages.add(_Message(text: _chatTree.text, isBot: true));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleOption(_ChatOption option) {
|
||||||
|
setState(() {
|
||||||
|
// Mensaje del usuario
|
||||||
|
_messages.add(_Message(text: option.label, isBot: false));
|
||||||
|
// Ir al siguiente nodo
|
||||||
|
_current = option.next;
|
||||||
|
_messages.add(_Message(text: _current.text, isBot: true,
|
||||||
|
isAnswer: _current.isAnswer));
|
||||||
|
});
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () {
|
||||||
|
_scroll.animateTo(_scroll.position.maxScrollExtent,
|
||||||
|
duration: const Duration(milliseconds: 300), curve: Curves.easeOut);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _reset() {
|
||||||
|
setState(() {
|
||||||
|
_messages.clear();
|
||||||
|
_current = _chatTree;
|
||||||
|
_messages.add(_Message(text: _chatTree.text, isBot: true));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
backgroundColor: AppColors.grisFondo,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||||
|
title: const Row(children: [
|
||||||
|
CircleAvatar(radius: 14,
|
||||||
|
backgroundColor: Colors.white24,
|
||||||
|
child: Icon(Icons.smart_toy, color: AppColors.dorado, size: 18)),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Asistente Celaya Limpia'),
|
||||||
|
]),
|
||||||
|
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||||
|
child: Container(height: 4, color: AppColors.dorado)),
|
||||||
|
actions: [
|
||||||
|
IconButton(icon: const Icon(Icons.refresh), tooltip: 'Reiniciar',
|
||||||
|
onPressed: _reset),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Column(children: [
|
||||||
|
// Mensajes
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: _scroll,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
itemCount: _messages.length,
|
||||||
|
itemBuilder: (_, i) => _MessageBubble(msg: _messages[i]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Opciones del nodo actual
|
||||||
|
if (_current.options.isNotEmpty)
|
||||||
|
Container(
|
||||||
|
color: Colors.white,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text('Selecciona una opcion:',
|
||||||
|
style: TextStyle(fontSize: 11, color: AppColors.grisTexto,
|
||||||
|
fontWeight: FontWeight.w500)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Wrap(spacing: 8, runSpacing: 8,
|
||||||
|
children: _current.options.map((opt) =>
|
||||||
|
ActionChip(
|
||||||
|
label: Text(opt.label, style: const TextStyle(fontSize: 12)),
|
||||||
|
backgroundColor: AppColors.guindaPrimary.withOpacity(0.1),
|
||||||
|
side: const BorderSide(color: AppColors.guindaPrimary),
|
||||||
|
labelStyle: const TextStyle(color: AppColors.guindaPrimary),
|
||||||
|
onPressed: () => _handleOption(opt),
|
||||||
|
)).toList()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
// Botón de reiniciar al llegar a una respuesta final
|
||||||
|
Container(
|
||||||
|
color: Colors.white,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: SizedBox(width: double.infinity,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: _reset,
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: AppColors.guindaPrimary,
|
||||||
|
side: const BorderSide(color: AppColors.guindaPrimary)),
|
||||||
|
icon: const Icon(Icons.arrow_back, size: 16),
|
||||||
|
label: const Text('Hacer otra pregunta'))),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override void dispose() { _scroll.dispose(); super.dispose(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Message {
|
||||||
|
final String text;
|
||||||
|
final bool isBot;
|
||||||
|
final bool isAnswer;
|
||||||
|
const _Message({required this.text, required this.isBot, this.isAnswer = false});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MessageBubble extends StatelessWidget {
|
||||||
|
final _Message msg;
|
||||||
|
const _MessageBubble({super.key, required this.msg});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: msg.isBot ? MainAxisAlignment.start : MainAxisAlignment.end,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (msg.isBot) ...[
|
||||||
|
CircleAvatar(radius: 16,
|
||||||
|
backgroundColor: AppColors.guindaPrimary,
|
||||||
|
child: const Icon(Icons.smart_toy, color: Colors.white, size: 16)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
Flexible(child: Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: msg.isBot
|
||||||
|
? (msg.isAnswer ? Colors.green.shade50 : Colors.white)
|
||||||
|
: AppColors.guindaPrimary,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: const Radius.circular(16),
|
||||||
|
topRight: const Radius.circular(16),
|
||||||
|
bottomLeft: Radius.circular(msg.isBot ? 4 : 16),
|
||||||
|
bottomRight: Radius.circular(msg.isBot ? 16 : 4),
|
||||||
|
),
|
||||||
|
border: msg.isBot ? Border.all(
|
||||||
|
color: msg.isAnswer
|
||||||
|
? Colors.green.shade200
|
||||||
|
: Colors.grey.shade200) : null,
|
||||||
|
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.06),
|
||||||
|
blurRadius: 4, offset: const Offset(0, 2))],
|
||||||
|
),
|
||||||
|
child: Text(msg.text,
|
||||||
|
style: TextStyle(fontSize: 13, height: 1.5,
|
||||||
|
color: msg.isBot ? AppColors.negroTexto : Colors.white)),
|
||||||
|
)),
|
||||||
|
if (!msg.isBot) const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
149
celaya_limpia/lib/screens/citizen/citizen_guia_screen.dart
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../core/app_colors.dart';
|
||||||
|
import 'ai_camera_screen.dart';
|
||||||
|
|
||||||
|
class CitizenGuiaScreen extends StatelessWidget {
|
||||||
|
const CitizenGuiaScreen({super.key});
|
||||||
|
|
||||||
|
static const _cats = [
|
||||||
|
_Cat(Icons.grass,Color(0xFF2E7D32),'Orgánicos','Restos de comida, jardín','🟢 Bolsa Verde',[
|
||||||
|
'Frutas y verduras','Cáscaras de huevo','Posos de café y té',
|
||||||
|
'Restos de comida preparada','Pasto y hojas','Cáscaras de semillas'],
|
||||||
|
['Aceites en exceso','Carnes en grandes cantidades']),
|
||||||
|
_Cat(Icons.recycling,Color(0xFF1565C0),'Reciclables','Papel, plástico, vidrio, metal','🔵 Bolsa Azul',[
|
||||||
|
'Botellas PET','Latas de aluminio','Cartón y papel limpio',
|
||||||
|
'Vidrio (botellas, frascos)','Periódico y revistas'],
|
||||||
|
['Vidrio roto sin envolver','Papel sucio o mojado','Unicel']),
|
||||||
|
_Cat(Icons.delete,Color(0xFF757575),'No Reciclables','Residuos que no se reusan','⚫ Bolsa Negra',[
|
||||||
|
'Pañales desechables','Toallas sanitarias','Papel higiénico usado',
|
||||||
|
'Colillas de cigarro','Cerámica rota'],['Baterías','Medicamentos','Aceite usado']),
|
||||||
|
_Cat(Icons.warning_amber,Color(0xFFC62828),'Peligrosos','Requieren manejo especial','🔴 Separado',[
|
||||||
|
'Agujas y jeringas','Medicamentos vencidos','Pilas y baterías',
|
||||||
|
'Aceite de cocina usado','Pintura y solventes'],[],isWarn:true),
|
||||||
|
_Cat(Icons.devices_other,Color(0xFFE65100),'Electrónicos (RAEE)','Aparatos electrónicos','🟠 Punto de acopio',[
|
||||||
|
'Celulares viejos','Computadoras','Televisiones',
|
||||||
|
'Focos ahorradores','Cables y cargadores'],[],isSpecial:true),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
backgroundColor: AppColors.grisFondo,
|
||||||
|
appBar: AppBar(automaticallyImplyLeading:false,
|
||||||
|
backgroundColor:AppColors.guindaPrimary, foregroundColor:Colors.white,
|
||||||
|
title:const Text('Guía de Separación'),
|
||||||
|
actions:[IconButton(icon:const Icon(Icons.camera_alt),
|
||||||
|
tooltip:'Clasificar con IA',
|
||||||
|
onPressed:()=>Navigator.push(context,MaterialPageRoute(builder:(_)=>const AiCameraScreen())))],
|
||||||
|
bottom:PreferredSize(preferredSize:const Size.fromHeight(4),
|
||||||
|
child:Container(height:4,color:AppColors.dorado))),
|
||||||
|
body:Column(children:[
|
||||||
|
Container(width:double.infinity,
|
||||||
|
color:AppColors.verdeExito.withOpacity(0.1),
|
||||||
|
padding:const EdgeInsets.symmetric(horizontal:16,vertical:8),
|
||||||
|
child:Row(children:[
|
||||||
|
const Icon(Icons.offline_bolt,color:AppColors.verdeExito,size:16),
|
||||||
|
const SizedBox(width:6),
|
||||||
|
const Text('Disponible sin conexión a internet',
|
||||||
|
style:TextStyle(color:AppColors.verdeExito,fontSize:12,fontWeight:FontWeight.w500)),
|
||||||
|
const Spacer(),
|
||||||
|
TextButton.icon(icon:const Icon(Icons.camera_alt,size:14),
|
||||||
|
label:const Text('Clasificar IA',style:TextStyle(fontSize:12)),
|
||||||
|
style:TextButton.styleFrom(foregroundColor:AppColors.guindaPrimary),
|
||||||
|
onPressed:()=>Navigator.push(context,MaterialPageRoute(builder:(_)=>const AiCameraScreen()))),
|
||||||
|
])),
|
||||||
|
// Importancia de separar
|
||||||
|
Container(margin:const EdgeInsets.fromLTRB(12,8,12,0),
|
||||||
|
padding:const EdgeInsets.all(12),
|
||||||
|
decoration:BoxDecoration(color:Colors.green.shade50,borderRadius:BorderRadius.circular(8),
|
||||||
|
border:Border.all(color:Colors.green.shade200)),
|
||||||
|
child:const Column(crossAxisAlignment:CrossAxisAlignment.start, children:[
|
||||||
|
Text('¿Por qué separar tu basura?',style:TextStyle(fontWeight:FontWeight.bold,color:Color(0xFF2E7D32))),
|
||||||
|
SizedBox(height:6),
|
||||||
|
Text('♻️ El 60% de los residuos en México pueden reciclarse o compostarse, pero solo el 5% lo hace.\n'
|
||||||
|
'🌱 Separar correctamente reduce la contaminación del suelo y agua, genera empleos verdes '
|
||||||
|
'y disminuye los gases de efecto invernadero producidos en rellenos sanitarios.',
|
||||||
|
style:TextStyle(fontSize:12,color:Colors.black87)),
|
||||||
|
])),
|
||||||
|
Expanded(child:ListView.builder(
|
||||||
|
padding:const EdgeInsets.all(12),
|
||||||
|
itemCount:_cats.length,
|
||||||
|
itemBuilder:(ctx,i)=>_CatCard(cat:_cats[i]))),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Cat {
|
||||||
|
final IconData icon; final Color color; final String title, subtitle, bolsa;
|
||||||
|
final List<String> items, noItems;
|
||||||
|
final bool isWarn, isSpecial;
|
||||||
|
const _Cat(this.icon,this.color,this.title,this.subtitle,this.bolsa,
|
||||||
|
this.items,this.noItems,{this.isWarn=false,this.isSpecial=false});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CatCard extends StatefulWidget {
|
||||||
|
final _Cat cat;
|
||||||
|
const _CatCard({super.key, required this.cat});
|
||||||
|
@override State<_CatCard> createState() => _CatCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CatCardState extends State<_CatCard> {
|
||||||
|
bool _open = false;
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final c = widget.cat;
|
||||||
|
return Card(margin:const EdgeInsets.only(bottom:10),elevation:2,
|
||||||
|
shape:RoundedRectangleBorder(borderRadius:BorderRadius.circular(10),
|
||||||
|
side:BorderSide(color:c.color.withOpacity(0.3))),
|
||||||
|
child:InkWell(borderRadius:BorderRadius.circular(10),
|
||||||
|
onTap:()=>setState(()=>_open=!_open),
|
||||||
|
child:Column(children:[
|
||||||
|
Container(decoration:BoxDecoration(color:c.color.withOpacity(0.1),
|
||||||
|
borderRadius:BorderRadius.vertical(top:const Radius.circular(10),
|
||||||
|
bottom:_open?Radius.zero:const Radius.circular(10))),
|
||||||
|
padding:const EdgeInsets.all(14),
|
||||||
|
child:Row(children:[
|
||||||
|
Container(width:40,height:40,decoration:BoxDecoration(color:c.color,borderRadius:BorderRadius.circular(8)),
|
||||||
|
child:Icon(c.icon,color:Colors.white,size:22)),
|
||||||
|
const SizedBox(width:10),
|
||||||
|
Expanded(child:Column(crossAxisAlignment:CrossAxisAlignment.start, children:[
|
||||||
|
Text(c.title,style:TextStyle(fontWeight:FontWeight.bold,fontSize:15,color:c.color)),
|
||||||
|
Text(c.subtitle,style:const TextStyle(color:AppColors.grisTexto,fontSize:11)),
|
||||||
|
Text(c.bolsa,style:TextStyle(fontSize:11,fontWeight:FontWeight.w600,color:c.color)),
|
||||||
|
])),
|
||||||
|
Icon(_open?Icons.expand_less:Icons.expand_more,color:c.color),
|
||||||
|
])),
|
||||||
|
if (_open) Padding(padding:const EdgeInsets.fromLTRB(14,0,14,14),
|
||||||
|
child:Column(crossAxisAlignment:CrossAxisAlignment.start, children:[
|
||||||
|
const SizedBox(height:8),
|
||||||
|
Text('✅ Qué va aquí:',style:TextStyle(fontWeight:FontWeight.bold,color:c.color,fontSize:12)),
|
||||||
|
const SizedBox(height:4),
|
||||||
|
...c.items.map((e)=>Padding(padding:const EdgeInsets.symmetric(vertical:2),
|
||||||
|
child:Row(children:[Icon(Icons.check_circle_outline,size:13,color:c.color),
|
||||||
|
const SizedBox(width:6),Text(e,style:const TextStyle(fontSize:12))]))),
|
||||||
|
if (c.noItems.isNotEmpty) ...[
|
||||||
|
const SizedBox(height:8),
|
||||||
|
const Text('❌ NO incluir:',style:TextStyle(fontWeight:FontWeight.bold,color:AppColors.rojoError,fontSize:12)),
|
||||||
|
...c.noItems.map((e)=>Padding(padding:const EdgeInsets.symmetric(vertical:2),
|
||||||
|
child:Row(children:[const Icon(Icons.cancel_outlined,size:13,color:AppColors.rojoError),
|
||||||
|
const SizedBox(width:6),Text(e,style:const TextStyle(fontSize:12,color:AppColors.rojoError))]))),
|
||||||
|
],
|
||||||
|
if (c.isSpecial) ...[
|
||||||
|
const SizedBox(height:8),
|
||||||
|
Container(padding:const EdgeInsets.all(8),
|
||||||
|
decoration:BoxDecoration(color:Colors.orange.shade50,borderRadius:BorderRadius.circular(6),
|
||||||
|
border:Border.all(color:Colors.orange.shade200)),
|
||||||
|
child:const Text('📍 Lleva a puntos de acopio autorizados por el municipio.',
|
||||||
|
style:TextStyle(fontSize:11))),
|
||||||
|
],
|
||||||
|
if (c.isWarn) ...[
|
||||||
|
const SizedBox(height:8),
|
||||||
|
Container(padding:const EdgeInsets.all(8),
|
||||||
|
decoration:BoxDecoration(color:Colors.red.shade50,borderRadius:BorderRadius.circular(6),
|
||||||
|
border:Border.all(color:Colors.red.shade200)),
|
||||||
|
child:const Text('⚠️ NUNCA mezcles residuos peligrosos con basura común.',
|
||||||
|
style:TextStyle(fontSize:11))),
|
||||||
|
],
|
||||||
|
])),
|
||||||
|
])));
|
||||||
|
}
|
||||||
|
}
|
||||||
582
celaya_limpia/lib/screens/citizen/citizen_home_screen.dart
Normal file
@@ -0,0 +1,582 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../../core/app_colors.dart';
|
||||||
|
import '../../database/db_helper.dart';
|
||||||
|
import '../../models/models.dart';
|
||||||
|
import '../../services/auth_service.dart';
|
||||||
|
import '../../services/route_simulator_service.dart';
|
||||||
|
import '../../data/routes_data.dart';
|
||||||
|
import '../../widgets/route_map_widget.dart';
|
||||||
|
import 'citizen_guia_screen.dart';
|
||||||
|
import 'citizen_reporte_screen.dart';
|
||||||
|
import 'add_domicilio_screen.dart';
|
||||||
|
import 'review_screen.dart';
|
||||||
|
import 'collection_calendar_screen.dart';
|
||||||
|
import 'notification_history_screen.dart';
|
||||||
|
import 'chatbot_screen.dart';
|
||||||
|
import '../settings_screen.dart';
|
||||||
|
|
||||||
|
class CitizenHomeScreen extends StatefulWidget {
|
||||||
|
const CitizenHomeScreen({super.key});
|
||||||
|
@override State<CitizenHomeScreen> createState() => _CitizenHomeScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CitizenHomeScreenState extends State<CitizenHomeScreen> {
|
||||||
|
int _tab = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final auth = context.watch<AuthService>();
|
||||||
|
final sim = context.watch<RouteSimulatorService>();
|
||||||
|
final dom = auth.primaryDomicilio;
|
||||||
|
final last = dom != null ? sim.getNotificationForRoute(dom.routeId) : null;
|
||||||
|
|
||||||
|
final tabs = [
|
||||||
|
_HomeTab(auth: auth, sim: sim),
|
||||||
|
const CitizenGuiaScreen(),
|
||||||
|
const CitizenReporteScreen(),
|
||||||
|
];
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: AppColors.grisFondo,
|
||||||
|
body: Stack(children: [
|
||||||
|
tabs[_tab],
|
||||||
|
if (last != null)
|
||||||
|
Positioned(
|
||||||
|
top: MediaQuery.of(context).padding.top + 8, left: 0, right: 0,
|
||||||
|
child: _NotifBanner(notif: last,
|
||||||
|
onDismiss: () => sim.dismissRouteNotification(dom?.routeId ?? '')),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
bottomNavigationBar: NavigationBar(
|
||||||
|
selectedIndex: _tab,
|
||||||
|
onDestinationSelected: (i) => setState(() => _tab = i),
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
indicatorColor: AppColors.guindaPrimary.withOpacity(0.15),
|
||||||
|
destinations: const [
|
||||||
|
NavigationDestination(icon:Icon(Icons.home_outlined),
|
||||||
|
selectedIcon:Icon(Icons.home,color:AppColors.guindaPrimary),label:'Inicio'),
|
||||||
|
NavigationDestination(icon:Icon(Icons.eco_outlined),
|
||||||
|
selectedIcon:Icon(Icons.eco,color:AppColors.guindaPrimary),label:'Guía'),
|
||||||
|
NavigationDestination(icon:Icon(Icons.report_outlined),
|
||||||
|
selectedIcon:Icon(Icons.report,color:AppColors.guindaPrimary),label:'Reportar'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomeTab extends StatefulWidget {
|
||||||
|
final AuthService auth;
|
||||||
|
final RouteSimulatorService sim;
|
||||||
|
const _HomeTab({required this.auth, required this.sim});
|
||||||
|
@override State<_HomeTab> createState() => _HomeTabState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomeTabState extends State<_HomeTab> {
|
||||||
|
RouteStatusModel? _routeStatus;
|
||||||
|
RouteDefinitionModel? _routeDef;
|
||||||
|
|
||||||
|
@override void initState() { super.initState(); _loadStatus(); }
|
||||||
|
|
||||||
|
Future<void> _loadStatus() async {
|
||||||
|
final dom = widget.auth.primaryDomicilio;
|
||||||
|
if (dom == null) return;
|
||||||
|
final s = await DbHelper.getRouteStatus(dom.routeId);
|
||||||
|
final rd = await DbHelper.getRouteDefinitionById(dom.routeId);
|
||||||
|
if (mounted) setState(() { _routeStatus = s; _routeDef = rd; });
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _isRouteProblematic {
|
||||||
|
final s = _routeStatus?.status ?? RouteStatus.enRuta;
|
||||||
|
return s == RouteStatus.cancelada || s == RouteStatus.fallaMecanica || s == RouteStatus.retrasada;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final dom = widget.auth.primaryDomicilio;
|
||||||
|
final allDoms = widget.auth.allDomicilios;
|
||||||
|
final routeId = dom?.routeId ?? '';
|
||||||
|
final route = dom != null ? getRouteById(dom.routeId) : null;
|
||||||
|
final isTruckClose = widget.sim.isTruckClose(routeId);
|
||||||
|
final isCompleted = widget.sim.isRouteCompleted(routeId);
|
||||||
|
final needsReview = widget.sim.needsReviewPrompt(routeId);
|
||||||
|
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: _loadStatus,
|
||||||
|
child: CustomScrollView(slivers: [
|
||||||
|
SliverAppBar(expandedHeight: 120, pinned: true,
|
||||||
|
backgroundColor: AppColors.guindaPrimary,
|
||||||
|
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||||
|
child: Container(height: 4, color: AppColors.dorado)),
|
||||||
|
flexibleSpace: FlexibleSpaceBar(background: Container(
|
||||||
|
color: AppColors.guindaPrimary,
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 50, 20, 16),
|
||||||
|
child: Row(children: [
|
||||||
|
const Icon(Icons.delete_sweep_rounded, color: AppColors.dorado, size: 30),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||||
|
Text('Hola, ${widget.auth.currentUser?.nombre.split(' ').first ?? ''}',
|
||||||
|
style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
|
const Text('Celaya Limpia', style: TextStyle(color: AppColors.dorado, fontSize: 12)),
|
||||||
|
])),
|
||||||
|
IconButton(icon: const Icon(Icons.logout, color: Colors.white70),
|
||||||
|
onPressed: () async {
|
||||||
|
await widget.auth.logout();
|
||||||
|
if (context.mounted) Navigator.pushReplacementNamed(context, '/login');
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
sliver: SliverList(delegate: SliverChildListDelegate([
|
||||||
|
|
||||||
|
// ── Selector de domicilio ────────────────────────────────────
|
||||||
|
if (allDoms.length > 1) _DomicilioSelector(
|
||||||
|
auth: widget.auth, onChanged: _loadStatus),
|
||||||
|
|
||||||
|
// ── Prompt de calificación ───────────────────────────────────
|
||||||
|
if (needsReview && dom != null)
|
||||||
|
_ReviewPromptCard(routeId: routeId, colonia: dom.colonia,
|
||||||
|
sim: widget.sim),
|
||||||
|
|
||||||
|
// ── Estado de ruta (cancelada/falla/retrasada) ───────────────
|
||||||
|
if (_isRouteProblematic)
|
||||||
|
_RouteStatusBanner(status: _routeStatus!)
|
||||||
|
else ...[
|
||||||
|
// ETA Card
|
||||||
|
_EtaCard(sim: widget.sim, routeId: routeId, dom: dom, route: route),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Información detallada de la ruta (días y horario)
|
||||||
|
if (_routeDef != null) _RouteInfoCard(routeDef: _routeDef!),
|
||||||
|
if (_routeDef == null && dom != null) _BasicRouteInfo(dom: dom),
|
||||||
|
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Mapa solo cuando camión está cerca (<15 min)
|
||||||
|
if (isTruckClose && route != null && !isCompleted) ...[
|
||||||
|
_WarningNoPursue(),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
RouteMapWidget(route: route, simulator: widget.sim, height: 220),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
// Aviso privacidad
|
||||||
|
_PrivacyBanner(),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Mis domicilios
|
||||||
|
_DomiciliosCard(auth: widget.auth),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Historial notificaciones
|
||||||
|
if (widget.sim.historyForRoute(routeId).isNotEmpty)
|
||||||
|
_HistorialCard(sim: widget.sim, routeId: routeId),
|
||||||
|
|
||||||
|
const SizedBox(height: 80),
|
||||||
|
])),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Selector de domicilio activo ──────────────────────────────────────────
|
||||||
|
class _DomicilioSelector extends StatelessWidget {
|
||||||
|
final AuthService auth; final VoidCallback onChanged;
|
||||||
|
const _DomicilioSelector({required this.auth, required this.onChanged});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(color: AppColors.guindaPrimary.withOpacity(0.3)),
|
||||||
|
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 4)]),
|
||||||
|
child: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<int>(
|
||||||
|
isExpanded: true,
|
||||||
|
value: auth.primaryDomicilio?.id,
|
||||||
|
icon: const Icon(Icons.swap_horiz, color: AppColors.guindaPrimary),
|
||||||
|
items: auth.allDomicilios.map((d) => DropdownMenuItem(
|
||||||
|
value: d.id,
|
||||||
|
child: Row(children: [
|
||||||
|
Icon(d.isPrimary ? Icons.home : Icons.location_on_outlined,
|
||||||
|
color: AppColors.guindaPrimary, size: 16),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Expanded(child: Text('${d.alias} — ${d.colonia}',
|
||||||
|
style: const TextStyle(fontSize: 13), overflow: TextOverflow.ellipsis)),
|
||||||
|
]))).toList(),
|
||||||
|
onChanged: (id) async {
|
||||||
|
if (id != null) {
|
||||||
|
await DbHelper.setPrimaryDomicilio(id, auth.currentUser!.id!);
|
||||||
|
await auth.reloadDomicilios();
|
||||||
|
onChanged();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Prompt de reseña ──────────────────────────────────────────────────────
|
||||||
|
class _ReviewPromptCard extends StatelessWidget {
|
||||||
|
final String routeId, colonia; final RouteSimulatorService sim;
|
||||||
|
const _ReviewPromptCard({required this.routeId, required this.colonia, required this.sim});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Card(
|
||||||
|
color: Colors.amber.shade50,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12),
|
||||||
|
side: BorderSide(color: Colors.amber.shade300, width: 1.5)),
|
||||||
|
child: Padding(padding: const EdgeInsets.all(14), child: Column(children: [
|
||||||
|
const Row(children: [
|
||||||
|
Text('⭐', style: TextStyle(fontSize: 24)),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Expanded(child: Text('¿Cómo estuvo el servicio de hoy?',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14))),
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
const Text('El camión pasó por tu colonia. Toma un momento para calificar el servicio.',
|
||||||
|
style: TextStyle(fontSize: 12, color: AppColors.grisTexto)),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Row(children: [
|
||||||
|
Expanded(child: ElevatedButton.icon(
|
||||||
|
onPressed: () => Navigator.push(context, MaterialPageRoute(
|
||||||
|
builder: (_) => ReviewScreen(routeId: routeId, colonia: colonia))),
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.amber,
|
||||||
|
foregroundColor: Colors.black87),
|
||||||
|
icon: const Icon(Icons.star, size: 16),
|
||||||
|
label: const Text('Calificar', style: TextStyle(fontWeight: FontWeight.bold)))),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
TextButton(onPressed: () => sim.clearReviewPrompt(routeId),
|
||||||
|
child: const Text('Después', style: TextStyle(color: AppColors.grisTexto))),
|
||||||
|
]),
|
||||||
|
])));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Info detallada de la ruta ─────────────────────────────────────────────
|
||||||
|
class _RouteInfoCard extends StatelessWidget {
|
||||||
|
final RouteDefinitionModel routeDef;
|
||||||
|
const _RouteInfoCard({required this.routeDef});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Card(
|
||||||
|
child: Padding(padding: const EdgeInsets.all(14), child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
const Row(children: [
|
||||||
|
Icon(Icons.schedule, color: AppColors.guindaPrimary, size: 16),
|
||||||
|
SizedBox(width: 6),
|
||||||
|
Text('Información de tu ruta', style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, color: AppColors.guindaPrimary)),
|
||||||
|
]),
|
||||||
|
const Divider(),
|
||||||
|
Text(routeDef.nombre, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(children: [
|
||||||
|
const Icon(Icons.access_time, size: 13, color: AppColors.grisTexto),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text('${routeDef.horaInicio} — ${routeDef.horaFin} (${_turnoLabel(routeDef.turno)})',
|
||||||
|
style: const TextStyle(fontSize: 12, color: AppColors.negroTexto)),
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
const Icon(Icons.calendar_today, size: 13, color: AppColors.grisTexto),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Expanded(child: Text(
|
||||||
|
routeDef.dias.map(AppDias.label).join(', '),
|
||||||
|
style: const TextStyle(fontSize: 12, color: AppColors.negroTexto))),
|
||||||
|
]),
|
||||||
|
])));
|
||||||
|
|
||||||
|
String _turnoLabel(String t) => t=='MATUTINO'?'🌄 Matutino':t=='VESPERTINO'?'🌅 Vespertino':'🌙 Nocturno';
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BasicRouteInfo extends StatelessWidget {
|
||||||
|
final DomicilioModel dom;
|
||||||
|
const _BasicRouteInfo({required this.dom});
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Card(
|
||||||
|
child: Padding(padding: const EdgeInsets.all(14), child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
const Row(children: [
|
||||||
|
Icon(Icons.schedule, color: AppColors.guindaPrimary, size: 16),
|
||||||
|
SizedBox(width: 6),
|
||||||
|
Text('Tu servicio de recolección', style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, color: AppColors.guindaPrimary)),
|
||||||
|
]),
|
||||||
|
const Divider(),
|
||||||
|
Text('Ruta: ${dom.routeId}', style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||||
|
Text('Horario: ${dom.horarioEstimado}',
|
||||||
|
style: const TextStyle(color: AppColors.grisTexto, fontSize: 12)),
|
||||||
|
])));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Aviso anti-persecución ────────────────────────────────────────────────
|
||||||
|
class _WarningNoPursue extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(color: Colors.red.shade50, borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.red.shade300)),
|
||||||
|
child: const Row(children: [
|
||||||
|
Icon(Icons.warning_amber_rounded, color: AppColors.rojoError, size: 20),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Expanded(child: Text(
|
||||||
|
'⚠️ Ya es momento de sacar tu basura.\n'
|
||||||
|
'🚫 NO persigas ni interceptes el camión en movimiento.\n'
|
||||||
|
'✅ Coloca tus bolsas en la acera y espera.',
|
||||||
|
style: TextStyle(fontSize: 11, color: AppColors.rojoError, fontWeight: FontWeight.w500))),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mis domicilios ────────────────────────────────────────────────────────
|
||||||
|
class _DomiciliosCard extends StatelessWidget {
|
||||||
|
final AuthService auth;
|
||||||
|
const _DomiciliosCard({required this.auth});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final doms = auth.allDomicilios;
|
||||||
|
return Card(child: Padding(padding: const EdgeInsets.all(14), child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Row(children: [
|
||||||
|
const Icon(Icons.home_outlined, color: AppColors.guindaPrimary, size: 16),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
const Expanded(child: Text('Mis Domicilios',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, color: AppColors.guindaPrimary))),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () async {
|
||||||
|
final result = await Navigator.push(context,
|
||||||
|
MaterialPageRoute(builder: (_) => const AddDomicilioScreen()));
|
||||||
|
if (result == true) await auth.reloadDomicilios();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.add, size: 14),
|
||||||
|
label: const Text('Agregar', style: TextStyle(fontSize: 12)),
|
||||||
|
style: TextButton.styleFrom(foregroundColor: AppColors.guindaPrimary)),
|
||||||
|
]),
|
||||||
|
const Divider(),
|
||||||
|
if (doms.isEmpty)
|
||||||
|
const Text('Sin domicilios registrados',
|
||||||
|
style: TextStyle(color: AppColors.grisTexto, fontSize: 12))
|
||||||
|
else
|
||||||
|
...doms.map((d) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Row(children: [
|
||||||
|
Icon(d.isPrimary ? Icons.home : Icons.location_on_outlined,
|
||||||
|
color: d.isPrimary ? AppColors.guindaPrimary : AppColors.grisTexto, size: 16),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text('${d.alias} — ${d.colonia}',
|
||||||
|
style: TextStyle(fontWeight: d.isPrimary ? FontWeight.bold : FontWeight.normal,
|
||||||
|
fontSize: 12)),
|
||||||
|
Text(d.calle, style: const TextStyle(color: AppColors.grisTexto, fontSize: 11)),
|
||||||
|
Text('${d.routeId} • ${d.horarioEstimado}',
|
||||||
|
style: const TextStyle(color: AppColors.grisTexto, fontSize: 10)),
|
||||||
|
])),
|
||||||
|
if (!d.isPrimary)
|
||||||
|
IconButton(icon: const Icon(Icons.star_border, size: 16, color: AppColors.dorado),
|
||||||
|
tooltip: 'Hacer principal',
|
||||||
|
onPressed: () async {
|
||||||
|
await DbHelper.setPrimaryDomicilio(d.id!, auth.currentUser!.id!);
|
||||||
|
await auth.reloadDomicilios();
|
||||||
|
}),
|
||||||
|
IconButton(icon: const Icon(Icons.edit_outlined, size: 14, color: AppColors.grisTexto),
|
||||||
|
onPressed: () async {
|
||||||
|
final result = await Navigator.push(context, MaterialPageRoute(
|
||||||
|
builder: (_) => AddDomicilioScreen(editing: d)));
|
||||||
|
if (result == true) await auth.reloadDomicilios();
|
||||||
|
}),
|
||||||
|
if (!d.isPrimary)
|
||||||
|
IconButton(icon: const Icon(Icons.delete_outline, size: 14, color: AppColors.rojoError),
|
||||||
|
onPressed: () async {
|
||||||
|
await DbHelper.deleteDomicilio(d.id!);
|
||||||
|
await auth.reloadDomicilios();
|
||||||
|
}),
|
||||||
|
]))),
|
||||||
|
])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Banner de ruta con problema ───────────────────────────────────────────
|
||||||
|
class _RouteStatusBanner extends StatelessWidget {
|
||||||
|
final RouteStatusModel status;
|
||||||
|
const _RouteStatusBanner({required this.status});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isCancelled = status.status == RouteStatus.cancelada;
|
||||||
|
final isFalla = status.status == RouteStatus.fallaMecanica;
|
||||||
|
final isRetrasada = status.status == RouteStatus.retrasada;
|
||||||
|
final color = isCancelled ? AppColors.rojoError : isFalla ? Colors.red.shade800 : AppColors.naranjaAlerta;
|
||||||
|
final icon = isCancelled ? Icons.cancel : isFalla ? Icons.build : Icons.access_time;
|
||||||
|
final titulo = isCancelled ? '❌ Ruta Cancelada Hoy'
|
||||||
|
: isFalla ? '🔧 Falla Mecánica en Servicio' : '⏱️ Servicio con Retraso';
|
||||||
|
|
||||||
|
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Container(width: double.infinity, padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Row(children: [
|
||||||
|
Icon(icon, color: Colors.white, size: 26),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(child: Text(titulo, style: const TextStyle(color: Colors.white,
|
||||||
|
fontSize: 17, fontWeight: FontWeight.bold))),
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(isCancelled
|
||||||
|
? 'El servicio no se realizará hoy. Guarda tus residuos para mañana.'
|
||||||
|
: isFalla
|
||||||
|
? 'El camión presentó una falla. El Ayuntamiento atiende la situación.'
|
||||||
|
: 'El camión presenta un retraso. El servicio se realizará con demora.',
|
||||||
|
style: const TextStyle(color: Colors.white, fontSize: 13)),
|
||||||
|
])),
|
||||||
|
if (status.mensaje != null && status.mensaje!.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Container(width: double.infinity, padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(color: color.withOpacity(0.4))),
|
||||||
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Row(children: [
|
||||||
|
Icon(Icons.admin_panel_settings, color: color, size: 16),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text('Mensaje del Ayuntamiento', style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, color: color, fontSize: 13)),
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(status.mensaje!, style: const TextStyle(fontSize: 13)),
|
||||||
|
])),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Container(padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.grey.shade300)),
|
||||||
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
const Text('💡 Recomendaciones:', style: TextStyle(fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12, color: AppColors.grisTexto)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(isCancelled
|
||||||
|
? '• Guarda tus bolsas en lugar cerrado\n• No dejes residuos en la acera\n• Revisa la app mañana'
|
||||||
|
: isRetrasada
|
||||||
|
? '• Espera el aviso de 15 minutos antes de sacar tu basura\n• El camión llegará eventualmente\n• Recibe la notificación en esta app'
|
||||||
|
: '• Espera confirmación del Ayuntamiento\n• Puede enviarse unidad de reemplazo',
|
||||||
|
style: const TextStyle(fontSize: 12, color: AppColors.grisTexto)),
|
||||||
|
])),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ETA Card ──────────────────────────────────────────────────────────────
|
||||||
|
class _EtaCard extends StatelessWidget {
|
||||||
|
final RouteSimulatorService sim; final String routeId; final dom; final route;
|
||||||
|
const _EtaCard({required this.sim, required this.routeId, required this.dom, required this.route});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(colors:[AppColors.guindaPrimary,AppColors.guindaDark],
|
||||||
|
begin:Alignment.topLeft,end:Alignment.bottomRight),
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
boxShadow: [BoxShadow(color:AppColors.guindaDark.withOpacity(0.4),blurRadius:8,offset:const Offset(0,4))]),
|
||||||
|
padding: const EdgeInsets.all(18),
|
||||||
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Row(children:[
|
||||||
|
const Icon(Icons.local_shipping,color:AppColors.dorado,size:22),
|
||||||
|
const SizedBox(width:8),
|
||||||
|
Expanded(child:Text(route?.name??dom?.routeId??'Tu ruta',
|
||||||
|
style:const TextStyle(color:AppColors.dorado,fontSize:13,fontWeight:FontWeight.w600))),
|
||||||
|
]),
|
||||||
|
const SizedBox(height:8),
|
||||||
|
Text(sim.getEtaText(routeId),
|
||||||
|
style:const TextStyle(color:Colors.white,fontSize:16,fontWeight:FontWeight.bold)),
|
||||||
|
const SizedBox(height:6),
|
||||||
|
if (dom!=null) Text('⏰ ${dom.horarioEstimado}',
|
||||||
|
style:const TextStyle(color:Colors.white60,fontSize:11)),
|
||||||
|
const SizedBox(height:10),
|
||||||
|
LinearProgressIndicator(
|
||||||
|
value:route!=null?(sim.getPositionIndex(routeId)+1)/route.positions.length:0,
|
||||||
|
backgroundColor:Colors.white24,
|
||||||
|
valueColor:const AlwaysStoppedAnimation<Color>(AppColors.dorado)),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Privacidad ────────────────────────────────────────────────────────────
|
||||||
|
class _PrivacyBanner extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Container(
|
||||||
|
padding:const EdgeInsets.all(10),
|
||||||
|
decoration:BoxDecoration(color:Colors.amber.shade50,borderRadius:BorderRadius.circular(8),
|
||||||
|
border:Border.all(color:Colors.amber.shade300)),
|
||||||
|
child:const Row(children:[
|
||||||
|
Icon(Icons.shield_outlined,color:Colors.amber,size:18),
|
||||||
|
SizedBox(width:6),
|
||||||
|
Expanded(child:Text('🔒 Solo ves la información de tu ruta asignada.',
|
||||||
|
style:TextStyle(fontSize:11,color:Colors.black87))),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Historial notificaciones ──────────────────────────────────────────────
|
||||||
|
class _HistorialCard extends StatelessWidget {
|
||||||
|
final RouteSimulatorService sim; final String routeId;
|
||||||
|
const _HistorialCard({required this.sim, required this.routeId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final notifs = sim.historyForRoute(routeId).take(5).toList();
|
||||||
|
return Card(child:Padding(padding:const EdgeInsets.all(14),child:Column(
|
||||||
|
crossAxisAlignment:CrossAxisAlignment.start, children:[
|
||||||
|
const Text('Alertas recientes',style:TextStyle(fontWeight:FontWeight.bold,color:AppColors.guindaPrimary)),
|
||||||
|
const Divider(),
|
||||||
|
...notifs.map((n){
|
||||||
|
final color = n.event==NotifEvent.truckProximity||n.event==NotifEvent.truckApproaching15min
|
||||||
|
?AppColors.naranjaAlerta:n.event==NotifEvent.routeCompleted||n.event==NotifEvent.reviewPrompt
|
||||||
|
?AppColors.verdeExito:n.event==NotifEvent.routeCancelled?AppColors.rojoError:AppColors.azulInfo;
|
||||||
|
return Padding(padding:const EdgeInsets.symmetric(vertical:3),
|
||||||
|
child:Row(children:[
|
||||||
|
Icon(Icons.circle,size:8,color:color),
|
||||||
|
const SizedBox(width:8),
|
||||||
|
Expanded(child:Text(n.title,style:const TextStyle(fontSize:12,fontWeight:FontWeight.w500))),
|
||||||
|
Text('${n.timestamp.hour.toString().padLeft(2,'0')}:${n.timestamp.minute.toString().padLeft(2,'0')}',
|
||||||
|
style:const TextStyle(fontSize:10,color:AppColors.grisTexto)),
|
||||||
|
]));
|
||||||
|
}),
|
||||||
|
])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Notif Banner ──────────────────────────────────────────────────────────
|
||||||
|
class _NotifBanner extends StatelessWidget {
|
||||||
|
final AppNotification notif; final VoidCallback onDismiss;
|
||||||
|
const _NotifBanner({required this.notif, required this.onDismiss});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isUrgent = notif.event==NotifEvent.truckProximity||notif.event==NotifEvent.truckApproaching15min;
|
||||||
|
final isReview = notif.event==NotifEvent.reviewPrompt;
|
||||||
|
final color = isUrgent?AppColors.naranjaAlerta
|
||||||
|
:isReview?Colors.amber.shade700
|
||||||
|
:notif.event==NotifEvent.routeCancelled?AppColors.rojoError
|
||||||
|
:notif.event==NotifEvent.gpsLost?Colors.red.shade800
|
||||||
|
:AppColors.azulInfo;
|
||||||
|
return Material(color:Colors.transparent,
|
||||||
|
child:Container(margin:const EdgeInsets.all(12),
|
||||||
|
decoration:BoxDecoration(color:color,borderRadius:BorderRadius.circular(12),
|
||||||
|
boxShadow:const[BoxShadow(color:Colors.black26,blurRadius:8,offset:Offset(0,4))]),
|
||||||
|
child:Padding(padding:const EdgeInsets.all(12),child:Row(children:[
|
||||||
|
Icon(isReview?Icons.star:Icons.notifications_active,color:Colors.white,size:24),
|
||||||
|
const SizedBox(width:10),
|
||||||
|
Expanded(child:Column(crossAxisAlignment:CrossAxisAlignment.start,
|
||||||
|
mainAxisSize:MainAxisSize.min,children:[
|
||||||
|
Text(notif.title,style:const TextStyle(color:Colors.white,fontWeight:FontWeight.bold,fontSize:13)),
|
||||||
|
Text(notif.body,style:const TextStyle(color:Colors.white70,fontSize:11),
|
||||||
|
maxLines:2,overflow:TextOverflow.ellipsis),
|
||||||
|
])),
|
||||||
|
IconButton(icon:const Icon(Icons.close,color:Colors.white,size:18),onPressed:onDismiss),
|
||||||
|
]))));
|
||||||
|
}
|
||||||
|
}
|
||||||
221
celaya_limpia/lib/screens/citizen/citizen_reporte_screen.dart
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import '../../core/app_colors.dart';
|
||||||
|
import '../../database/db_helper.dart';
|
||||||
|
import '../../models/models.dart';
|
||||||
|
import '../../services/auth_service.dart';
|
||||||
|
|
||||||
|
class CitizenReporteScreen extends StatefulWidget {
|
||||||
|
const CitizenReporteScreen({super.key});
|
||||||
|
@override State<CitizenReporteScreen> createState() => _CitizenReporteScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CitizenReporteScreenState extends State<CitizenReporteScreen> {
|
||||||
|
String _tipo = 'CAMION_NO_PASO';
|
||||||
|
final _desc = TextEditingController();
|
||||||
|
int _calif = 5;
|
||||||
|
bool _loading = false, _sent = false;
|
||||||
|
List<ReporteModel> _reportes = [];
|
||||||
|
File? _foto;
|
||||||
|
final _picker = ImagePicker();
|
||||||
|
|
||||||
|
static const _tipos = {
|
||||||
|
'CAMION_NO_PASO': 'El camion no paso',
|
||||||
|
'RETRASO': 'Retraso significativo',
|
||||||
|
'RESIDUOS_NO_RECOGIDOS': 'Residuos no recogidos',
|
||||||
|
'OTRO': 'Otro motivo',
|
||||||
|
};
|
||||||
|
|
||||||
|
@override void initState() { super.initState(); _load(); }
|
||||||
|
|
||||||
|
Future<void> _load() async {
|
||||||
|
final auth = context.read<AuthService>();
|
||||||
|
if (auth.currentUser == null) return;
|
||||||
|
final r = await DbHelper.getReportesByUser(auth.currentUser!.id!);
|
||||||
|
if (mounted) setState(() => _reportes = r);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickImage(ImageSource source) async {
|
||||||
|
try {
|
||||||
|
final picked = await _picker.pickImage(source: source, imageQuality: 70, maxWidth: 1024);
|
||||||
|
if (picked != null && mounted) setState(() => _foto = File(picked.path));
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('No se pudo acceder a la camara: $e'),
|
||||||
|
backgroundColor: AppColors.rojoError));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showPhotoOptions() {
|
||||||
|
showModalBottomSheet(context: context, builder: (_) => SafeArea(
|
||||||
|
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
ListTile(leading: const Icon(Icons.camera_alt, color: AppColors.guindaPrimary),
|
||||||
|
title: const Text('Tomar foto'),
|
||||||
|
onTap: () { Navigator.pop(context); _pickImage(ImageSource.camera); }),
|
||||||
|
ListTile(leading: const Icon(Icons.photo_library, color: AppColors.guindaPrimary),
|
||||||
|
title: const Text('Elegir de galeria'),
|
||||||
|
onTap: () { Navigator.pop(context); _pickImage(ImageSource.gallery); }),
|
||||||
|
if (_foto != null)
|
||||||
|
ListTile(leading: const Icon(Icons.delete_outline, color: AppColors.rojoError),
|
||||||
|
title: const Text('Quitar foto', style: TextStyle(color: AppColors.rojoError)),
|
||||||
|
onTap: () { Navigator.pop(context); setState(() => _foto = null); }),
|
||||||
|
])));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _send() async {
|
||||||
|
final auth = context.read<AuthService>();
|
||||||
|
if (auth.currentUser == null || _desc.text.trim().isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||||
|
content: Text('Describe el problema'),
|
||||||
|
backgroundColor: AppColors.rojoError));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() => _loading = true);
|
||||||
|
|
||||||
|
final db = await DbHelper.database;
|
||||||
|
await db.insert('reportes', {
|
||||||
|
'user_id': auth.currentUser!.id,
|
||||||
|
'tipo': _tipo,
|
||||||
|
'descripcion': _desc.text.trim(),
|
||||||
|
'colonia': auth.primaryDomicilio?.colonia ?? '',
|
||||||
|
'route_id': auth.primaryDomicilio?.routeId ?? '',
|
||||||
|
'fecha': DateTime.now().toIso8601String(),
|
||||||
|
'estado': 'PENDIENTE',
|
||||||
|
'calificacion': _calif,
|
||||||
|
'foto_path': _foto?.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
await _load();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() { _loading = false; _sent = true; _desc.clear(); _foto = null; });
|
||||||
|
await Future.delayed(const Duration(seconds: 2));
|
||||||
|
if (mounted) setState(() => _sent = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
backgroundColor: AppColors.grisFondo,
|
||||||
|
appBar: AppBar(automaticallyImplyLeading: false,
|
||||||
|
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||||
|
title: const Text('Reportar Incidencia'),
|
||||||
|
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||||
|
child: Container(height: 4, color: AppColors.dorado))),
|
||||||
|
body: _sent
|
||||||
|
? Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||||
|
const Icon(Icons.check_circle, color: AppColors.verdeExito, size: 64),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text('Reporte enviado', style: TextStyle(fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold, color: AppColors.verdeExito)),
|
||||||
|
const Text('El Ayuntamiento lo revisara pronto.',
|
||||||
|
style: TextStyle(color: AppColors.grisTexto)),
|
||||||
|
]))
|
||||||
|
: SingleChildScrollView(padding: const EdgeInsets.all(16), child: Column(children: [
|
||||||
|
Card(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: Padding(padding: const EdgeInsets.all(16), child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
const Text('Tipo de incidencia', style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, color: AppColors.guindaPrimary, fontSize: 15)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
..._tipos.entries.map((e) => RadioListTile<String>(dense: true,
|
||||||
|
value: e.key, groupValue: _tipo,
|
||||||
|
title: Text(e.value, style: const TextStyle(fontSize: 13)),
|
||||||
|
activeColor: AppColors.guindaPrimary,
|
||||||
|
onChanged: (v) => setState(() => _tipo = v!))),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
DropdownButtonFormField<int>(value: _calif,
|
||||||
|
decoration: const InputDecoration(labelText: 'Calificacion del servicio',
|
||||||
|
border: OutlineInputBorder()),
|
||||||
|
items: [5,4,3,2,1].map((n) => DropdownMenuItem(value: n,
|
||||||
|
child: Text(['Excelente','Bueno','Regular','Malo','Muy malo'][5-n]))).toList(),
|
||||||
|
onChanged: (v) => setState(() => _calif = v!)),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
TextField(controller: _desc, maxLines: 3,
|
||||||
|
decoration: const InputDecoration(hintText: 'Describe el problema...',
|
||||||
|
border: OutlineInputBorder(), filled: true, fillColor: Colors.white)),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Foto adjunta
|
||||||
|
const Text('Foto del incidente (opcional)',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: _showPhotoOptions,
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity, height: _foto != null ? 180 : 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: _foto != null
|
||||||
|
? AppColors.guindaPrimary : Colors.grey.shade300,
|
||||||
|
style: BorderStyle.solid)),
|
||||||
|
child: _foto != null
|
||||||
|
? Stack(children: [
|
||||||
|
ClipRRect(borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Image.file(_foto!, fit: BoxFit.cover,
|
||||||
|
width: double.infinity, height: 180)),
|
||||||
|
Positioned(top: 8, right: 8,
|
||||||
|
child: GestureDetector(onTap: () => setState(() => _foto = null),
|
||||||
|
child: Container(padding: const EdgeInsets.all(4),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: AppColors.rojoError, shape: BoxShape.circle),
|
||||||
|
child: const Icon(Icons.close, color: Colors.white, size: 16)))),
|
||||||
|
])
|
||||||
|
: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||||
|
const Icon(Icons.add_a_photo_outlined,
|
||||||
|
color: AppColors.grisTexto, size: 28),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
const Text('Agregar foto', style: TextStyle(
|
||||||
|
color: AppColors.grisTexto, fontSize: 12)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
SizedBox(width: double.infinity, height: 48,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: _loading ? null : _send,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||||
|
icon: _loading ? const SizedBox(width: 18, height: 18,
|
||||||
|
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||||
|
: const Icon(Icons.send),
|
||||||
|
label: const Text('ENVIAR REPORTE',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold)))),
|
||||||
|
]))),
|
||||||
|
if (_reportes.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Align(alignment: Alignment.centerLeft,
|
||||||
|
child: Text('Mis Reportes', style: TextStyle(fontWeight: FontWeight.bold,
|
||||||
|
color: AppColors.guindaPrimary, fontSize: 15))),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
..._reportes.map((r) => Card(margin: const EdgeInsets.only(bottom: 6),
|
||||||
|
child: ListTile(dense: true,
|
||||||
|
leading: CircleAvatar(backgroundColor: AppColors.guindaPrimary, radius: 16,
|
||||||
|
child: const Icon(Icons.report, color: Colors.white, size: 16)),
|
||||||
|
title: Text(_tipos[r.tipo] ?? r.tipo,
|
||||||
|
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600)),
|
||||||
|
subtitle: Text(r.descripcion, maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(fontSize: 11)),
|
||||||
|
trailing: Container(padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _estadoColor(r.estado).withOpacity(0.15),
|
||||||
|
borderRadius: BorderRadius.circular(10)),
|
||||||
|
child: Text(r.estado, style: TextStyle(fontSize: 9,
|
||||||
|
color: _estadoColor(r.estado), fontWeight: FontWeight.bold)))))),
|
||||||
|
],
|
||||||
|
])),
|
||||||
|
);
|
||||||
|
|
||||||
|
Color _estadoColor(String e) {
|
||||||
|
switch (e) {
|
||||||
|
case 'RESUELTO': return AppColors.verdeExito;
|
||||||
|
case 'EN_REVISION': return AppColors.azulInfo;
|
||||||
|
default: return AppColors.naranjaAlerta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override void dispose() { _desc.dispose(); super.dispose(); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import '../../core/app_colors.dart';
|
||||||
|
import '../../database/db_helper.dart';
|
||||||
|
import '../../models/models.dart';
|
||||||
|
import '../../services/auth_service.dart';
|
||||||
|
|
||||||
|
class CollectionCalendarScreen extends StatefulWidget {
|
||||||
|
const CollectionCalendarScreen({super.key});
|
||||||
|
@override State<CollectionCalendarScreen> createState() => _CollectionCalendarScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CollectionCalendarScreenState extends State<CollectionCalendarScreen> {
|
||||||
|
RouteDefinitionModel? _routeDef;
|
||||||
|
List<ReviewModel> _myReviews = [];
|
||||||
|
bool _loading = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() { super.initState(); _load(); }
|
||||||
|
|
||||||
|
Future<void> _load() async {
|
||||||
|
final auth = context.read<AuthService>();
|
||||||
|
final dom = auth.primaryDomicilio;
|
||||||
|
if (dom != null) {
|
||||||
|
final rd = await DbHelper.getRouteDefinitionById(dom.routeId);
|
||||||
|
final rv = await DbHelper.getAllReviews();
|
||||||
|
final mine = rv.where((r) => r.userId == auth.currentUser?.id).toList();
|
||||||
|
if (mounted) setState(() { _routeDef = rd; _myReviews = mine; _loading = false; });
|
||||||
|
} else {
|
||||||
|
if (mounted) setState(() => _loading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _shareSchedule() {
|
||||||
|
final auth = context.read<AuthService>();
|
||||||
|
final dom = auth.primaryDomicilio;
|
||||||
|
if (dom == null) return;
|
||||||
|
final rd = _routeDef;
|
||||||
|
final diasStr = rd?.dias.map(_diaLabel).join(', ') ?? 'Lunes, Miércoles y Viernes';
|
||||||
|
final horario = rd != null ? '${rd.horaInicio}–${rd.horaFin}' : dom.horarioEstimado;
|
||||||
|
|
||||||
|
Share.share(
|
||||||
|
'🗑️ Horario de recolección de basura\n'
|
||||||
|
'📍 Colonia: ${dom.colonia}\n'
|
||||||
|
'📅 Días: $diasStr\n'
|
||||||
|
'⏰ Horario: $horario\n'
|
||||||
|
'🚛 Ruta: ${dom.routeId}\n\n'
|
||||||
|
'Descarga Celaya Limpia para recibir avisos en tiempo real.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _diaLabel(String d) {
|
||||||
|
const m = {'LUNES':'Lu','MARTES':'Ma','MIERCOLES':'Mi',
|
||||||
|
'JUEVES':'Ju','VIERNES':'Vi','SABADO':'Sa','DOMINGO':'Do'};
|
||||||
|
return m[d] ?? d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Días del mes actual con marcas de recolección
|
||||||
|
List<Widget> _buildCalendar() {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final first = DateTime(now.year, now.month, 1);
|
||||||
|
final days = DateTime(now.year, now.month + 1, 0).day;
|
||||||
|
final dias = _routeDef?.dias ?? [];
|
||||||
|
|
||||||
|
const weekDays = ['LUNES','MARTES','MIERCOLES','JUEVES','VIERNES','SABADO','DOMINGO'];
|
||||||
|
final monthName = ['','Enero','Febrero','Marzo','Abril','Mayo','Junio',
|
||||||
|
'Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre'][now.month];
|
||||||
|
|
||||||
|
return [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
child: Text('$monthName ${now.year}',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16,
|
||||||
|
color: AppColors.guindaPrimary)),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
// Cabeceras días
|
||||||
|
Row(children: ['Lu','Ma','Mi','Ju','Vi','Sa','Do'].map((d) =>
|
||||||
|
Expanded(child: Center(child: Text(d, style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, fontSize: 11, color: AppColors.grisTexto))))).toList()),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
// Grilla de días
|
||||||
|
GridView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 7, childAspectRatio: 1),
|
||||||
|
itemCount: (first.weekday - 1) + days,
|
||||||
|
itemBuilder: (_, i) {
|
||||||
|
if (i < first.weekday - 1) return const SizedBox();
|
||||||
|
final day = i - (first.weekday - 1) + 1;
|
||||||
|
final date = DateTime(now.year, now.month, day);
|
||||||
|
final diaSem = weekDays[date.weekday - 1];
|
||||||
|
final isCollection = dias.contains(diaSem);
|
||||||
|
final isToday = day == now.day;
|
||||||
|
final isPast = date.isBefore(DateTime(now.year, now.month, now.day));
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.all(2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isCollection
|
||||||
|
? (isPast ? AppColors.guindaPrimary.withOpacity(0.4) : AppColors.guindaPrimary)
|
||||||
|
: (isToday ? Colors.grey.shade200 : null),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: isToday ? Border.all(color: AppColors.dorado, width: 2) : null,
|
||||||
|
),
|
||||||
|
child: Stack(alignment: Alignment.center, children: [
|
||||||
|
Text('$day', style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: isToday ? FontWeight.bold : FontWeight.normal,
|
||||||
|
color: isCollection ? Colors.white : AppColors.negroTexto,
|
||||||
|
)),
|
||||||
|
if (isCollection)
|
||||||
|
Positioned(bottom: 2, child: Container(
|
||||||
|
width: 4, height: 4,
|
||||||
|
decoration: const BoxDecoration(color: AppColors.dorado, shape: BoxShape.circle),
|
||||||
|
)),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final auth = context.read<AuthService>();
|
||||||
|
final dom = auth.primaryDomicilio;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: AppColors.grisFondo,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||||
|
title: const Text('Calendario de Recoleccion'),
|
||||||
|
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||||
|
child: Container(height: 4, color: AppColors.dorado)),
|
||||||
|
actions: [
|
||||||
|
IconButton(icon: const Icon(Icons.share), tooltip: 'Compartir horario',
|
||||||
|
onPressed: _shareSchedule),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: _loading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: SingleChildScrollView(padding: const EdgeInsets.all(16), child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
// Info de la ruta
|
||||||
|
if (dom != null)
|
||||||
|
Card(child: Padding(padding: const EdgeInsets.all(14), child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
const Row(children: [
|
||||||
|
Icon(Icons.local_shipping, color: AppColors.guindaPrimary, size: 18),
|
||||||
|
SizedBox(width: 6),
|
||||||
|
Text('Tu servicio de recoleccion', style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, color: AppColors.guindaPrimary)),
|
||||||
|
]),
|
||||||
|
const Divider(),
|
||||||
|
Text('Colonia: ${dom.colonia}',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13)),
|
||||||
|
Text('Ruta: ${dom.routeId}',
|
||||||
|
style: const TextStyle(color: AppColors.grisTexto, fontSize: 12)),
|
||||||
|
if (_routeDef != null) ...[
|
||||||
|
Text('Dias: ${_routeDef!.dias.map(_diaLabel).join(" · ")}',
|
||||||
|
style: const TextStyle(fontSize: 12)),
|
||||||
|
Text('Horario: ${_routeDef!.horaInicio} - ${_routeDef!.horaFin}',
|
||||||
|
style: const TextStyle(fontSize: 12)),
|
||||||
|
] else
|
||||||
|
Text(dom.horarioEstimado,
|
||||||
|
style: const TextStyle(color: AppColors.grisTexto, fontSize: 12)),
|
||||||
|
]))),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Leyenda
|
||||||
|
Row(children: [
|
||||||
|
_Legend(color: AppColors.guindaPrimary, label: 'Dia de recoleccion'),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_Legend(color: AppColors.dorado, label: 'Punto en dia activo'),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_Legend(color: Colors.grey.shade200, label: 'Hoy'),
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Calendario
|
||||||
|
Card(child: Padding(padding: const EdgeInsets.all(14), child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: _buildCalendar()))),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Consejos semanales
|
||||||
|
Card(color: Colors.blue.shade50,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10),
|
||||||
|
side: BorderSide(color: Colors.blue.shade200)),
|
||||||
|
child: Padding(padding: const EdgeInsets.all(14), child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
const Row(children: [
|
||||||
|
Icon(Icons.tips_and_updates, color: AppColors.azulInfo),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Consejo de la semana', style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, color: AppColors.azulInfo, fontSize: 14)),
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(_weeklyTip(), style: const TextStyle(fontSize: 13, color: AppColors.negroTexto)),
|
||||||
|
])),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Mis calificaciones
|
||||||
|
if (_myReviews.isNotEmpty) ...[
|
||||||
|
const Text('Mis calificaciones', style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, fontSize: 15, color: AppColors.guindaPrimary)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
..._myReviews.take(3).map((r) => Card(margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: ListTile(dense: true,
|
||||||
|
leading: CircleAvatar(backgroundColor: Colors.amber.shade100,
|
||||||
|
child: Text('${r.estrellas}', style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, color: Colors.amber))),
|
||||||
|
title: Text(r.colonia, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600)),
|
||||||
|
subtitle: Text(r.comentario, maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(fontSize: 11)),
|
||||||
|
trailing: Text(
|
||||||
|
'${DateTime.tryParse(r.fecha)?.day}/${DateTime.tryParse(r.fecha)?.month}',
|
||||||
|
style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)),
|
||||||
|
))),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
])),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _weeklyTip() {
|
||||||
|
final tips = [
|
||||||
|
'Separa tus residuos en organicos (restos de comida) e inorganicos (plasticos, metales). Facilita el reciclaje y reduce la contaminacion.',
|
||||||
|
'Coloca tus bolsas en la acera SOLO cuando recibas el aviso de 15 minutos. Sacarlas antes atrae fauna nociva.',
|
||||||
|
'El reciclaje de 1 tonelada de papel salva 17 arboles. Dobla tus cajas y periodicos antes de depositarlos.',
|
||||||
|
'Los aceites usados de cocina NO van a la basura. Llevalos a los puntos de acopio del municipio.',
|
||||||
|
'Composta tus restos organicos si tienes jardin. Reduce hasta un 40% tu basura y mejora tu suelo.',
|
||||||
|
'Las pilas y baterias son residuos peligrosos. Depositalas en los contenedores especiales de tiendas.',
|
||||||
|
'Un celular viejo contiene oro, plata y cobre. Llevalo a un punto RAEE para su reciclaje correcto.',
|
||||||
|
];
|
||||||
|
return tips[DateTime.now().weekday % tips.length];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Legend extends StatelessWidget {
|
||||||
|
final Color color; final String label;
|
||||||
|
const _Legend({required this.color, required this.label});
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
Container(width: 12, height: 12, decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(label, style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../../core/app_colors.dart';
|
||||||
|
import '../../database/db_helper.dart';
|
||||||
|
import '../../services/auth_service.dart';
|
||||||
|
|
||||||
|
class NotificationHistoryScreen extends StatefulWidget {
|
||||||
|
const NotificationHistoryScreen({super.key});
|
||||||
|
@override State<NotificationHistoryScreen> createState() => _NotificationHistoryScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NotificationHistoryScreenState extends State<NotificationHistoryScreen> {
|
||||||
|
List<Map<String, dynamic>> _notifs = [];
|
||||||
|
bool _loading = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() { super.initState(); _load(); }
|
||||||
|
|
||||||
|
Future<void> _load() async {
|
||||||
|
final auth = context.read<AuthService>();
|
||||||
|
if (auth.currentUser == null) return;
|
||||||
|
final n = await DbHelper.getNotifHistory(auth.currentUser!.id!);
|
||||||
|
await DbHelper.markAllNotifsRead(auth.currentUser!.id!);
|
||||||
|
if (mounted) setState(() { _notifs = n; _loading = false; });
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _color(String type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'truckProximity':
|
||||||
|
case 'truckApproaching15min': return AppColors.naranjaAlerta;
|
||||||
|
case 'routeCompleted':
|
||||||
|
case 'reviewPrompt': return AppColors.verdeExito;
|
||||||
|
case 'routeCancelled': return AppColors.rojoError;
|
||||||
|
case 'gpsLost': return Colors.red.shade800;
|
||||||
|
case 'truckStopped': return AppColors.naranjaAlerta;
|
||||||
|
default: return AppColors.azulInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData _icon(String type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'truckProximity':
|
||||||
|
case 'truckApproaching15min': return Icons.warning_amber_rounded;
|
||||||
|
case 'routeCompleted': return Icons.check_circle;
|
||||||
|
case 'reviewPrompt': return Icons.star;
|
||||||
|
case 'routeCancelled': return Icons.cancel;
|
||||||
|
case 'gpsLost': return Icons.gps_off;
|
||||||
|
default: return Icons.notifications;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _timeAgo(String fechaStr) {
|
||||||
|
final f = DateTime.tryParse(fechaStr);
|
||||||
|
if (f == null) return '';
|
||||||
|
final diff = DateTime.now().difference(f);
|
||||||
|
if (diff.inMinutes < 1) return 'Ahora';
|
||||||
|
if (diff.inMinutes < 60) return 'Hace ${diff.inMinutes} min';
|
||||||
|
if (diff.inHours < 24) return 'Hace ${diff.inHours}h';
|
||||||
|
return '${f.day}/${f.month}/${f.year}';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold(
|
||||||
|
backgroundColor: AppColors.grisFondo,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||||
|
title: const Text('Historial de Alertas'),
|
||||||
|
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||||
|
child: Container(height: 4, color: AppColors.dorado)),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await DbHelper.markAllNotifsRead(
|
||||||
|
context.read<AuthService>().currentUser!.id!);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
child: const Text('Marcar leídas', style: TextStyle(color: AppColors.dorado, fontSize: 12)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: _loading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: _notifs.isEmpty
|
||||||
|
? Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||||
|
Icon(Icons.notifications_none, color: Colors.grey.shade400, size: 64),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text('Sin alertas registradas', style: TextStyle(color: Colors.grey.shade500)),
|
||||||
|
]))
|
||||||
|
: ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
itemCount: _notifs.length,
|
||||||
|
itemBuilder: (_, i) {
|
||||||
|
final n = _notifs[i];
|
||||||
|
final isUnread = (n['leida'] as int?) == 0;
|
||||||
|
final color = _color(n['event_type'] ?? '');
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isUnread ? color.withOpacity(0.05) : Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(
|
||||||
|
color: isUnread ? color.withOpacity(0.3) : Colors.grey.shade200),
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: color.withOpacity(0.15),
|
||||||
|
child: Icon(_icon(n['event_type'] ?? ''), color: color, size: 20),
|
||||||
|
),
|
||||||
|
title: Text(n['title'] ?? '', style: TextStyle(
|
||||||
|
fontWeight: isUnread ? FontWeight.bold : FontWeight.normal,
|
||||||
|
fontSize: 13)),
|
||||||
|
subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text(n['body'] ?? '', style: const TextStyle(fontSize: 11), maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text('${n['route_id']} · ${_timeAgo(n['fecha'] ?? '')}',
|
||||||
|
style: TextStyle(fontSize: 10, color: color.withOpacity(0.7))),
|
||||||
|
]),
|
||||||
|
trailing: isUnread
|
||||||
|
? Container(width: 8, height: 8,
|
||||||
|
decoration: BoxDecoration(color: color, shape: BoxShape.circle))
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
183
celaya_limpia/lib/screens/citizen/review_screen.dart
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../../core/app_colors.dart';
|
||||||
|
import '../../database/db_helper.dart';
|
||||||
|
import '../../models/models.dart';
|
||||||
|
import '../../services/auth_service.dart';
|
||||||
|
import '../../services/route_simulator_service.dart';
|
||||||
|
|
||||||
|
class ReviewScreen extends StatefulWidget {
|
||||||
|
final String routeId;
|
||||||
|
final String colonia;
|
||||||
|
const ReviewScreen({super.key, required this.routeId, required this.colonia});
|
||||||
|
@override State<ReviewScreen> createState() => _ReviewScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ReviewScreenState extends State<ReviewScreen> {
|
||||||
|
int _estrellas = 5;
|
||||||
|
final _comentCtrl = TextEditingController();
|
||||||
|
bool _loading = false;
|
||||||
|
bool _sent = false;
|
||||||
|
|
||||||
|
static const _labels = ['', 'Muy malo', 'Malo', 'Regular', 'Bueno', 'Excelente'];
|
||||||
|
static const _colors = [
|
||||||
|
Colors.transparent, AppColors.rojoError, AppColors.naranjaAlerta,
|
||||||
|
Colors.amber, AppColors.verdeExito, AppColors.verdeExito,
|
||||||
|
];
|
||||||
|
|
||||||
|
Future<void> _enviar() async {
|
||||||
|
final auth = context.read<AuthService>();
|
||||||
|
if (auth.currentUser == null) return;
|
||||||
|
|
||||||
|
// Verificar si ya calificó hoy
|
||||||
|
final yaCalificado = await DbHelper.hasReviewedRoute(
|
||||||
|
auth.currentUser!.id!, widget.routeId);
|
||||||
|
if (yaCalificado && mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||||
|
content: Text('Ya calificaste este servicio hoy'),
|
||||||
|
backgroundColor: AppColors.azulInfo));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _loading = true);
|
||||||
|
await DbHelper.insertReview(ReviewModel(
|
||||||
|
userId: auth.currentUser!.id!,
|
||||||
|
colonia: widget.colonia,
|
||||||
|
routeId: widget.routeId,
|
||||||
|
estrellas: _estrellas,
|
||||||
|
comentario: _comentCtrl.text.trim().isEmpty
|
||||||
|
? 'Sin comentario' : _comentCtrl.text.trim(),
|
||||||
|
fecha: DateTime.now().toIso8601String(),
|
||||||
|
nombreUsuario: auth.currentUser!.nombre,
|
||||||
|
));
|
||||||
|
|
||||||
|
context.read<RouteSimulatorService>().clearReviewPrompt(widget.routeId);
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() { _loading = false; _sent = true; });
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: AppColors.grisFondo,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||||
|
title: const Text('Calificar el Servicio'),
|
||||||
|
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||||
|
child: Container(height: 4, color: AppColors.dorado)),
|
||||||
|
),
|
||||||
|
body: _sent
|
||||||
|
? Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||||
|
const Text('⭐', style: TextStyle(fontSize: 64)),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Text('¡Gracias por tu calificación!',
|
||||||
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold,
|
||||||
|
color: AppColors.guindaPrimary)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text('Tu opinión ayuda a mejorar el servicio\nde recolección en Celaya.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: AppColors.grisTexto)),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('Volver al inicio')),
|
||||||
|
]))
|
||||||
|
: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(children: [
|
||||||
|
// Header
|
||||||
|
Container(
|
||||||
|
width: double.infinity, padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.guindaPrimary.withOpacity(0.08),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: AppColors.guindaPrimary.withOpacity(0.2))),
|
||||||
|
child: Column(children: [
|
||||||
|
const Icon(Icons.local_shipping, color: AppColors.guindaPrimary, size: 36),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(widget.routeId, style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, color: AppColors.guindaPrimary)),
|
||||||
|
Text(widget.colonia, style: const TextStyle(
|
||||||
|
color: AppColors.grisTexto, fontSize: 12)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Estrellas
|
||||||
|
const Text('¿Cómo calificarías el servicio de hoy?',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(mainAxisAlignment: MainAxisAlignment.center, children: List.generate(5, (i) {
|
||||||
|
final star = i + 1;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => setState(() => _estrellas = star),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||||
|
child: Icon(
|
||||||
|
_estrellas >= star ? Icons.star : Icons.star_border,
|
||||||
|
color: _estrellas >= star ? Colors.amber : Colors.grey,
|
||||||
|
size: 44,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
})),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(_labels[_estrellas],
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold,
|
||||||
|
color: _colors[_estrellas])),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Comentario
|
||||||
|
const Align(alignment: Alignment.centerLeft,
|
||||||
|
child: Text('Comentario (opcional)',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w600))),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextField(
|
||||||
|
controller: _comentCtrl,
|
||||||
|
maxLines: 4,
|
||||||
|
maxLength: 200,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'Cuéntanos cómo estuvo el servicio...',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
filled: true, fillColor: Colors.white),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Aviso
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(color: Colors.blue.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.blue.shade200)),
|
||||||
|
child: const Row(children: [
|
||||||
|
Icon(Icons.info_outline, color: AppColors.azulInfo, size: 16),
|
||||||
|
SizedBox(width: 6),
|
||||||
|
Expanded(child: Text(
|
||||||
|
'Tu calificación es anónima para otros ciudadanos, '
|
||||||
|
'pero el Ayuntamiento la usará para mejorar el servicio.',
|
||||||
|
style: TextStyle(fontSize: 11, color: AppColors.azulInfo))),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
SizedBox(width: double.infinity, height: 50,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: _loading ? null : _enviar,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||||
|
icon: _loading
|
||||||
|
? const SizedBox(width: 18, height: 18,
|
||||||
|
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||||
|
: const Icon(Icons.star),
|
||||||
|
label: const Text('ENVIAR CALIFICACIÓN',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold)))),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override void dispose() { _comentCtrl.dispose(); super.dispose(); }
|
||||||
|
}
|
||||||