diff --git a/src/layout/melissa/MelissaAgenda.vue b/src/layout/melissa/MelissaAgenda.vue index ed61398..06cbc45 100644 --- a/src/layout/melissa/MelissaAgenda.vue +++ b/src/layout/melissa/MelissaAgenda.vue @@ -31,7 +31,6 @@ import ProximosFeriadosCard from '@/features/agenda/components/ProximosFeriadosC import MelissaAgendaHistoricoCard from './MelissaAgendaHistoricoCard.vue'; import PatientCreatePopover from '@/components/ui/PatientCreatePopover.vue'; import Popover from 'primevue/popover'; -import MelissaAgendaSearchPopover from './MelissaAgendaSearchPopover.vue'; import MelissaAgendaActionsPopover from './MelissaAgendaActionsPopover.vue'; import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue'; import ComponentCadastroRapido from '@/components/ComponentCadastroRapido.vue'; @@ -691,29 +690,15 @@ const fcOptions = computed(() => ({ })); // ── Busca da toolbar (datas + paciente/título) ──────────────── -// Componente MelissaAgendaSearchPopover (autocontido) gerencia o Cmd+K -// search inteiro — input, debounce, parsing de datas, resultados. -// Aqui só ficam o ref pro toggle (botão da toolbar + hotkey global) e -// os 2 handlers que decidem o que fazer com a escolha (gotoDate + -// auto-select de paciente quando há patient_id no evento). -const searchPopover = ref(null); - +// Busca agora vive na MelissaBusca (global, Ctrl+K em qualquer tela ou +// botao na .melissa-tray). Aqui fica so a fn gotoDate exposta pro +// MelissaLayout chamar quando o usuario escolhe "Ir para [data]" na +// busca global — vide defineExpose mais abaixo. function onBuscaGotoDate(date) { fcApi()?.gotoDate(date); refDate.value = new Date(date); } -function onBuscaSelectEvento(ev) { - if (!ev?.inicio_em) return; - fcApi()?.gotoDate(ev.inicio_em); - refDate.value = new Date(ev.inicio_em); - // Auto-seleciona o paciente se o evento tiver um — assim a agenda já - // fica filtrada por ele e o dock contextual aparece. - if (ev.patient_id) { - pacienteSelecionadoId.value = ev.patient_id; - } -} - // 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); @@ -760,18 +745,8 @@ async function onHistoricoOpen({ id }) { } } -// Atalho global Ctrl/Cmd+K abre a busca via ref do popover. -function _onSearchHotkey(e) { - if ((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')) { - e.preventDefault(); - // Anchor virtual no botão da toolbar — necessário pra Popover do - // PrimeVue posicionar corretamente. - const btn = document.querySelector('.ma-cal__search-btn'); - if (btn) searchPopover.value?.toggle({ currentTarget: btn, target: btn }); - } -} -onMounted(() => { window.addEventListener('keydown', _onSearchHotkey); }); -onBeforeUnmount(() => { window.removeEventListener('keydown', _onSearchHotkey); }); +// Ctrl+K e' tratado pela propria MelissaBusca (listener global no +// window) — removido o handler local pra nao disparar 2 vezes. // Toolbar — atalhos pra FC API function fcApi() { @@ -1321,12 +1296,20 @@ function openProntuario(patient) { if (!patient?.id) return; abrirProntuarioPorId(patient.id); } +// gotoDate exposto pro MelissaLayout chamar quando o usuario escolhe +// "Ir para [data]" na MelissaBusca (busca global). Reusa onBuscaGotoDate +// que ja atualiza fcApi + refDate. +function gotoDateExternal(date) { + onBuscaGotoDate(date); +} + defineExpose({ refetch: refetchEventosFc, openProntuario, setView, openSessoesPaciente, - openEditPatient + openEditPatient, + gotoDate: gotoDateExternal }); @@ -1629,21 +1612,10 @@ defineExpose({ /> - - - + @@ -2051,9 +2023,9 @@ defineExpose({ to { opacity: 1; transform: scale(1); } } -/* .ma-tsearch* migrou inteiro pra MelissaAgendaSearchPopover.vue (componente - autocontido). Cmd+K hotkey global continua aqui no parent — chama - `searchPopover.value?.toggle(...)` apontando pro botao .ma-cal__search-btn. */ +/* Busca da agenda migrou inteira pra MelissaBusca (componente global, + no MelissaLayout). Acesso por Ctrl+K em qualquer tela ou pela lupa na + .melissa-tray (canto inferior direito). Toolbar nao tem mais botao. */ /* .ma-actions* migrou inteiro pra MelissaAgendaActionsPopover.vue (componente autocontido, com utilities Tailwind no template). */ diff --git a/src/layout/melissa/MelissaAgendaSearchPopover.vue b/src/layout/melissa/MelissaAgendaSearchPopover.vue deleted file mode 100644 index 24d063d..0000000 --- a/src/layout/melissa/MelissaAgendaSearchPopover.vue +++ /dev/null @@ -1,327 +0,0 @@ - - - - - diff --git a/src/layout/melissa/MelissaBusca.vue b/src/layout/melissa/MelissaBusca.vue index 8c1d981..f2b7423 100644 --- a/src/layout/melissa/MelissaBusca.vue +++ b/src/layout/melissa/MelissaBusca.vue @@ -33,7 +33,7 @@ const props = defineProps({ } }); -const emit = defineEmits(['acao', 'paciente', 'evento', 'documento', 'intake']); +const emit = defineEmits(['acao', 'paciente', 'evento', 'documento', 'intake', 'goto-date']); const rootEl = ref(null); const inputEl = ref(null); @@ -62,12 +62,59 @@ function normalize(s) { .trim(); } +// Parser de data — portado de MelissaAgendaSearchPopover. +// Aceita: "hoje", "amanha"/"amanhã", "ontem", "DD/MM", "DD/MM/YYYY" +// (separadores /, - ou .). Retorna Date|null. Acao "Ir para esta data" +// so se torna visivel quando ha match (vide dateMatch computed). +function parseSearchAsDate(str) { + const t = String(str || '').trim().toLowerCase(); + if (!t) return null; + if (t === 'hoje') { const d = new Date(); d.setHours(0, 0, 0, 0); return d; } + if (t === 'amanha' || t === 'amanhã') { + const d = new Date(); d.setHours(0, 0, 0, 0); d.setDate(d.getDate() + 1); return d; + } + if (t === 'ontem') { + const d = new Date(); d.setHours(0, 0, 0, 0); d.setDate(d.getDate() - 1); return d; + } + const m = t.match(/^(\d{1,2})[/\-.](\d{1,2})(?:[/\-.](\d{2,4}))?$/); + if (m) { + const day = parseInt(m[1], 10); + const month = parseInt(m[2], 10); + let year = parseInt(m[3] || '', 10); + if (!year || Number.isNaN(year)) year = new Date().getFullYear(); + if (year < 100) year += 2000; + if (day < 1 || day > 31 || month < 1 || month > 12) return null; + const d = new Date(year, month - 1, day); + if (Number.isNaN(d.getTime())) return null; + return d; + } + return null; +} + function fmtHora(h) { const horas = Math.floor(h); const mins = Math.round((h - horas) * 60); return `${String(horas).padStart(2, '0')}:${String(mins).padStart(2, '0')}`; } +// Match de data — se a query parseia como data, a primeira "linha" do +// painel vira um card destacado "Ir para [data]" (igual ao popover da +// agenda). Click/Enter dispara emit('goto-date', date) e o MelissaLayout +// abre a agenda + navega o calendario. +const dateMatch = computed(() => parseSearchAsDate(query.value)); + +function fmtDataLonga(d) { + if (!(d instanceof Date) || Number.isNaN(d.getTime())) return ''; + // "Sábado, 20/06/2026" — primeira letra maiuscula no weekday + const s = d.toLocaleDateString('pt-BR', { + weekday: 'long', + day: '2-digit', + month: '2-digit', + year: 'numeric' + }); + return s.charAt(0).toUpperCase() + s.slice(1); +} + const filteredAtalhos = computed(() => { const q = normalize(query.value); if (!q) return props.atalhos.slice(0, 4); // 4 defaults quando vazio @@ -122,6 +169,9 @@ const rpcIntakes = computed(() => rpcResults.value.intakes || []); const flatList = computed(() => { const out = []; + // "Ir para [data]" sempre no topo quando query parseia como data — + // acao predominante (Enter direto seleciona ela). + if (dateMatch.value) out.push({ group: 'goto-date', item: dateMatch.value, idx: 0 }); recentItems.value.forEach((p, i) => out.push({ group: 'recent', item: p, idx: i })); filteredAtalhos.value.forEach((a, i) => out.push({ group: 'atalhos', item: a, idx: i })); filteredPacientes.value.forEach((p, i) => out.push({ group: 'pacientes', item: p, idx: i })); @@ -139,7 +189,8 @@ function findFlatIndex(group, idx) { } function selectEntry(entry) { - if (entry.group === 'atalhos') emit('acao', entry.item.id); + if (entry.group === 'goto-date') emit('goto-date', entry.item); + else if (entry.group === 'atalhos') emit('acao', entry.item.id); else if (entry.group === 'pacientes') emit('paciente', entry.item); else if (entry.group === 'recent') emit('paciente', { id: entry.item.id, nome: entry.item.nome, ...entry.item.extras }); else if (entry.group === 'eventos') emit('evento', entry.item); @@ -176,9 +227,17 @@ function onKeydown(e) { } else if (e.key === 'ArrowUp') { e.preventDefault(); activeIndex.value = Math.max(activeIndex.value - 1, 0); - } else if (e.key === 'Enter' && activeIndex.value >= 0) { - e.preventDefault(); - selectEntry(flatList.value[activeIndex.value]); + } else if (e.key === 'Enter') { + // Enter sem selecao explicita: pega o primeiro item do flatList + // (UX spotlight padrao — usuario digita "hoje" + Enter deve ir + // direto pra hoje sem precisar ArrowDown). + if (activeIndex.value >= 0) { + e.preventDefault(); + selectEntry(flatList.value[activeIndex.value]); + } else if (flatList.value.length > 0) { + e.preventDefault(); + selectEntry(flatList.value[0]); + } } // Escape é tratado pelo Dialog (dismissableMask + closable) } @@ -206,6 +265,14 @@ watch(query, (v) => { searching.value = false; return; } + // Query parseou como data: pula RPC (nao faz sentido buscar paciente + // chamado "20/06"). Card "Ir para data" cobre o caso sozinho. + if (dateMatch.value) { + ++searchSeq; // invalida requests em flight + resetRpcResults(); + searching.value = false; + return; + } searching.value = true; const mySeq = ++searchSeq; debounceT = setTimeout(async () => { @@ -241,6 +308,12 @@ onBeforeUnmount(() => { window.removeEventListener('keydown', onGlobalKeydown); if (debounceT) clearTimeout(debounceT); }); + +// Exposto pro MelissaLayout — a lupa unica na .melissa-tray chama +// melissaBuscaRef.openDialog() direto, e o provide('openMelissaBusca') +// reusa o mesmo metodo pra qualquer descendente que queira abrir o +// spotlight programaticamente. closeDialog alias do closePanel. +defineExpose({ openDialog, closeDialog: closePanel });