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:
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Arquivo: src/composables/useRecentPatients.js
|
||||
|
|
||||
| Tracking dos últimos pacientes acessados pelo usuário logado.
|
||||
| Armazenado em localStorage por user_id pra isolar sessões diferentes
|
||||
| no mesmo browser (multi-conta).
|
||||
|
|
||||
| Usado pelo GlobalSearch.vue / MelissaBusca.vue como "recently viewed"
|
||||
| quando o input está vazio, e pode ser embedido em dashboards.
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
const MAX_ITEMS = 5; // top N exibido
|
||||
const STORAGE_PREFIX = 'agpsi:recent-patients:';
|
||||
const STORAGE_EVENT = 'agpsi:recent-patients:changed';
|
||||
|
||||
function storageKey(userId) {
|
||||
return `${STORAGE_PREFIX}${userId || 'anon'}`;
|
||||
}
|
||||
|
||||
function loadFromStorage(userId) {
|
||||
try {
|
||||
const raw = localStorage.getItem(storageKey(userId));
|
||||
if (!raw) return [];
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveToStorage(userId, items) {
|
||||
try {
|
||||
localStorage.setItem(storageKey(userId), JSON.stringify(items));
|
||||
// Notifica outras instâncias do composable nesta mesma aba
|
||||
window.dispatchEvent(new CustomEvent(STORAGE_EVENT, { detail: { userId } }));
|
||||
} catch {
|
||||
// Quota cheia / modo privado — silenciar
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns composable reativo com `items` (array de pacientes recentes),
|
||||
* `addVisit(patient)` e `clear()`.
|
||||
*
|
||||
* Forma do patient esperado em addVisit:
|
||||
* { id: string, nome: string, ... } — extras (avatar, telefone, etc) são opt-in
|
||||
*
|
||||
* Forma do item armazenado:
|
||||
* { id, nome, visited_at: ISO, extras: {} }
|
||||
*/
|
||||
export function useRecentPatients() {
|
||||
const userId = ref(null);
|
||||
const items = ref([]);
|
||||
|
||||
async function resolveUserId() {
|
||||
if (userId.value) return userId.value;
|
||||
const { data } = await supabase.auth.getUser();
|
||||
userId.value = data?.user?.id || 'anon';
|
||||
return userId.value;
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const uid = await resolveUserId();
|
||||
items.value = loadFromStorage(uid);
|
||||
}
|
||||
|
||||
async function addVisit(patient) {
|
||||
if (!patient?.id) return;
|
||||
const uid = await resolveUserId();
|
||||
const current = loadFromStorage(uid);
|
||||
|
||||
const entry = {
|
||||
id: String(patient.id),
|
||||
nome: patient.nome_completo || patient.nome_social || patient.nome || '(sem nome)',
|
||||
visited_at: new Date().toISOString(),
|
||||
extras: {
|
||||
nome_social: patient.nome_social || null,
|
||||
avatar_url: patient.avatar_url || null,
|
||||
telefone: patient.telefone || null,
|
||||
email: patient.email_principal || patient.email || null
|
||||
}
|
||||
};
|
||||
|
||||
// Remove duplicata + insere no topo + limita a MAX_ITEMS
|
||||
const dedup = current.filter(x => x.id !== entry.id);
|
||||
dedup.unshift(entry);
|
||||
const trimmed = dedup.slice(0, MAX_ITEMS);
|
||||
|
||||
saveToStorage(uid, trimmed);
|
||||
items.value = trimmed;
|
||||
}
|
||||
|
||||
async function remove(patientId) {
|
||||
const uid = await resolveUserId();
|
||||
const current = loadFromStorage(uid);
|
||||
const filtered = current.filter(x => String(x.id) !== String(patientId));
|
||||
saveToStorage(uid, filtered);
|
||||
items.value = filtered;
|
||||
}
|
||||
|
||||
async function clear() {
|
||||
const uid = await resolveUserId();
|
||||
saveToStorage(uid, []);
|
||||
items.value = [];
|
||||
}
|
||||
|
||||
// Sincroniza entre instâncias do composable na mesma aba
|
||||
function onChange() {
|
||||
refresh();
|
||||
}
|
||||
function onStorage(ev) {
|
||||
if (typeof ev?.key === 'string' && ev.key.startsWith(STORAGE_PREFIX)) {
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refresh();
|
||||
window.addEventListener(STORAGE_EVENT, onChange);
|
||||
window.addEventListener('storage', onStorage);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener(STORAGE_EVENT, onChange);
|
||||
window.removeEventListener('storage', onStorage);
|
||||
});
|
||||
|
||||
const hasItems = computed(() => items.value.length > 0);
|
||||
|
||||
return {
|
||||
items,
|
||||
hasItems,
|
||||
addVisit,
|
||||
remove,
|
||||
clear,
|
||||
refresh
|
||||
};
|
||||
}
|
||||
|
||||
// ── Stateless helpers — usáveis fora de componentes (ex: action handlers) ──
|
||||
|
||||
/**
|
||||
* Registra uma visita SEM usar Vue reactivity. Útil pra hooks que não
|
||||
* estão dentro de setup() (ex: router.beforeEach, navigation guards).
|
||||
*/
|
||||
export async function registerPatientVisit(patient) {
|
||||
if (!patient?.id) return;
|
||||
const { data } = await supabase.auth.getUser();
|
||||
const uid = data?.user?.id || 'anon';
|
||||
const current = loadFromStorage(uid);
|
||||
const entry = {
|
||||
id: String(patient.id),
|
||||
nome: patient.nome_completo || patient.nome_social || patient.nome || '(sem nome)',
|
||||
visited_at: new Date().toISOString(),
|
||||
extras: {
|
||||
nome_social: patient.nome_social || null,
|
||||
avatar_url: patient.avatar_url || null,
|
||||
telefone: patient.telefone || null,
|
||||
email: patient.email_principal || patient.email || null
|
||||
}
|
||||
};
|
||||
const dedup = current.filter(x => x.id !== entry.id);
|
||||
dedup.unshift(entry);
|
||||
saveToStorage(uid, dedup.slice(0, MAX_ITEMS));
|
||||
}
|
||||
Reference in New Issue
Block a user