389 lines
18 KiB
Dart
389 lines
18 KiB
Dart
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''');
|
|
}
|
|
}
|