cinu
This commit is contained in:
230
RS_system/wwwroot/js/colaboraciones-offline-db.js
Normal file
230
RS_system/wwwroot/js/colaboraciones-offline-db.js
Normal 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);
|
||||
});
|
||||
310
RS_system/wwwroot/js/colaboraciones-sync.js
Normal file
310
RS_system/wwwroot/js/colaboraciones-sync.js
Normal 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();
|
||||
}
|
||||
241
RS_system/wwwroot/service-worker.js
Normal file
241
RS_system/wwwroot/service-worker.js
Normal file
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Service Worker for RS_system PWA
|
||||
* Implements offline-first architecture with strategic caching
|
||||
* Version: 1.0.0
|
||||
*/
|
||||
|
||||
const CACHE_VERSION = 'rs-system-v1.0.0';
|
||||
const STATIC_CACHE = `${CACHE_VERSION}-static`;
|
||||
const DYNAMIC_CACHE = `${CACHE_VERSION}-dynamic`;
|
||||
const API_CACHE = `${CACHE_VERSION}-api`;
|
||||
|
||||
// Critical resources to cache on install
|
||||
const STATIC_ASSETS = [
|
||||
'/',
|
||||
'/Home/Index',
|
||||
'/Colaboracion/Create',
|
||||
'/Colaboracion/Index',
|
||||
'/css/site.css',
|
||||
'/css/bootstrap.min.css',
|
||||
'/css/bootstrap-icons.min.css',
|
||||
'/js/site.js',
|
||||
'/js/colaboraciones-offline-db.js',
|
||||
'/js/colaboraciones-sync.js',
|
||||
'/lib/jquery/dist/jquery.min.js',
|
||||
'/lib/bootstrap/dist/js/bootstrap.bundle.min.js',
|
||||
'/manifest.json',
|
||||
'/Assets/icon-192x192.png',
|
||||
'/Assets/icon-512x512.png'
|
||||
];
|
||||
|
||||
// Install event - cache static assets
|
||||
self.addEventListener('install', (event) => {
|
||||
console.log('[Service Worker] Installing...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.open(STATIC_CACHE)
|
||||
.then((cache) => {
|
||||
console.log('[Service Worker] Caching static assets');
|
||||
return cache.addAll(STATIC_ASSETS);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('[Service Worker] Installation complete');
|
||||
return self.skipWaiting(); // Activate immediately
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[Service Worker] Installation failed:', error);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Activate event - clean up old caches
|
||||
self.addEventListener('activate', (event) => {
|
||||
console.log('[Service Worker] Activating...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames
|
||||
.filter((name) => {
|
||||
// Delete old version caches
|
||||
return name.startsWith('rs-system-') && name !== STATIC_CACHE && name !== DYNAMIC_CACHE && name !== API_CACHE;
|
||||
})
|
||||
.map((name) => {
|
||||
console.log('[Service Worker] Deleting old cache:', name);
|
||||
return caches.delete(name);
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('[Service Worker] Activation complete');
|
||||
return self.clients.claim(); // Take control immediately
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Fetch event - implement caching strategies
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Skip chrome extension and non-HTTP requests
|
||||
if (!url.protocol.startsWith('http')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// API requests - Network First, fallback to offline indicator
|
||||
if (url.pathname.includes('/api/') ||
|
||||
url.pathname.includes('/Colaboracion/Sync') ||
|
||||
url.pathname.includes('/Colaboracion/BuscarMiembros') ||
|
||||
url.pathname.includes('/Colaboracion/ObtenerUltimosPagos')) {
|
||||
|
||||
event.respondWith(networkFirstStrategy(request, API_CACHE));
|
||||
return;
|
||||
}
|
||||
|
||||
// POST requests - Network Only (never cache)
|
||||
if (request.method === 'POST') {
|
||||
event.respondWith(
|
||||
fetch(request).catch(() => {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
offline: true,
|
||||
message: 'Sin conexión. Por favor intente más tarde.'
|
||||
}),
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 503
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Static assets - Cache First, fallback to Network
|
||||
if (isStaticAsset(url.pathname)) {
|
||||
event.respondWith(cacheFirstStrategy(request, STATIC_CACHE));
|
||||
return;
|
||||
}
|
||||
|
||||
// Dynamic content (HTML pages) - Network First, fallback to Cache
|
||||
event.respondWith(networkFirstStrategy(request, DYNAMIC_CACHE));
|
||||
});
|
||||
|
||||
/**
|
||||
* Cache First Strategy
|
||||
* Try cache first, fallback to network, then cache the response
|
||||
*/
|
||||
function cacheFirstStrategy(request, cacheName) {
|
||||
return caches.match(request)
|
||||
.then((cachedResponse) => {
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
return fetch(request)
|
||||
.then((networkResponse) => {
|
||||
// Clone the response
|
||||
const responseToCache = networkResponse.clone();
|
||||
|
||||
caches.open(cacheName)
|
||||
.then((cache) => {
|
||||
cache.put(request, responseToCache);
|
||||
});
|
||||
|
||||
return networkResponse;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[Service Worker] Fetch failed:', error);
|
||||
// Return offline page if available
|
||||
return caches.match('/offline.html') || new Response('Offline');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Network First Strategy
|
||||
* Try network first, fallback to cache
|
||||
*/
|
||||
function networkFirstStrategy(request, cacheName) {
|
||||
return fetch(request)
|
||||
.then((networkResponse) => {
|
||||
// Clone and cache the response
|
||||
const responseToCache = networkResponse.clone();
|
||||
|
||||
caches.open(cacheName)
|
||||
.then((cache) => {
|
||||
cache.put(request, responseToCache);
|
||||
});
|
||||
|
||||
return networkResponse;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('[Service Worker] Network failed, trying cache:', error);
|
||||
|
||||
return caches.match(request)
|
||||
.then((cachedResponse) => {
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// If API request and no cache, return offline indicator
|
||||
if (request.url.includes('/api/') || request.url.includes('/Colaboracion/')) {
|
||||
return new Response(
|
||||
JSON.stringify({ offline: true }),
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 503
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if request is for a static asset
|
||||
*/
|
||||
function isStaticAsset(pathname) {
|
||||
const staticExtensions = ['.css', '.js', '.jpg', '.jpeg', '.png', '.gif', '.svg', '.woff', '.woff2', '.ttf', '.eot', '.ico'];
|
||||
return staticExtensions.some(ext => pathname.endsWith(ext));
|
||||
}
|
||||
|
||||
// Background Sync for future enhancement
|
||||
self.addEventListener('sync', (event) => {
|
||||
console.log('[Service Worker] Background sync triggered:', event.tag);
|
||||
|
||||
if (event.tag === 'sync-colaboraciones') {
|
||||
event.waitUntil(
|
||||
// This will be handled by colaboraciones-sync.js
|
||||
self.registration.showNotification('Sincronización completada', {
|
||||
body: 'Las colaboraciones offline se han sincronizado exitosamente.',
|
||||
icon: '/Assets/icon-192x192.png',
|
||||
badge: '/Assets/icon-192x192.png'
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Message handler for cache updates
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
|
||||
if (event.data && event.data.type === 'CLEAR_CACHE') {
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => caches.delete(cacheName))
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[Service Worker] Loaded and ready');
|
||||
Reference in New Issue
Block a user