roadmap #11: recently viewed (ultimos 5 pacientes acessados)

ROADMAP item #1.3 #11. localStorage por user_id pra isolar sessoes
diferentes no mesmo browser. ROADMAP sugeria localStorage OU tabela
user_recent_access — escolhi localStorage por simplicidade (sem
migration adicional + zero round-trip por visita).

composables/useRecentPatients.js:
- useRecentPatients() — composable reativo Tipo A: items + hasItems
  + addVisit + remove + clear + refresh
- registerPatientVisit(patient) — helper stateless pra usar fora
  de setup (ex: navigation guards, action handlers)
- Sincroniza entre instancias na mesma aba via CustomEvent + 'storage'
- Max 5 items. Dedup por id, novo no topo.

Wire-up de visita (registra ao carregar prontuario):
- MelissaPaciente.vue: registerPatientVisit no loadAll apos detail.load
- PatientProntuario.vue: registerPatientVisit em loadDetail apos p resolved

Wire-up de visualizacao (mostra quando query vazia):
- GlobalSearch.vue: grupo "Acessados recentemente" antes dos Atalhos.
  goTo("recent") navega pra /therapist/patients/:id.
- MelissaBusca.vue: grupo "Acessados recentemente". emit('paciente')
  reusando a logica do MelissaLayout que ja navega pra
  /melissa/paciente?id=X.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-21 05:17:51 -03:00
parent e7a9bdab5f
commit 2dae4a11ae
5 changed files with 248 additions and 0 deletions
+38
View File
@@ -15,9 +15,11 @@ import InputText from 'primevue/inputtext';
import { supabase } from '@/lib/supabase/client';
import { useTenantStore } from '@/stores/tenantStore';
import { searchPages } from './pagesIndex';
import { useRecentPatients } from '@/composables/useRecentPatients';
const router = useRouter();
const tenantStore = useTenantStore();
const { items: recentPatients, hasItems: hasRecentPatients } = useRecentPatients();
// ────────────────────────────────────────────────────────────
// State
@@ -67,9 +69,14 @@ const filteredPages = computed(() => {
// ────────────────────────────────────────────────────────────
// Flat list pra navegação por teclado
// ────────────────────────────────────────────────────────────
// Recently-viewed só aparece quando a query está vazia — não polui resultados de busca.
const showRecent = computed(() => !query.value.trim() && hasRecentPatients.value);
const recentItems = computed(() => showRecent.value ? (recentPatients.value || []).slice(0, 5) : []);
const flatList = computed(() => {
const out = [];
filteredActions.value.forEach((a, i) => out.push({ group: 'actions', item: a, idx: i }));
recentItems.value.forEach((p, i) => out.push({ group: 'recent', item: p, idx: i }));
results.value.patients.forEach((p, i) => out.push({ group: 'patients', item: p, idx: i }));
results.value.intakes.forEach((r, i) => out.push({ group: 'intakes', item: r, idx: i }));
results.value.appointments.forEach((a, i) => out.push({ group: 'appointments', item: a, idx: i }));
@@ -195,6 +202,16 @@ function onInputKeydown(e) {
}
async function goTo(entry) {
// Recent patients: usa id pra navegar pro prontuário do paciente
if (entry?.group === 'recent' && entry?.item?.id) {
showPanel.value = false;
query.value = '';
resetResults();
activeIndex.value = -1;
await router.push({ path: '/therapist/patients/' + entry.item.id });
return;
}
const target = entry?.item?.to || entry?.item?.deeplink || entry?.item?.path;
if (!target) return;
showPanel.value = false;
@@ -266,6 +283,27 @@ const kbdModifier = kbdIsMac ? '⌘' : 'Ctrl';
</div>
<template v-else>
<!-- Acessados recentemente ( quando query vazia) -->
<div v-if="showRecent" class="gs-group">
<div class="gs-group__title">Acessados recentemente</div>
<button
v-for="(p, i) in recentItems"
:key="'rp-' + p.id"
type="button"
class="gs-item"
:class="{ 'is-active': findFlatIndex('recent', i) === activeIndex }"
@mouseenter="activeIndex = findFlatIndex('recent', i)"
@click="goTo({ group: 'recent', item: p, idx: i })"
>
<span class="gs-item__icon"><i class="pi pi-history" /></span>
<span class="gs-item__main">
<span class="gs-item__label">{{ p.nome }}</span>
<span class="gs-item__sub">{{ p.extras?.telefone || p.extras?.email || 'Abrir prontuário' }}</span>
</span>
<i class="gs-item__go pi pi-arrow-right" />
</button>
</div>
<!-- Ações -->
<div v-if="filteredActions.length" class="gs-group">
<div class="gs-group__title">{{ query.trim() ? 'Ações' : 'Atalhos' }}</div>