Layout 100%, Notificações, SetupWizard

This commit is contained in:
Leonardo
2026-03-17 21:08:14 -03:00
parent 84d65e49c0
commit 66f67cd40f
77 changed files with 35823 additions and 15023 deletions
+7 -3
View File
@@ -4,6 +4,9 @@ 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';
import { useRouter } from 'vue-router';
const router = useRouter();
</script>
<template>
@@ -13,8 +16,9 @@ import StatsWidget from '@/components/dashboard/StatsWidget.vue';
<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 class="flex items-center gap-2">
<Button icon="pi pi-refresh" severity="secondary" outlined class="h-9 w-9 rounded-full" title="Recarregar" @click="$router.go(0)" />
<Button icon="pi pi-cog" severity="secondary" outlined class="h-9 w-9 rounded-full" title="Configurações" @click="router.push('/configuracoes')" />
</div>
</div>
</div>
@@ -30,4 +34,4 @@ import StatsWidget from '@/components/dashboard/StatsWidget.vue';
<NotificationsWidget />
</div>
</div>
</template>
</template>
@@ -324,268 +324,256 @@ watch(
</script>
<template>
<div class="p-4 md:p-6">
<Toast />
<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>
<!-- Sentinel -->
<div class=h-px />
<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>
<!-- 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)' }
>
<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>
<!-- Banner: acesso somente leitura para terapeutas -->
<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>
<Button
label=Recarregar
icon=pi pi-refresh
severity=secondary
outlined
: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>
</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>
<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
>
<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
>
<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>
<!-- Banner: somente leitura -->
<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"
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 text-base shrink-0" />
<span class="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="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 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>
<Button
size="small"
label="Aplicar"
severity="secondary"
outlined
:loading="applyingPreset"
:disabled="!isOwner || !tenantReady || loading || !!savingKey"
@click="applyPreset('coworking')"
/>
</div>
</template>
</Card>
<Button
size=small
label=Aplicar
severity=secondary
outlined
:loading=applyingPreset
:disabled=!isOwner || !tenantReady || loading || !!savingKey
@click=applyPreset('coworking')
/>
</div>
</div>
<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 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>
<Button
size="small"
label="Aplicar"
severity="secondary"
outlined
:loading="applyingPreset"
:disabled="!isOwner || !tenantReady || loading || !!savingKey"
@click="applyPreset('reception')"
/>
</div>
</template>
</Card>
<Button
size=small
label=Aplicar
severity=secondary
outlined
:loading=applyingPreset
:disabled=!isOwner || !tenantReady || loading || !!savingKey
@click=applyPreset('reception')
/>
</div>
</div>
<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 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>
<Button
size="small"
label="Aplicar"
severity="secondary"
outlined
:loading="applyingPreset"
:disabled="!isOwner || !tenantReady || loading || !!savingKey"
@click="applyPreset('full')"
/>
</div>
</template>
</Card>
<Button
size=small
label=Aplicar
severity=secondary
outlined
: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">
<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 .</li>
<li>RLS bloqueia acesso direto no banco.</li>
</ul>
</div>
</template>
</Card>
<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')
/>
<div
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 />
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>
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>
<li>RLS bloqueia acesso direto no banco.</li>
</ul>
</div>
</div>
<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 é 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>
<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')
/>
<div
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 />
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.
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>
</div>
<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>
<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')
/>
<div
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 />
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>
Isso prepara o terreno para a clínica operar como locação de sala, com agenda vinculando sala + profissional.
</div>
</div>
<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ê tem páginas de link externo. Isso vira o controle fino: a clínica decide se usa ou não.
</div>
</template>
</Card>
<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')
/>
<div
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 />
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>
Você tem páginas de link externo. Isso vira o controle fino: a clínica decide se usa ou não.
</div>
</div>
</div>
</div>
</template>
<style scoped>
/* (sem estilos adicionais por enquanto) */
</style>
@@ -51,6 +51,12 @@ const loadingHistory = ref(false)
const loadHistoryError = ref('')
const historySearch = ref('')
const HIGHLIGHT_MS = 24 * 60 * 60 * 1000
function isRecent (row) {
if (!row?.created_at) return false
return Date.now() - new Date(row.created_at).getTime() < HIGHLIGHT_MS
}
const filteredHistory = computed(() => {
const q = (historySearch.value || '').trim().toLowerCase()
const base = history.value || []
@@ -96,8 +102,12 @@ const activeTenantKind = ref(null)
const canManage = computed(() => {
const r = (effectiveRole.value || '').toString()
const isAdmin = r === 'clinic_admin' || r === 'tenant_admin'
// só pode gerenciar se for admin E o tenant for uma clínica (não pessoal/saas)
return isAdmin && activeTenantKind.value === 'clinic'
if (!isAdmin) return false
// Aceita qualquer kind de clínica: 'clinic', 'clinic_coworking', 'clinic_reception', 'clinic_full'
// Se activeTenantKind ainda não carregou (null), confia no role já normalizado
const k = String(activeTenantKind.value || '')
if (!k) return true
return k === 'clinic' || k.startsWith('clinic_')
})
const DEV_TEST_EMAILS = [
@@ -733,91 +743,76 @@ onMounted(async () => {
</script>
<template>
<div class="p-4 md:p-6">
<Toast />
<ConfirmDialog />
<Toast />
<ConfirmDialog />
<!-- 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" />
<!-- Sentinel -->
<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)' }"
>
<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)]">Profissionais da clínica</div>
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-0.5">
Gerencie terapeutas e secretarias vinculados ao seu tenant.
</div>
</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">
<div class="text-xl md:text-2xl font-semibold leading-tight">
Profissionais da clínica
</div>
<div class="opacity-70 text-sm">
Gerencie terapeutas e secretarias vinculados ao seu tenant.
</div>
</div>
<div class="flex items-center gap-2 flex-wrap">
<Button
label="Convidar terapeuta"
icon="pi pi-user-plus"
@click="openInvite('therapist')"
:disabled="!tenantReady || !canManage"
/>
<Button
label="Adicionar secretária"
icon="pi pi-users"
severity="secondary"
outlined
@click="openInvite('secretary')"
:disabled="!tenantReady || !canManage"
/>
</div>
</div>
<!-- Aviso (regra futura) -->
<div class="mt-3 rounded-2xl border border-[var(--surface-border)] bg-[color-mix(in_srgb,var(--surface-card),transparent_30%)] p-3">
<div class="flex gap-3">
<i class="pi pi-info-circle mt-0.5 opacity-70" />
<div class="text-sm">
<div class="font-semibold">Atenção</div>
<div class="opacity-80 leading-relaxed">
A regra impedir desvincular terapeuta com atendimentos agendados será ativada quando a agenda
registrar o terapeuta no evento (ex.: <span class="font-mono">agenda_eventos.terapeuta_id</span>)
ou quando existir a tabela de sessões/appointments. Por enquanto, a ação de desvincular apenas
desativa o vínculo.
</div>
</div>
</div>
</div>
<!-- Loading leve do tenant -->
<div v-if="!tenantReady" class="mt-2 text-sm opacity-70">
Carregando permissões da clínica
</div>
<!-- Aviso de permissão -->
<div v-else-if="!canManage" class="mt-2 text-sm text-orange-600">
Sua conta não tem permissão para gerenciar profissionais (apenas <b>clinic_admin</b>).
</div>
<!-- Debug (opcional) -->
<div v-if="debug" class="mt-2 text-xs opacity-70">
tenantId={{ tenantId }} | role={{ effectiveRole || '(vazio)' }} | canManage={{ canManage }}
</div>
<div class="flex items-center gap-2 flex-wrap shrink-0">
<Button
label="Convidar terapeuta"
icon="pi pi-user-plus"
@click="openInvite('therapist')"
:disabled="!tenantReady || !canManage"
/>
<Button
label="Adicionar secretária"
icon="pi pi-users"
severity="secondary"
outlined
@click="openInvite('secretary')"
:disabled="!tenantReady || !canManage"
/>
</div>
</div>
<!-- Loading leve do tenant -->
<div v-if="!tenantReady" class="text-[1rem] text-[var(--text-color-secondary)] opacity-70">
Carregando permissões da clínica
</div>
<!-- Aviso de permissão (somente se carregou e não tem permissão) -->
<div v-else-if="!canManage" class="text-[1rem] text-orange-600">
Sua conta não tem permissão para gerenciar profissionais (apenas <b>clinic_admin</b>).
</div>
<!-- Debug (opcional) -->
<div v-if="debug" class="text-[1rem] opacity-70">
tenantId={{ tenantId }} | role={{ effectiveRole || '(vazio)' }} | canManage={{ canManage }}
</div>
</div>
</div>
<div class="px-3 md:px-4 pb-8 flex flex-col gap-4">
<!-- 🔎 Aviso sobre logins de teste -->
<!-- 🔎 Aviso sobre logins de teste + atalhos de convite -->
<div
class="mb-4 overflow-hidden rounded-[2rem] border border-[var(--surface-border)] bg-[color-mix(in_srgb,var(--surface-card),transparent_18%)]"
>
<!-- 🔎 Aviso sobre logins de teste + atalhos de convite -->
<div
class="overflow-hidden rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)]"
>
<div class="p-5 md:p-6">
<div class="flex items-start gap-3">
<div class="h-10 w-10 rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] grid place-items-center shrink-0">
<div class="h-10 w-10 rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] grid place-items-center shrink-0">
<i class="pi pi-info-circle opacity-70" />
</div>
@@ -826,18 +821,18 @@ onMounted(async () => {
Logins de teste (ambiente de desenvolvimento)
</div>
<div class="text-sm opacity-80 mt-1 leading-relaxed">
<div class="text-[1rem] opacity-80 mt-1 leading-relaxed">
As credenciais fixas para testes (clinic, therapist, therapist2, therapist3, secretary, patient e saas)
estão disponíveis na tela inicial do sistema (<span class="font-mono">HomeCards</span>).
</div>
<div class="text-sm opacity-80 mt-2 leading-relaxed">
<div class="text-[1rem] opacity-80 mt-2 leading-relaxed">
Para utilizá-las, basta realizar <b>logout da sessão atual</b> e selecionar o perfil desejado na tela inicial.
</div>
<!-- Atalhos: abrir dialog e preencher o email -->
<div class="mt-4 flex flex-wrap items-center gap-2">
<span class="text-xs opacity-70 mr-1">Atalhos (DEV):</span>
<span class="text-[1rem] opacity-70 mr-1">Atalhos (DEV):</span>
<Button
label="Convidar therapist2"
@@ -872,13 +867,13 @@ onMounted(async () => {
<!-- Links de convite pendentes para testes -->
<div class="mt-3 flex flex-col gap-1">
<span class="text-xs opacity-70 mb-1">Links de convite pendentes (DEV):</span>
<span class="text-[1rem] opacity-70 mb-1">Links de convite pendentes (DEV):</span>
<div
v-for="item in devInviteLinks"
:key="item.email"
class="flex items-center gap-2 flex-wrap"
>
<span class="text-xs font-mono opacity-70 w-56 truncate">{{ item.email }}</span>
<span class="text-[1rem] font-mono opacity-70 w-56 truncate">{{ item.email }}</span>
<Button
label="Copiar link de convite"
icon="pi pi-copy"
@@ -888,7 +883,7 @@ onMounted(async () => {
:disabled="!item.token"
@click="copyInviteLink(item)"
/>
<span v-if="!item.token" class="text-xs opacity-50 italic">sem convite pendente</span>
<span v-if="!item.token" class="text-[1rem] opacity-50 italic">sem convite pendente</span>
</div>
</div>
</div>
@@ -897,16 +892,16 @@ onMounted(async () => {
</div>
<!-- DOCUMENTAÇÃO INTERNA (visível na tela, para QA) -->
<div class="mb-4 overflow-hidden rounded-[2rem] border border-[var(--surface-border)] bg-[color-mix(in_srgb,var(--surface-card),transparent_12%)]">
<div class="overflow-hidden rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)]">
<div class="p-5 md:p-6">
<div class="flex items-start gap-3">
<div class="h-10 w-10 rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] grid place-items-center shrink-0">
<div class="h-10 w-10 rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] grid place-items-center shrink-0">
<i class="pi pi-book opacity-70" />
</div>
<div class="min-w-0">
<div class="font-semibold text-lg leading-tight">Guia rápido Convites (Modelo B) e como testar</div>
<div class="text-sm opacity-80 mt-1 leading-relaxed">
<div class="text-[1rem] opacity-80 mt-1 leading-relaxed">
Esta área existe para facilitar o QA/validação do fluxo de convites no SaaS multi-tenant.
A tela pública de aceite está em:
<span class="font-mono">/accept-invite?token=&lt;uuid&gt;</span>.
@@ -914,9 +909,9 @@ onMounted(async () => {
</div>
<div class="mt-4 grid gap-3">
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] p-4">
<div class="font-semibold text-sm mb-2">Rotas e comportamento esperado</div>
<ul class="text-sm opacity-80 leading-relaxed list-disc pl-5 space-y-1">
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-4">
<div class="font-semibold text-[1rem] mb-2">Rotas e comportamento esperado</div>
<ul class="text-[1rem] opacity-80 leading-relaxed list-disc pl-5 space-y-1">
<li><b>Aceite público:</b> <span class="font-mono">/accept-invite?token=&lt;uuid&gt;</span></li>
<li><b>Login:</b> <span class="font-mono">/auth/login</span></li>
<li>
@@ -930,16 +925,16 @@ onMounted(async () => {
<li><b>Erros esperados:</b> token inválido/expirado, convite usado, e-mail diferente (mismatch).</li>
</ul>
<div class="mt-3 text-xs opacity-70 leading-relaxed">
<div class="mt-3 text-[1rem] opacity-70 leading-relaxed">
<b>Nota:</b> o backend foi corrigido para não depender do claim de email no JWT
(erro antigo <span class="font-mono">missing_email_claim</span>). O email é resolvido via
<span class="font-mono">auth.users</span> usando <span class="font-mono">SECURITY DEFINER</span>.
</div>
</div>
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] p-4">
<div class="font-semibold text-sm mb-2">Como testar (prático)</div>
<ol class="text-sm opacity-80 leading-relaxed list-decimal pl-5 space-y-1">
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-4">
<div class="font-semibold text-[1rem] mb-2">Como testar (prático)</div>
<ol class="text-[1rem] opacity-80 leading-relaxed list-decimal pl-5 space-y-1">
<li>Convidar alguém nesta tela (botões acima).</li>
<li>Abrir a aba <b>Convites</b> e copiar o link.</li>
<li>Abrir o link em aba anônima logar com o mesmo email aceitar.</li>
@@ -965,7 +960,7 @@ onMounted(async () => {
/>
</div>
<div class="mt-2 text-xs opacity-70">
<div class="mt-2 text-[1rem] opacity-70">
Dica: use aba anônima para testar o fluxo completo sem interferência de sessão.
</div>
</div>
@@ -974,18 +969,18 @@ onMounted(async () => {
</div>
<!-- Abas -->
<TabView class="rounded-[2rem] overflow-hidden">
<TabView class="rounded-md overflow-hidden">
<!-- =========================
ABA 1: EQUIPE
========================= -->
<TabPanel header="Equipe">
<div class="grid grid-cols-1 gap-4">
<Card class="rounded-[2rem]">
<Card class="rounded-md">
<template #title>
<div class="flex items-center justify-between gap-3 flex-wrap">
<div class="min-w-0">
<div class="font-semibold">Equipe</div>
<div class="text-sm opacity-70">
<div class="text-[1rem] opacity-70">
Membros ativos/inativos do tenant (somente <b>tenant_members</b>).
</div>
</div>
@@ -1015,7 +1010,7 @@ onMounted(async () => {
</template>
<template #content>
<div v-if="loadError" class="mb-3 rounded-2xl border border-red-200 bg-red-50 p-3 text-red-700 text-sm">
<div v-if="loadError" class="mb-3 rounded-md border border-red-200 bg-red-50 p-3 text-red-700 text-[1rem]">
{{ loadError }}
</div>
@@ -1024,14 +1019,15 @@ onMounted(async () => {
:loading="loading"
dataKey="user_id"
responsiveLayout="scroll"
class="p-datatable-sm"
class="p-datatable-sm prof-datatable"
sortField="role_sort"
:sortOrder="1"
:rowClass="(r) => isRecent(r) ? 'row-new-highlight' : ''"
>
<Column header="Pessoa" style="min-width: 18rem">
<template #body="{ data }">
<div class="flex items-center gap-3 min-w-0">
<div class="h-10 w-10 rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] grid place-items-center">
<div class="h-10 w-10 rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] grid place-items-center">
<i class="pi pi-user opacity-70" />
</div>
@@ -1040,15 +1036,15 @@ onMounted(async () => {
{{ data.full_name || 'Sem nome' }}
</div>
<div class="text-sm opacity-70 truncate">
<div class="text-[1rem] opacity-70 truncate">
{{ data.email || 'Sem email' }}
</div>
<div class="text-xs opacity-60 font-mono truncate">
<div class="text-[1rem] opacity-60 font-mono truncate">
tenant: {{ data.tenant_id }}
</div>
<div class="text-xs opacity-60 font-mono truncate">
<div class="text-[1rem] opacity-60 font-mono truncate">
uid: {{ data.user_id }}
</div>
</div>
@@ -1074,16 +1070,16 @@ onMounted(async () => {
<Column header="Vínculo" style="min-width: 12rem">
<template #body="{ data }">
<span v-if="canManage" class="text-sm opacity-70 italic">vinculado nesta clínica</span>
<span v-if="canManage" class="text-[1rem] opacity-70 italic">vinculado nesta clínica</span>
<template v-else>
<div v-if="myLinks.length > 0" class="flex flex-col gap-1">
<span
v-for="link in myLinks"
:key="link.tenant_id"
class="text-sm"
class="text-[1rem]"
>{{ link.clinic_name }}</span>
</div>
<span v-else class="text-sm opacity-50"></span>
<span v-else class="text-[1rem] opacity-50"></span>
</template>
</template>
</Column>
@@ -1113,7 +1109,7 @@ onMounted(async () => {
/>
</div>
<div v-if="data.is_self" class="text-xs opacity-60 mt-1 text-right">
<div v-if="data.is_self" class="text-[1rem] opacity-60 mt-1 text-right">
Você
</div>
</template>
@@ -1126,26 +1122,26 @@ onMounted(async () => {
</template>
</DataTable>
<small class="block mt-3 opacity-70">
<div class="text-[1rem] block mt-3 opacity-70">
Papel real salvo em <span class="font-mono">tenant_members.role</span>:
<b>tenant_admin</b>, <b>therapist</b>, <b>secretary</b>, <b>patient</b>.
No front, normalizamos <b>tenant_admin clinic_admin</b> (apenas para UI).
</small>
</div>
<small class="block mt-2 opacity-70">
<div class="text-[1rem] block mt-2 opacity-70">
<b>Status:</b> <span class="font-mono">active</span> = acesso liberado.
<span class="font-mono">inactive</span> = vínculo desativado (histórico).
</small>
</div>
</template>
</Card>
<!-- Meus Vínculos (visível apenas para terapeutas e secretárias) -->
<Card v-if="(effectiveRole === 'therapist' || effectiveRole === 'secretary') && (myLinks.length > 0 || loadingMyLinks)" class="rounded-[2rem]">
<Card v-if="(effectiveRole === 'therapist' || effectiveRole === 'secretary') && (myLinks.length > 0 || loadingMyLinks)" class="rounded-md">
<template #title>
<div class="flex items-center justify-between gap-3 flex-wrap">
<div class="min-w-0">
<div class="font-semibold">Meus vínculos</div>
<div class="text-sm opacity-70">
<div class="text-[1rem] opacity-70">
Clínicas às quais sua conta está associada.
</div>
</div>
@@ -1161,7 +1157,7 @@ onMounted(async () => {
</template>
<template #content>
<div v-if="loadMyLinksError" class="mb-3 rounded-2xl border border-red-200 bg-red-50 p-3 text-red-700 text-sm">
<div v-if="loadMyLinksError" class="mb-3 rounded-md border border-red-200 bg-red-50 p-3 text-red-700 text-[1rem]">
{{ loadMyLinksError }}
</div>
@@ -1169,15 +1165,15 @@ onMounted(async () => {
<div
v-for="link in myLinks"
:key="link.tenant_id"
class="flex items-center justify-between gap-4 rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] p-4"
class="flex items-center justify-between gap-4 rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-4"
>
<div class="flex items-center gap-3 min-w-0">
<div class="h-10 w-10 rounded-2xl border border-[var(--surface-border)] bg-[color-mix(in_srgb,var(--surface-card),transparent_30%)] grid place-items-center shrink-0">
<div class="h-10 w-10 rounded-md border border-[var(--surface-border)] bg-[color-mix(in_srgb,var(--surface-card),transparent_30%)] grid place-items-center shrink-0">
<i class="pi pi-building opacity-70" />
</div>
<div class="min-w-0">
<div class="font-semibold truncate">{{ link.clinic_name }}</div>
<div class="text-xs font-mono opacity-50 truncate">{{ link.tenant_id }}</div>
<div class="text-[1rem] font-mono opacity-50 truncate">{{ link.tenant_id }}</div>
</div>
</div>
@@ -1192,9 +1188,9 @@ onMounted(async () => {
</div>
</div>
<small class="block mt-3 opacity-70">
<div class="text-[1rem] block mt-3 opacity-70">
Um profissional pode estar vinculado a múltiplas clínicas simultaneamente.
</small>
</div>
</template>
</Card>
</div>
@@ -1202,12 +1198,12 @@ onMounted(async () => {
========================= -->
<TabPanel header="Convites">
<div class="grid grid-cols-1 gap-4">
<Card class="rounded-[2rem]">
<Card class="rounded-md">
<template #title>
<div class="flex items-center justify-between gap-3 flex-wrap">
<div class="min-w-0">
<div class="font-semibold">Convites pendentes</div>
<div class="text-sm opacity-70">
<div class="text-[1rem] opacity-70">
Convites do tenant que ainda não foram aceitos (tabela <b>tenant_invites</b>).
</div>
</div>
@@ -1237,7 +1233,7 @@ onMounted(async () => {
</template>
<template #content>
<div v-if="loadInvitesError" class="mb-3 rounded-2xl border border-red-200 bg-red-50 p-3 text-red-700 text-sm">
<div v-if="loadInvitesError" class="mb-3 rounded-md border border-red-200 bg-red-50 p-3 text-red-700 text-[1rem]">
{{ loadInvitesError }}
</div>
@@ -1246,18 +1242,19 @@ onMounted(async () => {
:loading="loadingInvites"
dataKey="token"
responsiveLayout="scroll"
class="p-datatable-sm"
class="p-datatable-sm invites-datatable"
sortField="created_at"
:sortOrder="-1"
:rowClass="(r) => isRecent(r) ? 'row-new-highlight' : ''"
>
<Column header="Email" style="min-width: 18rem">
<template #body="{ data }">
<div class="min-w-0">
<div class="font-semibold truncate">{{ data.email }}</div>
<div class="text-xs opacity-60 font-mono truncate">
<div class="text-[1rem] opacity-60 font-mono truncate">
token: {{ data.token }}
</div>
<div class="text-xs opacity-60 font-mono truncate">
<div class="text-[1rem] opacity-60 font-mono truncate">
tenant: {{ data.tenant_id }}
</div>
</div>
@@ -1272,13 +1269,13 @@ onMounted(async () => {
<Column header="Expira" style="width: 14rem">
<template #body="{ data }">
<span class="text-sm opacity-80">{{ formatDate(data.expires_at) }}</span>
<span class="text-[1rem] opacity-80">{{ formatDate(data.expires_at) }}</span>
</template>
</Column>
<Column header="Criado em" style="width: 14rem">
<template #body="{ data }">
<span class="text-sm opacity-80">{{ formatDate(data.created_at) }}</span>
<span class="text-[1rem] opacity-80">{{ formatDate(data.created_at) }}</span>
</template>
</Column>
@@ -1325,15 +1322,15 @@ onMounted(async () => {
</template>
</DataTable>
<small class="block mt-3 opacity-70">
<div class="text-[1rem] block mt-3 opacity-70">
<b>Modelo B:</b> convidar não cria membership. O membership aparece na aba <b>Equipe</b> após o aceite em
<span class="font-mono">/accept-invite?token=...</span>.
</small>
</div>
</template>
</Card>
<!-- QA TOOL: Auth Users (TEMPORÁRIO) -->
<Card class="rounded-[2rem] mt-2">
<Card class="rounded-md mt-2">
<template #title>
<div class="flex items-center justify-between">
<div class="font-semibold">Usuários cadastrados (Auth)</div>
@@ -1349,10 +1346,10 @@ onMounted(async () => {
</template>
<template #content>
<div class="mb-4 rounded-2xl border border-orange-200 bg-orange-50 p-4">
<div class="mb-4 rounded-md border border-orange-200 bg-orange-50 p-4">
<div class="flex gap-3">
<i class="pi pi-exclamation-triangle mt-0.5 text-orange-600" />
<div class="text-sm text-orange-800 leading-relaxed">
<div class="text-[1rem] text-orange-800 leading-relaxed">
<div class="font-semibold mb-1">
Aviso técnico View temporária para testes (QA)
</div>
@@ -1372,14 +1369,14 @@ onMounted(async () => {
Remover após validação:
</div>
<div class="mt-2 font-mono text-xs bg-white/60 border border-orange-200 rounded-xl p-2">
<div class="mt-2 font-mono text-[1rem] bg-white/60 border border-orange-200 rounded-md p-2">
drop view if exists public.v_auth_users_public;
</div>
</div>
</div>
</div>
<div v-if="loadUsersError" class="mb-3 rounded-2xl border border-red-200 bg-red-50 p-3 text-red-700 text-sm">
<div v-if="loadUsersError" class="mb-3 rounded-md border border-red-200 bg-red-50 p-3 text-red-700 text-[1rem]">
{{ loadUsersError }}
</div>
@@ -1393,7 +1390,7 @@ onMounted(async () => {
<Column field="email" header="Email" style="min-width: 18rem" />
<Column header="User ID" style="min-width: 22rem">
<template #body="{ data }">
<span class="text-xs font-mono opacity-70 break-all">{{ data.user_id }}</span>
<span class="text-[1rem] font-mono opacity-70 break-all">{{ data.user_id }}</span>
</template>
</Column>
<Column field="created_at" header="Criado em" />
@@ -1409,12 +1406,12 @@ onMounted(async () => {
========================= -->
<TabPanel header="Histórico">
<div class="grid grid-cols-1 gap-4">
<Card class="rounded-[2rem]">
<Card class="rounded-md">
<template #title>
<div class="flex items-center justify-between gap-3 flex-wrap">
<div class="min-w-0">
<div class="font-semibold">Histórico de desvinculados</div>
<div class="text-sm opacity-70">
<div class="text-[1rem] opacity-70">
Membros inativos e convites revogados ou expirados deste tenant.
</div>
</div>
@@ -1444,7 +1441,7 @@ onMounted(async () => {
</template>
<template #content>
<div v-if="loadHistoryError" class="mb-3 rounded-2xl border border-red-200 bg-red-50 p-3 text-red-700 text-sm">
<div v-if="loadHistoryError" class="mb-3 rounded-md border border-red-200 bg-red-50 p-3 text-red-700 text-[1rem]">
{{ loadHistoryError }}
</div>
@@ -1460,13 +1457,13 @@ onMounted(async () => {
<Column header="Pessoa / Email" style="min-width: 18rem">
<template #body="{ data }">
<div class="flex items-center gap-3 min-w-0">
<div class="h-10 w-10 rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] grid place-items-center shrink-0">
<div class="h-10 w-10 rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] grid place-items-center shrink-0">
<i :class="data.kind === 'member' ? 'pi pi-user opacity-70' : 'pi pi-envelope opacity-70'" />
</div>
<div class="min-w-0">
<div class="font-semibold truncate">{{ data.full_name || data.email || 'Sem identificação' }}</div>
<div v-if="data.full_name" class="text-sm opacity-70 truncate">{{ data.email }}</div>
<div class="text-xs opacity-50 font-mono truncate">
<div v-if="data.full_name" class="text-[1rem] opacity-70 truncate">{{ data.email }}</div>
<div class="text-[1rem] opacity-50 font-mono truncate">
{{ data.kind === 'member' ? 'uid: ' + data.user_id : 'token: ' + data.token }}
</div>
</div>
@@ -1502,10 +1499,10 @@ onMounted(async () => {
<Column header="Data" style="width: 14rem">
<template #body="{ data }">
<div class="text-sm opacity-80">
<div class="text-[1rem] opacity-80">
<div>Criado: {{ formatDate(data.created_at) }}</div>
<div v-if="data.revoked_at" class="text-xs text-red-500 mt-0.5">Revogado: {{ formatDate(data.revoked_at) }}</div>
<div v-else-if="data.expires_at && data.kind !== 'member'" class="text-xs opacity-60 mt-0.5">Expirou: {{ formatDate(data.expires_at) }}</div>
<div v-if="data.revoked_at" class="text-[1rem] text-red-500 mt-0.5">Revogado: {{ formatDate(data.revoked_at) }}</div>
<div v-else-if="data.expires_at && data.kind !== 'member'" class="text-[1rem] opacity-60 mt-0.5">Expirou: {{ formatDate(data.expires_at) }}</div>
</div>
</template>
</Column>
@@ -1532,9 +1529,9 @@ onMounted(async () => {
</template>
</DataTable>
<small class="block mt-3 opacity-70">
<div class="text-[1rem] block mt-3 opacity-70">
Membros inativos podem ser reativados a qualquer momento. Convites revogados/expirados são apenas registro histórico.
</small>
</div>
</template>
</Card>
</div>
@@ -1548,10 +1545,9 @@ onMounted(async () => {
dismissableMask
:style="{ width: 'min(520px, 94vw)' }"
:header="inviteHeader"
class="rounded-[2rem] overflow-hidden"
>
<div class="space-y-4">
<div class="text-sm opacity-80 leading-relaxed">
<div class="text-[1rem] text-[var(--text-color-secondary)] leading-relaxed">
Informe o email. Este fluxo cria um convite pendente (Modelo B) e ativa o vínculo após o aceite em
<span class="font-mono">/accept-invite</span>.
</div>
@@ -1577,7 +1573,7 @@ onMounted(async () => {
/>
</div>
<div v-if="invite.error" class="rounded-2xl border border-red-200 bg-red-50 p-3 text-red-700 text-sm">
<div v-if="invite.error" class="rounded-md border border-red-200 bg-red-50 p-3 text-[1rem] text-red-700">
{{ invite.error }}
</div>
</div>
@@ -1586,4 +1582,8 @@ onMounted(async () => {
</template>
<style scoped>
.prof-datatable :deep(tr.row-new-highlight td) { background-color: #f0fdf4 !important; }
.invites-datatable :deep(tr.row-new-highlight td) { background-color: #f0fdf4 !important; }
:global(.app-dark) .prof-datatable :deep(tr.row-new-highlight td) { background-color: rgba(16,185,129,0.08) !important; }
:global(.app-dark) .invites-datatable :deep(tr.row-new-highlight td) { background-color: rgba(16,185,129,0.08) !important; }
</style>