Agenda, Agendador, Configurações
This commit is contained in:
354
src/composables/useAjuda.js
Normal file
354
src/composables/useAjuda.js
Normal file
@@ -0,0 +1,354 @@
|
||||
// src/composables/useAjuda.js
|
||||
// Composable singleton para o drawer de ajuda.
|
||||
// - Home: docs da sessão atual + outros docs paginados + FAQ
|
||||
// - Navegação interna com stack (voltar)
|
||||
// - Votação por usuário (útil / não útil)
|
||||
|
||||
import { ref, computed } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { sessionRole, sessionIsSaasAdmin } from '@/app/session'
|
||||
|
||||
const ADMIN_ROLES = ['clinic_admin', 'tenant_admin']
|
||||
|
||||
function isCurrentUserAdmin () {
|
||||
return sessionIsSaasAdmin.value || ADMIN_ROLES.includes(sessionRole.value)
|
||||
}
|
||||
|
||||
// ── Singleton state ────────────────────────────────────────────
|
||||
const cache = new Map() // path → { docs, relatedDocs, faqItens }
|
||||
const docCache = new Map() // id → { docs, relatedDocs, faqItens }
|
||||
|
||||
// Estado da sessão atual
|
||||
const sessionDocs = ref([])
|
||||
const sessionFaq = ref([])
|
||||
const sessionPath = ref('')
|
||||
|
||||
// Estado da home (todos os docs paginados + FAQ global)
|
||||
const allDocs = ref([])
|
||||
const allDocsTotal = ref(0)
|
||||
const allDocsPage = ref(0)
|
||||
const allDocsLoading = ref(false)
|
||||
const ALL_DOCS_PAGE_SIZE = 8
|
||||
|
||||
const globalFaq = ref([])
|
||||
const globalFaqLoading = ref(false)
|
||||
|
||||
// Drawer
|
||||
const drawerOpen = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
// Stack de navegação — cada entrada: { currentDoc, label }
|
||||
const navStack = ref([])
|
||||
// null = home, objeto = doc aberto
|
||||
const currentDoc = ref(null)
|
||||
|
||||
const isHome = computed(() => currentDoc.value === null)
|
||||
const isNavigating = computed(() => navStack.value.length > 0)
|
||||
|
||||
// Votos do usuário { [docId]: true | false | null }
|
||||
const meusVotos = ref({})
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────
|
||||
function normalizePath (path) {
|
||||
return String(path || '').split('?')[0].split('#')[0].replace(/\/$/, '') || '/'
|
||||
}
|
||||
|
||||
async function fetchFaqItensForDocs (docIds) {
|
||||
if (!docIds.length) return []
|
||||
const { data } = await supabase
|
||||
.from('saas_faq_itens')
|
||||
.select('id, doc_id, pergunta, resposta, ordem')
|
||||
.in('doc_id', docIds)
|
||||
.eq('ativo', true)
|
||||
.order('doc_id')
|
||||
.order('ordem')
|
||||
return data || []
|
||||
}
|
||||
|
||||
// ── Fetch por rota (chamado pelo AppLayout ao mudar de rota) ──
|
||||
export async function fetchDocsForPath (rawPath) {
|
||||
const path = normalizePath(rawPath)
|
||||
sessionPath.value = path
|
||||
currentDoc.value = null
|
||||
navStack.value = []
|
||||
// Reseta paginação de outros docs ao mudar de rota
|
||||
allDocs.value = []
|
||||
allDocsPage.value = 0
|
||||
|
||||
if (cache.has(path)) {
|
||||
const cached = cache.get(path)
|
||||
sessionDocs.value = cached.docs
|
||||
sessionFaq.value = cached.faqItens
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('saas_docs')
|
||||
.select('id, titulo, conteudo, medias, tipo_acesso, docs_relacionados, ordem, categoria, exibir_no_faq, votos_util, votos_nao_util')
|
||||
.eq('pagina_path', path)
|
||||
.eq('ativo', true)
|
||||
.order('ordem')
|
||||
|
||||
if (error) throw error
|
||||
|
||||
const userIsAdmin = isCurrentUserAdmin()
|
||||
const mainDocs = (data || []).filter(d =>
|
||||
d.tipo_acesso === 'usuario' || userIsAdmin
|
||||
)
|
||||
|
||||
const allIds = [...new Set(mainDocs.flatMap(d => d.docs_relacionados || []))]
|
||||
let related = []
|
||||
if (allIds.length) {
|
||||
const { data: relData } = await supabase
|
||||
.from('saas_docs')
|
||||
.select('id, titulo, pagina_path')
|
||||
.in('id', allIds)
|
||||
.eq('ativo', true)
|
||||
related = relData || []
|
||||
}
|
||||
|
||||
const itens = await fetchFaqItensForDocs(mainDocs.map(d => d.id))
|
||||
|
||||
cache.set(path, { docs: mainDocs, relatedDocs: related, faqItens: itens })
|
||||
sessionDocs.value = mainDocs
|
||||
sessionFaq.value = itens
|
||||
} catch (e) {
|
||||
console.warn('[useAjuda] erro ao buscar docs:', e?.message || e)
|
||||
sessionDocs.value = []
|
||||
sessionFaq.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── Todos os docs paginados (para a home do drawer) ───────────
|
||||
export async function loadAllDocs (page = 0) {
|
||||
allDocsLoading.value = true
|
||||
try {
|
||||
const from = page * ALL_DOCS_PAGE_SIZE
|
||||
const to = from + ALL_DOCS_PAGE_SIZE - 1
|
||||
|
||||
const userIsAdmin = isCurrentUserAdmin()
|
||||
let query = supabase
|
||||
.from('saas_docs')
|
||||
.select('id, titulo, pagina_path, categoria, votos_util, votos_nao_util', { count: 'exact' })
|
||||
.eq('ativo', true)
|
||||
.order('titulo')
|
||||
.range(from, to)
|
||||
|
||||
if (!userIsAdmin) query = query.eq('tipo_acesso', 'usuario')
|
||||
|
||||
const { data, count, error } = await query
|
||||
if (error) throw error
|
||||
|
||||
// Exclui docs da sessão atual
|
||||
const sessionIds = new Set(sessionDocs.value.map(d => d.id))
|
||||
const filtered = (data || []).filter(d => !sessionIds.has(d.id))
|
||||
|
||||
allDocs.value = page === 0 ? filtered : [...allDocs.value, ...filtered]
|
||||
allDocsTotal.value = count || 0
|
||||
allDocsPage.value = page
|
||||
} catch (e) {
|
||||
console.warn('[useAjuda] erro ao carregar todos os docs:', e?.message || e)
|
||||
} finally {
|
||||
allDocsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── FAQ global (itens fora da sessão atual) ───────────────────
|
||||
export async function loadGlobalFaq () {
|
||||
globalFaqLoading.value = true
|
||||
try {
|
||||
const userIsAdmin = isCurrentUserAdmin()
|
||||
let q = supabase
|
||||
.from('saas_docs')
|
||||
.select('id, titulo')
|
||||
.eq('ativo', true)
|
||||
.eq('exibir_no_faq', true)
|
||||
|
||||
if (!userIsAdmin) q = q.eq('tipo_acesso', 'usuario')
|
||||
|
||||
const { data: faqDocs } = await q
|
||||
if (!faqDocs?.length) { globalFaq.value = []; return }
|
||||
|
||||
const sessionIds = new Set(sessionDocs.value.map(d => d.id))
|
||||
const outrosIds = faqDocs.filter(d => !sessionIds.has(d.id)).map(d => d.id)
|
||||
|
||||
if (!outrosIds.length) { globalFaq.value = []; return }
|
||||
|
||||
const itens = await fetchFaqItensForDocs(outrosIds)
|
||||
globalFaq.value = itens.map(item => ({
|
||||
...item,
|
||||
_docTitulo: faqDocs.find(d => d.id === item.doc_id)?.titulo || ''
|
||||
}))
|
||||
} catch (e) {
|
||||
console.warn('[useAjuda] erro ao carregar FAQ global:', e?.message || e)
|
||||
globalFaq.value = []
|
||||
} finally {
|
||||
globalFaqLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── Votos do usuário ──────────────────────────────────────────
|
||||
export async function loadMeusVotos () {
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
if (!user) return
|
||||
const { data } = await supabase
|
||||
.from('saas_doc_votos')
|
||||
.select('doc_id, util')
|
||||
.eq('user_id', user.id)
|
||||
meusVotos.value = {}
|
||||
for (const v of (data || [])) meusVotos.value[v.doc_id] = v.util
|
||||
} catch (e) {
|
||||
console.warn('[useAjuda] erro ao carregar votos:', e?.message || e)
|
||||
}
|
||||
}
|
||||
|
||||
export async function votar (docId, util) {
|
||||
try {
|
||||
const { data, error } = await supabase.rpc('saas_votar_doc', {
|
||||
p_doc_id: docId,
|
||||
p_util: util
|
||||
})
|
||||
if (error) throw error
|
||||
|
||||
const acao = data?.acao
|
||||
meusVotos.value = {
|
||||
...meusVotos.value,
|
||||
[docId]: acao === 'removido' ? null : util
|
||||
}
|
||||
_atualizarContadorLocal(docId, util, acao)
|
||||
} catch (e) {
|
||||
console.warn('[useAjuda] erro ao votar:', e?.message || e)
|
||||
}
|
||||
}
|
||||
|
||||
function _atualizarContadorLocal (docId, util, acao) {
|
||||
const ajustar = (doc) => {
|
||||
if (!doc || doc.id !== docId) return doc
|
||||
let { votos_util = 0, votos_nao_util = 0 } = doc
|
||||
if (acao === 'registrado') {
|
||||
if (util) votos_util++; else votos_nao_util++
|
||||
} else if (acao === 'removido') {
|
||||
if (util) votos_util = Math.max(0, votos_util - 1)
|
||||
else votos_nao_util = Math.max(0, votos_nao_util - 1)
|
||||
} else if (acao === 'atualizado') {
|
||||
if (util) { votos_util++; votos_nao_util = Math.max(0, votos_nao_util - 1) }
|
||||
else { votos_nao_util++; votos_util = Math.max(0, votos_util - 1) }
|
||||
}
|
||||
return { ...doc, votos_util, votos_nao_util }
|
||||
}
|
||||
sessionDocs.value = sessionDocs.value.map(ajustar)
|
||||
allDocs.value = allDocs.value.map(ajustar)
|
||||
if (currentDoc.value) {
|
||||
currentDoc.value = { ...currentDoc.value, docs: currentDoc.value.docs.map(ajustar) }
|
||||
}
|
||||
cache.forEach((val, key) => {
|
||||
if (val.docs.some(d => d.id === docId)) cache.delete(key)
|
||||
})
|
||||
docCache.delete(docId)
|
||||
}
|
||||
|
||||
// ── Navegação interna ─────────────────────────────────────────
|
||||
export async function navigateToDoc (docId, label = '') {
|
||||
if (!docId) return
|
||||
|
||||
navStack.value.push({ currentDoc: currentDoc.value, label: label || 'Voltar' })
|
||||
loading.value = true
|
||||
try {
|
||||
if (docCache.has(docId)) {
|
||||
currentDoc.value = docCache.get(docId)
|
||||
return
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('saas_docs')
|
||||
.select('id, titulo, conteudo, medias, tipo_acesso, docs_relacionados, ordem, categoria, exibir_no_faq, votos_util, votos_nao_util')
|
||||
.eq('id', docId)
|
||||
.eq('ativo', true)
|
||||
.maybeSingle()
|
||||
|
||||
if (error) throw error
|
||||
if (!data) { navStack.value.pop(); return }
|
||||
|
||||
const userIsAdmin = isCurrentUserAdmin()
|
||||
if (data.tipo_acesso !== 'usuario' && !userIsAdmin) {
|
||||
navStack.value.pop(); return
|
||||
}
|
||||
|
||||
const relIds = data.docs_relacionados || []
|
||||
let related = []
|
||||
if (relIds.length) {
|
||||
const { data: relData } = await supabase
|
||||
.from('saas_docs')
|
||||
.select('id, titulo, pagina_path')
|
||||
.in('id', relIds)
|
||||
.eq('ativo', true)
|
||||
related = relData || []
|
||||
}
|
||||
|
||||
const itens = await fetchFaqItensForDocs([data.id])
|
||||
const entry = { docs: [data], relatedDocs: related, faqItens: itens }
|
||||
docCache.set(docId, entry)
|
||||
currentDoc.value = entry
|
||||
} catch (e) {
|
||||
console.warn('[useAjuda] erro ao navegar para doc:', e?.message || e)
|
||||
navStack.value.pop()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
export function navBack () {
|
||||
const prev = navStack.value.pop()
|
||||
if (prev) currentDoc.value = prev.currentDoc
|
||||
}
|
||||
|
||||
export function invalidateAjudaCache (path) {
|
||||
if (path) cache.delete(normalizePath(path))
|
||||
else cache.clear()
|
||||
docCache.clear()
|
||||
}
|
||||
|
||||
// ── Composable público ────────────────────────────────────────
|
||||
export function useAjuda () {
|
||||
const hasAjuda = computed(() => true) // sempre habilitado
|
||||
|
||||
const allDocsHasMore = computed(() =>
|
||||
allDocs.value.length < allDocsTotal.value - sessionDocs.value.length
|
||||
)
|
||||
|
||||
function openDrawer () {
|
||||
drawerOpen.value = true
|
||||
currentDoc.value = null
|
||||
if (!allDocs.value.length) loadAllDocs(0)
|
||||
if (!globalFaq.value.length) loadGlobalFaq()
|
||||
if (!Object.keys(meusVotos.value).length) loadMeusVotos()
|
||||
}
|
||||
|
||||
function closeDrawer () {
|
||||
drawerOpen.value = false
|
||||
navStack.value = []
|
||||
currentDoc.value = null
|
||||
}
|
||||
|
||||
function loadMoreDocs () {
|
||||
loadAllDocs(allDocsPage.value + 1)
|
||||
}
|
||||
|
||||
return {
|
||||
sessionDocs, sessionFaq, sessionPath,
|
||||
allDocs, allDocsTotal, allDocsLoading, allDocsHasMore,
|
||||
globalFaq, globalFaqLoading,
|
||||
currentDoc, isHome, isNavigating, navStack,
|
||||
drawerOpen, loading,
|
||||
hasAjuda,
|
||||
meusVotos,
|
||||
openDrawer, closeDrawer,
|
||||
navigateToDoc, navBack,
|
||||
votar, loadMoreDocs,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user