carousel, agenda arquivados, agenda cor, agenda arquivados, grupos pacientes, pacientes arquivados - desativados, sessoes verificadas, ajuste notificações, Prontuario, Agenda Animation, Menu Profile, bagdes Profile, Offline

This commit is contained in:
Leonardo
2026-03-18 09:26:09 -03:00
parent 66f67cd40f
commit d6d2fe29d1
55 changed files with 3655 additions and 1512 deletions
@@ -24,7 +24,7 @@ const applyingPreset = ref(false)
// evita cliques enquanto o contexto inicial ainda tá montando
const booting = ref(true)
// guarda features que o plano bloqueou (pra não ficar clicando e errando)
// guarda features que o plano bloqueou (pra não ficar "clicando e errando")
const planDenied = ref(new Set())
const tenantId = computed(() =>
@@ -110,7 +110,7 @@ function requestMenuRefresh () {
async function afterFeaturesChanged () {
if (!tenantId.value) return
// ✅ refresh suave (evita pisca vazio)
// ✅ refresh suave (evita "pisca vazio")
await tf.fetchForTenant(tenantId.value, { force: false })
// ✅ nunca navegar/replace aqui
@@ -292,7 +292,7 @@ watch(
clearPlanDenied()
try {
// ✅ não force no mount para evitar pisca
// ✅ não force no mount para evitar "pisca"
await tf.fetchForTenant(id, { force: false })
// ✅ reset só quando estiver estável (debounced)
@@ -327,246 +327,246 @@ watch(
<Toast />
<!-- Sentinel -->
<div class=h-px />
<div class="h-px" />
<!-- Hero sticky -->
<div
class=sticky mx-3 md:mx-4 mb-4 z-20 overflow-hidden rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5
:style={ top: 'var(--layout-sticky-top, 56px)' }
class="sticky mx-3 md:mx-4 mb-4 z-20 overflow-hidden rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5"
:style="{ top: 'var(--layout-sticky-top, 56px)' }"
>
<div class=absolute inset-0 pointer-events-none overflow-hidden aria-hidden=true>
<div class=absolute rounded-full blur-[70px] w-72 h-72 -top-16 -right-20 bg-indigo-400/10 />
<div class=absolute rounded-full blur-[70px] w-80 h-80 top-10 -left-24 bg-emerald-400/10 />
<div class=absolute rounded-full blur-[70px] w-72 h-72 -bottom-20 right-24 bg-fuchsia-400/10 />
<div class="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
<div class="absolute rounded-full blur-[70px] w-72 h-72 -top-16 -right-20 bg-indigo-400/10" />
<div class="absolute rounded-full blur-[70px] w-80 h-80 top-10 -left-24 bg-emerald-400/10" />
<div class="absolute rounded-full blur-[70px] w-72 h-72 -bottom-20 right-24 bg-fuchsia-400/10" />
</div>
<div class=relative z-10 flex flex-col gap-2>
<div class=flex items-center justify-between gap-3 flex-wrap>
<div class=min-w-0>
<div class=text-[1rem] font-bold tracking-tight text-[var(--text-color)]>Tipos de Clínica</div>
<div class=text-[1rem] text-[var(--text-color-secondary)] mt-0.5>
<div class="relative z-10 flex flex-col gap-2">
<div class="flex items-center justify-between gap-3 flex-wrap">
<div class="min-w-0">
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)]">Tipos de Clínica</div>
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-0.5">
Ative/desative recursos por clínica. Controla menu, rotas e acesso no banco (RLS).
</div>
</div>
<div class=shrink-0 flex items-center gap-2>
<div class="shrink-0 flex items-center gap-2">
<Button
label=Recarregar
icon=pi pi-refresh
severity=secondary
label="Recarregar"
icon="pi pi-refresh"
severity="secondary"
outlined
:loading=loading
:disabled=applyingPreset || !!savingKey
@click=reload
:loading="loading"
:disabled="applyingPreset || !!savingKey"
@click="reload"
/>
</div>
</div>
<div class=flex flex-wrap items-center gap-2>
<span class=inline-flex items-center gap-2 rounded-full border border-[var(--surface-border)] px-3 py-1 text-[1rem] text-[var(--text-color-secondary)]>
<i class=pi pi-building />
Tenant: <b class=font-mono>{{ tenantId || '' }}</b>
<div class="flex flex-wrap items-center gap-2">
<span class="inline-flex items-center gap-2 rounded-full border border-[var(--surface-border)] px-3 py-1 text-[1rem] text-[var(--text-color-secondary)]">
<i class="pi pi-building" />
Tenant: <b class="font-mono">{{ tenantId || '"' }}</b>
</span>
<span class=inline-flex items-center gap-2 rounded-full border border-[var(--surface-border)] px-3 py-1 text-[1rem] text-[var(--text-color-secondary)]>
<i class=pi pi-user />
Role: <b>{{ role || '' }}</b>
<span class="inline-flex items-center gap-2 rounded-full border border-[var(--surface-border)] px-3 py-1 text-[1rem] text-[var(--text-color-secondary)]">
<i class="pi pi-user" />
Role: <b>{{ role || '"' }}</b>
</span>
<span
v-if=!tenantReady
class=inline-flex items-center gap-2 rounded-full border border-[var(--surface-border)] px-3 py-1 text-[1rem] text-[var(--text-color-secondary)] opacity-70
v-if="!tenantReady"
class="inline-flex items-center gap-2 rounded-full border border-[var(--surface-border)] px-3 py-1 text-[1rem] text-[var(--text-color-secondary)] opacity-70"
>
<i class=pi pi-spin pi-spinner />
<i class="pi pi-spin pi-spinner" />
Carregando contexto
</span>
<span
v-else-if=loading
class=inline-flex items-center gap-2 rounded-full border border-[var(--surface-border)] px-3 py-1 text-[1rem] text-[var(--text-color-secondary)] opacity-70
v-else-if="loading"
class="inline-flex items-center gap-2 rounded-full border border-[var(--surface-border)] px-3 py-1 text-[1rem] text-[var(--text-color-secondary)] opacity-70"
>
<i class=pi pi-spin pi-spinner />
<i class="pi pi-spin pi-spinner" />
Atualizando módulos
</span>
</div>
</div>
</div>
<div class=px-3 md:px-4 pb-8 flex flex-col gap-4>
<div class="px-3 md:px-4 pb-8 flex flex-col gap-4">
<!-- Banner: somente leitura -->
<div
v-if=!isOwner && tenantReady
class=flex items-center gap-3 rounded-md border border-amber-400/40 bg-amber-400/10 px-5 py-4 text-[1rem]
v-if="!isOwner && tenantReady"
class="flex items-center gap-3 rounded-md border border-amber-400/40 bg-amber-400/10 px-5 py-4 text-[1rem]"
>
<i class=pi pi-lock text-amber-400 shrink-0 />
<span class=text-[1rem] text-[var(--text-color)] opacity-90>
<i class="pi pi-lock text-amber-400 shrink-0" />
<span class="text-[1rem] text-[var(--text-color)] opacity-90">
Você está visualizando as configurações da clínica em <b>modo somente leitura</b>.
Apenas o administrador pode ativar ou desativar módulos.
</span>
</div>
<!-- Presets -->
<div class=grid grid-cols-1 md:grid-cols-3 gap-3>
<div class=rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5>
<div class=flex items-start justify-between gap-3>
<div class=min-w-0>
<div class=text-[1rem] font-semibold text-[var(--text-color)]>Preset: Coworking</div>
<div class=mt-1 text-[1rem] text-[var(--text-color-secondary)]>
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<div class="text-[1rem] font-semibold text-[var(--text-color)]">Preset: Coworking</div>
<div class="mt-1 text-[1rem] text-[var(--text-color-secondary)]">
Para aluguel de salas: sem pacientes, com salas.
</div>
</div>
<Button
size=small
label=Aplicar
severity=secondary
size="small"
label="Aplicar"
severity="secondary"
outlined
:loading=applyingPreset
:disabled=!isOwner || !tenantReady || loading || !!savingKey
@click=applyPreset('coworking')
:loading="applyingPreset"
:disabled="!isOwner || !tenantReady || loading || !!savingKey"
@click="applyPreset('coworking')"
/>
</div>
</div>
<div class=rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5>
<div class=flex items-start justify-between gap-3>
<div class=min-w-0>
<div class=text-[1rem] font-semibold text-[var(--text-color)]>Preset: Clínica com recepção</div>
<div class=mt-1 text-[1rem] text-[var(--text-color-secondary)]>
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<div class="text-[1rem] font-semibold text-[var(--text-color)]">Preset: Clínica com recepção</div>
<div class="mt-1 text-[1rem] text-[var(--text-color-secondary)]">
Para secretária gerenciar agenda (pacientes opcional).
</div>
</div>
<Button
size=small
label=Aplicar
severity=secondary
size="small"
label="Aplicar"
severity="secondary"
outlined
:loading=applyingPreset
:disabled=!isOwner || !tenantReady || loading || !!savingKey
@click=applyPreset('reception')
:loading="applyingPreset"
:disabled="!isOwner || !tenantReady || loading || !!savingKey"
@click="applyPreset('reception')"
/>
</div>
</div>
<div class=rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5>
<div class=flex items-start justify-between gap-3>
<div class=min-w-0>
<div class=text-[1rem] font-semibold text-[var(--text-color)]>Preset: Clínica completa</div>
<div class=mt-1 text-[1rem] text-[var(--text-color-secondary)]>
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<div class="text-[1rem] font-semibold text-[var(--text-color)]">Preset: Clínica completa</div>
<div class="mt-1 text-[1rem] text-[var(--text-color-secondary)]">
Pacientes + recepção + salas (se quiser).
</div>
</div>
<Button
size=small
label=Aplicar
severity=secondary
size="small"
label="Aplicar"
severity="secondary"
outlined
:loading=applyingPreset
:disabled=!isOwner || !tenantReady || loading || !!savingKey
@click=applyPreset('full')
:loading="applyingPreset"
:disabled="!isOwner || !tenantReady || loading || !!savingKey"
@click="applyPreset('full')"
/>
</div>
</div>
</div>
<!-- Modules -->
<div class=grid grid-cols-1 lg:grid-cols-2 gap-3>
<div class=rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-3">
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5">
<ModuleRow
title=Pacientes
desc=Habilita gestão de pacientes por clínica. Todo paciente tem um responsável (therapist).
icon=pi pi-users
:enabled=isOn('patients')
:loading=savingKey === 'patients'
:disabled=isLocked('patients')
@toggle=toggle('patients')
title="Pacientes"
desc="Habilita gestão de pacientes por clínica. Todo paciente tem um responsável (therapist)."
icon="pi pi-users"
:enabled="isOn('patients')"
:loading="savingKey === 'patients'"
:disabled="isLocked('patients')"
@toggle="toggle('patients')"
/>
<div
v-if=planDenied.has('patients')
class=mt-3 text-[1rem] rounded-md border border-[var(--surface-border)] p-3 opacity-90
v-if="planDenied.has('patients')"
class="mt-3 text-[1rem] rounded-md border border-[var(--surface-border)] p-3 opacity-90"
>
<i class=pi pi-lock mr-2 />
<i class="pi pi-lock mr-2" />
Este módulo foi bloqueado pelo plano atual do tenant.
</div>
<Divider class=my-4 />
<div class=text-[1rem] text-[var(--text-color-secondary)] leading-relaxed>
<Divider class="my-4" />
<div class="text-[1rem] text-[var(--text-color-secondary)] leading-relaxed">
Quando desligado:
<ul class=mt-2 list-disc pl-5 space-y-1>
<li>Menu Pacientes some.</li>
<li>Rotas com <span class=font-mono>meta.tenantFeature = 'patients'</span> redirecionam pra .</li>
<ul class="mt-2 list-disc pl-5 space-y-1">
<li>Menu "Pacientes" some.</li>
<li>Rotas com <span class="font-mono">meta.tenantFeature = 'patients'</span> redirecionam pra .</li>
<li>RLS bloqueia acesso direto no banco.</li>
</ul>
</div>
</div>
<div class=rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5>
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5">
<ModuleRow
title=Recepção / Secretária
desc=Permite um papel de secretária gerenciar a agenda dos profissionais (sem precisar ver tudo do paciente).
icon=pi pi-briefcase
:enabled=isOn('shared_reception')
:loading=savingKey === 'shared_reception'
:disabled=isLocked('shared_reception')
@toggle=toggle('shared_reception')
title="Recepção / Secretária"
desc="Permite um papel de secretária gerenciar a agenda dos profissionais (sem precisar ver tudo do paciente)."
icon="pi pi-briefcase"
:enabled="isOn('shared_reception')"
:loading="savingKey === 'shared_reception'"
:disabled="isLocked('shared_reception')"
@toggle="toggle('shared_reception')"
/>
<div
v-if=planDenied.has('shared_reception')
class=mt-3 text-[1rem] rounded-md border border-[var(--surface-border)] p-3 opacity-90
v-if="planDenied.has('shared_reception')"
class="mt-3 text-[1rem] rounded-md border border-[var(--surface-border)] p-3 opacity-90"
>
<i class=pi pi-lock mr-2 />
<i class="pi pi-lock mr-2" />
Este módulo foi bloqueado pelo plano atual do tenant.
</div>
<Divider class=my-4 />
<div class=text-[1rem] text-[var(--text-color-secondary)] leading-relaxed>
Observação: este módulo é produto (UX + permissões). A base aqui é o toggle.
<Divider class="my-4" />
<div class="text-[1rem] text-[var(--text-color-secondary)] leading-relaxed">
Observação: este módulo é "produto" (UX + permissões). A base aqui é o toggle.
Depois a gente cria:
<ul class=mt-2 list-disc pl-5 space-y-1>
<li>role <span class=font-mono>secretary</span> em <span class=font-mono>tenant_members</span></li>
<ul class="mt-2 list-disc pl-5 space-y-1">
<li>role <span class="font-mono">secretary</span> em <span class="font-mono">tenant_members</span></li>
<li>policies e telas para a secretária</li>
<li>nível de visibilidade do paciente na agenda</li>
</ul>
</div>
</div>
<div class=rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5>
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5">
<ModuleRow
title=Salas / Coworking
desc=Habilita cadastro e reserva de salas/recursos no agendamento.
icon=pi pi-building
:enabled=isOn('rooms')
:loading=savingKey === 'rooms'
:disabled=isLocked('rooms')
@toggle=toggle('rooms')
title="Salas / Coworking"
desc="Habilita cadastro e reserva de salas/recursos no agendamento."
icon="pi pi-building"
:enabled="isOn('rooms')"
:loading="savingKey === 'rooms'"
:disabled="isLocked('rooms')"
@toggle="toggle('rooms')"
/>
<div
v-if=planDenied.has('rooms')
class=mt-3 text-[1rem] rounded-md border border-[var(--surface-border)] p-3 opacity-90
v-if="planDenied.has('rooms')"
class="mt-3 text-[1rem] rounded-md border border-[var(--surface-border)] p-3 opacity-90"
>
<i class=pi pi-lock mr-2 />
<i class="pi pi-lock mr-2" />
Este módulo foi bloqueado pelo plano atual do tenant.
</div>
<Divider class=my-4 />
<div class=text-[1rem] text-[var(--text-color-secondary)] leading-relaxed>
<Divider class="my-4" />
<div class="text-[1rem] text-[var(--text-color-secondary)] leading-relaxed">
Isso prepara o terreno para a clínica operar como locação de sala, com agenda vinculando sala + profissional.
</div>
</div>
<div class=rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5>
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5">
<ModuleRow
title=Link externo de cadastro
desc=Libera fluxo público de intake/cadastro externo para a clínica.
icon=pi pi-link
:enabled=isOn('intake_public')
:loading=savingKey === 'intake_public'
:disabled=isLocked('intake_public')
@toggle=toggle('intake_public')
title="Link externo de cadastro"
desc="Libera fluxo público de intake/cadastro externo para a clínica."
icon="pi pi-link"
:enabled="isOn('intake_public')"
:loading="savingKey === 'intake_public'"
:disabled="isLocked('intake_public')"
@toggle="toggle('intake_public')"
/>
<div
v-if=planDenied.has('intake_public')
class=mt-3 text-[1rem] rounded-md border border-[var(--surface-border)] p-3 opacity-90
v-if="planDenied.has('intake_public')"
class="mt-3 text-[1rem] rounded-md border border-[var(--surface-border)] p-3 opacity-90"
>
<i class=pi pi-lock mr-2 />
<i class="pi pi-lock mr-2" />
Este módulo foi bloqueado pelo plano atual do tenant.
</div>
<Divider class=my-4 />
<div class=text-[1rem] text-[var(--text-color-secondary)] leading-relaxed>
<Divider class="my-4" />
<div class="text-[1rem] text-[var(--text-color-secondary)] leading-relaxed">
Você tem páginas de link externo. Isso vira o controle fino: a clínica decide se usa ou não.
</div>
</div>