Melissa: hub Configuracoes + Embed + 9 Pages novas + dialog blueprint dark

Sprints 04-29 + 04-30 acumuladas.

- MelissaConfiguracoes: hub 2-col com 6 grupos (Layout/Conta/Agenda/
  Financeiro/WhatsApp/Sistema), tudo embedado via MelissaEmbed.
- MelissaEmbed: wrapper generico que injeta layout-variant=melissa
  e remove cromos pra reaproveitar Pages tradicionais.
- 9 Melissa Pages novas: CadastrosRecebidos, Compromissos, Configuracoes,
  Conversas, Embed, Grupos, Medicos, Recorrencias, Tags.
- Dialog blueprint atualizado: bg-gray-100 (hardcoded light) ->
  bg-[var(--surface-ground)] (tema-aware). 22 dialogs migrados em
  9 arquivos. Anti-pattern documentado.
- PatientsCadastroPage: bug fix dropdown Grupo (optionLabel nome->name),
  toggle vertical/abas com persist localStorage, sticky margin-top.
- Surface picker no popover do MelissaLayout (8 swatches).
- useTopbarPlanMenu, useMelissaWhatsapp, useMelissaPacientesAside novos.
- Migration: status agenda remarcado/confirmado.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-04 11:41:19 -03:00
parent 269c380d9c
commit 86311ef305
52 changed files with 16214 additions and 1027 deletions
+293 -19
View File
@@ -54,6 +54,7 @@ const CATEGORIAS = [
{ key: 'agenda', label: 'Minha Agenda', icon: 'pi pi-calendar' },
{ key: 'pacientes', label: 'Meus Pacientes', icon: 'pi pi-users' },
{ key: 'cadastros-recebidos', label: 'Cadastros recebidos', icon: 'pi pi-inbox' },
{ key: 'agendamentos-recebidos', label: 'Agendamentos recebidos', icon: 'pi pi-bell' },
{ key: 'meu-link-cadastro', label: 'Meu link de cadastro', icon: 'pi pi-link', tipo: 'link-cadastro' }
]
},
@@ -64,7 +65,9 @@ const CATEGORIAS = [
{ key: 'compromissos', label: 'Compromissos determinados', icon: 'pi pi-flag' },
{ key: 'grupos', label: 'Grupos de pacientes', icon: 'pi pi-th-large' },
{ key: 'tags', label: 'Tags', icon: 'pi pi-tag' },
{ key: 'medicos', label: 'Médicos e referências', icon: 'pi pi-user-edit' }
{ key: 'medicos', label: 'Médicos e referências', icon: 'pi pi-user-edit' },
{ key: 'online-scheduling', label: 'Agendador online', icon: 'pi pi-calendar-clock' },
{ key: 'link-externo', label: 'Link externo de cadastro', icon: 'pi pi-share-alt' }
]
}
]
@@ -74,28 +77,98 @@ const CATEGORIAS = [
label: 'WhatsApp',
icon: 'pi pi-whatsapp',
color: '#22c55e',
groups: []
groups: [
{
title: 'Atendimento',
items: [
{ key: 'conversas', label: 'Conversas', icon: 'pi pi-comments' },
{ key: 'notificacoes', label: 'Notificações enviadas', icon: 'pi pi-bell' }
]
},
{
title: 'Configuração',
items: [
{ key: 'wa-canal', label: 'Configurar canal', icon: 'pi pi-cog', route: { name: 'ConfiguracoesWhatsapp' } },
{ key: 'wa-templates', label: 'Templates de mensagem', icon: 'pi pi-file-edit', route: { name: 'ConfiguracoesWhatsappTemplates' } },
{ key: 'wa-creditos', label: 'Créditos', icon: 'pi pi-credit-card', route: { name: 'ConfiguracoesCreditosWhatsapp' } }
]
}
]
},
{
key: 'prontuarios',
label: 'Prontuários',
icon: 'pi pi-file',
color: '#0ea5e9',
groups: []
groups: [
{
title: 'Acesso',
items: [
// Sem route — emit('select', 'pacientes') aciona o MelissaPacientes
// (lá o duplo-click no card abre PatientProntuario). Mantém o
// user dentro do Melissa em vez de jogar pra rota externa.
{ key: 'pacientes', label: 'Abrir por paciente', icon: 'pi pi-users' }
]
},
{
title: 'Documentos',
items: [
{ key: 'documentos', label: 'Documentos', icon: 'pi pi-file' },
{ key: 'documentos-templates', label: 'Templates de documentos', icon: 'pi pi-file-edit' }
]
}
]
},
{
key: 'financeiro',
label: 'Financeiro',
icon: 'pi pi-wallet',
color: '#f59e0b',
groups: []
groups: [
{
title: 'Principais',
items: [
// Sem route — abre embedado via MelissaEmbed dentro do overlay Melissa
{ key: 'financeiro', label: 'Visão geral', icon: 'pi pi-chart-line' },
{ key: 'financeiro-lancamentos', label: 'Lançamentos', icon: 'pi pi-list' }
]
},
{
title: 'Análise',
items: [
{ key: 'relatorios', label: 'Relatórios', icon: 'pi pi-chart-bar' }
]
}
]
},
{
key: 'configuracoes',
label: 'Configurações',
icon: 'pi pi-cog',
color: '#94a3b8',
groups: []
groups: [
{
title: 'Layout Melissa',
items: [
// Sem `route` — emit('select', 'aparencia') abre página interna do Melissa
{ key: 'aparencia', label: 'Aparência e cronômetro', icon: 'pi pi-palette' }
]
},
{
title: 'Agenda',
items: [
{ key: 'cfg-agenda', label: 'Agenda', icon: 'pi pi-calendar', route: { name: 'ConfiguracoesAgenda' } },
{ key: 'cfg-agendador', label: 'Agendador externo', icon: 'pi pi-link', route: { name: 'ConfiguracoesAgendador' } }
]
},
{
title: 'WhatsApp',
items: [
{ key: 'cfg-wa', label: 'Canal de WhatsApp', icon: 'pi pi-whatsapp', route: { name: 'ConfiguracoesWhatsapp' } },
{ key: 'cfg-wa-templates', label: 'Templates', icon: 'pi pi-file-edit', route: { name: 'ConfiguracoesWhatsappTemplates' } }
]
}
]
}
];
@@ -103,6 +176,11 @@ const CATEGORIAS = [
const selectedKey = ref(CATEGORIAS[0].key); // primeira categoria por default
const copiado = ref(false);
// Drill-down mobile: false = lista de categorias, true = sub-itens da
// categoria escolhida. CSS controla visibilidade via translateX em <lg.
// Em desktop o flag é ignorado (ambas colunas sempre visíveis).
const mobileSubView = ref(false);
const categoriaAtiva = computed(() =>
CATEGORIAS.find((c) => c.key === selectedKey.value) || CATEGORIAS[0]
);
@@ -115,10 +193,23 @@ function selecionarCategoria(key) {
selectedKey.value = key;
copiado.value = false;
themeViewActive.value = false; // sai do tema ao mudar categoria
mobileSubView.value = true; // drill-down em mobile
}
function voltarParaCategorias() {
mobileSubView.value = false;
themeViewActive.value = false;
}
function clicarSubItem(item) {
if (item.tipo === 'link-cadastro') return; // inline, não navega
// Se item tem route definida, navega direto (rota externa ao Melissa).
// Senão, emite 'select' pro pai decidir (seções internas ao MelissaLayout).
if (item.route) {
emit('close');
safePush(item.route);
return;
}
emit('select', item.key);
}
@@ -172,16 +263,13 @@ function navAndClose(target, fallback) {
safePush(target, fallback);
}
function goPerfil() { navAndClose({ name: 'account-profile' }, '/account/profile'); }
function goSeguranca() { navAndClose({ name: 'account-security' }, '/account/security'); }
function goPlano() {
const r = role.value || sessionRole.value;
if (r === 'clinic_admin' || r === 'tenant_admin' || r === 'admin') {
return navAndClose({ name: 'admin-meu-plano' }, '/admin/meu-plano');
}
if (r === 'supervisor') return navAndClose({ name: 'supervisor.meu-plano' }, '/supervisor/meu-plano');
return navAndClose({ name: 'therapist-meu-plano' }, '/therapist/meu-plano');
}
// Atalhos de Conta — abrem embedados dentro do MelissaConfiguracoes
// (em vez de navegar pra rota externa). Cada um vira uma section pré-
// selecionada na sidebar de configs.
function goPerfil() { emit('select', 'perfil'); emit('close'); }
function goPlano() { emit('select', 'plano'); emit('close'); }
function goNegocio() { emit('select', 'negocio'); emit('close'); }
function goSeguranca() { emit('select', 'seguranca'); emit('close'); }
async function toggleDarkAndPersist() {
try {
@@ -203,6 +291,7 @@ const themeViewActive = ref(false);
function toggleThemeView() {
themeViewActive.value = !themeViewActive.value;
if (themeViewActive.value) mobileSubView.value = true; // drill-down em mobile
}
function saveThemeToStorage() {
@@ -252,11 +341,22 @@ async function sair() {
<template>
<div class="mm-layer" @click.self="emit('close')">
<div class="mm-panel">
<div class="mm-panel" :class="{ 'is-mobile-sub': mobileSubView }">
<!-- ESQUERDA: categorias -->
<nav class="mm-side">
<div class="mm-side__head">
<div class="mm-side__title">Menu</div>
<!-- Fechar (mobile only): em desktop o ψ continua visível
no canto inferior pra fechar; em mobile o menu cobre
tudo, então precisa de botão dedicado. -->
<button
class="mm-side__close mm-side__close--mobile-only"
title="Fechar menu"
aria-label="Fechar menu"
@click="emit('close')"
>
<i class="pi pi-times" />
</button>
</div>
<div class="mm-side__list">
@@ -291,6 +391,9 @@ async function sair() {
<button class="mm-foot-item" @click="goPlano">
<i class="pi pi-credit-card" /><span>Meus Planos</span>
</button>
<button class="mm-foot-item" @click="goNegocio">
<i class="pi pi-briefcase" /><span>Meu Negócio</span>
</button>
<button class="mm-foot-item" @click="goSeguranca">
<i class="pi pi-shield" /><span>Segurança</span>
</button>
@@ -328,6 +431,17 @@ async function sair() {
<!-- DIREITA: sub-itens OU cores do tema -->
<aside class="mm-aside">
<div class="mm-aside__head">
<!-- Voltar (mobile only): aparece em <lg quando o
drill-down está em modo "sub-itens". Em desktop as
duas colunas convivem, voltar não faz sentido. -->
<button
class="mm-aside__back mm-aside__back--mobile-only"
title="Voltar"
aria-label="Voltar pra categorias"
@click="voltarParaCategorias"
>
<i class="pi pi-arrow-left" />
</button>
<div class="mm-aside__title">
{{ themeViewActive ? 'Cores do Tema' : categoriaAtiva.label }}
</div>
@@ -490,11 +604,18 @@ async function sair() {
</template>
<style scoped>
/* ─── Layer (overlay full-screen, transparente) ───────────── */
/* ─── Layer (overlay full-screen com blur sutil) ─────────────
Aplica um leve escurecimento + blur-xs (2px) atrás do menu pra dar
sensação de "modal" e desfocar o conteúdo embaixo. Em mobile (<lg)
o media query mais embaixo aumenta a intensidade pra cobrir todo
o viewport com força. */
.mm-layer {
position: absolute;
inset: 0;
z-index: 50;
background: rgba(0, 0, 0, 0.25);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
}
/* ─── Painel float ───────────────────────────────────────── */
@@ -525,7 +646,13 @@ async function sair() {
border-right: 1px solid var(--m-border);
background: var(--m-bg-soft);
}
.mm-side__head { padding: 18px 18px 8px; }
.mm-side__head {
padding: 18px 18px 8px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.mm-side__title {
text-transform: uppercase;
letter-spacing: 0.18em;
@@ -533,6 +660,22 @@ async function sair() {
font-size: 0.62rem;
font-weight: 600;
}
/* Botão fechar — só visível em mobile (≤lg). Vira display:flex no @media. */
.mm-side__close {
display: none;
width: 32px;
height: 32px;
align-items: center;
justify-content: center;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
color: var(--m-text);
border-radius: 9px;
cursor: pointer;
font-family: inherit;
transition: background-color 140ms ease;
}
.mm-side__close:hover { background: var(--m-bg-soft-hover); }
.mm-side__list {
flex: 1;
overflow-y: auto;
@@ -724,12 +867,39 @@ async function sair() {
flex-direction: column;
padding: 18px;
}
.mm-aside__head { margin-bottom: 14px; }
.mm-aside__head {
margin-bottom: 14px;
display: flex;
align-items: center;
gap: 12px;
}
.mm-aside__title {
color: var(--m-text);
font-size: 1.15rem;
font-weight: 500;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Botão voltar — só visível em mobile (≤lg) com drill-down ativo. */
.mm-aside__back {
display: none;
width: 32px;
height: 32px;
align-items: center;
justify-content: center;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
color: var(--m-text);
border-radius: 9px;
cursor: pointer;
flex-shrink: 0;
font-family: inherit;
transition: background-color 140ms ease, transform 140ms ease;
}
.mm-aside__back:hover { background: var(--m-bg-soft-hover); transform: translateX(-1px); }
.mm-aside__body {
flex: 1;
overflow-y: auto;
@@ -1039,4 +1209,108 @@ async function sair() {
font-size: 0.75rem;
line-height: 1.4;
}
/* ═══════════════════════════════════════════════════════════════
Responsivo <lg (≤1023px) — drawer da esquerda (paridade com Agenda)
───────────────────────────────────────────────────────────────
- .mm-layer vira backdrop fullscreen (escurece + blur), click fora fecha
- .mm-panel vira drawer 360px (mesmo tamanho do .ma-mobile-drawer),
desliza da esquerda
- .mm-side e .mm-aside viram camadas absolutas, alternam via
translateX controlado pelo modificador .is-mobile-sub
- Botão "fechar" no header da side, "voltar" no header do aside
- z-index do .mm-layer sobe pra 90 pra cobrir o ψ (70) e o dock (65)
═══════════════════════════════════════════════════════════════ */
@media (max-width: 1023px) {
/* Layer = backdrop. Click fora (no próprio layer) fecha via @click.self
que já existe no template. position:fixed garante cobertura mesmo
se algum ancestor estiver scrollado. */
.mm-layer {
position: fixed;
inset: 0;
z-index: 90;
background: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
.mm-panel {
position: fixed;
top: 0;
left: 0;
bottom: auto;
width: min(360px, 88vw); /* paridade com .ma-mobile-drawer */
height: 100dvh;
max-height: 100dvh;
border-radius: 0;
border-top: none;
border-left: none;
border-bottom: none;
border-right: 1px solid var(--m-border);
overflow: hidden;
}
/* As duas colunas viram camadas full do painel, animadas via translateX. */
.mm-side,
.mm-aside {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
transition: transform 280ms cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform;
}
.mm-side {
transform: translateX(0);
z-index: 1;
border-right: none;
}
.mm-aside {
transform: translateX(100%);
z-index: 2;
background: var(--m-bg-medium);
}
/* Modo "sub-itens" (drill-down ativo) */
.mm-panel.is-mobile-sub .mm-side {
transform: translateX(-12%); /* leve parallax pra dar profundidade */
}
.mm-panel.is-mobile-sub .mm-aside {
transform: translateX(0);
}
/* Botões mobile-only ganham display */
.mm-side__close--mobile-only { display: inline-flex; }
.mm-aside__back--mobile-only { display: inline-flex; }
/* Header da side fica um pouco mais aberto pra acomodar o close */
.mm-side__head {
padding-top: 14px;
padding-bottom: 14px;
}
.mm-side__title {
font-size: 0.7rem; /* lê melhor em mobile */
}
/* Aside head: o título fica MAIS espaçado no topo, e o aside ganha
padding lateral menor (telas pequenas precisam de cada pixel). */
.mm-aside {
padding: 14px 14px 18px;
}
.mm-aside__head {
margin-bottom: 12px;
}
/* Sub-itens com mais respiro vertical (toque tem que pegar) */
.mm-sub {
padding: 12px 12px;
font-size: 0.92rem;
}
.mm-cat {
padding: 12px 12px;
font-size: 0.95rem;
}
.mm-cat__icon { width: 36px; height: 36px; }
/* Footer continua na tela 1 (lista de categorias) */
}
</style>