/* |-------------------------------------------------------------------------- | 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, } }