ZERADO
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
<script setup>
|
||||
import BestSellingWidget from '@/components/dashboard/BestSellingWidget.vue';
|
||||
import NotificationsWidget from '@/components/dashboard/NotificationsWidget.vue';
|
||||
import RecentSalesWidget from '@/components/dashboard/RecentSalesWidget.vue';
|
||||
import RevenueStreamWidget from '@/components/dashboard/RevenueStreamWidget.vue';
|
||||
import StatsWidget from '@/components/dashboard/StatsWidget.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card mb-0">
|
||||
<div class="flex justify-between mb-4">
|
||||
<div>
|
||||
<span class="text-primary font-medium">Área</span>
|
||||
<span class="text-muted-color"> da Clínica</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-center bg-blue-100 dark:bg-blue-400/10 rounded-border" style="width: 2.5rem; height: 2.5rem">
|
||||
<i class="pi pi-user text-blue-500 text-xl!"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-12 gap-8">
|
||||
<StatsWidget />
|
||||
|
||||
<div class="col-span-12 xl:col-span-6">
|
||||
<RecentSalesWidget />
|
||||
<BestSellingWidget />
|
||||
</div>
|
||||
<div class="col-span-12 xl:col-span-6">
|
||||
<RevenueStreamWidget />
|
||||
<NotificationsWidget />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<h1>Online Scheduling (Manage)</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// placeholder para futura implementação
|
||||
</script>
|
||||
@@ -0,0 +1,591 @@
|
||||
<!-- src/views/pages/admin/ClinicTypesPage.vue -->
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watch, nextTick } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
|
||||
import ModuleRow from '@/features/clinic/components/ModuleRow.vue'
|
||||
|
||||
import { useTenantStore } from '@/stores/tenantStore'
|
||||
import { useTenantFeaturesStore } from '@/stores/tenantFeaturesStore'
|
||||
import { useMenuStore } from '@/stores/menuStore'
|
||||
|
||||
const toast = useToast()
|
||||
const route = useRoute()
|
||||
|
||||
const tenantStore = useTenantStore()
|
||||
const tf = useTenantFeaturesStore()
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
const savingKey = ref(null)
|
||||
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”)
|
||||
const planDenied = ref(new Set())
|
||||
|
||||
const tenantId = computed(() =>
|
||||
tenantStore.activeTenantId ||
|
||||
tenantStore.tenantId ||
|
||||
tenantStore.currentTenantId ||
|
||||
tenantStore.tenant?.id ||
|
||||
null
|
||||
)
|
||||
|
||||
const role = computed(() => tenantStore.activeRole || tenantStore.role || null)
|
||||
|
||||
// ✅ Somente owners/admins da clínica podem alterar features.
|
||||
// Terapeutas enxergam a página em modo somente-leitura (sem toggles, sem presets).
|
||||
const isOwner = computed(() =>
|
||||
role.value === 'owner' || role.value === 'admin'
|
||||
)
|
||||
|
||||
const loading = computed(() => tf.loading || tenantStore.loading || booting.value)
|
||||
|
||||
const tenantReady = computed(() => !!tenantId.value && tenantStore.loaded)
|
||||
|
||||
function isOn (key) {
|
||||
if (!tenantId.value) return false
|
||||
try { return !!tf.isEnabled(key, tenantId.value) } catch { return false }
|
||||
}
|
||||
|
||||
function labelOf (key) {
|
||||
if (key === 'patients') return 'Pacientes'
|
||||
if (key === 'shared_reception') return 'Recepção / Secretária'
|
||||
if (key === 'rooms') return 'Salas / Coworking'
|
||||
if (key === 'intake_public') return 'Link externo de cadastro'
|
||||
return key
|
||||
}
|
||||
|
||||
function isPlanDeniedError (e) {
|
||||
const msg = String(e?.message || e || '')
|
||||
return msg.toLowerCase().includes('não permitida') && msg.toLowerCase().includes('plano')
|
||||
}
|
||||
|
||||
function markPlanDenied (key, e) {
|
||||
if (!key) return
|
||||
if (!isPlanDeniedError(e)) return
|
||||
const s = new Set(planDenied.value)
|
||||
s.add(key)
|
||||
planDenied.value = s
|
||||
}
|
||||
|
||||
function clearPlanDenied () {
|
||||
planDenied.value = new Set()
|
||||
}
|
||||
|
||||
function isLocked (key) {
|
||||
return (
|
||||
!isOwner.value ||
|
||||
!tenantReady.value ||
|
||||
loading.value ||
|
||||
applyingPreset.value ||
|
||||
!!savingKey.value ||
|
||||
planDenied.value.has(key)
|
||||
)
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// 🧠 Menu refresh (debounced)
|
||||
// evita "menu sumindo" ao resetar durante loading
|
||||
// ===============================
|
||||
let menuRefreshT = null
|
||||
function requestMenuRefresh () {
|
||||
if (menuRefreshT) clearTimeout(menuRefreshT)
|
||||
menuRefreshT = setTimeout(() => {
|
||||
if (tf.loading || tenantStore.loading || booting.value) {
|
||||
return requestMenuRefresh()
|
||||
}
|
||||
if (typeof menuStore.reset === 'function') menuStore.reset()
|
||||
}, 150)
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Recalcular menu SEM router.replace().
|
||||
* O menu some quando o reset acontece enquanto stores ainda carregam.
|
||||
*/
|
||||
async function afterFeaturesChanged () {
|
||||
if (!tenantId.value) return
|
||||
|
||||
// ✅ refresh suave (evita “pisca vazio”)
|
||||
await tf.fetchForTenant(tenantId.value, { force: false })
|
||||
|
||||
// ✅ nunca navegar/replace aqui
|
||||
requestMenuRefresh()
|
||||
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
async function reload () {
|
||||
if (!tenantId.value) return
|
||||
clearPlanDenied()
|
||||
|
||||
await tf.fetchForTenant(tenantId.value, { force: true })
|
||||
requestMenuRefresh()
|
||||
|
||||
toast.add({
|
||||
severity: 'info',
|
||||
summary: 'Atualizado',
|
||||
detail: 'Módulos recarregados.',
|
||||
life: 2000
|
||||
})
|
||||
}
|
||||
|
||||
async function toggle (key) {
|
||||
if (!isOwner.value) {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'Acesso restrito',
|
||||
detail: 'Apenas o administrador da clínica pode alterar módulos.',
|
||||
life: 3000
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!tenantId.value) {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'Sem tenant ativo',
|
||||
detail: 'Selecione/ative um tenant primeiro.',
|
||||
life: 2500
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (planDenied.value.has(key)) {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'Indisponível no plano',
|
||||
detail: `${labelOf(key)} não está disponível no plano atual.`,
|
||||
life: 2800
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (savingKey.value) return
|
||||
savingKey.value = key
|
||||
|
||||
try {
|
||||
const next = !isOn(key)
|
||||
|
||||
await tf.setForTenant(tenantId.value, key, next)
|
||||
await afterFeaturesChanged()
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Atualizado',
|
||||
detail: `${labelOf(key)}: ${next ? 'Ativado' : 'Desativado'}`,
|
||||
life: 2500
|
||||
})
|
||||
} catch (e) {
|
||||
markPlanDenied(key, e)
|
||||
|
||||
toast.add({
|
||||
severity: isPlanDeniedError(e) ? 'warn' : 'error',
|
||||
summary: isPlanDeniedError(e) ? 'Bloqueado pelo plano' : 'Erro',
|
||||
detail: e?.message || 'Falha ao atualizar módulo',
|
||||
life: 3800
|
||||
})
|
||||
} finally {
|
||||
savingKey.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function applyPreset (preset) {
|
||||
if (!isOwner.value) {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'Acesso restrito',
|
||||
detail: 'Apenas o administrador da clínica pode aplicar presets.',
|
||||
life: 3000
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!tenantId.value) return
|
||||
if (applyingPreset.value) return
|
||||
|
||||
clearPlanDenied()
|
||||
applyingPreset.value = true
|
||||
|
||||
try {
|
||||
const map = {
|
||||
coworking: {
|
||||
patients: false,
|
||||
shared_reception: false,
|
||||
rooms: true,
|
||||
intake_public: false
|
||||
},
|
||||
reception: {
|
||||
patients: false,
|
||||
shared_reception: true,
|
||||
rooms: false,
|
||||
intake_public: false
|
||||
},
|
||||
full: {
|
||||
patients: true,
|
||||
shared_reception: true,
|
||||
rooms: true,
|
||||
intake_public: true
|
||||
}
|
||||
}
|
||||
|
||||
const cfg = map[preset]
|
||||
if (!cfg) return
|
||||
|
||||
for (const [k, v] of Object.entries(cfg)) {
|
||||
try {
|
||||
await tf.setForTenant(tenantId.value, k, v)
|
||||
} catch (e) {
|
||||
markPlanDenied(k, e)
|
||||
toast.add({
|
||||
severity: isPlanDeniedError(e) ? 'warn' : 'error',
|
||||
summary: isPlanDeniedError(e) ? 'Bloqueado pelo plano' : 'Erro',
|
||||
detail: `${labelOf(k)}: ${e?.message || 'falha ao aplicar'}`,
|
||||
life: 4200
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await afterFeaturesChanged()
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Preset aplicado',
|
||||
detail: 'Configuração atualizada.',
|
||||
life: 2500
|
||||
})
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Erro',
|
||||
detail: e?.message || 'Falha ao aplicar preset',
|
||||
life: 3500
|
||||
})
|
||||
} finally {
|
||||
applyingPreset.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Carrega tenant/session se necessário
|
||||
onMounted(async () => {
|
||||
try {
|
||||
if (!tenantStore.loaded && !tenantStore.loading) {
|
||||
await tenantStore.loadSessionAndTenant()
|
||||
}
|
||||
} finally {
|
||||
// o watch do tenantId fará o fetch; aqui só destrava a tela
|
||||
booting.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// Busca features sempre que o tenant ficar disponível (e no mount)
|
||||
watch(
|
||||
() => tenantId.value,
|
||||
async (id) => {
|
||||
if (!id) return
|
||||
|
||||
booting.value = true
|
||||
clearPlanDenied()
|
||||
|
||||
try {
|
||||
// ✅ não force no mount para evitar “pisca”
|
||||
await tf.fetchForTenant(id, { force: false })
|
||||
|
||||
// ✅ reset só quando estiver estável (debounced)
|
||||
requestMenuRefresh()
|
||||
|
||||
await nextTick()
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Erro ao carregar módulos',
|
||||
detail: e?.message || 'Falha ao buscar tenant_features',
|
||||
life: 4000
|
||||
})
|
||||
} finally {
|
||||
booting.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// blindagem: se a rota mudar dentro da área admin e o menu tiver resetado,
|
||||
// solicita refresh leve (sem navigation)
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
async () => {
|
||||
requestMenuRefresh()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4 md:p-6">
|
||||
<Toast />
|
||||
|
||||
<!-- Header -->
|
||||
<div class="mb-4 overflow-hidden rounded-[2rem] border border-[var(--surface-border)] bg-[var(--surface-card)]">
|
||||
<div class="relative p-5 md:p-7">
|
||||
<!-- blobs sutis -->
|
||||
<div class="pointer-events-none absolute inset-0 opacity-80">
|
||||
<div class="absolute -top-16 -right-20 h-72 w-72 rounded-full bg-indigo-400/10 blur-3xl" />
|
||||
<div class="absolute top-10 -left-24 h-80 w-80 rounded-full bg-emerald-400/10 blur-3xl" />
|
||||
<div class="absolute -bottom-20 right-24 h-72 w-72 rounded-full bg-fuchsia-400/10 blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div class="relative flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between gap-3 flex-wrap">
|
||||
<div class="min-w-0">
|
||||
<h1 class="text-xl md:text-2xl font-semibold leading-tight">Tipos de Clínica</h1>
|
||||
<p class="mt-1 text-sm opacity-80">
|
||||
Ative/desative recursos por clínica. Isso controla menu, rotas (guard) e acesso no banco (RLS).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0 flex items-center gap-2">
|
||||
<Button
|
||||
label="Recarregar"
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
outlined
|
||||
:loading="loading"
|
||||
:disabled="applyingPreset || !!savingKey"
|
||||
@click="reload"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex flex-wrap items-center gap-2 text-xs opacity-80">
|
||||
<span class="inline-flex items-center gap-2 rounded-full border border-[var(--surface-border)] px-3 py-1">
|
||||
<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">
|
||||
<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 opacity-70"
|
||||
>
|
||||
<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 opacity-70"
|
||||
>
|
||||
<i class="pi pi-spin pi-spinner" />
|
||||
Atualizando módulos…
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ⚠️ Banner: acesso somente leitura para terapeutas -->
|
||||
<div
|
||||
v-if="!isOwner && tenantReady"
|
||||
class="mb-4 flex items-center gap-3 rounded-[2rem] border border-amber-400/40 bg-amber-400/10 px-5 py-4 text-sm"
|
||||
>
|
||||
<i class="pi pi-lock text-amber-400 text-base shrink-0" />
|
||||
<span class="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="mb-4 grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<Card class="rounded-[2rem]">
|
||||
<template #content>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm font-semibold">Preset: Coworking</div>
|
||||
<div class="mt-1 text-xs opacity-80">
|
||||
Para aluguel de salas: sem pacientes, com salas.
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
label="Aplicar"
|
||||
severity="secondary"
|
||||
outlined
|
||||
:loading="applyingPreset"
|
||||
:disabled="!isOwner || !tenantReady || loading || !!savingKey"
|
||||
@click="applyPreset('coworking')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<Card class="rounded-[2rem]">
|
||||
<template #content>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm font-semibold">Preset: Clínica com recepção</div>
|
||||
<div class="mt-1 text-xs opacity-80">
|
||||
Para secretária gerenciar agenda (pacientes opcional).
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
label="Aplicar"
|
||||
severity="secondary"
|
||||
outlined
|
||||
:loading="applyingPreset"
|
||||
:disabled="!isOwner || !tenantReady || loading || !!savingKey"
|
||||
@click="applyPreset('reception')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<Card class="rounded-[2rem]">
|
||||
<template #content>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm font-semibold">Preset: Clínica completa</div>
|
||||
<div class="mt-1 text-xs opacity-80">
|
||||
Pacientes + recepção + salas (se quiser).
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
label="Aplicar"
|
||||
severity="secondary"
|
||||
outlined
|
||||
:loading="applyingPreset"
|
||||
:disabled="!isOwner || !tenantReady || loading || !!savingKey"
|
||||
@click="applyPreset('full')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Modules -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
||||
<Card class="rounded-[2rem]">
|
||||
<template #content>
|
||||
<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')"
|
||||
/>
|
||||
<div
|
||||
v-if="planDenied.has('patients')"
|
||||
class="mt-3 text-xs rounded-2xl border border-[var(--surface-border)] p-3 opacity-90"
|
||||
>
|
||||
<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-xs opacity-80 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 cá.</li>
|
||||
<li>RLS bloqueia acesso direto no banco.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<Card class="rounded-[2rem]">
|
||||
<template #content>
|
||||
<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')"
|
||||
/>
|
||||
<div
|
||||
v-if="planDenied.has('shared_reception')"
|
||||
class="mt-3 text-xs rounded-2xl border border-[var(--surface-border)] p-3 opacity-90"
|
||||
>
|
||||
<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-xs opacity-80 leading-relaxed">
|
||||
Observação: este módulo é “produto” (UX + permissões). A base aqui é só 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>
|
||||
<li>policies e telas para a secretária</li>
|
||||
<li>nível de visibilidade do paciente na agenda</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<Card class="rounded-[2rem]">
|
||||
<template #content>
|
||||
<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')"
|
||||
/>
|
||||
<div
|
||||
v-if="planDenied.has('rooms')"
|
||||
class="mt-3 text-xs rounded-2xl border border-[var(--surface-border)] p-3 opacity-90"
|
||||
>
|
||||
<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-xs opacity-80 leading-relaxed">
|
||||
Isso prepara o terreno para a clínica operar como locação de sala, com agenda vinculando sala + profissional.
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<Card class="rounded-[2rem]">
|
||||
<template #content>
|
||||
<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')"
|
||||
/>
|
||||
<div
|
||||
v-if="planDenied.has('intake_public')"
|
||||
class="mt-3 text-xs rounded-2xl border border-[var(--surface-border)] p-3 opacity-90"
|
||||
>
|
||||
<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-xs opacity-80 leading-relaxed">
|
||||
Você já tem páginas de link externo. Isso vira o controle fino: a clínica decide se usa ou não.
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* (sem estilos adicionais por enquanto) */
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user