Correcao Sidebar Classico e Rail, Correcao Layout, Ajuste de Breakpoint para Tailwind, Ajuste AppTopbar, Ajuste Menu PopOver, Recriado Paleta de Cores, Inserido algumas animações leves, Reajuste Cor items NOVOS da tabela, Drawer Ajuda Corrigido no Logout, Whatsapp, sms, email, recursos extras

This commit is contained in:
Leonardo
2026-03-24 21:26:58 -03:00
parent a89d1f5560
commit 53a4980396
453 changed files with 121427 additions and 174407 deletions
+220 -220
View File
@@ -15,259 +15,259 @@
|--------------------------------------------------------------------------
-->
<script setup>
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { computed, ref, onMounted, onBeforeUnmount } from 'vue';
import { useRoute, useRouter } from 'vue-router';
const route = useRoute()
const router = useRouter()
const route = useRoute();
const router = useRouter();
const showMenu = ref(false)
const showMenu = ref(false);
// ── Hero sticky ────────────────────────────────────────────
const headerEl = ref(null)
const headerSentinelRef = ref(null)
const headerStuck = ref(false)
let _observer = null
const headerEl = ref(null);
const headerSentinelRef = ref(null);
const headerStuck = ref(false);
let _observer = null;
const secoes = [
{
key: 'agenda',
label: 'Agenda',
desc: 'Horários semanais, exceções, duração e intervalo padrão.',
icon: 'pi pi-calendar',
to: '/configuracoes/agenda',
tags: ['Horários', 'Exceções', 'Duração']
},
{
key: 'bloqueios',
label: 'Bloqueios',
desc: 'Feriados nacionais, municipais e períodos bloqueados.',
icon: 'pi pi-ban',
to: '/configuracoes/bloqueios',
tags: ['Feriados', 'Períodos', 'Recorrentes']
},
{
key: 'agendador',
label: 'Agendador Online',
desc: 'Link público para pacientes solicitarem horários.',
icon: 'pi pi-calendar-clock',
to: '/configuracoes/agendador',
tags: ['PRO', 'Link', 'Pix', 'LGPD']
},
{
key: 'pagamento',
label: 'Pagamento',
desc: 'Formas de pagamento: Pix, depósito, dinheiro, cartão, convênio.',
icon: 'pi pi-wallet',
to: '/configuracoes/pagamento',
tags: ['Pix', 'TED', 'Cartão', 'Convênio']
},
{
key: 'precificacao',
label: 'Precificação',
desc: 'Valor padrão da sessão e preços por tipo de compromisso.',
icon: 'pi pi-tag',
to: '/configuracoes/precificacao',
tags: ['Valores', 'Sessão', 'Compromisso']
},
{
key: 'descontos',
label: 'Descontos por Paciente',
desc: 'Descontos recorrentes aplicados automaticamente.',
icon: 'pi pi-percentage',
to: '/configuracoes/descontos',
tags: ['Desconto', 'Paciente', 'Automático']
},
{
key: 'excecoes-financeiras',
label: 'Exceções Financeiras',
desc: 'O que cobrar em faltas, cancelamentos e situações excepcionais.',
icon: 'pi pi-exclamation-triangle',
to: '/configuracoes/excecoes-financeiras',
tags: ['Falta', 'Cancelamento', 'Cobrança']
},
{
key: 'convenios',
label: 'Convênios',
desc: 'Cadastre os convênios que você atende e seus valores.',
icon: 'pi pi-id-card',
to: '/configuracoes/convenios',
tags: ['Convênio', 'Plano de Saúde', 'Tabela']
},
{
key: 'empresa',
label: 'Minha Empresa',
desc: 'CNPJ, endereço, logomarca e redes sociais.',
icon: 'pi pi-building',
to: '/configuracoes/empresa',
tags: ['CNPJ', 'Endereço', 'Logo']
},
{
key: 'email-templates',
label: 'Templates de E-mail',
desc: 'Personalize os e-mails enviados aos pacientes.',
icon: 'pi pi-envelope',
to: '/configuracoes/email-templates',
tags: ['E-mail', 'Notificações', 'Personalizar']
},
]
{
key: 'agenda',
label: 'Agenda',
desc: 'Horários semanais, exceções, duração e intervalo padrão.',
icon: 'pi pi-calendar',
to: '/configuracoes/agenda',
tags: ['Horários', 'Exceções', 'Duração']
},
{
key: 'bloqueios',
label: 'Bloqueios',
desc: 'Feriados nacionais, municipais e períodos bloqueados.',
icon: 'pi pi-ban',
to: '/configuracoes/bloqueios',
tags: ['Feriados', 'Períodos', 'Recorrentes']
},
{
key: 'agendador',
label: 'Agendador Online',
desc: 'Link público para pacientes solicitarem horários.',
icon: 'pi pi-calendar-clock',
to: '/configuracoes/agendador',
tags: ['PRO', 'Link', 'Pix', 'LGPD']
},
{
key: 'pagamento',
label: 'Pagamento',
desc: 'Formas de pagamento: Pix, depósito, dinheiro, cartão, convênio.',
icon: 'pi pi-wallet',
to: '/configuracoes/pagamento',
tags: ['Pix', 'TED', 'Cartão', 'Convênio']
},
{
key: 'precificacao',
label: 'Precificação',
desc: 'Valor padrão da sessão e preços por tipo de compromisso.',
icon: 'pi pi-tag',
to: '/configuracoes/precificacao',
tags: ['Valores', 'Sessão', 'Compromisso']
},
{
key: 'descontos',
label: 'Descontos por Paciente',
desc: 'Descontos recorrentes aplicados automaticamente.',
icon: 'pi pi-percentage',
to: '/configuracoes/descontos',
tags: ['Desconto', 'Paciente', 'Automático']
},
{
key: 'excecoes-financeiras',
label: 'Exceções Financeiras',
desc: 'O que cobrar em faltas, cancelamentos e situações excepcionais.',
icon: 'pi pi-exclamation-triangle',
to: '/configuracoes/excecoes-financeiras',
tags: ['Falta', 'Cancelamento', 'Cobrança']
},
{
key: 'convenios',
label: 'Convênios',
desc: 'Cadastre os convênios que você atende e seus valores.',
icon: 'pi pi-id-card',
to: '/configuracoes/convenios',
tags: ['Convênio', 'Plano de Saúde', 'Tabela']
},
{
key: 'empresa',
label: 'Minha Empresa',
desc: 'CNPJ, endereço, logomarca e redes sociais.',
icon: 'pi pi-building',
to: '/configuracoes/empresa',
tags: ['CNPJ', 'Endereço', 'Logo']
},
{
key: 'email-templates',
label: 'Templates de E-mail',
desc: 'Personalize os e-mails enviados aos pacientes.',
icon: 'pi pi-envelope',
to: '/configuracoes/email-templates',
tags: ['E-mail', 'Notificações', 'Personalizar']
},
{
key: 'whatsapp',
label: 'WhatsApp',
desc: 'Configure a integração WhatsApp e personalize as mensagens.',
icon: 'pi pi-whatsapp',
to: '/configuracoes/whatsapp',
tags: ['WhatsApp', 'Mensagens', 'Notificações']
},
{
key: 'sms',
label: 'SMS',
desc: 'Gerencie créditos SMS e personalize as mensagens enviadas.',
icon: 'pi pi-comment',
to: '/configuracoes/sms',
tags: ['SMS', 'Créditos', 'Mensagens']
},
{
key: 'recursos-extras',
label: 'Recursos Extras',
desc: 'Amplíe as funcionalidades com recursos adicionais e créditos.',
icon: 'pi pi-box',
to: '/configuracoes/recursos-extras',
tags: ['Add-ons', 'Créditos', 'Extra']
}
];
const activeTo = computed(() => {
const p = route.path || ''
const hit = [...secoes]
.sort((a, b) => b.to.length - a.to.length)
.find(s => p === s.to || p.startsWith(s.to + '/'))
return hit?.to || '/configuracoes/agenda'
})
const p = route.path || '';
const hit = [...secoes].sort((a, b) => b.to.length - a.to.length).find((s) => p === s.to || p.startsWith(s.to + '/'));
return hit?.to || '/configuracoes/agenda';
});
const activeSecao = computed(() => secoes.find(s => s.to === activeTo.value))
const activeSecao = computed(() => secoes.find((s) => s.to === activeTo.value));
function ir(to) {
if (!to) return
if (route.path !== to) router.push(to)
if (!to) return;
if (route.path !== to) router.push(to);
}
onMounted(() => {
requestAnimationFrame(() => {
showMenu.value = true
})
const rootMargin = `${document.querySelector('.l2-main') ? '0px' : '-56px'} 0px 0px 0px`
_observer = new IntersectionObserver(
([entry]) => { headerStuck.value = !entry.isIntersecting },
{ threshold: 0, rootMargin }
)
if (headerSentinelRef.value) _observer.observe(headerSentinelRef.value)
})
requestAnimationFrame(() => {
showMenu.value = true;
});
onBeforeUnmount(() => { _observer?.disconnect() })
const rootMargin = `${document.querySelector('.l2-main') ? '0px' : '-56px'} 0px 0px 0px`;
_observer = new IntersectionObserver(
([entry]) => {
headerStuck.value = !entry.isIntersecting;
},
{ threshold: 0, rootMargin }
);
if (headerSentinelRef.value) _observer.observe(headerSentinelRef.value);
});
onBeforeUnmount(() => {
_observer?.disconnect();
});
</script>
<template>
<!-- Sentinel -->
<div ref="headerSentinelRef" class="h-px" />
<!-- Sentinel -->
<div ref="headerSentinelRef" class="h-px" />
<!-- Hero compacto -->
<div
ref="headerEl"
class="sticky top-[var(--layout-sticky-top,56px)] z-20 overflow-hidden rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] px-3 py-2.5 mx-3 md:mx-4 mb-3"
:class="{ 'rounded-tl-none rounded-tr-none': headerStuck }"
>
<!-- Blobs decorativos -->
<div class="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
<div class="absolute w-64 h-64 -top-16 -right-8 rounded-full blur-[60px] bg-emerald-300/10" />
<div class="absolute w-72 h-72 top-0 -left-16 rounded-full blur-[60px] bg-indigo-500/[0.09]" />
</div>
<div class="relative z-[1] flex items-center gap-3">
<!-- Brand -->
<div class="flex items-center gap-2 flex-shrink-0">
<div class="grid place-items-center w-9 h-9 rounded-md flex-shrink-0 bg-indigo-500/[0.12] text-[var(--p-primary-500,#6366f1)]">
<i class="pi pi-cog text-base" />
</div>
<div class="min-w-0 lg:block">
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)]">Configurações</div>
<div class="text-xs text-[var(--text-color-secondary)]">
<span v-if="activeSecao">
<i :class="activeSecao.icon" class="text-xs mr-1 opacity-60" />{{ activeSecao.label }}
</span>
<span v-else>Configurações gerais</span>
</div>
</div>
</div>
<!-- Ações -->
<div class="flex items-center gap-2 ml-auto">
<Button icon="pi pi-arrow-left" severity="secondary" outlined class="h-9 w-9 rounded-full" v-tooltip.bottom="'Voltar'" @click="router.back()" />
</div>
</div>
</div>
<!-- Cards de seção (stats row) -->
<div class="flex flex-wrap gap-2 px-3 md:px-4 mb-3">
<button
v-for="s in secoes"
:key="s.key"
class="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] cursor-pointer whitespace-nowrap transition-[border-color,background,box-shadow] duration-150 hover:shadow-[0_2px_8px_rgba(0,0,0,0.06)]"
:class="
activeTo === s.to
? 'cfg-sec-card--active'
: 'hover:border-indigo-500/40'
"
@click="ir(s.to)"
>
<i
:class="[s.icon, activeTo === s.to ? 'text-[var(--primary-color,#6366f1)]' : 'text-[var(--text-color-secondary)] opacity-75']"
class="text-[0.78rem]"
/>
<span
class="text-[0.78rem] font-semibold"
:class="activeTo === s.to ? 'text-[var(--primary-color,#6366f1)]' : 'text-[var(--text-color)]'"
>{{ s.label }}</span>
</button>
</div>
<!-- Layout: sidebar + conteúdo -->
<div class="flex flex-col xl:flex-row gap-3 px-3 md:px-4 pb-5 items-start">
<!-- Sidebar (oculto no mobile) -->
<!-- Hero compacto -->
<div
class="hidden xl:flex flex-col gap-1 w-[300px] shrink-0"
style="position: sticky; top: calc(var(--layout-sticky-top, 56px) + 58px); align-self: flex-start;"
ref="headerEl"
class="sticky top-[var(--layout-sticky-top,56px)] z-20 overflow-hidden rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] px-3 py-2.5 mx-3 md:mx-4 mb-3"
:class="{ 'rounded-tl-none rounded-tr-none': headerStuck }"
>
<div class="border border-[var(--surface-border)] rounded-md bg-[var(--surface-card)] overflow-hidden">
<!-- Cabeçalho -->
<div class="flex items-center gap-1.5 px-3.5 py-2.5 border-b border-[var(--surface-border)] text-[0.7rem] font-bold uppercase tracking-[0.06em] text-[var(--text-color-secondary)] opacity-65">
<i class="pi pi-cog" />
<span>Seções</span>
<!-- Blobs decorativos -->
<div class="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
<div class="absolute w-64 h-64 -top-16 -right-8 rounded-full blur-[60px] bg-emerald-300/10" />
<div class="absolute w-72 h-72 top-0 -left-16 rounded-full blur-[60px] bg-indigo-500/[0.09]" />
</div>
<!-- Itens -->
<TransitionGroup name="menu" tag="div" class="flex flex-col gap-0.5">
<button
v-for="(s, i) in showMenu ? secoes : []"
:key="s.key"
:style="{ transitionDelay: `${i * 50}ms` }"
class="flex items-center gap-2.5 px-3.5 py-2.5 border-b last:border-b-0 bg-transparent cursor-pointer w-full text-left transition-colors duration-[120ms] hover:bg-[var(--surface-hover)]"
:class="activeTo === s.to ? 'cfg-nav-item--active border border-[var(--primary-color,#6366f1)] last:border-b-1 last:rounded-bl-[6px] last:rounded-br-[6px]' : ''"
@click="ir(s.to)"
>
<i
:class="[s.icon, activeTo === s.to ? 'text-[var(--primary-color,#6366f1)] opacity-100' : 'text-[var(--text-color-secondary)] opacity-60']"
class="text-[0.85rem] flex-shrink-0 w-4 text-center"
/>
<div class="flex-1 min-w-0 flex flex-col gap-px ">
<span
class="text-[0.90rem] font-semibold truncate"
:class="activeTo === s.to ? 'text-[var(--primary-color,#6366f1)]' : 'text-[var(--text-color)]'"
>{{ s.label }}</span>
<span class="text-[0.88rem] text-[var(--text-color-secondary)] opacity-70">{{ s.desc }}</span>
<div class="relative z-[1] flex items-center gap-3">
<!-- Brand -->
<div class="flex items-center gap-2 flex-shrink-0">
<div class="grid place-items-center w-9 h-9 rounded-md flex-shrink-0 bg-indigo-500/[0.12] text-[var(--p-primary-500,#6366f1)]">
<i class="pi pi-cog text-base" />
</div>
<div class="min-w-0 lg:block">
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)]">Configurações</div>
<div class="text-xs text-[var(--text-color-secondary)]">
<span v-if="activeSecao"> <i :class="activeSecao.icon" class="text-xs mr-1 opacity-60" />{{ activeSecao.label }} </span>
<span v-else>Configurações gerais</span>
</div>
</div>
</div>
<i
class="pi pi-chevron-right text-[0.6rem] flex-shrink-0"
:class="activeTo === s.to ? 'text-[var(--primary-color,#6366f1)] opacity-60' : 'text-[var(--text-color-secondary)] opacity-30'"
/>
</button>
</TransitionGroup>
</div>
<!-- Ações -->
<div class="flex items-center gap-2 ml-auto">
<Button icon="pi pi-arrow-left" severity="secondary" outlined class="h-9 w-9 rounded-full" v-tooltip.bottom="'Voltar'" @click="router.back()" />
</div>
</div>
</div>
<!-- Conteúdo da seção -->
<div class="flex-1 min-w-0 w-full">
<router-view />
<!-- Cards de seção (stats row) -->
<div class="flex flex-wrap gap-2 px-3 md:px-4 mb-3">
<button
v-for="s in secoes"
:key="s.key"
class="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] cursor-pointer whitespace-nowrap transition-[border-color,background,box-shadow] duration-150 hover:shadow-[0_2px_8px_rgba(0,0,0,0.06)]"
:class="activeTo === s.to ? 'cfg-sec-card--active' : 'hover:border-indigo-500/40'"
@click="ir(s.to)"
>
<i :class="[s.icon, activeTo === s.to ? 'text-[var(--primary-color,#6366f1)]' : 'text-[var(--text-color-secondary)] opacity-75']" class="text-[0.78rem]" />
<span class="text-[0.78rem] font-semibold" :class="activeTo === s.to ? 'text-[var(--primary-color,#6366f1)]' : 'text-[var(--text-color)]'">{{ s.label }}</span>
</button>
</div>
</div>
<!-- Layout: sidebar + conteúdo -->
<div class="flex flex-col xl:flex-row gap-3 px-3 md:px-4 pb-5 items-start">
<!-- Sidebar (oculto no mobile) -->
<div class="hidden xl:flex flex-col gap-1 w-[300px] shrink-0" style="position: sticky; top: calc(var(--layout-sticky-top, 56px) + 58px); align-self: flex-start">
<div class="border border-[var(--surface-border)] rounded-md bg-[var(--surface-card)] overflow-hidden">
<!-- Cabeçalho -->
<div class="flex items-center gap-1.5 px-3.5 py-2.5 border-b border-[var(--surface-border)] text-[0.7rem] font-bold uppercase tracking-[0.06em] text-[var(--text-color-secondary)] opacity-65">
<i class="pi pi-cog" />
<span>Seções</span>
</div>
<!-- Itens -->
<TransitionGroup name="menu" tag="div" class="flex flex-col gap-0.5">
<button
v-for="(s, i) in showMenu ? secoes : []"
:key="s.key"
:style="{ transitionDelay: `${i * 50}ms` }"
class="flex items-center gap-2.5 px-3.5 py-2.5 border-b last:border-b-0 bg-transparent cursor-pointer w-full text-left transition-colors duration-[120ms] hover:bg-[var(--surface-hover)]"
:class="activeTo === s.to ? 'cfg-nav-item--active border border-[var(--primary-color,#6366f1)] last:border-b-1 last:rounded-bl-[6px] last:rounded-br-[6px]' : ''"
@click="ir(s.to)"
>
<i :class="[s.icon, activeTo === s.to ? 'text-[var(--primary-color,#6366f1)] opacity-100' : 'text-[var(--text-color-secondary)] opacity-60']" class="text-[0.85rem] flex-shrink-0 w-4 text-center" />
<div class="flex-1 min-w-0 flex flex-col gap-px">
<span class="text-[0.90rem] font-semibold truncate" :class="activeTo === s.to ? 'text-[var(--primary-color,#6366f1)]' : 'text-[var(--text-color)]'">{{ s.label }}</span>
<span class="text-[0.88rem] text-[var(--text-color-secondary)] opacity-70">{{ s.desc }}</span>
</div>
<i class="pi pi-chevron-right text-[0.6rem] flex-shrink-0" :class="activeTo === s.to ? 'text-[var(--primary-color,#6366f1)] opacity-60' : 'text-[var(--text-color-secondary)] opacity-30'" />
</button>
</TransitionGroup>
</div>
</div>
<!-- Conteúdo da seção -->
<div class="flex-1 min-w-0 w-full">
<router-view />
</div>
</div>
</template>
<style scoped>
.cfg-sec-card--active {
border-color: var(--primary-color, #6366f1);
background: color-mix(in srgb, var(--primary-color) 8%, var(--surface-card));
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 12%, transparent);
border-color: var(--primary-color, #6366f1);
background: color-mix(in srgb, var(--primary-color) 8%, var(--surface-card));
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 12%, transparent);
}
.cfg-nav-item--active {
background: color-mix(in srgb, var(--primary-color) 6%, var(--surface-card));
background: color-mix(in srgb, var(--primary-color) 6%, var(--surface-card));
}
</style>
</style>