import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; import '../models/models.dart'; class DbHelper { static Database? _db; static Future get database async { _db ??= await _initDb(); return _db!; } static Future _initDb() async { final path = join(await getDatabasesPath(), 'celaya_v3.db'); return openDatabase(path, version: 3, onCreate: _onCreate, onUpgrade: _onUpgrade); } static Future _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)'''); // NOTAS DE ADMIN SOBRE REPORTES await db.execute('''CREATE TABLE reporte_notas( id INTEGER PRIMARY KEY AUTOINCREMENT, reporte_id INTEGER NOT NULL, admin_id INTEGER NOT NULL, admin_nombre TEXT NOT NULL, nota TEXT NOT NULL, fecha TEXT NOT NULL)'''); // EVIDENCIAS DEL ADMIN EN REPORTES await db.execute('''CREATE TABLE reporte_evidencias( id INTEGER PRIMARY KEY AUTOINCREMENT, reporte_id INTEGER NOT NULL, admin_id INTEGER NOT NULL, pie_imagen TEXT NOT NULL, foto_path TEXT, fecha TEXT NOT NULL)'''); // CHAT POR REPORTE (admin <-> ciudadano) await db.execute('''CREATE TABLE reporte_chat( id INTEGER PRIMARY KEY AUTOINCREMENT, reporte_id INTEGER NOT NULL, user_id INTEGER NOT NULL, rol TEXT NOT NULL, mensaje TEXT NOT NULL, fecha TEXT NOT NULL, leido INTEGER DEFAULT 0)'''); 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}); } // Migración incremental — se ejecuta al actualizar la app static Future _onUpgrade(Database db, int oldV, int newV) async { // Lista de migraciones seguras (todas usan IF NOT EXISTS o ignoran errores) final sqls = [ // Columnas que pueden faltar "ALTER TABLE reportes ADD COLUMN foto_path TEXT", // Tabla reviews (puede no existir en instalaciones viejas) '''CREATE TABLE IF NOT EXISTS 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')''', // Tabla user_meta '''CREATE TABLE IF NOT EXISTS user_meta( user_id INTEGER PRIMARY KEY, activo INTEGER DEFAULT 1, notas TEXT)''', // Tabla notification_history '''CREATE TABLE IF NOT EXISTS 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)''', // Tablas de gestión de reportes '''CREATE TABLE IF NOT EXISTS reporte_notas( id INTEGER PRIMARY KEY AUTOINCREMENT, reporte_id INTEGER NOT NULL, admin_id INTEGER NOT NULL, admin_nombre TEXT NOT NULL, nota TEXT NOT NULL, fecha TEXT NOT NULL)''', '''CREATE TABLE IF NOT EXISTS reporte_evidencias( id INTEGER PRIMARY KEY AUTOINCREMENT, reporte_id INTEGER NOT NULL, admin_id INTEGER NOT NULL, pie_imagen TEXT NOT NULL, foto_path TEXT, fecha TEXT NOT NULL)''', '''CREATE TABLE IF NOT EXISTS reporte_chat( id INTEGER PRIMARY KEY AUTOINCREMENT, reporte_id INTEGER NOT NULL, user_id INTEGER NOT NULL, rol TEXT NOT NULL, mensaje TEXT NOT NULL, fecha TEXT NOT NULL, leido INTEGER DEFAULT 0)''', // Tabla route_definitions '''CREATE TABLE IF NOT EXISTS 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)''', ]; for (final sql in sqls) { try { await db.execute(sql); } catch (_) {} } } // ── USERS ──────────────────────────────────────────────────────────────── static Future insertUser(UserModel u) async => (await database).insert('users', u.toMap(), conflictAlgorithm: ConflictAlgorithm.abort); static Future 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 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> 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 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> 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 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 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 deleteDomicilio(int id) async => (await database).delete('domicilios', where:'id=?', whereArgs:[id]); static Future> 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 insertRouteDefinition(RouteDefinitionModel r) async => (await database).insert('route_definitions', r.toMap(), conflictAlgorithm: ConflictAlgorithm.replace); static Future> getAllRouteDefinitions() async { final res = await (await database).query('route_definitions', orderBy:'route_id ASC'); return res.map((m) => RouteDefinitionModel.fromMap(m)).toList(); } static Future 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 updateRouteDefinition(RouteDefinitionModel r) async => (await database).update('route_definitions', r.toMap(), where:'route_id=?', whereArgs:[r.routeId]); // ── ROUTE STATUS ───────────────────────────────────────────────────────── static Future upsertRouteStatus(RouteStatusModel s) async => (await database).insert('route_status', s.toMap(), conflictAlgorithm: ConflictAlgorithm.replace); static Future 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> getAllRouteStatuses() async { final res = await (await database).query('route_status'); return res.map((m) => RouteStatusModel.fromMap(m)).toList(); } // ── ASIGNACIONES ───────────────────────────────────────────────────────── static Future 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> 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> getAllAsignaciones() async { final res = await (await database).query('asignaciones'); return res.map((m) => AssignmentModel.fromMap(m)).toList(); } // ── ALERTAS ────────────────────────────────────────────────────────────── static Future insertAlerta(AlertaModel a) async => (await database).insert('alertas', a.toMap()); static Future> 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> 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 resolverAlerta(int id) async => (await database).update('alertas', {'resuelta':1}, where:'id=?', whereArgs:[id]); // ── REPORTES ───────────────────────────────────────────────────────────── static Future insertReporte(ReporteModel r) async => (await database).insert('reportes', r.toMap()); static Future> getAllReportes() async { final res = await (await database).query('reportes', orderBy:'fecha DESC'); return res.map((m) => ReporteModel.fromMap(m)).toList(); } static Future> 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>> 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 updateReporteEstado(int id, String estado) async => (await database).update('reportes', {'estado':estado}, where:'id=?', whereArgs:[id]); // ── REVIEWS ────────────────────────────────────────────────────────────── static Future insertReview(ReviewModel r) async => (await database).insert('reviews', r.toMap()); static Future> getAllReviews() async { final res = await (await database).query('reviews', orderBy:'fecha DESC'); return res.map((m) => ReviewModel.fromMap(m)).toList(); } static Future 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>> 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 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>> 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 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 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>> 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 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 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 updateConductor(int id, String nombre, String email) async => (await database).update('users', {'nombre':nombre,'email':email}, where:'id=?', whereArgs:[id]); // ── ESTADÍSTICAS ───────────────────────────────────────────────────────── static Future> 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>> 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>> 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>> 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'''); } // ── NOTAS DE ADMIN ─────────────────────────────────────────────────────── static Future insertReporteNota(int reporteId, int adminId, String adminNombre, String nota) async => (await database).insert('reporte_notas', { 'reporte_id': reporteId, 'admin_id': adminId, 'admin_nombre': adminNombre, 'nota': nota, 'fecha': DateTime.now().toIso8601String(), }); static Future>> getReporteNotas(int reporteId) async => (await database).query('reporte_notas', where: 'reporte_id=?', whereArgs: [reporteId], orderBy: 'fecha ASC'); // ── EVIDENCIAS DEL ADMIN ───────────────────────────────────────────────── static Future insertReporteEvidencia(int reporteId, int adminId, String pie, String? fotoPath) async => (await database).insert('reporte_evidencias', { 'reporte_id': reporteId, 'admin_id': adminId, 'pie_imagen': pie, 'foto_path': fotoPath, 'fecha': DateTime.now().toIso8601String(), }); static Future>> getReporteEvidencias(int reporteId) async => (await database).query('reporte_evidencias', where: 'reporte_id=?', whereArgs: [reporteId], orderBy: 'fecha ASC'); // ── CHAT POR REPORTE ───────────────────────────────────────────────────── static Future insertChatMsg(int reporteId, int userId, String rol, String mensaje) async => (await database).insert('reporte_chat', { 'reporte_id': reporteId, 'user_id': userId, 'rol': rol, 'mensaje': mensaje, 'fecha': DateTime.now().toIso8601String(), 'leido': 0, }); static Future>> getChatMsgs(int reporteId) async => (await database).query('reporte_chat', where: 'reporte_id=?', whereArgs: [reporteId], orderBy: 'fecha ASC'); static Future getChatUnread(int reporteId, String rolLector) async { final res = await (await database).rawQuery( "SELECT COUNT(*) as c FROM reporte_chat WHERE reporte_id=? AND rol!=? AND leido=0", [reporteId, rolLector]); return (res.first['c'] as int? ?? 0); } static Future markChatRead(int reporteId, String rolLector) async => (await database).update('reporte_chat', {'leido': 1}, where: 'reporte_id=? AND rol!=?', whereArgs: [reporteId, rolLector]); }