Sistema de Suporte , Documentação
This commit is contained in:
@@ -0,0 +1,801 @@
|
||||
<!-- src/features/agenda/components/dev/AgendaDevDocs.vue
|
||||
Documentação técnica da Agenda — exibida apenas em modo suporte/dev.
|
||||
Acessível via SupportDebugBanner → botão "Docs". -->
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false }
|
||||
})
|
||||
const emit = defineEmits(['update:visible'])
|
||||
|
||||
function close () { emit('update:visible', false) }
|
||||
|
||||
const activeTab = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
:visible="visible"
|
||||
@update:visible="emit('update:visible', $event)"
|
||||
modal
|
||||
header="📘 Documentação Técnica — Agenda"
|
||||
:style="{ width: '860px', maxWidth: '96vw' }"
|
||||
:pt="{ content: { style: 'padding: 0' } }"
|
||||
>
|
||||
<TabView v-model:activeIndex="activeTab" class="dev-docs">
|
||||
|
||||
<!-- ── Tab 0: Visão Geral ─────────────────────────────── -->
|
||||
<TabPanel header="Visão Geral">
|
||||
<div class="dd-section">
|
||||
|
||||
<div class="dd-alert">
|
||||
Este painel é visível <strong>apenas em modo suporte SaaS</strong> ativo.
|
||||
Nunca é exibido em produção sem um token válido.
|
||||
</div>
|
||||
|
||||
<h3 class="dd-h3">Propósito</h3>
|
||||
<p class="dd-p">
|
||||
A <strong>Agenda</strong> é o núcleo do sistema. Permite ao terapeuta gerenciar
|
||||
sessões, bloqueios, recorrências e compromissos determinísticos — tudo integrado
|
||||
ao sistema de precificação, convênios e agendamento online.
|
||||
</p>
|
||||
|
||||
<h3 class="dd-h3">Stack técnica</h3>
|
||||
<div class="dd-grid-2">
|
||||
<div class="dd-card">
|
||||
<div class="dd-card-title">Frontend</div>
|
||||
<ul class="dd-list">
|
||||
<li><span class="dd-tag blue">Vue 3</span> Composition API + <code><script setup></code></li>
|
||||
<li><span class="dd-tag purple">FullCalendar</span> timeGrid / dayGrid + interaction plugin</li>
|
||||
<li><span class="dd-tag green">PrimeVue</span> Dialog, DataTable, Select, Toast</li>
|
||||
<li><span class="dd-tag orange">Pinia</span> tenantStore, entitlementsStore, menuStore</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="dd-card">
|
||||
<div class="dd-card-title">Backend</div>
|
||||
<ul class="dd-list">
|
||||
<li><span class="dd-tag teal">Supabase</span> PostgreSQL + Row Level Security</li>
|
||||
<li><span class="dd-tag red">RPCs</span> Funções PL/pgSQL para operações críticas</li>
|
||||
<li><span class="dd-tag yellow">Realtime</span> Não utilizado na agenda (pull-based)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="dd-h3">Arquitetura em camadas</h3>
|
||||
<div class="dd-layers">
|
||||
<div class="dd-layer dd-layer--page">
|
||||
<strong>Pages</strong>
|
||||
<span>AgendaTerapeutaPage · AgendaClinicaPage · CompromissosDeterminados</span>
|
||||
</div>
|
||||
<div class="dd-layer-arrow">▼</div>
|
||||
<div class="dd-layer dd-layer--composable">
|
||||
<strong>Composables</strong>
|
||||
<span>useAgendaSettings · useAgendaEvents · useRecurrence · useCommitmentServices · useDeterminedCommitments</span>
|
||||
</div>
|
||||
<div class="dd-layer-arrow">▼</div>
|
||||
<div class="dd-layer dd-layer--service">
|
||||
<strong>Services / Repositories</strong>
|
||||
<span>agendaRepository · agendaClinicRepository · agendaMappers</span>
|
||||
</div>
|
||||
<div class="dd-layer-arrow">▼</div>
|
||||
<div class="dd-layer dd-layer--db">
|
||||
<strong>Supabase</strong>
|
||||
<span>Tables + RPCs + RLS Policies</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="dd-h3">Rotas registradas</h3>
|
||||
<table class="dd-table">
|
||||
<thead><tr><th>Path</th><th>Name</th><th>Área</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>/therapist/agenda</code></td><td>therapist-agenda</td><td>Terapeuta</td></tr>
|
||||
<tr><td><code>/therapist/agenda/recorrencias</code></td><td>therapist-agenda-recorrencias</td><td>Terapeuta</td></tr>
|
||||
<tr><td><code>/therapist/agenda/compromissos</code></td><td>therapist-agenda-compromissos</td><td>Terapeuta</td></tr>
|
||||
<tr><td><code>/therapist/agendamentos-recebidos</code></td><td>therapist-agendamentos-recebidos</td><td>Terapeuta</td></tr>
|
||||
<tr><td><code>/admin/agenda/clinica</code></td><td>admin-agenda-clinica</td><td>Clínica</td></tr>
|
||||
<tr><td><code>/admin/agenda/compromissos</code></td><td>admin-agenda-compromissos</td><td>Clínica</td></tr>
|
||||
<tr><td><code>/agendar/:slug</code></td><td>agendador.publico</td><td>Público</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
<!-- ── Tab 1: Tabelas ─────────────────────────────────── -->
|
||||
<TabPanel header="Tabelas">
|
||||
<div class="dd-section">
|
||||
|
||||
<p class="dd-p">Todas as tabelas usam <strong>Row Level Security (RLS)</strong> habilitada.</p>
|
||||
|
||||
<h3 class="dd-h3">Core</h3>
|
||||
<table class="dd-table">
|
||||
<thead><tr><th>Tabela</th><th>Propósito</th><th>Colunas-chave</th></tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>agenda_configuracoes</code></td>
|
||||
<td>Configurações da agenda por owner (terapeuta ou clínica)</td>
|
||||
<td>owner_id, tenant_id, slot_duration_minutes, start_time, end_time, days_of_week</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>agenda_eventos</code></td>
|
||||
<td>Eventos individuais (sessões, bloqueios avulsos)</td>
|
||||
<td>id, owner_id, tenant_id, patient_id, starts_at, ends_at, status, recurrence_rule_id, tipo</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>agenda_bloqueios</code></td>
|
||||
<td>Períodos bloqueados na agenda</td>
|
||||
<td>owner_id, starts_at, ends_at, motivo, recorrente</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>agenda_regras_semanais</code></td>
|
||||
<td>Horários de trabalho semanais por dia</td>
|
||||
<td>owner_id, day_of_week (0–6), start_time, end_time</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3 class="dd-h3">Recorrência</h3>
|
||||
<table class="dd-table">
|
||||
<thead><tr><th>Tabela</th><th>Propósito</th><th>Colunas-chave</th></tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>recurrence_rules</code></td>
|
||||
<td>Regra mãe de uma série recorrente</td>
|
||||
<td>id, owner_id, patient_id, frequency, interval, start_date, end_date, days_of_week, status</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>recurrence_exceptions</code></td>
|
||||
<td>Exceções a uma regra (cancelamentos, alterações pontuais)</td>
|
||||
<td>rule_id, original_date, action (skip | reschedule), new_starts_at</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>recurrence_rule_services</code></td>
|
||||
<td>Serviços-template associados a uma regra recorrente</td>
|
||||
<td>rule_id, service_id, quantity, unit_price</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3 class="dd-h3">Compromissos e Serviços</h3>
|
||||
<table class="dd-table">
|
||||
<thead><tr><th>Tabela</th><th>Propósito</th><th>Colunas-chave</th></tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>determined_commitments</code></td>
|
||||
<td>Tipos de compromisso determinístico (ex: Avaliação, Supervisão)</td>
|
||||
<td>id, owner_id, tenant_id, name, color, duration_minutes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>determined_commitment_fields</code></td>
|
||||
<td>Campos customizados por tipo de compromisso</td>
|
||||
<td>commitment_id, field_key, label, type, required</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>commitment_services</code></td>
|
||||
<td>Serviços vinculados a um evento específico</td>
|
||||
<td>evento_id, service_id, quantity, unit_price, insurance_plan_id</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>services</code></td>
|
||||
<td>Catálogo de serviços do terapeuta/clínica</td>
|
||||
<td>id, owner_id, tenant_id, name, default_price, active</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>professional_pricing</code></td>
|
||||
<td>Precificação por tipo de compromisso e profissional</td>
|
||||
<td>owner_id, commitment_type_id, price, insurance_plan_id</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>insurance_plans</code></td>
|
||||
<td>Convênios cadastrados</td>
|
||||
<td>id, owner_id, name, active, notes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>insurance_plan_services</code></td>
|
||||
<td>Serviços cobertos por um convênio</td>
|
||||
<td>plan_id, service_id, covered_price</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>patient_discounts</code></td>
|
||||
<td>Descontos individuais por paciente</td>
|
||||
<td>owner_id, patient_id, discount_type (pct | fixed), value</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>financial_exceptions</code></td>
|
||||
<td>Exceções financeiras globais ou por owner</td>
|
||||
<td>id, owner_id, scope, rule_key, value</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3 class="dd-h3">Suporte técnico</h3>
|
||||
<table class="dd-table">
|
||||
<thead><tr><th>Tabela</th><th>Propósito</th><th>Colunas-chave</th></tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>support_sessions</code></td>
|
||||
<td>Tokens de acesso temporário gerados pelo SaaS admin</td>
|
||||
<td>id, tenant_id, token, expires_at, created_at</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
<!-- ── Tab 2: Composables ─────────────────────────────── -->
|
||||
<TabPanel header="Composables">
|
||||
<div class="dd-section">
|
||||
<p class="dd-p">
|
||||
Todos os composables ficam em <code>src/features/agenda/composables/</code>.
|
||||
Cada um é responsável por um domínio isolado.
|
||||
</p>
|
||||
|
||||
<table class="dd-table">
|
||||
<thead>
|
||||
<tr><th>Composable</th><th>Responsabilidade</th><th>Retorna / Expõe</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>useAgendaSettings</code></td>
|
||||
<td>Carrega e persiste as configurações da agenda (horários, slots)</td>
|
||||
<td>settings, saveSettings, loading</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>useAgendaEvents</code></td>
|
||||
<td>CRUD de eventos em <code>agenda_eventos</code>; mapeia para FullCalendar</td>
|
||||
<td>events, createEvent, updateEvent, deleteEvent, loadEvents</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>useRecurrence</code></td>
|
||||
<td>Geração dinâmica de ocorrências a partir de <code>recurrence_rules</code>; split e cancel de série</td>
|
||||
<td>rules, loadRules, createRule, splitAt, cancelFrom, exceptions</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>useCommitmentServices</code></td>
|
||||
<td>CRUD dos serviços vinculados a um evento (<code>commitment_services</code>)</td>
|
||||
<td>services, addService, removeService, updateService</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>useDeterminedCommitments</code></td>
|
||||
<td>Carrega tipos de compromisso determinístico do tenant</td>
|
||||
<td>commitments, loading, loadCommitments</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>useAgendaClinicStaff</code></td>
|
||||
<td>Carrega membros do staff da clínica via <code>v_tenant_staff</code></td>
|
||||
<td>staff, loadStaff, loading</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>useAgendaClinicEvents</code></td>
|
||||
<td>CRUD de eventos da clínica (visão multi-profissional)</td>
|
||||
<td>events, loadEvents, createEvent, updateEvent, deleteEvent</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>useServices</code></td>
|
||||
<td>CRUD do catálogo de serviços do profissional</td>
|
||||
<td>services, addService, editService, deleteService</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>useProfessionalPricing</code></td>
|
||||
<td>Precificação por tipo de compromisso e convênio</td>
|
||||
<td>pricing, loadPricing, savePricing</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>useInsurancePlans</code></td>
|
||||
<td>CRUD de convênios e seus serviços cobertos</td>
|
||||
<td>plans, addPlan, editPlan, deletePlan, planServices</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>usePatientDiscounts</code></td>
|
||||
<td>Descontos individuais por paciente</td>
|
||||
<td>discounts, addDiscount, removeDiscount</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>useFinancialExceptions</code></td>
|
||||
<td>Exceções financeiras globais ou por owner</td>
|
||||
<td>exceptions, saveException, deleteException</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>useFeriados</code></td>
|
||||
<td>Carrega feriados nacionais/estaduais para bloquear slots</td>
|
||||
<td>feriados, isHoliday, loadFeriados</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3 class="dd-h3">Services / Repositories</h3>
|
||||
<table class="dd-table">
|
||||
<thead><tr><th>Arquivo</th><th>Propósito</th></tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>agendaRepository.js</code></td>
|
||||
<td>Queries Supabase para terapeuta — eventos, bloqueios, configurações</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>agendaClinicRepository.js</code></td>
|
||||
<td>Queries Supabase para clínica — eventos multi-profissional, staff</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>agendaMappers.js</code></td>
|
||||
<td>
|
||||
Converte objetos do banco para formato FullCalendar.<br>
|
||||
<code>mapAgendaEventosToCalendarEvents()</code> · <code>buildWeeklyBreakBackgroundEvents()</code> · <code>minutesToDuration()</code>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
<!-- ── Tab 3: RPCs ────────────────────────────────────── -->
|
||||
<TabPanel header="RPCs">
|
||||
<div class="dd-section">
|
||||
<p class="dd-p">
|
||||
Funções PL/pgSQL executadas via <code>supabase.rpc()</code>.
|
||||
Todas validam permissões server-side — sem bypass pelo frontend.
|
||||
</p>
|
||||
|
||||
<div class="dd-rpc-card">
|
||||
<div class="dd-rpc-name">validate_support_session</div>
|
||||
<div class="dd-rpc-desc">Valida um token de suporte e retorna o tenant associado. Único ponto de ativação do modo debug.</div>
|
||||
<div class="dd-rpc-row">
|
||||
<span class="dd-rpc-label">Parâmetros</span>
|
||||
<code>p_token TEXT</code>
|
||||
</div>
|
||||
<div class="dd-rpc-row">
|
||||
<span class="dd-rpc-label">Retorna</span>
|
||||
<code>{ valid: boolean, tenant_id: uuid }</code>
|
||||
</div>
|
||||
<div class="dd-rpc-row">
|
||||
<span class="dd-rpc-label">Chamado em</span>
|
||||
<code>supportDebugStore.validateAndActivate(token)</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dd-rpc-card">
|
||||
<div class="dd-rpc-name">create_support_session</div>
|
||||
<div class="dd-rpc-desc">Cria uma sessão de suporte com TTL. Requer role saas_admin. Valida e insere em <code>support_sessions</code>.</div>
|
||||
<div class="dd-rpc-row">
|
||||
<span class="dd-rpc-label">Parâmetros</span>
|
||||
<code>p_tenant_id UUID, p_ttl_minutes INT</code>
|
||||
</div>
|
||||
<div class="dd-rpc-row">
|
||||
<span class="dd-rpc-label">Retorna</span>
|
||||
<code>{ token: text, expires_at: timestamptz, session_id: uuid }</code>
|
||||
</div>
|
||||
<div class="dd-rpc-row">
|
||||
<span class="dd-rpc-label">Chamado em</span>
|
||||
<code>supportSessionService.createSupportSession()</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dd-rpc-card">
|
||||
<div class="dd-rpc-name">revoke_support_session</div>
|
||||
<div class="dd-rpc-desc">Invalida imediatamente um token de suporte, definindo <code>expires_at = now()</code>.</div>
|
||||
<div class="dd-rpc-row">
|
||||
<span class="dd-rpc-label">Parâmetros</span>
|
||||
<code>p_token TEXT</code>
|
||||
</div>
|
||||
<div class="dd-rpc-row">
|
||||
<span class="dd-rpc-label">Retorna</span>
|
||||
<code>boolean</code>
|
||||
</div>
|
||||
<div class="dd-rpc-row">
|
||||
<span class="dd-rpc-label">Chamado em</span>
|
||||
<code>supportSessionService.revokeSupportSession()</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dd-rpc-card">
|
||||
<div class="dd-rpc-name">split_recurrence_at</div>
|
||||
<div class="dd-rpc-desc">
|
||||
Divide uma série recorrente em dois: encerra a original na data informada e cria uma nova regra a partir daí.
|
||||
Preserva exceções anteriores à divisão na regra original.
|
||||
</div>
|
||||
<div class="dd-rpc-row">
|
||||
<span class="dd-rpc-label">Parâmetros</span>
|
||||
<code>p_rule_id UUID, p_split_date DATE</code>
|
||||
</div>
|
||||
<div class="dd-rpc-row">
|
||||
<span class="dd-rpc-label">Retorna</span>
|
||||
<code>{ old_rule_id: uuid, new_rule_id: uuid }</code>
|
||||
</div>
|
||||
<div class="dd-rpc-row">
|
||||
<span class="dd-rpc-label">Chamado em</span>
|
||||
<code>useRecurrence.splitAt(ruleId, date)</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dd-rpc-card">
|
||||
<div class="dd-rpc-name">cancel_recurrence_from</div>
|
||||
<div class="dd-rpc-desc">
|
||||
Cancela todas as ocorrências de uma série a partir de uma data.
|
||||
Insere exceções do tipo <code>skip</code> para cada ocorrência futura e marca a regra como <code>cancelled</code>.
|
||||
</div>
|
||||
<div class="dd-rpc-row">
|
||||
<span class="dd-rpc-label">Parâmetros</span>
|
||||
<code>p_rule_id UUID, p_from_date DATE</code>
|
||||
</div>
|
||||
<div class="dd-rpc-row">
|
||||
<span class="dd-rpc-label">Retorna</span>
|
||||
<code>boolean</code>
|
||||
</div>
|
||||
<div class="dd-rpc-row">
|
||||
<span class="dd-rpc-label">Chamado em</span>
|
||||
<code>useRecurrence.cancelFrom(ruleId, date)</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
<!-- ── Tab 4: Fluxos ──────────────────────────────────── -->
|
||||
<TabPanel header="Fluxos">
|
||||
<div class="dd-section">
|
||||
|
||||
<h3 class="dd-h3">1 — Criar evento simples</h3>
|
||||
<ol class="dd-steps">
|
||||
<li>Usuário clica num slot do FullCalendar → evento <code>dateClick</code></li>
|
||||
<li><code>AgendaTerapeutaPage</code> abre <code>AgendaEventDialog</code> com horário pré-preenchido</li>
|
||||
<li>Usuário preenche paciente, tipo, serviços → confirma</li>
|
||||
<li><code>useAgendaEvents.createEvent()</code> → INSERT em <code>agenda_eventos</code></li>
|
||||
<li>Retorno → <code>mapAgendaEventosToCalendarEvents()</code> → evento adicionado ao FullCalendar</li>
|
||||
</ol>
|
||||
|
||||
<h3 class="dd-h3">2 — Criar recorrência</h3>
|
||||
<ol class="dd-steps">
|
||||
<li>No dialog do evento, usuário ativa "Repetir" e configura frequência/dias</li>
|
||||
<li><code>useRecurrence.createRule(payload)</code> → INSERT em <code>recurrence_rules</code></li>
|
||||
<li>Opcionalmente, serviços-template são inseridos em <code>recurrence_rule_services</code></li>
|
||||
<li>Frontend gera ocorrências client-side a partir da regra (sem tabela de ocorrências)</li>
|
||||
<li>Ocorrências são renderizadas como eventos "virtuais" no FullCalendar</li>
|
||||
</ol>
|
||||
|
||||
<h3 class="dd-h3">3 — Editar ocorrência única de uma série</h3>
|
||||
<ol class="dd-steps">
|
||||
<li>Usuário clica em um evento recorrente → dialog pergunta: "Esta ocorrência" ou "Todas a partir daqui"</li>
|
||||
<li><strong>Esta ocorrência:</strong> INSERT em <code>recurrence_exceptions</code> com <code>action = reschedule</code></li>
|
||||
<li><strong>A partir daqui:</strong> chama RPC <code>split_recurrence_at</code> → cria nova regra para o trecho futuro</li>
|
||||
<li>FullCalendar re-renderiza com as exceções aplicadas</li>
|
||||
</ol>
|
||||
|
||||
<h3 class="dd-h3">4 — Cancelar série recorrente</h3>
|
||||
<ol class="dd-steps">
|
||||
<li>Usuário abre evento recorrente → "Cancelar a partir de..."</li>
|
||||
<li>RPC <code>cancel_recurrence_from(ruleId, fromDate)</code> é chamado</li>
|
||||
<li>Regra recebe <code>status = cancelled</code>; exceções <code>skip</code> são inseridas</li>
|
||||
<li>Composable invalida cache local e recarrega regras</li>
|
||||
</ol>
|
||||
|
||||
<h3 class="dd-h3">5 — Ativar modo suporte técnico</h3>
|
||||
<ol class="dd-steps">
|
||||
<li>SaaS admin acessa <code>/saas/support</code> e gera um token com TTL</li>
|
||||
<li>Admin envia URL: <code>/therapist/agenda?support=TOKEN</code> para o terapeuta</li>
|
||||
<li>Terapeuta abre a URL; <code>AgendaTerapeutaPage._initSupportMode()</code> detecta query param</li>
|
||||
<li>RPC <code>validate_support_session(token)</code> é chamado — valida e retorna <code>tenant_id</code></li>
|
||||
<li><code>supportDebugStore.isActive = true</code> → <code>SupportDebugBanner</code> aparece via Teleport</li>
|
||||
<li>Todos os <code>logEvent / logAPI / logRecurrence</code> passam a ser registrados no painel</li>
|
||||
<li>Token expira automaticamente após o TTL — sem ação do admin necessária</li>
|
||||
</ol>
|
||||
|
||||
<h3 class="dd-h3">6 — Precificação de uma sessão</h3>
|
||||
<ol class="dd-steps">
|
||||
<li>Ao abrir/criar um evento, <code>useCommitmentServices</code> carrega serviços do evento</li>
|
||||
<li><code>useProfessionalPricing</code> cruza tipo de compromisso × profissional × convênio</li>
|
||||
<li>Se paciente tem convênio ativo, <code>useInsurancePlans</code> verifica cobertura</li>
|
||||
<li>Se paciente tem desconto, <code>usePatientDiscounts</code> aplica sobre o valor base</li>
|
||||
<li>Valor final é salvo em <code>commitment_services</code> no evento</li>
|
||||
</ol>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
<!-- ── Tab 5: Suporte ─────────────────────────────────── -->
|
||||
<TabPanel header="Sistema de Suporte">
|
||||
<div class="dd-section">
|
||||
|
||||
<h3 class="dd-h3">Níveis de log disponíveis</h3>
|
||||
<table class="dd-table">
|
||||
<thead><tr><th>Nível</th><th>Helper</th><th>Quando usar</th><th>Cor no painel</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>event</code></td><td><code>logEvent(source, msg, data)</code></td><td>Lifecycle, mudanças de estado gerais</td><td>Cinza</td></tr>
|
||||
<tr><td><code>api</code></td><td><code>logAPI(source, msg, data)</code></td><td>Queries Supabase (antes e depois)</td><td>Azul</td></tr>
|
||||
<tr><td><code>agenda</code></td><td><code>logAgenda(source, msg, data)</code></td><td>Eventos, slots, calendar renders</td><td>Roxo claro</td></tr>
|
||||
<tr><td><code>recurrence</code></td><td><code>logRecurrence(msg, data)</code></td><td>Geração e manipulação de regras</td><td>Roxo</td></tr>
|
||||
<tr><td><code>tenant</code></td><td><code>logTenant(source, msg, data)</code></td><td>Carregamento de sessão, role, tenant ativo</td><td>Ciano</td></tr>
|
||||
<tr><td><code>menu</code></td><td><code>logMenu(msg, data)</code></td><td>Build e reset do menuStore</td><td>Verde limão</td></tr>
|
||||
<tr><td><code>profile</code></td><td><code>logProfile(msg, data)</code></td><td>Perfil: carga e save de settings</td><td>Rosa</td></tr>
|
||||
<tr><td><code>auth</code></td><td><code>logAuth(msg, data)</code></td><td>Login, logout, refresh, MFA</td><td>Laranja</td></tr>
|
||||
<tr><td><code>guard</code></td><td><code>logGuard(msg, data)</code></td><td>Navegação via router guards</td><td>Verde</td></tr>
|
||||
<tr><td><code>perf</code></td><td><code>logPerf(source, label)</code></td><td>Medição de performance (retorna end())</td><td>Amarelo</td></tr>
|
||||
<tr><td><code>error</code></td><td><code>logError(source, msg, err)</code></td><td>Erros capturados em catch</td><td>Vermelho</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3 class="dd-h3">Como adicionar logs a um composable</h3>
|
||||
<pre class="dd-code">import { logAPI, logError, logPerf, logAgenda } from '@/support/supportLogger'
|
||||
|
||||
async function loadEvents (ownerId, range) {
|
||||
const end = logPerf('useAgendaEvents', 'loadEvents')
|
||||
logAPI('useAgendaEvents', 'loadEvents start', { ownerId, range })
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('agenda_eventos')
|
||||
.select('*')
|
||||
.eq('owner_id', ownerId)
|
||||
|
||||
if (error) throw error
|
||||
|
||||
end({ count: data.length })
|
||||
logAgenda('useAgendaEvents', 'eventos carregados', { count: data.length })
|
||||
return data
|
||||
} catch (e) {
|
||||
logError('useAgendaEvents', 'loadEvents ERRO', e)
|
||||
throw e
|
||||
}
|
||||
}</pre>
|
||||
|
||||
<h3 class="dd-h3">Segurança do modo suporte</h3>
|
||||
<ul class="dd-list dd-list--spaced">
|
||||
<li>Token validado <strong>server-side</strong> via RPC — sem bypass pelo frontend possível</li>
|
||||
<li>Token tem TTL máximo de 2 horas; expira automaticamente no banco</li>
|
||||
<li>Apenas usuários com role <code>saas_admin</code> podem criar tokens (validado no RPC)</li>
|
||||
<li>Revogar um token invalida imediatamente (<code>expires_at = now()</code>)</li>
|
||||
<li><code>SupportDebugBanner</code> só é renderizado quando <code>supportStore.isActive === true</code></li>
|
||||
<li>Em produção sem token válido, zero overhead — todos os <code>log*()</code> retornam imediatamente</li>
|
||||
</ul>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
</TabView>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dev-docs {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.dd-section {
|
||||
padding: 1.25rem 1.5rem 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Alert */
|
||||
.dd-alert {
|
||||
background: color-mix(in srgb, var(--yellow-500) 12%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--yellow-500) 35%, transparent);
|
||||
border-left: 4px solid var(--yellow-500);
|
||||
border-radius: 8px;
|
||||
padding: 0.6rem 1rem;
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
.dd-h3 {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--text-color-secondary);
|
||||
opacity: 0.6;
|
||||
margin: 0.5rem 0 0;
|
||||
}
|
||||
|
||||
.dd-p {
|
||||
margin: 0;
|
||||
color: var(--text-color-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Tags */
|
||||
.dd-tag {
|
||||
display: inline-block;
|
||||
padding: 1px 7px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.dd-tag.blue { background: #dbeafe; color: #1d4ed8; }
|
||||
.dd-tag.purple { background: #ede9fe; color: #6d28d9; }
|
||||
.dd-tag.green { background: #dcfce7; color: #15803d; }
|
||||
.dd-tag.orange { background: #ffedd5; color: #c2410c; }
|
||||
.dd-tag.teal { background: #ccfbf1; color: #0f766e; }
|
||||
.dd-tag.red { background: #fee2e2; color: #b91c1c; }
|
||||
.dd-tag.yellow { background: #fef9c3; color: #a16207; }
|
||||
|
||||
/* Dark mode tags */
|
||||
:global(.app-dark) .dd-tag.blue { background: #1e3a5f; color: #93c5fd; }
|
||||
:global(.app-dark) .dd-tag.purple { background: #2e1065; color: #c4b5fd; }
|
||||
:global(.app-dark) .dd-tag.green { background: #14532d; color: #86efac; }
|
||||
:global(.app-dark) .dd-tag.orange { background: #431407; color: #fdba74; }
|
||||
:global(.app-dark) .dd-tag.teal { background: #042f2e; color: #5eead4; }
|
||||
:global(.app-dark) .dd-tag.red { background: #450a0a; color: #fca5a5; }
|
||||
:global(.app-dark) .dd-tag.yellow { background: #422006; color: #fde68a; }
|
||||
|
||||
/* Grid 2 col */
|
||||
.dd-grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.dd-grid-2 { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.dd-card {
|
||||
background: var(--surface-ground);
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 10px;
|
||||
padding: 0.85rem 1rem;
|
||||
}
|
||||
.dd-card-title {
|
||||
font-weight: 700;
|
||||
font-size: 0.78rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-color-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
.dd-list {
|
||||
margin: 0;
|
||||
padding-left: 1.2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
.dd-list--spaced { gap: 0.5rem; }
|
||||
|
||||
/* Table */
|
||||
.dd-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.dd-table th {
|
||||
text-align: left;
|
||||
padding: 0.45rem 0.75rem;
|
||||
background: var(--surface-ground);
|
||||
border-bottom: 2px solid var(--surface-border);
|
||||
font-weight: 700;
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--text-color-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.dd-table td {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
vertical-align: top;
|
||||
color: var(--text-color);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.dd-table tr:hover td {
|
||||
background: var(--surface-ground);
|
||||
}
|
||||
.dd-table code {
|
||||
font-size: 0.78rem;
|
||||
background: var(--surface-ground);
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
/* Layers */
|
||||
.dd-layers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
}
|
||||
.dd-layer {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 1rem;
|
||||
padding: 0.6rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.dd-layer strong {
|
||||
flex-shrink: 0;
|
||||
min-width: 120px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.dd-layer span {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
.dd-layer--page { background: color-mix(in srgb, var(--primary-color) 10%, transparent); }
|
||||
.dd-layer--composable { background: color-mix(in srgb, var(--purple-500, #8b5cf6) 10%, transparent); }
|
||||
.dd-layer--service { background: color-mix(in srgb, var(--teal-500, #14b8a6) 10%, transparent); }
|
||||
.dd-layer--db { background: color-mix(in srgb, var(--orange-500, #f97316) 10%, transparent); }
|
||||
.dd-layer-arrow {
|
||||
text-align: center;
|
||||
color: var(--text-color-secondary);
|
||||
opacity: 0.4;
|
||||
line-height: 1.4;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* RPC cards */
|
||||
.dd-rpc-card {
|
||||
background: var(--surface-ground);
|
||||
border: 1px solid var(--surface-border);
|
||||
border-left: 4px solid var(--primary-color);
|
||||
border-radius: 8px;
|
||||
padding: 0.85rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.dd-rpc-name {
|
||||
font-weight: 700;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.88rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.dd-rpc-desc {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.dd-rpc-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
.dd-rpc-label {
|
||||
font-weight: 700;
|
||||
min-width: 90px;
|
||||
color: var(--text-color-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dd-rpc-row code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
background: var(--surface-card);
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.77rem;
|
||||
}
|
||||
|
||||
/* Steps */
|
||||
.dd-steps {
|
||||
margin: 0;
|
||||
padding-left: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
color: var(--text-color-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.dd-steps code {
|
||||
background: var(--surface-ground);
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.77rem;
|
||||
}
|
||||
|
||||
/* Code block */
|
||||
.dd-code {
|
||||
background: #0f172a;
|
||||
color: #cbd5e1;
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1.1rem;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
font-size: 0.75rem;
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user