feat: Add major features - Mejoras 5-10

- Mejora 5: Órdenes de Compra integration in obra detail
- Mejora 6: Portal de Cliente with JWT auth for clients
- Mejora 7: Diagrama de Gantt for project visualization
- Mejora 8: Push Notifications with service worker
- Mejora 9: Activity Log system with templates
- Mejora 10: PWA support with offline capabilities

New features include:
- Fotos gallery with upload/delete
- Bitácora de obra with daily logs
- PDF export for reports, gastos, presupuestos
- Control de asistencia for employees
- Client portal with granular permissions
- Gantt chart with task visualization
- Push notification system
- Activity timeline component
- PWA manifest, icons, and install prompt

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mexus
2026-01-19 03:09:38 +00:00
parent 86bfbd2039
commit a08e7057e8
69 changed files with 12435 additions and 26 deletions

183
public/sw.js Normal file
View File

@@ -0,0 +1,183 @@
// Service Worker para PWA y Notificaciones Push
// Mexus App - Construction Management System
const CACHE_NAME = 'mexus-app-v1';
const STATIC_CACHE = 'mexus-static-v1';
const DYNAMIC_CACHE = 'mexus-dynamic-v1';
// Recursos estáticos para cachear
const STATIC_ASSETS = [
'/',
'/manifest.json',
'/icons/icon-192x192.png',
'/icons/icon-512x512.png',
'/apple-touch-icon.png',
'/favicon.png',
];
// Instalación del Service Worker
self.addEventListener('install', (event) => {
console.log('Service Worker instalado');
event.waitUntil(
caches.open(STATIC_CACHE)
.then((cache) => {
console.log('Cacheando recursos estáticos');
return cache.addAll(STATIC_ASSETS);
})
.then(() => self.skipWaiting())
);
});
// Activación del Service Worker
self.addEventListener('activate', (event) => {
console.log('Service Worker activado');
event.waitUntil(
Promise.all([
// Limpiar caches antiguos
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== STATIC_CACHE && name !== DYNAMIC_CACHE)
.map((name) => caches.delete(name))
);
}),
clients.claim(),
])
);
});
// Estrategia de fetch: Network first, fallback to cache
self.addEventListener('fetch', (event) => {
// Skip non-GET requests
if (event.request.method !== 'GET') return;
// Skip API requests (siempre online para datos frescos)
if (event.request.url.includes('/api/')) return;
// Skip chrome-extension and other non-http(s) requests
if (!event.request.url.startsWith('http')) return;
event.respondWith(
fetch(event.request)
.then((response) => {
// Clone response para guardarlo en cache
const responseClone = response.clone();
// Solo cachear respuestas exitosas
if (response.status === 200) {
caches.open(DYNAMIC_CACHE).then((cache) => {
cache.put(event.request, responseClone);
});
}
return response;
})
.catch(() => {
// Si falla la red, buscar en cache
return caches.match(event.request).then((response) => {
if (response) {
return response;
}
// Si es una página, mostrar página offline
if (event.request.headers.get('accept')?.includes('text/html')) {
return caches.match('/');
}
return new Response('Offline', { status: 503 });
});
})
);
});
// Recibir notificaciones push
self.addEventListener('push', (event) => {
console.log('Push recibido:', event);
let data = {
title: 'Mexus App',
body: 'Nueva notificación',
icon: '/icon-192x192.png',
badge: '/badge-72x72.png',
url: '/',
};
try {
if (event.data) {
data = { ...data, ...event.data.json() };
}
} catch (e) {
console.error('Error parsing push data:', e);
}
const options = {
body: data.body,
icon: data.icon || '/icon-192x192.png',
badge: data.badge || '/badge-72x72.png',
vibrate: [100, 50, 100],
data: {
url: data.url || '/',
dateOfArrival: Date.now(),
},
actions: [
{
action: 'open',
title: 'Abrir',
icon: '/icons/checkmark.png',
},
{
action: 'close',
title: 'Cerrar',
icon: '/icons/xmark.png',
},
],
tag: data.tag || 'default',
renotify: true,
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
// Clic en notificación
self.addEventListener('notificationclick', (event) => {
console.log('Notificación clickeada:', event);
event.notification.close();
if (event.action === 'close') {
return;
}
const url = event.notification.data?.url || '/';
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true })
.then((clientList) => {
// Si ya hay una ventana abierta, enfocarla y navegar
for (const client of clientList) {
if (client.url.includes(self.location.origin) && 'focus' in client) {
client.focus();
return client.navigate(url);
}
}
// Si no hay ventana, abrir una nueva
if (clients.openWindow) {
return clients.openWindow(url);
}
})
);
});
// Cerrar notificación
self.addEventListener('notificationclose', (event) => {
console.log('Notificación cerrada:', event);
});
// Sincronización en background (para futuras mejoras)
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-notifications') {
console.log('Sync de notificaciones');
}
});