Agenda google, avisos globais, feriados + avisos globais, templates de email, configuracoes empresa, preview empresa.
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user