Melissa polish + Prontuario Visao Geral + agenda historico

Sprints B (05-03) e C (05-04) acumulados:

- NotificationDrawer/Item redesign (visual mais limpo, ações inline)
- Dock pins compose (useMelissaDockPins) + cache store global (melissaCacheStore)
- MelissaAgenda: timeline FullCalendar parity + cards resumo, histórico
  card com useMelissaAgendaHistorico, MelissaEventoPanel ajustado
- useFeriados: cache opt-in pra evitar fetch redundante de feriados
- PatientProntuario: aba Visão Geral nova; PatientConversationsTab polish
- AgendaClinicMosaic / AgendaTerapeutaPage / useAgendaSettings: ajustes
  de paridade com Melissa
- DocumentsListPage: pequenos ajustes
- DB migration 20260504000001: fix do trigger pra status 'excluido' nas
  cancel_notifications

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-06 09:11:55 -03:00
parent 86311ef305
commit 957e912a7f
19 changed files with 5203 additions and 285 deletions
+118 -28
View File
@@ -25,10 +25,9 @@ import ptBrLocale from '@fullcalendar/core/locales/pt-br';
import { useMelissaEventosRange, useMelissaTodasSessoesPaciente, searchEventosByText } from './composables/useMelissaEventos';
import { MELISSA_AGENDA_KEY } from './composables/useMelissaAgenda';
import { useMelissaPacientesAside } from './composables/useMelissaPacientesAside';
import { useFeriados } from '@/composables/useFeriados';
import { useTenantStore } from '@/stores/tenantStore';
import { useAgendaSettings } from '@/features/agenda/composables/useAgendaSettings';
import ProximosFeriadosCard from '@/features/agenda/components/ProximosFeriadosCard.vue';
import MelissaAgendaHistoricoCard from './MelissaAgendaHistoricoCard.vue';
import PatientCreatePopover from '@/components/ui/PatientCreatePopover.vue';
import Popover from 'primevue/popover';
import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue';
@@ -531,11 +530,15 @@ const fcOptions = computed(() => ({
eventDrop: (info) => {
if (!M) { info.revert?.(); return; }
M.persistMoveOrResize(info, 'Sessão movida');
// audit_logs grava no AFTER trigger; pequeno delay garante que
// a query do histórico já pegue a entrada nova.
setTimeout(() => historicoCardRef.value?.refetch(), 700);
},
// Resize → muda duração da sessão
eventResize: (info) => {
if (!M) { info.revert?.(); return; }
M.persistMoveOrResize(info, 'Duração alterada');
setTimeout(() => historicoCardRef.value?.refetch(), 700);
},
// Click-drag em área vazia → abre dialog pra criar evento novo, com
// start/end pré-preenchidos. AgendaEventDialog cuida do resto (seleção
@@ -547,8 +550,18 @@ const fcOptions = computed(() => ({
eventContent: (arg) => {
const ext = arg.event.extendedProps || {};
const titulo = arg.event.title || '—';
const time = arg.timeText || '';
const isSessao = String(ext.tipo || '').toLowerCase() === 'sessao';
const fmtHour = (d) => {
if (!d) return '';
const h = d.getHours();
const m = d.getMinutes();
return m === 0 ? `${h}h` : `${h}:${String(m).padStart(2, '0')}h`;
};
const range = arg.event.start && arg.event.end
? `${fmtHour(arg.event.start)}-${fmtHour(arg.event.end)}`
: (arg.timeText || '');
// Badges só pra sessões — compromissos pessoais/bloqueios/feriados
// não têm status nem modalidade relevantes pra exibir.
let badgesHtml = '';
@@ -567,11 +580,11 @@ const fcOptions = computed(() => ({
// Pra eventos não-sessão (compromisso, bloqueio etc.) mantém o
// antigo `__meta` com modalidade ou título secundário.
const metaFallback = !isSessao ? (ext.modalidade || ext.titulo || '') : '';
const titleLine = `<div class="mc-fc-event__title"><span class="mc-fc-event__name">${escHtml(titulo)}</span>${range ? ` <span class="mc-fc-event__hour">(${escHtml(range)})</span>` : ''}</div>`;
return {
html: `
<div class="mc-fc-event">
<div class="mc-fc-event__time">${escHtml(time)}</div>
<div class="mc-fc-event__title">${escHtml(titulo)}</div>
${titleLine}
${badgesHtml}
${metaFallback ? `<div class="mc-fc-event__meta">${escHtml(metaFallback)}</div>` : ''}
</div>
@@ -684,6 +697,43 @@ function irParaData(date) {
searchDateMatch.value = null;
}
// Card de histórico (audit_logs) — ref pra disparar refetch após
// mutações; handler que abre o evento clicado pelo id.
const historicoCardRef = ref(null);
async function onHistoricoOpen({ id }) {
if (!id) return;
try {
const { data, error } = await supabase
.from('agenda_eventos')
.select('*, patients!agenda_eventos_patient_id_fkey(nome_completo, status, avatar_url)')
.eq('id', id)
.maybeSingle();
if (error || !data) {
// Evento pode ter sido deletado depois da entrada — fail-soft.
return;
}
// Foca no dia + emite seleção pro MelissaLayout abrir o panel.
const ev = {
...data,
patient_id: data.patient_id,
paciente_nome: data.patients?.nome_completo || '',
paciente_status: data.patients?.status || '',
paciente_avatar: data.patients?.avatar_url || '',
startH: new Date(data.inicio_em).getHours() + new Date(data.inicio_em).getMinutes() / 60,
endH: new Date(data.fim_em).getHours() + new Date(data.fim_em).getMinutes() / 60,
label: data.patients?.nome_completo || data.titulo || data.titulo_custom || '—'
};
if (data.inicio_em) {
fcApi()?.gotoDate(data.inicio_em);
refDate.value = new Date(data.inicio_em);
}
emit('select-evento', ev);
} catch (e) {
// eslint-disable-next-line no-console
console.warn('[onHistoricoOpen]', e);
}
}
function onSelecionarResultado(ev) {
if (!ev?.inicio_em) return;
fcApi()?.gotoDate(ev.inicio_em);
@@ -764,12 +814,15 @@ const miniRefDate = ref(new Date());
// ── Feriados (nacionais via algoritmo + municipais/personalizados via DB)
// + Jornada semanal (workRules) pra marcar dias fechados em cinza no mini-cal.
// Pattern espelha AgendaTerapeutaPage:113,129-134,1143.
// IMPORTANTE: declarado APÓS miniRefDate porque o watch abaixo lê
// miniRefDate.value durante o setup (rastreio de dependências).
// Reusa as refs do composable injetado M — antes essa página instanciava
// novamente useFeriados() e useAgendaSettings(), gerando duplicação de
// queries (feriados municipais + agenda_configuracoes + agenda_regras).
const tenantStore = useTenantStore();
const { todos: feriadosTodos, load: loadFeriados, ano: feriadosAno, fcEvents: feriadoFcEvents } = useFeriados();
const { workRules, load: loadAgendaSettings } = useAgendaSettings();
const feriadosTodos = M.feriados;
const feriadoFcEvents = M.feriadoFcEvents;
const feriadosAno = M.feriadosAno;
const loadFeriados = M.loadFeriadosBase;
const workRules = M.workRules;
// Set de dias da semana ativos (0=dom..6=sáb). Fallback seg-sex se sem regras
// — mesmo default do AgendaTerapeutaPage:370.
@@ -779,15 +832,10 @@ const workDowSet = computed(() => {
return new Set([1, 2, 3, 4, 5]);
});
onMounted(() => {
const tid = tenantStore.activeTenantId || tenantStore.tenantId || tenantStore.tenant?.id || null;
if (tid) loadFeriados(tid, new Date().getFullYear());
// workRules é por owner_id (RLS), não precisa do tenant
loadAgendaSettings();
});
// Recarrega feriados quando mini-cal navega pra outro ano (municipais variam).
// Nacionais são puro algoritmo — recomputam automático no useFeriados via `ano`.
// Carga inicial de feriados/settings já é feita pelo useMelissaAgenda no
// mount (watch immediate em clinicTenantId + loadSettings paralelo). Não
// duplicamos aqui — só mantemos o reload de feriados quando o mini-cal
// navega pra outro ano (municipais variam por ano).
watch(
() => miniRefDate.value.getFullYear(),
(novoAno) => {
@@ -1080,6 +1128,21 @@ function abrirSessoesPaciente() {
verTodasSessoes.value = true;
fetchTodasSessoes(pacienteSelecionadoId.value);
}
// API pública pro MelissaLayout (botão "Sessões" do MelissaEventoPanel):
// seleciona o paciente e abre o overlay "Todas as sessões" no mesmo
// fluxo do .ma-dock-actions. Importante: setar pacienteSelecionadoId
// ANTES de verTodasSessoes — o watch logo abaixo reseta verTodasSessoes
// quando pacienteSelecionadoId muda, então fazemos a ordem inversa.
function openSessoesPaciente(patientId) {
if (!patientId) return;
const id = String(patientId);
if (pacienteSelecionadoId.value !== id) {
pacienteSelecionadoId.value = id;
}
verTodasSessoes.value = true;
fetchTodasSessoes(id);
}
function voltarParaPeriodo() {
verTodasSessoes.value = false;
resetTodasSessoes();
@@ -1125,6 +1188,14 @@ function abrirProntuarioPaciente() {
prontuarioPatient.value = { ...p };
prontuarioOpen.value = true;
}
// API pública pra MelissaLayout chamar via ref (botão "Editar paciente"
// do MelissaEventoPanel). Abre o PatientCadastroDialog já no modo edição.
function openEditPatient(patientId) {
if (!patientId) return;
editPatientId.value = String(patientId);
cadastroFullDialog.value = true;
}
function editarPacienteSelecionado() {
if (!pacienteSelecionadoId.value) return;
editPatientId.value = String(pacienteSelecionadoId.value);
@@ -1171,7 +1242,9 @@ function openProntuario(patient) {
defineExpose({
refetch: refetchEventosFc,
openProntuario,
setView
setView,
openSessoesPaciente,
openEditPatient
});
</script>
@@ -1887,6 +1960,14 @@ defineExpose({
@bloqueado="onFeriadoBloqueado"
/>
<!-- Histórico de ações na agenda (audit_logs) — útil pra
rastrear movimentações recentes. Click na entrada
abre o evento (se ainda existe). -->
<MelissaAgendaHistoricoCard
ref="historicoCardRef"
@open-evento="onHistoricoOpen"
/>
</aside>
</Teleport>
</div>
@@ -3210,22 +3291,26 @@ html.app-dark .ma-tsearch__result--date .ma-tsearch__result-sub {
color: var(--m-text);
font-family: inherit;
}
.ma-cal__fc :deep(.mc-fc-event__time) {
font-size: 0.6rem;
color: var(--m-text-muted);
font-variant-numeric: tabular-nums;
line-height: 1.1;
}
.ma-cal__fc :deep(.mc-fc-event__title) {
/* Mesmo tamanho/peso da .ma-pat__name (lista de pacientes) pra
alinhar a hierarquia visual entre aside e calendário. */
alinhar a hierarquia visual entre aside e calendário.
Nome + hora em linha única; ellipsis corta o nome antes da hora. */
font-size: 0.85rem;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.2;
margin-top: 1px;
}
.ma-cal__fc :deep(.mc-fc-event__name) {
font-weight: 500;
}
.ma-cal__fc :deep(.mc-fc-event__hour) {
font-size: 0.7rem;
font-weight: 400;
color: var(--m-text-muted);
font-variant-numeric: tabular-nums;
margin-left: 2px;
}
.ma-cal__fc :deep(.mc-fc-event__meta) {
font-size: 0.6rem;
@@ -3495,6 +3580,11 @@ html:not(.app-dark) .ma-cal__fc :deep(.mc-fc-event__badge--modal.is-presencial)
background: var(--m-bg-medium) !important;
border-color: var(--m-border) !important;
border-radius: 12px !important;
min-height: 158px;
/* flex: 0 0 auto — toma o tamanho natural do conteúdo (incluindo
expansão da confirmação inline) e NÃO encolhe quando o histórico
crescer. O histórico (flex: 1 abaixo) absorve o restante. */
flex: 0 0 auto;
}
:deep(.ma-w-feriados .border-b) {
border-color: var(--m-border);