This commit is contained in:
2026-02-22 14:38:53 -06:00
parent bec656b105
commit a73de4a4fa
47 changed files with 4290 additions and 3 deletions

View File

@@ -0,0 +1,230 @@
/**
* IndexedDB Wrapper for Offline Colaboraciones
* Stores pending colaboraciones when offline using GUID-based IDs
*/
const ColaboracionesOfflineDB = {
dbName: 'ColaboracionesOfflineDB',
version: 1,
storeName: 'colaboraciones',
/**
* Initialize the database
*/
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create object store if it doesn't exist
if (!db.objectStoreNames.contains(this.storeName)) {
const objectStore = db.createObjectStore(this.storeName, {
keyPath: 'id' // GUID generated client-side
});
// Indexes for querying
objectStore.createIndex('syncStatus', 'syncStatus', { unique: false });
objectStore.createIndex('timestamp', 'timestamp', { unique: false });
objectStore.createIndex('updatedAt', 'updatedAt', { unique: false });
objectStore.createIndex('miembroId', 'miembroId', { unique: false });
}
};
});
},
/**
* Generate a GUID (v4 UUID)
*/
generateGuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
},
/**
* Add a new colaboracion to offline queue
*/
async addColaboracion(colaboracionData) {
const db = await this.init();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
// Prepare record with GUID and sync metadata
const record = {
id: this.generateGuid(), // GUID generated client-side
miembroId: colaboracionData.miembroId,
mesInicial: colaboracionData.mesInicial,
anioInicial: colaboracionData.anioInicial,
mesFinal: colaboracionData.mesFinal,
anioFinal: colaboracionData.anioFinal,
montoTotal: colaboracionData.montoTotal,
observaciones: colaboracionData.observaciones || '',
tiposSeleccionados: colaboracionData.tiposSeleccionados || [],
tipoPrioritario: colaboracionData.tipoPrioritario || null,
registradoPor: colaboracionData.registradoPor || 'Usuario',
syncStatus: 'pending', // pending, syncing, synced, failed
timestamp: new Date().toISOString(),
updatedAt: new Date().toISOString(),
retryCount: 0
};
const request = store.add(record);
request.onsuccess = () => {
console.log('[OfflineDB] Colaboración guardada con ID:', record.id);
resolve(record);
};
request.onerror = () => {
console.error('[OfflineDB] Error al guardar:', request.error);
reject(request.error);
};
});
},
/**
* Get all pending colaboraciones
*/
async getPending() {
const db = await this.init();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const index = store.index('syncStatus');
const request = index.getAll('pending');
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
},
/**
* Get all colaboraciones (any status)
*/
async getAll() {
const db = await this.init();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
},
/**
* Update sync status of a colaboracion
*/
async updateSyncStatus(id, status, retryCount = 0) {
const db = await this.init();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const getRequest = store.get(id);
getRequest.onsuccess = () => {
const record = getRequest.result;
if (record) {
record.syncStatus = status;
record.retryCount = retryCount;
record.lastSyncAttempt = new Date().toISOString();
const updateRequest = store.put(record);
updateRequest.onsuccess = () => resolve(record);
updateRequest.onerror = () => reject(updateRequest.error);
} else {
reject(new Error('Record not found'));
}
};
getRequest.onerror = () => reject(getRequest.error);
});
},
/**
* Remove a colaboracion by ID (after successful sync)
*/
async remove(id) {
const db = await this.init();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.delete(id);
request.onsuccess = () => {
console.log('[OfflineDB] Colaboración eliminada:', id);
resolve();
};
request.onerror = () => reject(request.error);
});
},
/**
* Get count of pending colaboraciones
*/
async getPendingCount() {
const db = await this.init();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const index = store.index('syncStatus');
const request = index.count('pending');
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
},
/**
* Clear all records (use with caution)
*/
async clearAll() {
const db = await this.init();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.clear();
request.onsuccess = () => {
console.log('[OfflineDB] All records cleared');
resolve();
};
request.onerror = () => reject(request.error);
});
},
/**
* Get a specific colaboracion by ID
*/
async getById(id) {
const db = await this.init();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
};
// Initialize database when script loads
ColaboracionesOfflineDB.init().catch(error => {
console.error('[OfflineDB] Initialization failed:', error);
});

View File

@@ -0,0 +1,310 @@
/**
* Sync Manager for Colaboraciones
* Handles connection monitoring, offline queue, and synchronization
*/
const ColaboracionesSyncManager = {
isOnline: navigator.onLine,
isSyncing: false,
syncInProgress: false,
statusIndicator: null,
pendingBadge: null,
maxRetries: 3,
retryDelay: 2000, // milliseconds
/**
* Initialize the sync manager
*/
init() {
console.log('[SyncManager] Initializing...');
// Get UI elements
this.statusIndicator = document.getElementById('offlineStatus');
this.pendingBadge = document.getElementById('pendingBadge');
// Listen for online/offline events
window.addEventListener('online', () => this.handleOnline());
window.addEventListener('offline', () => this.handleOffline());
// Set initial status
this.updateStatusUI();
this.updatePendingBadge();
// If online, try to sync pending items
if (this.isOnline) {
setTimeout(() => this.syncPending(), 1000);
}
console.log('[SyncManager] Initialized. Status:', this.isOnline ? 'Online' : 'Offline');
},
/**
* Handle online event
*/
async handleOnline() {
console.log('[SyncManager] Connection restored');
this.isOnline = true;
this.updateStatusUI();
// Auto-sync after short delay
setTimeout(() => this.syncPending(), 500);
},
/**
* Handle offline event
*/
handleOffline() {
console.log('[SyncManager] Connection lost');
this.isOnline = false;
this.updateStatusUI();
},
/**
* Update status indicator UI
*/
updateStatusUI() {
if (!this.statusIndicator) return;
if (this.isSyncing) {
this.statusIndicator.className = 'badge bg-warning ms-2';
this.statusIndicator.innerHTML = '<i class="bi bi-arrow-repeat"></i> Sincronizando';
this.statusIndicator.style.display = 'inline-block';
} else if (this.isOnline) {
this.statusIndicator.className = 'badge bg-success ms-2';
this.statusIndicator.innerHTML = '<i class="bi bi-wifi"></i> En línea';
this.statusIndicator.style.display = 'inline-block';
} else {
this.statusIndicator.className = 'badge bg-secondary ms-2';
this.statusIndicator.innerHTML = '<i class="bi bi-wifi-off"></i> Sin conexión';
this.statusIndicator.style.display = 'inline-block';
}
},
/**
* Update pending items badge
*/
async updatePendingBadge() {
if (!this.pendingBadge) return;
try {
const count = await ColaboracionesOfflineDB.getPendingCount();
if (count > 0) {
this.pendingBadge.textContent = count;
this.pendingBadge.style.display = 'inline-block';
} else {
this.pendingBadge.style.display = 'none';
}
} catch (error) {
console.error('[SyncManager] Error updating badge:', error);
}
},
/**
* Save colaboracion (online or offline)
*/
async saveColaboracion(colaboracionData) {
if (this.isOnline) {
try {
// Try to save directly to server
const result = await this.sendToServer(colaboracionData);
if (result.success) {
return {
success: true,
message: 'Colaboración registrada exitosamente',
online: true
};
} else {
throw new Error(result.message || 'Error al guardar');
}
} catch (error) {
console.warn('[SyncManager] Online save failed, using offline mode:', error);
// Fall back to offline save
return await this.saveOffline(colaboracionData);
}
} else {
// Save offline
return await this.saveOffline(colaboracionData);
}
},
/**
* Save to offline queue
*/
async saveOffline(colaboracionData) {
try {
const record = await ColaboracionesOfflineDB.addColaboracion(colaboracionData);
await this.updatePendingBadge();
return {
success: true,
offline: true,
message: 'Guardado offline. Se sincronizará automáticamente cuando haya conexión.',
id: record.id
};
} catch (error) {
console.error('[SyncManager] Offline save failed:', error);
return {
success: false,
message: 'Error al guardar en modo offline: ' + error.message
};
}
},
/**
* Send colaboracion to server
*/
async sendToServer(colaboracionData) {
const formData = new FormData();
formData.append('MiembroId', colaboracionData.miembroId);
formData.append('MesInicial', colaboracionData.mesInicial);
formData.append('AnioInicial', colaboracionData.anioInicial);
formData.append('MesFinal', colaboracionData.mesFinal);
formData.append('AnioFinal', colaboracionData.anioFinal);
formData.append('MontoTotal', colaboracionData.montoTotal);
formData.append('Observaciones', colaboracionData.observaciones || '');
if (colaboracionData.tipoPrioritario) {
formData.append('TipoPrioritario', colaboracionData.tipoPrioritario);
}
if (colaboracionData.tiposSeleccionados && colaboracionData.tiposSeleccionados.length > 0) {
colaboracionData.tiposSeleccionados.forEach(tipo => {
formData.append('TiposSeleccionados', tipo);
});
}
const response = await fetch('/Colaboracion/Create', {
method: 'POST',
body: formData
});
if (response.redirected) {
// Success - ASP.NET redirected to Index
return { success: true };
}
const text = await response.text();
// Check if response contains success indicators
if (text.includes('exitosamente') || response.ok) {
return { success: true };
}
throw new Error('Error en la respuesta del servidor');
},
/**
* Synchronize all pending colaboraciones
*/
async syncPending() {
if (!this.isOnline || this.syncInProgress) {
console.log('[SyncManager] Sync skipped. Online:', this.isOnline, 'InProgress:', this.syncInProgress);
return;
}
try {
this.syncInProgress = true;
this.isSyncing = true;
this.updateStatusUI();
const pending = await ColaboracionesOfflineDB.getPending();
if (pending.length === 0) {
console.log('[SyncManager] No pending items to sync');
return;
}
console.log(`[SyncManager] Syncing ${pending.length} pending item(s)...`);
let successCount = 0;
let failCount = 0;
for (const item of pending) {
try {
// Update status to syncing
await ColaboracionesOfflineDB.updateSyncStatus(item.id, 'syncing', item.retryCount);
// Try to send to server
const result = await this.sendToServer(item);
if (result.success) {
// Remove from offline DB
await ColaboracionesOfflineDB.remove(item.id);
successCount++;
console.log(`[SyncManager] Item ${item.id} synced successfully`);
} else {
throw new Error(result.message || 'Unknown error');
}
} catch (error) {
console.error(`[SyncManager] Sync failed for item ${item.id}:`, error);
// Update retry count
const newRetryCount = (item.retryCount || 0) + 1;
if (newRetryCount >= this.maxRetries) {
await ColaboracionesOfflineDB.updateSyncStatus(item.id, 'failed', newRetryCount);
failCount++;
} else {
await ColaboracionesOfflineDB.updateSyncStatus(item.id, 'pending', newRetryCount);
}
}
}
await this.updatePendingBadge();
// Show results
if (successCount > 0) {
toastr.success(`${successCount} colaboración(es) sincronizada(s) exitosamente`);
// Reload page to show updated data
setTimeout(() => {
window.location.reload();
}, 1500);
}
if (failCount > 0) {
toastr.error(`${failCount} colaboración(es) no se pudieron sincronizar. Se reintentará automáticamente.`);
}
} catch (error) {
console.error('[SyncManager] Sync error:', error);
toastr.error('Error durante la sincronización');
} finally {
this.syncInProgress = false;
this.isSyncing = false;
this.updateStatusUI();
}
},
/**
* Manually trigger sync
*/
async manualSync() {
if (!this.isOnline) {
toastr.warning('No hay conexión a internet');
return;
}
toastr.info('Iniciando sincronización...');
await this.syncPending();
},
/**
* Check if currently online
*/
checkOnlineStatus() {
return this.isOnline;
}
};
// Auto-initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
ColaboracionesSyncManager.init();
});
} else {
ColaboracionesSyncManager.init();
}