- Dica: “Gerar usuário” preenche automaticamente com dados fictícios.
+ Dica: "Gerar usuário" preenche automaticamente com dados fictícios.
@@ -136,7 +136,7 @@ import { supabase } from '@/lib/supabase/client'
const { canSee } = useRoleGuard()
/**
- * Lista “curada” de pensadores influentes na psicanálise e seu entorno.
+ * Lista "curada" de pensadores influentes na psicanálise e seu entorno.
* Usada para geração rápida de dados fictícios.
*/
const PSICANALISE_PENSADORES = Object.freeze([
diff --git a/src/components/agenda/AgendaOnlineGradeCard.vue b/src/components/agenda/AgendaOnlineGradeCard.vue
index 876fa9c..9a2157a 100644
--- a/src/components/agenda/AgendaOnlineGradeCard.vue
+++ b/src/components/agenda/AgendaOnlineGradeCard.vue
@@ -134,7 +134,7 @@ onMounted(load)
-
+
Tipo de slots
@@ -158,7 +158,7 @@ onMounted(load)
Jornada do dia
- (Isso vem das suas “janelas semanais”)
+ (Isso vem das suas "janelas semanais")
diff --git a/src/components/agenda/PausasChipsEditor.vue b/src/components/agenda/PausasChipsEditor.vue
index b6b8f4f..9172b5b 100644
--- a/src/components/agenda/PausasChipsEditor.vue
+++ b/src/components/agenda/PausasChipsEditor.vue
@@ -76,7 +76,7 @@ function normalizeIntervals(list) {
return merged
}
-// retorna “sobras” de [s,e] depois de remover intervalos ocupados
+// retorna "sobras" de [s,e] depois de remover intervalos ocupados
function subtractIntervals(s, e, occupiedMerged) {
let segments = [{ s, e }]
for (const occ of occupiedMerged) {
@@ -127,7 +127,7 @@ function addPauseSmart({ label, inicio, fim }) {
fim: minToHHMM(seg.e)
}))
- // se houve “recorte”, avisa
+ // se houve "recorte", avisa
if (segments.length !== 1 || (segments[0].s !== s || segments[0].e !== e)) {
toast.add({
severity: 'info',
diff --git a/src/components/landing/FeaturesWidget.vue b/src/components/landing/FeaturesWidget.vue
index 6352155..49d3a8e 100644
--- a/src/components/landing/FeaturesWidget.vue
+++ b/src/components/landing/FeaturesWidget.vue
@@ -122,7 +122,7 @@
Joséphine Miller
Peak Interactive
- “Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.”
+ "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
diff --git a/src/components/notifications/NotificationDrawer.vue b/src/components/notifications/NotificationDrawer.vue
index a7671c5..409fbd9 100644
--- a/src/components/notifications/NotificationDrawer.vue
+++ b/src/components/notifications/NotificationDrawer.vue
@@ -103,7 +103,7 @@ function goToHistory () {
@@ -672,6 +684,15 @@ watch(() => route.path, () => hideMobileMenu())
border: 1px solid var(--surface-border);
color: var(--text-color-secondary);
}
+.rs__badge {
+ font-size: 0.62rem;
+ font-weight: 700;
+ padding: 1px 6px;
+ border-radius: 999px;
+ background: var(--primary-color);
+ color: #fff;
+ line-height: 1;
+}
/* ── Slide-in da esquerda ────────────────────────────────── */
.rs-slide-enter-active,
diff --git a/src/layout/AppSidebar.vue b/src/layout/AppSidebar.vue
index faff434..6d6744e 100644
--- a/src/layout/AppSidebar.vue
+++ b/src/layout/AppSidebar.vue
@@ -11,7 +11,7 @@ let outsideClickListener = null
// ✅ rota mudou:
// - atualiza activePath sempre (desktop e mobile)
-// - fecha menu SOMENTE no mobile (evita “sumir” no desktop / inconsistências)
+// - fecha menu SOMENTE no mobile (evita "sumir" no desktop / inconsistências)
watch(
() => route.path,
(newPath) => {
diff --git a/src/layout/AppTopbar.vue b/src/layout/AppTopbar.vue
index 5658377..a2b9b6f 100644
--- a/src/layout/AppTopbar.vue
+++ b/src/layout/AppTopbar.vue
@@ -495,7 +495,7 @@ async function logout () {
}
/**
- * ✅ Bootstrap entitlements (resolve “menu não alterna” sem depender do guard)
+ * ✅ Bootstrap entitlements (resolve "menu não alterna" sem depender do guard)
* - se tem tenant ativo => carrega tenant entitlements
* - senão => carrega user entitlements
*/
diff --git a/src/layout/composables/layout.js b/src/layout/composables/layout.js
index c89df11..1c8814b 100644
--- a/src/layout/composables/layout.js
+++ b/src/layout/composables/layout.js
@@ -44,7 +44,7 @@ const layoutState = reactive({
*
* Motivo: você aplica tema cedo (main.js / user_settings) e depois
* usa o composable em páginas/Topbar/Configurator. Se não sincronizar,
- * isDarkTheme pode ficar “mentindo”.
+ * isDarkTheme pode ficar "mentindo".
*/
let _syncedDarkFromDomOnce = false
function syncDarkFromDomOnce () {
diff --git a/src/navigation/index.js b/src/navigation/index.js
index af084c6..419635f 100644
--- a/src/navigation/index.js
+++ b/src/navigation/index.js
@@ -46,7 +46,7 @@ function resolveMenu (builder, ctx) {
}
}
-// core menu anti-“sumir”
+// core menu anti-"sumir"
function coreMenu () {
return [
{
diff --git a/src/navigation/menus/clinic.menu.js b/src/navigation/menus/clinic.menu.js
index fa2562b..8c75077 100644
--- a/src/navigation/menus/clinic.menu.js
+++ b/src/navigation/menus/clinic.menu.js
@@ -11,7 +11,8 @@ export default function adminMenu (ctx = {}) {
label: 'Agenda da Clínica',
icon: 'pi pi-fw pi-calendar',
to: { name: 'admin-agenda-clinica' },
- feature: 'agenda.view'
+ feature: 'agenda.view',
+ badgeKey: 'agendaHoje'
},
// ✅ Compromissos determinísticos (tipos)
@@ -41,7 +42,7 @@ export default function adminMenu (ctx = {}) {
{ label: 'Grupos', icon: 'pi pi-fw pi-sitemap', to: { name: 'admin-pacientes-grupos' } },
{ label: 'Tags', icon: 'pi pi-fw pi-tags', to: { name: 'admin-pacientes-tags' } },
{ label: 'Link Externo', icon: 'pi pi-fw pi-link', to: { name: 'admin-pacientes-link-externo' } },
- { label: 'Cadastros recebidos', icon: 'pi pi-inbox', to: { name: 'admin-pacientes-recebidos' } }
+ { label: 'Cadastros recebidos', icon: 'pi pi-inbox', to: { name: 'admin-pacientes-recebidos' }, badgeKey: 'cadastrosRecebidos' }
]
},
@@ -79,7 +80,8 @@ export default function adminMenu (ctx = {}) {
icon: 'pi pi-fw pi-inbox',
to: { name: 'admin-agendamentos-recebidos' },
feature: 'online_scheduling.manage',
- proBadge: true
+ proBadge: true,
+ badgeKey: 'agendamentosRecebidos'
}
]
}
diff --git a/src/navigation/menus/therapist.menu.js b/src/navigation/menus/therapist.menu.js
index 5209819..e48edd5 100644
--- a/src/navigation/menus/therapist.menu.js
+++ b/src/navigation/menus/therapist.menu.js
@@ -11,7 +11,7 @@ export default [
{
label: 'Agenda',
items: [
- { label: 'Agenda', icon: 'pi pi-fw pi-calendar', to: '/therapist/agenda', feature: 'agenda.view', proBadge: true },
+ { label: 'Agenda', icon: 'pi pi-fw pi-calendar', to: '/therapist/agenda', feature: 'agenda.view', proBadge: true, badgeKey: 'agendaHoje' },
{ label: 'Compromissos', icon: 'pi pi-fw pi-clock', to: '/therapist/agenda/compromissos', feature: 'agenda.view', proBadge: true }
]
},
@@ -23,7 +23,7 @@ export default [
{ label: 'Grupo de pacientes', icon: 'pi pi-fw pi-users', to: '/therapist/patients/grupos' },
{ label: 'Tags', icon: 'pi pi-tags', to: '/therapist/patients/tags' },
{ label: 'Meu link de cadastro', icon: 'pi pi-link', to: '/therapist/patients/link-externo' },
- { label: 'Cadastros recebidos', icon: 'pi pi-inbox', to: '/therapist/patients/cadastro/recebidos' }
+ { label: 'Cadastros recebidos', icon: 'pi pi-inbox', to: '/therapist/patients/cadastro/recebidos', badgeKey: 'cadastrosRecebidos' }
]
},
@@ -42,7 +42,8 @@ export default [
icon: 'pi pi-fw pi-inbox',
to: '/therapist/agendamentos-recebidos',
feature: 'online_scheduling.manage',
- proBadge: true
+ proBadge: true,
+ badgeKey: 'agendamentosRecebidos'
}
]
},
diff --git a/src/router/accessRedirects.js b/src/router/accessRedirects.js
index 4db1673..732ad7c 100644
--- a/src/router/accessRedirects.js
+++ b/src/router/accessRedirects.js
@@ -6,12 +6,12 @@
* - Entitlements (plano): usuário poderia acessar se tivesse feature → manda pro /upgrade
*
* Por que isso existe?
- * - Evitar o bug clássico: “pessoa sem permissão caiu no /upgrade”
+ * - Evitar o bug clássico: "pessoa sem permissão caiu no /upgrade"
* - Padronizar o comportamento do app em um único lugar
* - Deixar claro: RBAC ≠ Plano
*
* Convenção recomendada:
- * - RBAC (role): sempre é bloqueio (403) OU home do papel (UX mais “suave”)
+ * - RBAC (role): sempre é bloqueio (403) OU home do papel (UX mais "suave")
* - Plano (feature): sempre é upgrade (porque o usuário *poderia* ter acesso pagando)
*/
@@ -35,16 +35,16 @@ export function roleHomePath (role) {
/**
* RBAC (papel) → padrão: acesso negado (403).
*
- * Se você preferir UX “suave”, pode mandar para a home do papel.
+ * Se você preferir UX "suave", pode mandar para a home do papel.
* Eu deixei as duas opções:
* - use403 = true → sempre /pages/access (recomendado para clareza)
- * - use403 = false → home do papel (útil quando você quer “auto-corrigir” navegação)
+ * - use403 = false → home do papel (útil quando você quer "auto-corrigir" navegação)
*/
export function denyByRole ({ to, currentRole, use403 = true } = {}) {
// ✅ padrão forte: 403 (não é caso de upgrade)
if (use403) return { path: '/pages/access' }
- // modo “suave”: manda pra home do papel
+ // modo "suave": manda pra home do papel
const fallback = roleHomePath(currentRole)
// evita loop: se já está no fallback, manda pra página de acesso negado
diff --git a/src/router/guards.js b/src/router/guards.js
index d975e03..082248c 100644
--- a/src/router/guards.js
+++ b/src/router/guards.js
@@ -635,7 +635,7 @@ export function applyGuards (router) {
}
}
- // 🔥 FIX: se ainda assim não resolveu tenant/role e estamos em tenant area, não negue “por engano”
+ // 🔥 FIX: se ainda assim não resolveu tenant/role e estamos em tenant area, não negue "por engano"
if (isTenantArea && (!tenant.activeTenantId || !tenant.activeRole)) {
sessionStorage.setItem('redirect_after_login', to.fullPath)
_perfEnd()
diff --git a/src/services/agendaSlotsBloqueadosService.js b/src/services/agendaSlotsBloqueadosService.js
index 5e20a45..24b03aa 100644
--- a/src/services/agendaSlotsBloqueadosService.js
+++ b/src/services/agendaSlotsBloqueadosService.js
@@ -32,7 +32,7 @@ export async function setSlotBloqueado(ownerId, diaSemana, horaInicio, isBloquea
return true
}
- // “desbloquear”: deletar (ou marcar ativo=false; aqui vou deletar por simplicidade)
+ // "desbloquear": deletar (ou marcar ativo=false; aqui vou deletar por simplicidade)
const { error } = await supabase
.from('agenda_slots_bloqueados_semanais')
.delete()
diff --git a/src/sql-arquivos/patient_lifecycle.sql b/src/sql-arquivos/patient_lifecycle.sql
new file mode 100644
index 0000000..90d440f
--- /dev/null
+++ b/src/sql-arquivos/patient_lifecycle.sql
@@ -0,0 +1,85 @@
+-- ============================================================
+-- CICLO DE VIDA DE PACIENTES — patient_lifecycle.sql
+-- Rodar no Supabase SQL Editor (idempotente)
+-- ============================================================
+
+-- ------------------------------------------------------------
+-- 1.1 Expandir CHECK de status para incluir 'Arquivado'
+-- ------------------------------------------------------------
+ALTER TABLE public.patients
+ DROP CONSTRAINT IF EXISTS patients_status_check;
+
+ALTER TABLE public.patients
+ ADD CONSTRAINT patients_status_check
+ CHECK (status = ANY(ARRAY[
+ 'Ativo'::text,
+ 'Inativo'::text,
+ 'Alta'::text,
+ 'Encaminhado'::text,
+ 'Arquivado'::text
+ ]));
+
+-- ------------------------------------------------------------
+-- 1.2 can_delete_patient(uuid) → boolean
+-- Retorna false se existir histórico clínico ou financeiro
+-- ------------------------------------------------------------
+CREATE OR REPLACE FUNCTION public.can_delete_patient(p_patient_id uuid)
+RETURNS boolean
+LANGUAGE sql STABLE SECURITY DEFINER
+AS $$
+ SELECT NOT EXISTS (
+ SELECT 1 FROM public.agenda_eventos WHERE patient_id = p_patient_id
+ UNION ALL
+ SELECT 1 FROM public.recurrence_rules WHERE patient_id = p_patient_id
+ UNION ALL
+ SELECT 1 FROM public.billing_contracts WHERE patient_id = p_patient_id
+ );
+$$;
+
+GRANT EXECUTE ON FUNCTION public.can_delete_patient(uuid)
+ TO postgres, anon, authenticated, service_role;
+
+-- ------------------------------------------------------------
+-- 1.3 safe_delete_patient(uuid) → jsonb
+-- Verifica histórico antes de deletar fisicamente
+-- ------------------------------------------------------------
+CREATE OR REPLACE FUNCTION public.safe_delete_patient(p_patient_id uuid)
+RETURNS jsonb
+LANGUAGE plpgsql SECURITY DEFINER
+AS $$
+BEGIN
+ -- Bloqueia se houver histórico
+ IF NOT public.can_delete_patient(p_patient_id) THEN
+ RETURN jsonb_build_object(
+ 'ok', false,
+ 'error', 'has_history',
+ 'message', 'Este paciente possui histórico clínico ou financeiro e não pode ser removido. Você pode desativar ou arquivar o paciente.'
+ );
+ END IF;
+
+ -- Verifica ownership via RLS (owner_id ou responsible_member_id)
+ IF NOT EXISTS (
+ SELECT 1 FROM public.patients
+ WHERE id = p_patient_id
+ AND (
+ owner_id = auth.uid()
+ OR responsible_member_id IN (
+ SELECT id FROM public.tenant_members WHERE user_id = auth.uid()
+ )
+ )
+ ) THEN
+ RETURN jsonb_build_object(
+ 'ok', false,
+ 'error', 'forbidden',
+ 'message', 'Sem permissão para excluir este paciente.'
+ );
+ END IF;
+
+ DELETE FROM public.patients WHERE id = p_patient_id;
+
+ RETURN jsonb_build_object('ok', true);
+END;
+$$;
+
+GRANT EXECUTE ON FUNCTION public.safe_delete_patient(uuid)
+ TO postgres, anon, authenticated, service_role;
diff --git a/src/theme/theme.options.js b/src/theme/theme.options.js
index a07ddd1..e852498 100644
--- a/src/theme/theme.options.js
+++ b/src/theme/theme.options.js
@@ -48,7 +48,7 @@ export const surfaces = [
]
/**
- * ✅ noir: primary “vira” surface (o bloco que você pediu pra ficar aqui)
+ * ✅ noir: primary "vira" surface (o bloco que você pediu pra ficar aqui)
*/
export const noirPrimaryFromSurface = {
50: '{surface.50}',
@@ -72,7 +72,7 @@ export function getSurfacePalette(surfaceName) {
}
/**
- * ✅ Ponto único: “Preset Extension” baseado no layoutConfig atual
+ * ✅ Ponto único: "Preset Extension" baseado no layoutConfig atual
* Use assim: updatePreset(getPresetExt(layoutConfig))
*/
export function getPresetExt(layoutConfig) {
diff --git a/src/views/pages/NotFound.vue b/src/views/pages/NotFound.vue
index 80cf6ee..39ebc0c 100644
--- a/src/views/pages/NotFound.vue
+++ b/src/views/pages/NotFound.vue
@@ -55,7 +55,7 @@ function goDashboard () {
-
+
@@ -95,7 +95,7 @@ function goDashboard () {
/>
-
+
Se isso estiver acontecendo com frequência, pode ser um problema de rota ou permissão.
diff --git a/src/views/pages/clinic/clinic/ClinicFeaturesPage.vue b/src/views/pages/clinic/clinic/ClinicFeaturesPage.vue
index ebdb478..f156e8e 100644
--- a/src/views/pages/clinic/clinic/ClinicFeaturesPage.vue
+++ b/src/views/pages/clinic/clinic/ClinicFeaturesPage.vue
@@ -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(
-
+
-
-
-
-
+
+
+
+
-
-
-
-
Tipos de Clínica
-
+
+
+
+
Tipos de Clínica
+
Ative/desative recursos por clínica. Controla menu, rotas e acesso no banco (RLS).
-
-
+
+
Você está visualizando as configurações da clínica em modo somente leitura.
Apenas o administrador pode ativar ou desativar módulos.
-
-
-
-
-
Preset: Coworking
-
+
+
+
+
+
Preset: Coworking
+
Para aluguel de salas: sem pacientes, com salas.
-
-
-
-
Preset: Clínica com recepção
-
+
+
+
+
Preset: Clínica com recepção
+
Para secretária gerenciar agenda (pacientes opcional).
-
-
-
-
Preset: Clínica completa
-
+
+
+
+
Preset: Clínica completa
+
Pacientes + recepção + salas (se quiser).
-
-
+
+
-
+
Este módulo foi bloqueado pelo plano atual do tenant.
-
-
+
+
Quando desligado:
-
-
Menu “Pacientes” some.
-
Rotas com meta.tenantFeature = 'patients' redirecionam pra cá.
+
+
Menu "Pacientes" some.
+
Rotas com meta.tenantFeature = 'patients' redirecionam pra cá.
RLS bloqueia acesso direto no banco.
-
+
-
+
Este módulo foi bloqueado pelo plano atual do tenant.
-
-
- Observação: este módulo é “produto” (UX + permissões). A base aqui é só o toggle.
+
+
+ Observação: este módulo é "produto" (UX + permissões). A base aqui é só o toggle.
Depois a gente cria:
-
-
role secretary em tenant_members
+
+
role secretary em tenant_members
policies e telas para a secretária
nível de visibilidade do paciente na agenda
-
+
-
+
Este módulo foi bloqueado pelo plano atual do tenant.
-
-
+
+
Isso prepara o terreno para a clínica operar como locação de sala, com agenda vinculando sala + profissional.
-
+
-
+
Este módulo foi bloqueado pelo plano atual do tenant.
-
-
+
+
Você já tem páginas de link externo. Isso vira o controle fino: a clínica decide se usa ou não.
diff --git a/src/views/pages/public/AcceptInvitePage.vue b/src/views/pages/public/AcceptInvitePage.vue
index fb6582c..6cdb50f 100644
--- a/src/views/pages/public/AcceptInvitePage.vue
+++ b/src/views/pages/public/AcceptInvitePage.vue
@@ -183,7 +183,7 @@ function friendlyError (err) {
function safeRpcError (rpcError) {
const raw = (rpcError?.message || '').toString().trim()
- // Por padrão: mensagem amigável. Se quiser ver a “real”, coloque em debugDetails.
+ // Por padrão: mensagem amigável. Se quiser ver a "real", coloque em debugDetails.
const friendly = friendlyError(rpcError)
return { friendly, raw }
}
@@ -241,7 +241,7 @@ async function acceptInvite (token) {
const { friendly, raw } = safeRpcError(error)
state.error = friendly
- // Se você quiser ver a mensagem “crua” para debug, descomente a linha abaixo:
+ // Se você quiser ver a mensagem "crua" para debug, descomente a linha abaixo:
// state.debugDetails = raw
// Opcional: toast discreto
diff --git a/src/views/pages/public/CadastroPacienteExterno.vue b/src/views/pages/public/CadastroPacienteExterno.vue
index 63fcf75..4a16a7c 100644
--- a/src/views/pages/public/CadastroPacienteExterno.vue
+++ b/src/views/pages/public/CadastroPacienteExterno.vue
@@ -2,7 +2,7 @@
-
+
@@ -1233,7 +1233,7 @@ function validate () {
// Progress (conceitual e útil)
// ------------------------------------------------------
const progressPct = computed(() => {
- // contagem simples e honesta: dá sensação de avanço sem “gamificar demais”
+ // contagem simples e honesta: dá sensação de avanço sem "gamificar demais"
const checks = [
!!cleanStr(form.nome_completo),
!!digitsOnly(form.telefone),
diff --git a/src/views/pages/public/Landingpage-v1.vue b/src/views/pages/public/Landingpage-v1.vue
index 2cb9d59..52102fc 100644
--- a/src/views/pages/public/Landingpage-v1.vue
+++ b/src/views/pages/public/Landingpage-v1.vue
@@ -64,7 +64,7 @@
Centralize a rotina clínica em um lugar só: pacientes, sessões, lembretes e indicadores.
- O objetivo não é “burocratizar”: é deixar o consultório respirável.
+ O objetivo não é "burocratizar": é deixar o consultório respirável.
@@ -101,7 +101,7 @@
- “A diferença entre ter uma agenda e ter um sistema mora nos detalhes.”
+ "A diferença entre ter uma agenda e ter um sistema mora nos detalhes."
@@ -322,7 +322,7 @@
3) Acompanhar
- Financeiro e indicadores acompanham o movimento. Menos “cadê?”, mais previsibilidade.
+ Financeiro e indicadores acompanham o movimento. Menos "cadê?", mais previsibilidade.
diff --git a/src/views/pages/public/Signup.vue b/src/views/pages/public/Signup.vue
index 793c554..1656cc2 100644
--- a/src/views/pages/public/Signup.vue
+++ b/src/views/pages/public/Signup.vue
@@ -21,7 +21,7 @@ const email = ref('')
const password = ref('')
const loading = ref(false)
-// validação simples (sem “viajar”)
+// validação simples (sem "viajar")
const emailOk = computed(() => /\S+@\S+\.\S+/.test(String(email.value || '').trim()))
const passwordOk = computed(() => String(password.value || '').length >= 6)
const canSubmit = computed(() => !loading.value && emailOk.value && passwordOk.value)
diff --git a/src/views/pages/saas/SaasPlanFeaturesMatrixPage.vue b/src/views/pages/saas/SaasPlanFeaturesMatrixPage.vue
index df6b193..6ea040c 100644
--- a/src/views/pages/saas/SaasPlanFeaturesMatrixPage.vue
+++ b/src/views/pages/saas/SaasPlanFeaturesMatrixPage.vue
@@ -156,7 +156,7 @@ function isEnabled (planId, featureId) {
/**
* ✅ Toggle agora NÃO salva no banco.
- * Apenas altera o estado local (links) e marca como “pendente”.
+ * Apenas altera o estado local (links) e marca como "pendente".
*/
function toggleLocal (planId, featureId, nextValue) {
if (loading.value || saving.value) return
diff --git a/src/views/pages/saas/SaasPlansPage.vue b/src/views/pages/saas/SaasPlansPage.vue
index 3cb1f8b..bae3246 100644
--- a/src/views/pages/saas/SaasPlansPage.vue
+++ b/src/views/pages/saas/SaasPlansPage.vue
@@ -433,91 +433,91 @@ onBeforeUnmount(() => {
-
+