- Updated docs and inventory files - Added new architecture docs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
312 lines
8.6 KiB
TypeScript
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();
|