diff --git a/src/layout/melissa/MelissaBusca.vue b/src/layout/melissa/MelissaBusca.vue index 92bd0cd..c454385 100644 --- a/src/layout/melissa/MelissaBusca.vue +++ b/src/layout/melissa/MelissaBusca.vue @@ -14,7 +14,8 @@ * Quando promover pra produção: trocar a busca por chamada à RPC * `search_global` + manter a mesma estrutura de panel/items. */ -import { ref, computed, onMounted, onBeforeUnmount } from 'vue'; +import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'; +import { supabase } from '@/lib/supabase/client'; const props = defineProps({ pacientes: { type: Array, default: () => [] }, @@ -31,7 +32,7 @@ const props = defineProps({ } }); -const emit = defineEmits(['acao', 'paciente', 'evento']); +const emit = defineEmits(['acao', 'paciente', 'evento', 'documento', 'intake']); const rootEl = ref(null); const inputEl = ref(null); @@ -39,6 +40,14 @@ const query = ref(''); const showPanel = ref(false); const activeIndex = ref(-1); +// RPC search_global results — preenchidos por debounce conforme o usuário digita. +// Cliente-side (props.pacientes / props.eventos) continua sendo fallback rápido +// pra digitação curta (1 char) e como complemento aos primeiros resultados. +const rpcResults = ref({ patients: [], appointments: [], documents: [], services: [], intakes: [] }); +const searching = ref(false); +let debounceT = null; +let searchSeq = 0; + function normalize(s) { return String(s || '') .normalize('NFD') @@ -62,9 +71,22 @@ const filteredAtalhos = computed(() => { }).slice(0, 5); }); +// Pacientes — combina RPC (autoritativo, todos os pacientes) com props (preview de hoje). +// RPC tem prioridade; props complementa quando RPC ainda não trouxe nada. const filteredPacientes = computed(() => { const q = normalize(query.value); if (q.length < 2) return []; + + const rpc = rpcResults.value.patients || []; + if (rpc.length) { + return rpc.slice(0, 5).map(p => ({ + id: p.id, + nome: p.nome_completo || p.nome_social || p.nome || '(sem nome)', + email: p.email, + telefone: p.telefone + })); + } + // Fallback client-side return props.pacientes .filter((p) => normalize(p.nome).includes(q)) .slice(0, 5); @@ -83,11 +105,19 @@ const filteredEventos = computed(() => { .slice(0, 5); }); +// Resultados exclusivos da RPC (não há fallback client-side) +const rpcAppointments = computed(() => rpcResults.value.appointments || []); +const rpcDocuments = computed(() => rpcResults.value.documents || []); +const rpcIntakes = computed(() => rpcResults.value.intakes || []); + const flatList = computed(() => { const out = []; 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 })); filteredEventos.value.forEach((e, i) => out.push({ group: 'eventos', item: e, idx: i })); + rpcAppointments.value.forEach((a, i) => out.push({ group: 'rpc-appointments', item: a, idx: i })); + rpcDocuments.value.forEach((d, i) => out.push({ group: 'rpc-documents', item: d, idx: i })); + rpcIntakes.value.forEach((r, i) => out.push({ group: 'rpc-intakes', item: r, idx: i })); return out; }); @@ -101,6 +131,9 @@ function selectEntry(entry) { if (entry.group === 'atalhos') emit('acao', entry.item.id); else if (entry.group === 'pacientes') emit('paciente', entry.item); else if (entry.group === 'eventos') emit('evento', entry.item); + else if (entry.group === 'rpc-appointments') emit('evento', entry.item); + else if (entry.group === 'rpc-documents') emit('documento', entry.item); + else if (entry.group === 'rpc-intakes') emit('intake', entry.item); closePanel(); } @@ -149,6 +182,49 @@ function onGlobalKeydown(e) { } } +// ── RPC search_global debounced ─────────────────────────────────────────── +// Mesmo padrão do GlobalSearch.vue: query >= 2 chars dispara em 200ms, +// controla ordem via searchSeq pra ignorar respostas obsoletas. +function resetRpcResults() { + rpcResults.value = { patients: [], appointments: [], documents: [], services: [], intakes: [] }; +} + +watch(query, (v) => { + if (debounceT) clearTimeout(debounceT); + const q = String(v || '').trim(); + if (q.length < 2) { + resetRpcResults(); + searching.value = false; + return; + } + searching.value = true; + const mySeq = ++searchSeq; + debounceT = setTimeout(async () => { + try { + const { data, error } = await supabase.rpc('search_global', { p_q: q, p_limit: 6 }); + if (mySeq !== searchSeq) return; + if (error) { + console.error('[MelissaBusca search_global]', error); + resetRpcResults(); + } else { + rpcResults.value = { + patients: Array.isArray(data?.patients) ? data.patients : [], + appointments: Array.isArray(data?.appointments) ? data.appointments : [], + documents: Array.isArray(data?.documents) ? data.documents : [], + services: Array.isArray(data?.services) ? data.services : [], + intakes: Array.isArray(data?.intakes) ? data.intakes : [] + }; + } + } catch (e) { + if (mySeq !== searchSeq) return; + console.error('[MelissaBusca search_global exception]', e); + resetRpcResults(); + } finally { + if (mySeq === searchSeq) searching.value = false; + } + }, 200); +}); + onMounted(() => { document.addEventListener('mousedown', onClickOutside); window.addEventListener('keydown', onGlobalKeydown); @@ -156,6 +232,7 @@ onMounted(() => { onBeforeUnmount(() => { document.removeEventListener('mousedown', onClickOutside); window.removeEventListener('keydown', onGlobalKeydown); + if (debounceT) clearTimeout(debounceT); }); @@ -248,6 +325,66 @@ onBeforeUnmount(() => { + + +