erp-transportistas-v2/mobile/src/services/OfflineStorage.ts
Adrian Flores Cortes 6ed7f9e2ec [BACKUP] Pre-restructure workspace backup 2026-01-29
- Updated docs and inventory files
- Added new architecture docs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 17:35:54 -06:00

312 lines
8.6 KiB
TypeScript

/**
* 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<void> {
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<string> {
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<OperacionPendiente[]> {
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<void> {
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<void> {
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<number> {
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<void> {
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<Array<{
id: number;
latitud: number;
longitud: number;
velocidad: number;
rumbo: number;
precision: number;
timestamp: string;
}>> {
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<void> {
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<void> {
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<void> {
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<object | null> {
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<object[]> {
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<void> {
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<string | null> {
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<void> {
if (!this.db) throw new Error('Database not initialized');
await this.db.runAsync(`DELETE FROM auth_cache`);
}
// ==================== Utilities ====================
async limpiarTodo(): Promise<void> {
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();