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:
@@ -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): só 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>
|
||||
|
||||
Reference in New Issue
Block a user