feat(mvp-rep): formas de pagamento do ERP + suporte offline completo
Formas de pagamento: - Endpoint GET /catalog/payment-methods lendo vw_formas_pagamento filtrado por ativa=1 e integrar_sar=1 - FormaPagamento schema/type no shared api-interface - Hook useFormasPagamento (staleTime 1h) substituindo lista hardcoded Offline (FR-4.2 / NFR-2.1–2.4): - IndexedDB queue: lib/offline/idb.ts + order-queue.ts sem deps externos - NewOrderPage detecta !navigator.onLine → enqueueOrder() → toast + reset - useOfflineSync: auto-sync ao reconectar (POST orders + PATCH transmit) - usePendingOrders: fila reativa via CustomEvents - AppShell: banner offline + useOfflineSync() global - OrdersPage: seção de pedidos pendentes com retry/descartar - sw.js: network-first para API GETs cacheáveis + stale-while-revalidate para assets + app shell navigate fallback Docs: - architecture.md: documento de decisões de arquitetura do SAR MVP Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,106 @@
|
||||
// Service Worker SAR — C6 Web Push
|
||||
// Recebe push events e exibe notificação nativa. Clique abre a URL do payload.
|
||||
// Service Worker SAR
|
||||
// C4/NFR-2: cache de API para uso offline (network-first, fallback to cache)
|
||||
// C6: Web Push
|
||||
// App shell: stale-while-revalidate para assets estáticos
|
||||
|
||||
const API_CACHE = 'sar-api-v2';
|
||||
const SHELL_CACHE = 'sar-shell-v2';
|
||||
|
||||
// Paths de API que valem ser cacheados para offline
|
||||
// Auth e mutations (POST/PATCH) nunca são interceptados
|
||||
const CACHEABLE_API = [
|
||||
'/api/v1/clients',
|
||||
'/api/v1/catalog',
|
||||
'/api/v1/orders',
|
||||
'/api/v1/dashboard',
|
||||
'/api/v1/auth/me',
|
||||
];
|
||||
|
||||
// ── Fetch ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const { request } = event;
|
||||
if (request.method !== 'GET') return;
|
||||
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (url.pathname.startsWith('/api/v1/')) {
|
||||
const cacheable = CACHEABLE_API.some((p) => url.pathname.startsWith(p));
|
||||
if (cacheable) {
|
||||
event.respondWith(networkFirst(request, API_CACHE));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.mode === 'navigate') {
|
||||
// App shell HTML — network first, cache fallback
|
||||
event.respondWith(shellNavigate(request));
|
||||
return;
|
||||
}
|
||||
|
||||
// Assets estáticos (JS/CSS/fontes/imagens) — stale-while-revalidate
|
||||
if (/\.(js|css|woff2?|png|svg|ico)$/.test(url.pathname)) {
|
||||
event.respondWith(staleWhileRevalidate(request, SHELL_CACHE));
|
||||
}
|
||||
});
|
||||
|
||||
// Network-first: tenta rede, cai no cache se offline
|
||||
async function networkFirst(request, cacheName) {
|
||||
const cache = await caches.open(cacheName);
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
if (response.ok) {
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
return response;
|
||||
} catch {
|
||||
const cached = await cache.match(request);
|
||||
if (cached) return cached;
|
||||
return offlineResponse();
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate: network first; fallback para a raiz cacheada
|
||||
async function shellNavigate(request) {
|
||||
const cache = await caches.open(SHELL_CACHE);
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
if (response.ok) {
|
||||
cache.put(request, response.clone());
|
||||
// Sempre armazena a raiz como fallback universal
|
||||
cache.put(new Request('/'), response.clone());
|
||||
}
|
||||
return response;
|
||||
} catch {
|
||||
const cached = (await cache.match(request)) ?? (await cache.match('/'));
|
||||
if (cached) return cached;
|
||||
return offlineResponse();
|
||||
}
|
||||
}
|
||||
|
||||
// Stale-while-revalidate: responde do cache, atualiza em background
|
||||
async function staleWhileRevalidate(request, cacheName) {
|
||||
const cache = await caches.open(cacheName);
|
||||
const cached = await cache.match(request);
|
||||
const networkFetch = fetch(request).then((response) => {
|
||||
if (response.ok) cache.put(request, response.clone());
|
||||
return response;
|
||||
});
|
||||
return cached ?? networkFetch;
|
||||
}
|
||||
|
||||
function offlineResponse() {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
type: 'sar:offline',
|
||||
title: 'Sem conexão',
|
||||
status: 503,
|
||||
}),
|
||||
{ status: 503, headers: { 'Content-Type': 'application/json' } },
|
||||
);
|
||||
}
|
||||
|
||||
// ── Push (C6) ─────────────────────────────────────────────────────────────────
|
||||
|
||||
self.addEventListener('push', (event) => {
|
||||
const data = event.data?.json() ?? {};
|
||||
|
||||
Reference in New Issue
Block a user