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
+13 -10
View File
@@ -126,18 +126,22 @@ watch(() => props.entityId, async (v) => {
if (v) await api.loadEmails(props.entityType, v);
else api.emails.value = [];
});
// Re-emite `change` sempre que a lista mudar (load, add, edit, remove,
// setPrimary). Permite que o parent trackee count e faça validação de
// "pelo menos 1 email obrigatório". immediate:true garante emit no load.
watch(api.emails, (arr) => emit('change', arr), { deep: true, immediate: true });
// Exposto pro parent — flush em lote dos emails pendentes (modo "novo
// paciente" antes do save). Ver doc no useContactEmails.flushPending.
async function flushPending(entityType, entityId) {
return api.flushPending(entityType, entityId);
}
defineExpose({ flushPending });
</script>
<template>
<div class="flex flex-col gap-2">
<div class="text-[0.7rem] text-[var(--text-color-secondary)] flex items-start gap-1.5 px-1">
<i class="pi pi-info-circle text-sky-500 mt-0.5 shrink-0" />
<span>
Marque um email como <strong>principal</strong> ele é usado pra
<strong>envio de faturas, templates e notificações por email</strong>.
</span>
</div>
<div v-if="api.loading.value" class="text-xs text-center py-3 text-[var(--text-color-secondary)]">
<i class="pi pi-spin pi-spinner mr-1" /> Carregando
</div>
@@ -232,6 +236,7 @@ watch(() => props.entityId, async (v) => {
</div>
</div>
<!-- Botão + (sempre habilitado: sem entityId vai pro modo pendente) -->
<Button
v-if="!readonly && !showAddForm"
label="Adicionar email"
@@ -240,8 +245,6 @@ watch(() => props.entityId, async (v) => {
outlined
size="small"
class="self-start rounded-full"
:disabled="!props.entityId"
v-tooltip.right="!props.entityId ? 'Salve o cadastro primeiro pra adicionar emails' : null"
@click="openAddForm"
/>
</div>
+15 -13
View File
@@ -153,20 +153,24 @@ watch(() => props.entityId, async (v) => {
if (v) await api.loadPhones(props.entityType, v);
else api.phones.value = [];
});
// Re-emite `change` sempre que a lista mudar (load, add, edit, remove,
// setPrimary). Permite que o parent trackee count e faça validação de
// "pelo menos 1 telefone obrigatório" sem precisar inspeccionar o
// componente. immediate:true garante emit no load inicial.
watch(api.phones, (arr) => emit('change', arr), { deep: true, immediate: true });
// Exposto pro parent — usado pelo PatientsCadastroPage no fluxo de criação:
// telefones inseridos antes de salvar o paciente ficam em modo pendente
// (id: 'pending_*') e são gravados em lote depois que o paciente recebe id.
async function flushPending(entityType, entityId) {
return api.flushPending(entityType, entityId);
}
defineExpose({ flushPending });
</script>
<template>
<div class="flex flex-col gap-2">
<!-- Aviso sobre telefone principal -->
<div class="text-[0.7rem] text-[var(--text-color-secondary)] flex items-start gap-1.5 px-1">
<i class="pi pi-info-circle text-sky-500 mt-0.5 shrink-0" />
<span>
Marque um telefone como <strong>principal</strong> ele é usado pra
<strong>cobranças, lembretes automáticos e contato padrão</strong>.
Número vindo do CRM WhatsApp recebe a etiqueta <strong>"vinculado"</strong>.
</span>
</div>
<!-- Lista de telefones -->
<div v-if="api.loading.value" class="text-xs text-center py-3 text-[var(--text-color-secondary)]">
<i class="pi pi-spin pi-spinner mr-1" /> Carregando
@@ -325,7 +329,7 @@ watch(() => props.entityId, async (v) => {
</div>
</div>
<!-- Botão + -->
<!-- Botão + (sempre habilitado: sem entityId vai pro modo pendente) -->
<Button
v-if="!readonly && !showAddForm"
label="Adicionar telefone"
@@ -334,8 +338,6 @@ watch(() => props.entityId, async (v) => {
outlined
size="small"
class="self-start rounded-full"
:disabled="!props.entityId"
v-tooltip.right="!props.entityId ? 'Salve o cadastro primeiro pra adicionar telefones' : null"
@click="openAddForm"
/>
</div>
+5 -1
View File
@@ -89,7 +89,11 @@ async function onCreated(data) {
:closable="false"
:dismissableMask="false"
:maximizable="false"
:style="{ width: '90vw', maxWidth: '1100px', height: maximized ? '100vh' : '90vh' }"
:style="{
width: maximized ? '100vw' : '90vw',
maxWidth: maximized ? 'none' : '1100px',
height: maximized ? '100vh' : '90vh'
}"
:contentStyle="{ padding: 0, overflow: 'auto', height: '100%' }"
pt:mask:class="backdrop-blur-xs"
>