/** * Offline Storage Service * ERP Transportistas * Sprint S8 - TASK-007 * * SQLite-based local storage for offline operations. */ import * as SQLite from 'expo-sqlite'; import { TipoOperacionOffline, PrioridadSync, type OperacionPendiente } from '../types'; const DB_NAME = 'erp_transportistas_offline.db'; class OfflineStorageService { private db: SQLite.SQLiteDatabase | null = null; async initialize(): Promise { this.db = await SQLite.openDatabaseAsync(DB_NAME); // Create tables await this.db.execAsync(` CREATE TABLE IF NOT EXISTS operaciones_pendientes ( id TEXT PRIMARY KEY, tipo TEXT NOT NULL, prioridad INTEGER NOT NULL DEFAULT 1, payload TEXT NOT NULL, intentos INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS posiciones_gps ( id INTEGER PRIMARY KEY AUTOINCREMENT, latitud REAL NOT NULL, longitud REAL NOT NULL, velocidad REAL, rumbo REAL, precision REAL, timestamp TEXT NOT NULL, sincronizado INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS viajes_cache ( id TEXT PRIMARY KEY, data TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS auth_cache ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_operaciones_prioridad ON operaciones_pendientes(prioridad, created_at); CREATE INDEX IF NOT EXISTS idx_posiciones_sincronizado ON posiciones_gps(sincronizado); `); } // ==================== Operaciones Pendientes ==================== async encolarOperacion( tipo: TipoOperacionOffline, payload: object, prioridad: PrioridadSync = PrioridadSync.MEDIA ): Promise { if (!this.db) throw new Error('Database not initialized'); const id = `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const now = new Date().toISOString(); await this.db.runAsync( `INSERT INTO operaciones_pendientes (id, tipo, prioridad, payload, intentos, created_at) VALUES (?, ?, ?, ?, 0, ?)`, [id, tipo, prioridad, JSON.stringify(payload), now] ); return id; } async obtenerOperacionesPendientes(limite: number = 50): Promise { if (!this.db) throw new Error('Database not initialized'); const rows = await this.db.getAllAsync<{ id: string; tipo: string; prioridad: number; payload: string; intentos: number; created_at: string; }>( `SELECT * FROM operaciones_pendientes ORDER BY prioridad ASC, created_at ASC LIMIT ?`, [limite] ); return rows.map((row) => ({ id: row.id, tipo: row.tipo as TipoOperacionOffline, prioridad: row.prioridad as PrioridadSync, payload: row.payload, intentos: row.intentos, createdAt: row.created_at, })); } async marcarOperacionSincronizada(id: string): Promise { if (!this.db) throw new Error('Database not initialized'); await this.db.runAsync( `DELETE FROM operaciones_pendientes WHERE id = ?`, [id] ); } async incrementarIntentos(id: string): Promise { if (!this.db) throw new Error('Database not initialized'); await this.db.runAsync( `UPDATE operaciones_pendientes SET intentos = intentos + 1 WHERE id = ?`, [id] ); } async contarOperacionesPendientes(): Promise { if (!this.db) throw new Error('Database not initialized'); const result = await this.db.getFirstAsync<{ count: number }>( `SELECT COUNT(*) as count FROM operaciones_pendientes` ); return result?.count || 0; } // ==================== Posiciones GPS ==================== async guardarPosicion( latitud: number, longitud: number, velocidad: number, rumbo: number, precision: number ): Promise { if (!this.db) throw new Error('Database not initialized'); const now = new Date().toISOString(); await this.db.runAsync( `INSERT INTO posiciones_gps (latitud, longitud, velocidad, rumbo, precision, timestamp, sincronizado) VALUES (?, ?, ?, ?, ?, ?, 0)`, [latitud, longitud, velocidad, rumbo, precision, now] ); } async obtenerPosicionesNoSincronizadas(limite: number = 100): Promise> { if (!this.db) throw new Error('Database not initialized'); const rows = await this.db.getAllAsync<{ id: number; latitud: number; longitud: number; velocidad: number; rumbo: number; precision: number; timestamp: string; }>( `SELECT * FROM posiciones_gps WHERE sincronizado = 0 ORDER BY timestamp ASC LIMIT ?`, [limite] ); return rows; } async marcarPosicionesSincronizadas(ids: number[]): Promise { if (!this.db || ids.length === 0) return; const placeholders = ids.map(() => '?').join(','); await this.db.runAsync( `UPDATE posiciones_gps SET sincronizado = 1 WHERE id IN (${placeholders})`, ids ); } async limpiarPosicionesSincronizadas(): Promise { if (!this.db) throw new Error('Database not initialized'); // Keep last 24 hours of synced positions for reference const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); await this.db.runAsync( `DELETE FROM posiciones_gps WHERE sincronizado = 1 AND timestamp < ?`, [cutoff] ); } // ==================== Viajes Cache ==================== async guardarViajeCache(viaje: object): Promise { if (!this.db) throw new Error('Database not initialized'); const viajeData = viaje as { id: string }; const now = new Date().toISOString(); await this.db.runAsync( `INSERT OR REPLACE INTO viajes_cache (id, data, updated_at) VALUES (?, ?, ?)`, [viajeData.id, JSON.stringify(viaje), now] ); } async obtenerViajeCache(id: string): Promise { if (!this.db) throw new Error('Database not initialized'); const row = await this.db.getFirstAsync<{ data: string }>( `SELECT data FROM viajes_cache WHERE id = ?`, [id] ); return row ? JSON.parse(row.data) : null; } async obtenerTodosViajesCache(): Promise { if (!this.db) throw new Error('Database not initialized'); const rows = await this.db.getAllAsync<{ data: string }>( `SELECT data FROM viajes_cache ORDER BY updated_at DESC` ); return rows.map((row) => JSON.parse(row.data)); } // ==================== Auth Cache ==================== async guardarAuthData(key: string, value: string): Promise { if (!this.db) throw new Error('Database not initialized'); await this.db.runAsync( `INSERT OR REPLACE INTO auth_cache (key, value) VALUES (?, ?)`, [key, value] ); } async obtenerAuthData(key: string): Promise { if (!this.db) throw new Error('Database not initialized'); const row = await this.db.getFirstAsync<{ value: string }>( `SELECT value FROM auth_cache WHERE key = ?`, [key] ); return row?.value || null; } async limpiarAuthData(): Promise { if (!this.db) throw new Error('Database not initialized'); await this.db.runAsync(`DELETE FROM auth_cache`); } // ==================== Utilities ==================== async limpiarTodo(): Promise { if (!this.db) throw new Error('Database not initialized'); await this.db.execAsync(` DELETE FROM operaciones_pendientes; DELETE FROM posiciones_gps; DELETE FROM viajes_cache; DELETE FROM auth_cache; `); } async getEstadisticas(): Promise<{ operacionesPendientes: number; posicionesNoSincronizadas: number; viajesCacheados: number; }> { if (!this.db) throw new Error('Database not initialized'); const ops = await this.db.getFirstAsync<{ count: number }>( `SELECT COUNT(*) as count FROM operaciones_pendientes` ); const pos = await this.db.getFirstAsync<{ count: number }>( `SELECT COUNT(*) as count FROM posiciones_gps WHERE sincronizado = 0` ); const viajes = await this.db.getFirstAsync<{ count: number }>( `SELECT COUNT(*) as count FROM viajes_cache` ); return { operacionesPendientes: ops?.count || 0, posicionesNoSincronizadas: pos?.count || 0, viajesCacheados: viajes?.count || 0, }; } } export const offlineStorage = new OfflineStorageService();