Agenda google, avisos globais, feriados + avisos globais, templates de email, configuracoes empresa, preview empresa.

This commit is contained in:
Leonardo
2026-03-18 15:47:37 -03:00
parent d6d2fe29d1
commit 29ed349cf2
21 changed files with 5366 additions and 41 deletions
+225
View File
@@ -0,0 +1,225 @@
// src/stores/noticeStore.js
// Store Pinia — lógica de prioridade, filtragem, dismiss e regras de exibição
import { defineStore } from 'pinia'
import {
fetchActiveNotices,
loadUserDismissals,
saveDismissal,
trackView,
trackClick as svcTrackClick
} from '@/features/notices/noticeService'
// ── Storage helpers ────────────────────────────────────────────
function dismissKey (id, version) {
return `notice_dismissed_${id}_v${version}`
}
function viewKey (id) {
return `notice_views_${id}`
}
function lastSeenKey (id) {
return `notice_last_seen_${id}`
}
function isDismissedLocally (id, version, scope) {
const store = scope === 'session' ? sessionStorage : localStorage
return store.getItem(dismissKey(id, version)) === '1'
}
function setDismissedLocally (id, version, scope) {
const store = scope === 'session' ? sessionStorage : localStorage
store.setItem(dismissKey(id, version), '1')
}
function getViewCount (id) {
return parseInt(localStorage.getItem(viewKey(id)) || '0', 10)
}
function incrementViewCount (id) {
const count = getViewCount(id) + 1
localStorage.setItem(viewKey(id), String(count))
return count
}
function getLastSeen (id) {
const v = localStorage.getItem(lastSeenKey(id))
return v ? new Date(v) : null
}
function setLastSeen (id) {
localStorage.setItem(lastSeenKey(id), new Date().toISOString())
}
// ── Contexto da rota → string de contexto ─────────────────────
export function routeToContext (path = '') {
if (path.startsWith('/saas')) return 'saas'
if (path.startsWith('/admin')) return 'clinic'
if (path.startsWith('/therapist')) return 'therapist'
if (path.startsWith('/supervisor')) return 'supervisor'
if (path.startsWith('/editor')) return 'editor'
if (path.startsWith('/portal')) return 'portal'
return 'public'
}
// ── Store ──────────────────────────────────────────────────────
export const useNoticeStore = defineStore('noticeStore', {
state: () => ({
notices: [], // todos os notices ativos buscados
activeNotice: null, // o notice sendo exibido agora
userDismissals: [], // dismissals do banco (scope = 'user')
loading: false,
lastFetch: null, // timestamp da última busca
currentRole: null, // role do usuário atual
currentContext: null, // contexto da rota atual
}),
actions: {
// ── Inicialização ──────────────────────────────────────────
async init (role, routePath) {
this.currentRole = role || null
this.currentContext = routeToContext(routePath)
// Não rebusca se buscou há menos de 5 minutos
const CACHE_MS = 5 * 60 * 1000
if (this.lastFetch && Date.now() - this.lastFetch < CACHE_MS) {
this._recalcActive()
return
}
await this._fetchAndApply()
},
async _fetchAndApply () {
this.loading = true
try {
const [notices, dismissals] = await Promise.all([
fetchActiveNotices(),
loadUserDismissals()
])
this.notices = notices
this.userDismissals = dismissals
this.lastFetch = Date.now()
this._recalcActive()
} catch (e) {
console.warn('[NoticeStore] falha ao buscar avisos:', e?.message || e)
} finally {
this.loading = false
}
},
// ── Filtragem + prioridade ─────────────────────────────────
_recalcActive () {
const role = this.currentRole
const context = this.currentContext
const candidates = this.notices.filter(n => {
// 1. Segmentação por role (array vazio = todos)
if (n.roles?.length && role && !n.roles.includes(role)) return false
// 2. Segmentação por context (array vazio = todos)
if (n.contexts?.length && context && !n.contexts.includes(context)) return false
// 3. Dismiss check
if (this._isDismissed(n)) return false
// 4. show_once
if (n.show_once && getViewCount(n.id) > 0) return false
// 5. max_views
if (n.max_views != null && getViewCount(n.id) >= n.max_views) return false
// 6. cooldown
if (n.cooldown_minutes) {
const last = getLastSeen(n.id)
if (last) {
const diffMin = (Date.now() - last.getTime()) / 60_000
if (diffMin < n.cooldown_minutes) return false
}
}
return true
})
// Ordena por priority desc (já vem ordenado do server, mas garante)
candidates.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
this.activeNotice = candidates[0] || null
},
_isDismissed (notice) {
const { id, version, dismiss_scope: scope } = notice
if (scope === 'user') {
const entry = this.userDismissals.find(d => d.notice_id === id)
return !!entry && entry.version >= version
}
return isDismissedLocally(id, version, scope || 'device')
},
// ── Dismiss ────────────────────────────────────────────────
async dismiss (notice) {
if (!notice?.dismissible) return
const { id, version, dismiss_scope: scope, persist_dismiss: persist } = notice
if (scope === 'user' && persist) {
await saveDismissal(id, version)
this.userDismissals = [
...this.userDismissals.filter(d => d.notice_id !== id),
{ notice_id: id, version }
]
} else if (persist) {
setDismissedLocally(id, version, scope || 'device')
} else {
// sem persistência: usa session como temporário
setDismissedLocally(id, version, 'session')
}
this._recalcActive()
},
// ── Tracking ───────────────────────────────────────────────
onView (notice) {
if (!notice?.id) return
incrementViewCount(notice.id)
setLastSeen(notice.id)
trackView(notice.id)
},
async onCtaClick (notice) {
if (!notice?.id) return
await svcTrackClick(notice.id)
},
// ── Atualiza contexto quando rota muda ─────────────────────
updateContext (routePath, role) {
const newCtx = routeToContext(routePath)
const newRole = role || this.currentRole
if (newCtx !== this.currentContext || newRole !== this.currentRole) {
this.currentContext = newCtx
this.currentRole = newRole
this._recalcActive()
}
},
// ── Força re-busca ─────────────────────────────────────────
async refresh () {
this.lastFetch = null
await this._fetchAndApply()
}
}
})