// Push & Notification manager — Central da Vida // Modelo: permissão de notificações + agendamento local via setTimeout. // Não depende de backend; quando integrarmos VAPID/FCM, plugamos aqui. (function () { const PREFS_KEY = 'cdv:pushPrefs:v1'; const DEFAULT_PREFS = { enabled: false, // permissão concedida e usuário aceitou leadMinutes: 10, // minutos antes do horário da tarefa quiet: { enabled: true, start: 22, end: 7 }, // janela silenciosa }; function loadPrefs() { try { const raw = localStorage.getItem(PREFS_KEY); if (!raw) return { ...DEFAULT_PREFS }; const parsed = JSON.parse(raw); return { ...DEFAULT_PREFS, ...parsed, quiet: { ...DEFAULT_PREFS.quiet, ...(parsed.quiet || {}) } }; } catch (e) { return { ...DEFAULT_PREFS }; } } function savePrefs(p) { try { localStorage.setItem(PREFS_KEY, JSON.stringify(p)); } catch (e) {} } const supported = typeof window !== 'undefined' && 'Notification' in window; function permission() { return supported ? Notification.permission : 'unsupported'; } async function requestPermission() { if (!supported) return 'unsupported'; if (Notification.permission === 'granted') return 'granted'; try { const result = await Notification.requestPermission(); return result; } catch (e) { return 'denied'; } } function isQuietHour(prefs, date = new Date()) { if (!prefs.quiet || !prefs.quiet.enabled) return false; const h = date.getHours(); const { start, end } = prefs.quiet; // Janela atravessa meia-noite (ex: 22-7)? if (start > end) return h >= start || h < end; return h >= start && h < end; } // Mostra agora (respeita modo silencioso) function showNow(title, body, options = {}) { if (!supported || Notification.permission !== 'granted') return false; const prefs = loadPrefs(); if (isQuietHour(prefs) && !options.bypassQuiet) { console.log('[push] suprimida pelo modo silencioso:', title); return false; } try { new Notification(title, { body, icon: options.icon, badge: options.badge, tag: options.tag, data: options.data, requireInteraction: !!options.requireInteraction, }); return true; } catch (e) { console.warn('[push] erro ao exibir:', e); return false; } } // Agendador local — em memória, perde ao recarregar (precisa re-agendar no boot) const timers = new Map(); // key → timeoutId function cancel(key) { if (timers.has(key)) { clearTimeout(timers.get(key)); timers.delete(key); } } function cancelAll() { for (const id of timers.values()) clearTimeout(id); timers.clear(); } // Agenda uma notificação para `when` (Date ou ISO string ou ms) function scheduleAt(key, when, title, body, options = {}) { if (!supported || Notification.permission !== 'granted') return { ok: false, reason: 'no-permission' }; const ts = typeof when === 'number' ? when : new Date(when).getTime(); const delay = ts - Date.now(); if (delay <= 0) return { ok: false, reason: 'past' }; if (delay > 24 * 60 * 60 * 1000) return { ok: false, reason: 'too-far' }; // máx 24h cancel(key); const id = setTimeout(() => { timers.delete(key); showNow(title, body, options); }, delay); timers.set(key, id); return { ok: true, in: delay }; } // Re-agenda todas as tarefas pendentes de hoje (chame ao bootar e quando state muda) function rescheduleTasks(tasks) { if (!supported || Notification.permission !== 'granted') return 0; const prefs = loadPrefs(); if (!prefs.enabled) return 0; cancelAll(); let n = 0; const today = new Date(); today.setHours(0, 0, 0, 0); const todayKey = today.toISOString().slice(0, 10); (tasks || []).forEach(t => { if (t.done) return; if (t.date !== 'hoje') return; if (!t.time || t.time === '—') return; const [hh, mm] = t.time.split(':').map(Number); if (Number.isNaN(hh)) return; const when = new Date(); when.setHours(hh, mm || 0, 0, 0); // Aplica lead time (ex: 10 min antes) const target = when.getTime() - prefs.leadMinutes * 60 * 1000; if (target <= Date.now()) return; // já passou const ok = scheduleAt( 'task:' + t.id + ':' + todayKey, target, t.title, `Daqui ${prefs.leadMinutes} min · ${t.time}`, { tag: 'task-' + t.id, data: { route: 'tasks', taskId: t.id } } ); if (ok.ok) n++; }); return n; } async function registerServiceWorker() { if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) return null; try { const reg = await navigator.serviceWorker.register('service-worker.js'); return reg; } catch (e) { console.warn('[push] SW falhou:', e); return null; } } window.Push = { supported, permission, requestPermission, loadPrefs, savePrefs, isQuietHour, showNow, scheduleAt, cancel, cancelAll, rescheduleTasks, registerServiceWorker, }; })();