368 lines
12 KiB
JavaScript
368 lines
12 KiB
JavaScript
/*
|
|
|--------------------------------------------------------------------------
|
|
| Agência PSI
|
|
|--------------------------------------------------------------------------
|
|
| Criado e desenvolvido por Leonardo Nohama
|
|
|
|
|
| Tecnologia aplicada à escuta.
|
|
| Estrutura para o cuidado.
|
|
|
|
|
| Arquivo: src/composables/useAjuda.js
|
|
| Data: 2026
|
|
| Local: São Carlos/SP — Brasil
|
|
|--------------------------------------------------------------------------
|
|
| © 2026 — Todos os direitos reservados
|
|
|--------------------------------------------------------------------------
|
|
*/
|
|
// - 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,
|
|
}
|
|
} |