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: 1, onCreate: _onCreate); } static Future _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', calle TEXT NOT NULL, colonia TEXT NOT NULL, 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, 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)'''); // 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)'''); // RESEÑAS del servicio 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')'''); // Seed: admin y conductor 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'}); } // ── 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; // 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}); } 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 { // 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%'", whereArgs:[userId, routeId]); return res.isNotEmpty; } // Promedio por colonia para el admin 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'''); } }