Actualizacion del programa

This commit is contained in:
2026-05-23 01:40:39 -06:00
parent 458af32fcf
commit c6a1a67469
132 changed files with 11009 additions and 168 deletions

View File

@@ -16,13 +16,11 @@ class DbHelper {
}
static Future<void> _onCreate(Database db, int v) async {
// Usuarios
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)''');
// Domicilios (User → Domicilio → Colonia/Zona → Ruta)
await db.execute('''CREATE TABLE domicilios(
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL, alias TEXT DEFAULT 'Casa',
@@ -30,7 +28,6 @@ class DbHelper {
route_id TEXT NOT NULL, horario_estimado TEXT NOT NULL,
is_primary INTEGER DEFAULT 0)''');
// Definiciones de rutas creadas por admin
await db.execute('''CREATE TABLE route_definitions(
id INTEGER PRIMARY KEY AUTOINCREMENT,
route_id TEXT UNIQUE NOT NULL, nombre TEXT NOT NULL,
@@ -38,33 +35,29 @@ class DbHelper {
hora_fin TEXT NOT NULL, turno TEXT NOT NULL,
colonias TEXT NOT NULL, activa INTEGER DEFAULT 1)''');
// Estado de rutas
await db.execute('''CREATE TABLE route_status(
route_id TEXT PRIMARY KEY, status TEXT NOT NULL,
mensaje TEXT, updated_at TEXT)''');
// Asignaciones conductor
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)''');
// Alertas del sistema
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)''');
// Reportes ciudadanos
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)''');
estado TEXT DEFAULT 'PENDIENTE', calificacion INTEGER DEFAULT 5,
foto_path TEXT)''');
// RESEÑAS del servicio
await db.execute('''CREATE TABLE reviews(
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL, colonia TEXT NOT NULL,
@@ -72,11 +65,22 @@ class DbHelper {
comentario TEXT NOT NULL, fecha TEXT NOT NULL,
nombre_usuario TEXT DEFAULT 'Ciudadano')''');
// Seed: admin y conductor
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'});
await db.insert('users', {'nombre':'Juan Conductor','email':'conductor@celaya.gob.mx',
'password':'conductor123','rol':'CONDUCTOR'});
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 ────────────────────────────────────────────────────────────────
@@ -101,7 +105,6 @@ class DbHelper {
// ── DOMICILIOS ───────────────────────────────────────────────────────────
static Future<int> insertDomicilio(DomicilioModel d) async {
final db = await database;
// Si es el primero del usuario, marcarlo como primario
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});
@@ -255,7 +258,6 @@ class DbHelper {
}
static Future<bool> hasReviewedRoute(int userId, String routeId) async {
// Verifica si el usuario ya calificó esta ruta hoy
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%'",
@@ -263,7 +265,6 @@ class DbHelper {
return res.isNotEmpty;
}
// Promedio por colonia para el admin
static Future<List<Map<String, dynamic>>> getReviewSummaryByColonia() async {
final db = await database;
return db.rawQuery('''
@@ -272,8 +273,116 @@ class DbHelper {
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 colonia
ORDER BY promedio ASC''');
GROUP BY semana ORDER BY semana DESC LIMIT 8''');
}
}