diff --git a/src/layout/melissa/MelissaLayout.vue b/src/layout/melissa/MelissaLayout.vue index 4c5d062..1f4a549 100644 --- a/src/layout/melissa/MelissaLayout.vue +++ b/src/layout/melissa/MelissaLayout.vue @@ -575,9 +575,50 @@ function setPreset(name) { // ─────────────────────────────────────────────────────────────── -// Settings popover (canto superior direito) +// Settings popover (canto inferior direito — vive na .melissa-tray) // ─────────────────────────────────────────────────────────────── const settingsOpen = ref(false); +const cogBtnEl = ref(null); + +// "More" tray popup — visivel so em mobile ( { + if (open) document.addEventListener('mousedown', onSettingsDocMouseDown, true); + else document.removeEventListener('mousedown', onSettingsDocMouseDown, true); +}); +onBeforeUnmount(() => document.removeEventListener('mousedown', onSettingsDocMouseDown, true)); + +// Mesmo padrao pro popup "More" do tray em mobile: ignora o proprio +// botao trigger (senao fecha + reabre no click) e fecha em qualquer +// outro lugar fora do panel. +function onTrayMoreDocMouseDown(e) { + if (!trayMoreOpen.value) return; + const t = e.target; + if (!(t instanceof Element)) return; + if (t.closest('.melissa-tray__more-panel')) return; + if (trayMoreBtnEl.value?.contains(t)) return; + trayMoreOpen.value = false; +} +watch(trayMoreOpen, (open) => { + if (open) document.addEventListener('mousedown', onTrayMoreDocMouseDown, true); + else document.removeEventListener('mousedown', onTrayMoreDocMouseDown, true); +}); +onBeforeUnmount(() => document.removeEventListener('mousedown', onTrayMoreDocMouseDown, true)); // ─────────────────────────────────────────────────────────────── // Timeline horizontal — range/eco/posicoes/auto-scroll/cursor "Agora" @@ -587,10 +628,19 @@ const settingsOpen = ref(false); // Pai so passa eventos brutos + workRules/settings/feriados via props, // e recebe @evento (pra abrir dialog) + @clear-filter (pra limpar tipo). -// Contagens por tipo + frase resumo do dia +// Contagens por tipo + frase resumo do dia. Pra sessao tambem quebro +// por status (cancelado/remarcado) pra montar o sufixo "(x foi cancelado, +// x foi remarcado)" depois do chip de atendimentos. const contagensDia = computed(() => { - const c = { sessao: 0, supervisao: 0, reuniao: 0 }; - for (const ev of eventosHojeReais.value) c[ev.tipo] = (c[ev.tipo] || 0) + 1; + const c = { sessao: 0, supervisao: 0, reuniao: 0, sessaoCancelada: 0, sessaoRemarcada: 0 }; + for (const ev of eventosHojeReais.value) { + c[ev.tipo] = (c[ev.tipo] || 0) + 1; + if (ev.tipo === 'sessao') { + const s = String(ev.status || '').toLowerCase(); + if (s === 'cancelado' || s === 'cancelada') c.sessaoCancelada += 1; + else if (s === 'remarcado') c.sessaoRemarcada += 1; + } + } return c; }); @@ -598,11 +648,34 @@ function pluralizar(n, singular, plural) { return `${n} ${n === 1 ? singular : plural}`; } +// Sufixo "(1 foi cancelado, 2 foram remarcados)" depois do chip de +// atendimentos quando houver sessoes cancel/remarcado no dia. +function _statusSuffix(qtdCancel, qtdRemarc) { + const partes = []; + if (qtdCancel > 0) { + partes.push(qtdCancel === 1 + ? `${qtdCancel} foi cancelado` + : `${qtdCancel} foram cancelados`); + } + if (qtdRemarc > 0) { + partes.push(qtdRemarc === 1 + ? `${qtdRemarc} foi remarcado` + : `${qtdRemarc} foram remarcados`); + } + return partes.length ? ` (${partes.join(', ')})` : ''; +} + // Partes estruturadas pro template renderizar cada contagem como link clicável const resumoPartes = computed(() => { const c = contagensDia.value; const partes = []; - if (c.sessao > 0) partes.push({ tipo: 'sessao', text: pluralizar(c.sessao, 'atendimento', 'atendimentos') }); + if (c.sessao > 0) { + partes.push({ + tipo: 'sessao', + text: pluralizar(c.sessao, 'atendimento', 'atendimentos'), + suffix: _statusSuffix(c.sessaoCancelada, c.sessaoRemarcada) + }); + } if (c.supervisao > 0) partes.push({ tipo: 'supervisao', text: pluralizar(c.supervisao, 'supervisão', 'supervisões') }); if (c.reuniao > 0) partes.push({ tipo: 'reuniao', text: pluralizar(c.reuniao, 'reunião', 'reuniões') }); return partes; @@ -1745,6 +1818,22 @@ function _callOnAgenda(action) { if (secaoAberta.value !== 'agenda') abrirSecao('agenda'); } +// MelissaBusca @goto-date — usuario digitou "hoje"/"20/06" na busca +// global. Abre a agenda se fechada (via _callOnAgenda que enfileira a +// action ate o ref aparecer) e chama gotoDate exposto pela MelissaAgenda. +function onBuscaGotoDate(date) { + if (!(date instanceof Date) || Number.isNaN(date.getTime())) return; + _callOnAgenda((agenda) => agenda.gotoDate?.(date)); +} + +// Ref + provide pra qualquer secao filha pedir pra abrir a busca global +// programaticamente. UI nao tem mais botao por secao (lupa unica fica +// na .melissa-tray), mas o inject permanece exposto pra acoes contextuais +// futuras (ex: "buscar paciente" num componente filho que quer abrir +// o spotlight com query pre-preenchida). +const melissaBuscaRef = ref(null); +provide('openMelissaBusca', () => melissaBuscaRef.value?.openDialog?.()); + function onAbrirProntuario() { const ev = eventoSelecionado.value; if (!ev?.patient_id) { @@ -1987,6 +2076,30 @@ const { toqueTermino, testarToque } = useMelissaToques('sino'); function abrirCronometro() { cronoRef.value?.abrir(); } + +// Click no botao ⏱ de um evento da timeline (ou no CTA do card +// "Proximo paciente" quando o evento esta em curso). Pre-seleciona +// o paciente + autostart. Se ja houver cronometro rodando de outro +// paciente, mostra toast sem trocar (opcao b decidida 2026-05-22). +function onIniciarCronometroFromEvento(ev) { + if (!ev?.patient_id) return; + // Plano programado: horario original do evento na agenda. So passa se + // os campos forem numericos validos — abrir() sanitiza de novo internamente. + const sessionPlan = (typeof ev.startH === 'number' && typeof ev.endH === 'number') + ? { startH: ev.startH, endH: ev.endH } + : null; + const ret = cronoRef.value?.abrir({ pacienteId: ev.patient_id, autostart: true, sessionPlan }); + if (ret && !ret.opened && ret.alreadyRunning && !ret.samePaciente) { + const atualNome = pacientesReais.value.find((p) => String(p.id) === String(ret.pacienteId))?.nome + || 'outro paciente'; + toast.add({ + severity: 'warn', + summary: 'Cronômetro já ativo', + detail: `Sessão de ${atualNome} em andamento. Pare o cronômetro atual antes de iniciar outro.`, + life: 3500 + }); + } +} function fecharCronometro() { cronoRef.value?.fechar(); } @@ -2347,71 +2460,6 @@ function onKeydown(e) {
- - - - -
- - - - - - - - - - - - - - - -
-
@@ -2428,6 +2476,7 @@ function onKeydown(e) { @@ -2448,6 +2498,7 @@ function onKeydown(e) { :filtro-tipo="filtroTipo" @evento="abrirEvento" @clear-filter="limparFiltro" + @iniciar-cronometro="onIniciarCronometroFromEvento" /> @@ -2460,14 +2511,24 @@ function onKeydown(e) { :class="cardsLayout === 'linha-unica' ? 'cards-row--linha' : 'cards-row--wrap'" >