From 84d65e49c04ea5a6515ca94e05b596acf2ee6c1d Mon Sep 17 00:00:00 2001 From: Leonardo Date: Mon, 16 Mar 2026 09:41:18 -0300 Subject: [PATCH] =?UTF-8?q?Sistema=20de=20Suporte=20,=20Documenta=C3=A7?= =?UTF-8?q?=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agenda/components/dev/AgendaDevDocs.vue | 801 ++++++++++++++++++ src/support/components/SupportDebugBanner.vue | 345 ++++++-- src/support/supportLogger.js | 66 +- src/support/supportSessionService.js | 101 ++- src/views/pages/saas/SaasSupportPage.vue | 657 ++++++++------ 5 files changed, 1615 insertions(+), 355 deletions(-) create mode 100644 src/features/agenda/components/dev/AgendaDevDocs.vue diff --git a/src/features/agenda/components/dev/AgendaDevDocs.vue b/src/features/agenda/components/dev/AgendaDevDocs.vue new file mode 100644 index 0000000..13e673a --- /dev/null +++ b/src/features/agenda/components/dev/AgendaDevDocs.vue @@ -0,0 +1,801 @@ + + + + + + diff --git a/src/support/components/SupportDebugBanner.vue b/src/support/components/SupportDebugBanner.vue index b6f5aee..944f720 100644 --- a/src/support/components/SupportDebugBanner.vue +++ b/src/support/components/SupportDebugBanner.vue @@ -1,55 +1,88 @@ @@ -139,13 +225,14 @@ function formatTime (iso) { font-size: 12px; } +/* ── Barra ───────────────────────────────────────────────── */ .support-banner__bar { display: flex; align-items: center; justify-content: space-between; - background: #b45309; + background: #92400e; color: #fff; - padding: 6px 16px; + padding: 5px 14px; gap: 12px; } @@ -155,38 +242,51 @@ function formatTime (iso) { gap: 10px; font-weight: 600; letter-spacing: 0.05em; + min-width: 0; + overflow: hidden; } .support-banner__bar-right { display: flex; align-items: center; - gap: 6px; + gap: 5px; + flex-shrink: 0; } .support-banner__pulse { display: inline-block; - width: 8px; - height: 8px; + width: 7px; + height: 7px; border-radius: 50%; background: #fcd34d; + flex-shrink: 0; animation: pulse 1.4s ease-in-out infinite; } @keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.5; transform: scale(1.3); } + 50% { opacity: 0.5; transform: scale(1.4); } } .support-banner__tenant { font-weight: 400; - opacity: 0.75; + opacity: 0.7; font-size: 11px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 260px; } -.support-banner__toggle, -.support-banner__clear, -.support-banner__close { - background: rgba(255,255,255,0.15); +.support-banner__count { + font-weight: 400; + opacity: 0.55; + font-size: 10px; + flex-shrink: 0; +} + +.support-banner__toggle { + background: rgba(255,255,255,0.14); border: none; color: #fff; cursor: pointer; @@ -198,36 +298,56 @@ function formatTime (iso) { gap: 5px; transition: background 0.15s; } -.support-banner__toggle:hover, -.support-banner__clear:hover, -.support-banner__close:hover { - background: rgba(255,255,255,0.28); +.support-banner__toggle:hover { background: rgba(255,255,255,0.26); } + +.support-banner__icon-btn { + width: 26px; + height: 26px; + background: rgba(255,255,255,0.1); + border: none; + color: #fff; + cursor: pointer; + border-radius: 4px; + display: grid; + place-items: center; + transition: background 0.15s; } +.support-banner__icon-btn:hover { background: rgba(255,255,255,0.25); } .support-banner__err-badge { background: #ef4444; color: #fff; border-radius: 10px; - padding: 0 7px; + padding: 0 6px; font-size: 10px; font-weight: 700; } -/* Painel */ +/* ── Painel ──────────────────────────────────────────────── */ .support-banner__panel { background: #0f172a; - border-top: 2px solid #b45309; + border-top: 2px solid #92400e; display: flex; flex-direction: column; - max-height: 360px; + max-height: 380px; +} + +/* ── Toolbar ─────────────────────────────────────────────── */ +.support-banner__toolbar { + display: flex; + align-items: center; + gap: 8px; + padding: 7px 10px; + border-bottom: 1px solid #1e293b; + flex-shrink: 0; + flex-wrap: wrap; } .support-banner__filters { display: flex; - gap: 6px; - padding: 8px 12px; - border-bottom: 1px solid #1e293b; - flex-shrink: 0; + gap: 4px; + flex-wrap: wrap; + flex: 1; } .support-banner__filter-btn { @@ -235,61 +355,116 @@ function formatTime (iso) { border: 1px solid #334155; color: #94a3b8; border-radius: 4px; - padding: 2px 10px; + padding: 2px 9px; font-size: 11px; cursor: pointer; display: flex; align-items: center; - gap: 5px; - transition: all 0.15s; + gap: 4px; + transition: all 0.12s; + white-space: nowrap; } .support-banner__filter-btn--active { - background: #b45309; - border-color: #b45309; + background: #92400e; + border-color: #92400e; color: #fff; } .support-banner__filter-count { - background: rgba(255,255,255,0.15); + background: rgba(255,255,255,0.13); border-radius: 8px; - padding: 0 5px; + padding: 0 4px; font-size: 10px; } +/* ── Search ──────────────────────────────────────────────── */ +.support-banner__search-wrap { + position: relative; + flex-shrink: 0; + display: flex; + align-items: center; +} + +.support-banner__search-icon { + position: absolute; + left: 7px; + color: #475569; + font-size: 11px; + pointer-events: none; +} + +.support-banner__search { + background: #1e293b; + border: 1px solid #334155; + border-radius: 4px; + color: #e2e8f0; + font-size: 11px; + font-family: inherit; + padding: 3px 24px 3px 24px; + width: 180px; + outline: none; + transition: border-color 0.15s; +} +.support-banner__search:focus { border-color: #92400e; } +.support-banner__search::placeholder { color: #475569; } + +.support-banner__search-clear { + position: absolute; + right: 5px; + background: none; + border: none; + color: #475569; + cursor: pointer; + font-size: 10px; + display: grid; + place-items: center; + padding: 2px; +} +.support-banner__search-clear:hover { color: #94a3b8; } + +/* ── Logs list ───────────────────────────────────────────── */ .support-banner__logs { overflow-y: auto; flex: 1; - padding: 4px 0; + padding: 2px 0; + scrollbar-width: thin; + scrollbar-color: #1e293b transparent; } .support-banner__empty { color: #475569; text-align: center; padding: 20px; + font-style: italic; } .support-banner__log-entry { display: flex; flex-wrap: wrap; align-items: baseline; - gap: 6px; - padding: 3px 12px; - border-bottom: 1px solid #1e293b; + gap: 5px; + padding: 2px 12px; + border-bottom: 1px solid #0f1829; transition: background 0.1s; } .support-banner__log-entry:hover { background: #1e293b; } -.support-banner__log-entry--error { border-left: 3px solid #ef4444; } -.support-banner__log-entry--api { border-left: 3px solid #3b82f6; } +.support-banner__log-entry--error { border-left: 3px solid #ef4444; } +.support-banner__log-entry--api { border-left: 3px solid #3b82f6; } .support-banner__log-entry--recurrence { border-left: 3px solid #8b5cf6; } -.support-banner__log-entry--guard { border-left: 3px solid #10b981; } -.support-banner__log-entry--perf { border-left: 3px solid #f59e0b; } -.support-banner__log-entry--event { border-left: 3px solid #64748b; } +.support-banner__log-entry--guard { border-left: 3px solid #10b981; } +.support-banner__log-entry--perf { border-left: 3px solid #f59e0b; } +.support-banner__log-entry--tenant { border-left: 3px solid #06b6d4; } +.support-banner__log-entry--menu { border-left: 3px solid #84cc16; } +.support-banner__log-entry--profile { border-left: 3px solid #ec4899; } +.support-banner__log-entry--auth { border-left: 3px solid #f97316; } +.support-banner__log-entry--agenda { border-left: 3px solid #a78bfa; } +.support-banner__log-entry--event { border-left: 3px solid #475569; } -.support-banner__log-time { color: #475569; font-size: 10px; flex-shrink: 0; } +.support-banner__log-time { color: #334155; font-size: 10px; flex-shrink: 0; } .support-banner__log-level { color: #f59e0b; font-size: 10px; font-weight: 700; text-transform: uppercase; flex-shrink: 0; } .support-banner__log-source { color: #7c3aed; font-size: 10px; flex-shrink: 0; } -.support-banner__log-msg { color: #e2e8f0; flex: 1; } +.support-banner__log-msg { color: #cbd5e1; flex: 1; } .support-banner__log-expand { background: #1e293b; @@ -300,16 +475,17 @@ function formatTime (iso) { cursor: pointer; font-size: 10px; font-family: monospace; + flex-shrink: 0; } .support-banner__log-expand:hover { color: #e2e8f0; } .support-banner__log-data { width: 100%; - margin: 4px 0 0; - background: #0f172a; + margin: 3px 0 2px; + background: #020617; border: 1px solid #1e293b; color: #94a3b8; - padding: 8px; + padding: 7px; border-radius: 4px; font-size: 10px; overflow-x: auto; @@ -317,17 +493,26 @@ function formatTime (iso) { word-break: break-all; } +/* mark de busca */ +:deep(.sbh) { + background: rgba(250, 204, 21, 0.3); + color: #fde68a; + border-radius: 2px; + padding: 0 1px; +} + +/* ── Footer ──────────────────────────────────────────────── */ .support-banner__footer { display: flex; gap: 8px; padding: 4px 12px; - color: #475569; + color: #334155; font-size: 10px; border-top: 1px solid #1e293b; flex-shrink: 0; } -/* Transition */ +/* ── Transition ──────────────────────────────────────────── */ .support-slide-enter-active, .support-slide-leave-active { transition: transform 0.25s ease; } .support-slide-enter-from, diff --git a/src/support/supportLogger.js b/src/support/supportLogger.js index d9a3a17..b3d1cea 100644 --- a/src/support/supportLogger.js +++ b/src/support/supportLogger.js @@ -11,8 +11,8 @@ * Usar sempre este módulo para logs de diagnóstico. * * Uso: - * import { logEvent, logAPI, logError, logRecurrence } from '@/support/supportLogger' - * logEvent('useRecurrence', 'loadRules', { ownerId, startISO }) + * import { logEvent, logAPI, logError, logTenant } from '@/support/supportLogger' + * logTenant('loadSessionAndTenant', { tenantId, role }) */ import { useSupportDebugStore } from './supportDebugStore' @@ -26,6 +26,11 @@ export const LOG_LEVEL = { RECURRENCE: 'recurrence', GUARD: 'guard', PERF: 'perf', + TENANT: 'tenant', + MENU: 'menu', + PROFILE: 'profile', + AUTH: 'auth', + AGENDA: 'agenda', } // ─── Função base ───────────────────────────────────────────────────────────── @@ -47,7 +52,6 @@ function _log (level, source, message, data = null) { store.addLog(entry) - // Agrupa no console para não poluir — só visível quando debug ativo const prefix = `[${level.toUpperCase()}][${source}]` if (level === LOG_LEVEL.ERROR) { console.error(prefix, message, data ?? '') @@ -58,24 +62,17 @@ function _log (level, source, message, data = null) { // ─── API pública ───────────────────────────────────────────────────────────── -/** - * Log de evento geral (lifecycle, state changes) - * Substitui console.log genérico dos composables - */ +/** Log de evento geral (lifecycle, state changes) */ export function logEvent (source, message, data = null) { _log(LOG_LEVEL.EVENT, source, message, data) } -/** - * Log de chamada de API (Supabase queries) - */ +/** Log de chamada de API (Supabase queries) */ export function logAPI (source, message, data = null) { _log(LOG_LEVEL.API, source, message, data) } -/** - * Log de erro capturado - */ +/** Log de erro capturado */ export function logError (source, message, error = null) { const data = error ? { message: error?.message, code: error?.code, details: error?.details } @@ -83,25 +80,48 @@ export function logError (source, message, error = null) { _log(LOG_LEVEL.ERROR, source, message, data) } -/** - * Log específico do sistema de recorrência - * Substitui os console.log de useRecurrence - */ +/** Log específico do sistema de recorrência */ export function logRecurrence (message, data = null) { _log(LOG_LEVEL.RECURRENCE, 'useRecurrence', message, data) } -/** - * Log de navegação/guard do router - * Substitui console.time/timeLog/timeEnd de guards.js - */ +/** Log de navegação/guard do router */ export function logGuard (message, data = null) { _log(LOG_LEVEL.GUARD, 'router.guard', message, data) } +/** Log de tenant: carregamento de sessão, troca de role, tenant ativo */ +export function logTenant (source, message, data = null) { + _log(LOG_LEVEL.TENANT, source, message, data) +} + +/** Log do sistema de menu: build, reset, model changes */ +export function logMenu (message, data = null) { + _log(LOG_LEVEL.MENU, 'menuStore', message, data) +} + +/** Log da página de perfil: carregamento, salvamento de settings */ +export function logProfile (message, data = null) { + _log(LOG_LEVEL.PROFILE, 'ProfilePage', message, data) +} + +/** Log de autenticação: login, logout, refresh, MFA */ +export function logAuth (message, data = null) { + _log(LOG_LEVEL.AUTH, 'auth', message, data) +} + +/** Log da agenda: eventos, slots, appointments, recurrence */ +export function logAgenda (source, message, data = null) { + _log(LOG_LEVEL.AGENDA, source, message, data) +} + /** - * Log de performance (substitui console.time) - * Retorna uma função que finaliza a medição + * Log de performance — retorna função que finaliza a medição. + * + * Uso: + * const end = logPerf('useAgenda', 'loadEvents') + * ...await work... + * end({ count: events.length }) */ export function logPerf (source, label) { let store diff --git a/src/support/supportSessionService.js b/src/support/supportSessionService.js index 3cb2324..e80b57f 100644 --- a/src/support/supportSessionService.js +++ b/src/support/supportSessionService.js @@ -6,52 +6,112 @@ * Usado apenas pelo painel do admin — nunca pelo terapeuta/paciente. * * Fluxo: - * 1. Admin seleciona tenant - * 2. createSession(tenantId) → { token, expires_at } + * 1. Admin seleciona tenant + TTL + nota opcional + * 2. createSupportSession(tenantId, ttlMinutes, note) → { token, expires_at } * 3. Admin recebe URL pronta para copiar - * 4. Admin pode listar sessões ativas e revogar + * 4. Admin pode listar sessões ativas, histórico e revogar */ import { supabase } from '@/lib/supabase/client' +const TAG = '[supportSessionService]' + /** * Cria uma sessão de suporte para um tenant. * Requer: usuário autenticado com role saas_admin (validado no RPC). * - * @param {string} tenantId - UUID do tenant a ser depurado - * @param {number} ttlMinutes - TTL em minutos (1–120, default 60) + * @param {string} tenantId - UUID do tenant a ser depurado + * @param {number} ttlMinutes - TTL em minutos (1–120, default 60) + * @param {string} [note] - Nota opcional sobre o motivo do suporte * @returns {{ token: string, expires_at: string, session_id: string }} */ -export async function createSupportSession (tenantId, ttlMinutes = 60) { +export async function createSupportSession (tenantId, ttlMinutes = 60, note = '') { if (!tenantId) throw new Error('tenant_id é obrigatório.') + console.log(`${TAG} createSupportSession`, { tenantId, ttlMinutes, note: note || '(sem nota)' }) + const { data, error } = await supabase .rpc('create_support_session', { p_tenant_id: tenantId, p_ttl_minutes: ttlMinutes, }) - if (error) throw error + if (error) { + console.error(`${TAG} createSupportSession ERRO`, error) + throw error + } if (!data?.token) throw new Error('Resposta inválida do servidor.') + // Salva nota localmente associada ao session_id (banco não tem coluna note) + if (note?.trim()) { + try { + const notes = JSON.parse(sessionStorage.getItem('support_notes') || '{}') + notes[data.session_id || data.token] = note.trim() + sessionStorage.setItem('support_notes', JSON.stringify(notes)) + } catch {} + } + + console.log(`${TAG} sessão criada`, { token: `${data.token.slice(0, 8)}…`, expires_at: data.expires_at }) return data } /** - * Lista sessões de suporte ativas do admin logado. - * Retorna somente sessões não expiradas. + * Lista sessões de suporte ATIVAS (não expiradas). * * @returns {Array} */ export async function listActiveSupportSessions () { + console.log(`${TAG} listActiveSupportSessions`) + const { data, error } = await supabase .from('support_sessions') .select('id, tenant_id, token, expires_at, created_at') .gt('expires_at', new Date().toISOString()) .order('created_at', { ascending: false }) - if (error) throw error - return data || [] + if (error) { + console.error(`${TAG} listActiveSupportSessions ERRO`, error) + throw error + } + + const sessions = data || [] + console.log(`${TAG} ${sessions.length} sessão(ões) ativa(s)`) + + // Enriquece com notas salvas localmente + const notes = _loadNotes() + return sessions.map(s => ({ ...s, _note: notes[s.id] || notes[s.token] || '' })) +} + +/** + * Lista histórico de sessões (ativas + expiradas), limitado às últimas N. + * + * @param {number} limit - quantidade máxima (default 50) + * @returns {Array} + */ +export async function listSessionHistory (limit = 50) { + console.log(`${TAG} listSessionHistory limit=${limit}`) + + const { data, error } = await supabase + .from('support_sessions') + .select('id, tenant_id, token, expires_at, created_at') + .order('created_at', { ascending: false }) + .limit(limit) + + if (error) { + console.error(`${TAG} listSessionHistory ERRO`, error) + throw error + } + + const sessions = data || [] + console.log(`${TAG} histórico: ${sessions.length} sessão(ões)`) + + const notes = _loadNotes() + const now = new Date() + return sessions.map(s => ({ + ...s, + _note: notes[s.id] || notes[s.token] || '', + _expired: new Date(s.expires_at) < now, + })) } /** @@ -63,10 +123,17 @@ export async function listActiveSupportSessions () { export async function revokeSupportSession (token) { if (!token) throw new Error('Token é obrigatório.') + console.log(`${TAG} revokeSupportSession token=${token.slice(0, 8)}…`) + const { data, error } = await supabase .rpc('revoke_support_session', { p_token: token }) - if (error) throw error + if (error) { + console.error(`${TAG} revokeSupportSession ERRO`, error) + throw error + } + + console.log(`${TAG} sessão revogada:`, !!data) return !!data } @@ -81,3 +148,13 @@ export function buildSupportUrl (token, basePath = '/therapist/agenda') { const origin = window.location.origin return `${origin}${basePath}?support=${token}` } + +// ─── Helpers internos ───────────────────────────────────────────────────────── + +function _loadNotes () { + try { + return JSON.parse(sessionStorage.getItem('support_notes') || '{}') + } catch { + return {} + } +} diff --git a/src/views/pages/saas/SaasSupportPage.vue b/src/views/pages/saas/SaasSupportPage.vue index 901e5be..b38649a 100644 --- a/src/views/pages/saas/SaasSupportPage.vue +++ b/src/views/pages/saas/SaasSupportPage.vue @@ -1,237 +1,60 @@ - - + +