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:
@@ -15,9 +15,11 @@ import InputText from 'primevue/inputtext';
|
|||||||
import { supabase } from '@/lib/supabase/client';
|
import { supabase } from '@/lib/supabase/client';
|
||||||
import { useTenantStore } from '@/stores/tenantStore';
|
import { useTenantStore } from '@/stores/tenantStore';
|
||||||
import { searchPages } from './pagesIndex';
|
import { searchPages } from './pagesIndex';
|
||||||
|
import { useRecentPatients } from '@/composables/useRecentPatients';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const tenantStore = useTenantStore();
|
const tenantStore = useTenantStore();
|
||||||
|
const { items: recentPatients, hasItems: hasRecentPatients } = useRecentPatients();
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
// State
|
// State
|
||||||
@@ -67,9 +69,14 @@ const filteredPages = computed(() => {
|
|||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
// Flat list pra navegação por teclado
|
// 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 flatList = computed(() => {
|
||||||
const out = [];
|
const out = [];
|
||||||
filteredActions.value.forEach((a, i) => out.push({ group: 'actions', item: a, idx: i }));
|
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.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.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 }));
|
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) {
|
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;
|
const target = entry?.item?.to || entry?.item?.deeplink || entry?.item?.path;
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
showPanel.value = false;
|
showPanel.value = false;
|
||||||
@@ -266,6 +283,27 @@ const kbdModifier = kbdIsMac ? '⌘' : 'Ctrl';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<!-- Acessados recentemente (só 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 -->
|
<!-- Ações -->
|
||||||
<div v-if="filteredActions.length" class="gs-group">
|
<div v-if="filteredActions.length" class="gs-group">
|
||||||
<div class="gs-group__title">{{ query.trim() ? 'Ações' : 'Atalhos' }}</div>
|
<div class="gs-group__title">{{ query.trim() ? 'Ações' : 'Atalhos' }}</div>
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ import { useConversationDrawerStore } from '@/stores/conversationDrawerStore';
|
|||||||
|
|
||||||
import DocumentsListPage from '@/features/documents/DocumentsListPage.vue';
|
import DocumentsListPage from '@/features/documents/DocumentsListPage.vue';
|
||||||
import PatientConversationsTab from './PatientConversationsTab.vue';
|
import PatientConversationsTab from './PatientConversationsTab.vue';
|
||||||
|
import { registerPatientVisit } from '@/composables/useRecentPatients';
|
||||||
|
|
||||||
// ── PROPS / EMITS ──────────────────────────────────────────────
|
// ── PROPS / EMITS ──────────────────────────────────────────────
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -934,6 +935,8 @@ async function loadDetail(id) {
|
|||||||
const [p, rel] = await Promise.all([getPatientById(id), getPatientRelations(id)]);
|
const [p, rel] = await Promise.all([getPatientById(id), getPatientRelations(id)]);
|
||||||
if (!p) throw new Error('Paciente não retornou dados (RLS bloqueando ou ID não existe no banco).');
|
if (!p) throw new Error('Paciente não retornou dados (RLS bloqueando ou ID não existe no banco).');
|
||||||
patientFull.value = p;
|
patientFull.value = p;
|
||||||
|
// Registra no "recentemente acessados" (localStorage)
|
||||||
|
try { await registerPatientVisit(p); } catch { /* ignore */ }
|
||||||
const [g, t] = await Promise.all([
|
const [g, t] = await Promise.all([
|
||||||
getGroupsByIds(rel.groupIds || []),
|
getGroupsByIds(rel.groupIds || []),
|
||||||
getTagsByIds(rel.tagIds || []),
|
getTagsByIds(rel.tagIds || []),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue';
|
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||||
import { supabase } from '@/lib/supabase/client';
|
import { supabase } from '@/lib/supabase/client';
|
||||||
|
import { useRecentPatients } from '@/composables/useRecentPatients';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
pacientes: { type: Array, default: () => [] },
|
pacientes: { type: Array, default: () => [] },
|
||||||
@@ -48,6 +49,11 @@ const searching = ref(false);
|
|||||||
let debounceT = null;
|
let debounceT = null;
|
||||||
let searchSeq = 0;
|
let searchSeq = 0;
|
||||||
|
|
||||||
|
// Recently viewed (localStorage) — só aparece quando o input está vazio
|
||||||
|
const { items: recentPatients, hasItems: hasRecentPatients } = useRecentPatients();
|
||||||
|
const showRecent = computed(() => !query.value.trim() && hasRecentPatients.value);
|
||||||
|
const recentItems = computed(() => showRecent.value ? (recentPatients.value || []).slice(0, 5) : []);
|
||||||
|
|
||||||
function normalize(s) {
|
function normalize(s) {
|
||||||
return String(s || '')
|
return String(s || '')
|
||||||
.normalize('NFD')
|
.normalize('NFD')
|
||||||
@@ -112,6 +118,7 @@ const rpcIntakes = computed(() => rpcResults.value.intakes || []);
|
|||||||
|
|
||||||
const flatList = computed(() => {
|
const flatList = computed(() => {
|
||||||
const out = [];
|
const out = [];
|
||||||
|
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 }));
|
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 }));
|
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 }));
|
filteredEventos.value.forEach((e, i) => out.push({ group: 'eventos', item: e, idx: i }));
|
||||||
@@ -130,6 +137,7 @@ function findFlatIndex(group, idx) {
|
|||||||
function selectEntry(entry) {
|
function selectEntry(entry) {
|
||||||
if (entry.group === 'atalhos') emit('acao', entry.item.id);
|
if (entry.group === 'atalhos') emit('acao', entry.item.id);
|
||||||
else if (entry.group === 'pacientes') emit('paciente', entry.item);
|
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);
|
else if (entry.group === 'eventos') emit('evento', entry.item);
|
||||||
else if (entry.group === 'rpc-appointments') 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-documents') emit('documento', entry.item);
|
||||||
@@ -261,6 +269,26 @@ onBeforeUnmount(() => {
|
|||||||
Nada encontrado pra "<span class="text-white/80">{{ query.trim() }}</span>"
|
Nada encontrado pra "<span class="text-white/80">{{ query.trim() }}</span>"
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Acessados recentemente (só quando query vazia) -->
|
||||||
|
<div v-if="showRecent" class="mb-group">
|
||||||
|
<div class="mb-group__title">Acessados recentemente</div>
|
||||||
|
<button
|
||||||
|
v-for="(p, i) in recentItems"
|
||||||
|
:key="'mr-' + p.id"
|
||||||
|
class="mb-item"
|
||||||
|
:class="{ 'is-active': findFlatIndex('recent', i) === activeIndex }"
|
||||||
|
@click="selectEntry({ group: 'recent', item: p })"
|
||||||
|
@mouseenter="activeIndex = findFlatIndex('recent', i)"
|
||||||
|
>
|
||||||
|
<span class="mb-item__icon"><i class="pi pi-history" /></span>
|
||||||
|
<span class="mb-item__main">
|
||||||
|
<span class="mb-item__label">{{ p.nome }}</span>
|
||||||
|
<span class="mb-item__sub">{{ p.extras?.telefone || p.extras?.email || 'Abrir prontuário' }}</span>
|
||||||
|
</span>
|
||||||
|
<i class="mb-item__go pi pi-arrow-right" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Atalhos -->
|
<!-- Atalhos -->
|
||||||
<div v-if="filteredAtalhos.length" class="mb-group">
|
<div v-if="filteredAtalhos.length" class="mb-group">
|
||||||
<div class="mb-group__title">{{ query.trim() ? 'Ações' : 'Atalhos' }}</div>
|
<div class="mb-group__title">{{ query.trim() ? 'Ações' : 'Atalhos' }}</div>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import DocumentsListPage from '@/features/documents/DocumentsListPage.vue';
|
|||||||
import PatientConversationsTab from '@/features/patients/prontuario/PatientConversationsTab.vue';
|
import PatientConversationsTab from '@/features/patients/prontuario/PatientConversationsTab.vue';
|
||||||
import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue';
|
import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue';
|
||||||
import { usePatientDetail } from '@/features/patients/composables/usePatientDetail';
|
import { usePatientDetail } from '@/features/patients/composables/usePatientDetail';
|
||||||
|
import { registerPatientVisit } from '@/composables/useRecentPatients';
|
||||||
import { usePatientSessions } from '@/features/patients/composables/usePatientSessions';
|
import { usePatientSessions } from '@/features/patients/composables/usePatientSessions';
|
||||||
import { usePatientFinancial } from '@/features/patients/composables/usePatientFinancial';
|
import { usePatientFinancial } from '@/features/patients/composables/usePatientFinancial';
|
||||||
import { usePatientMessages } from '@/features/patients/composables/usePatientMessages';
|
import { usePatientMessages } from '@/features/patients/composables/usePatientMessages';
|
||||||
@@ -617,6 +618,13 @@ async function loadAll(id) {
|
|||||||
documentsHook.load(id),
|
documentsHook.load(id),
|
||||||
recorrenciasHook.load(id)
|
recorrenciasHook.load(id)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Registra visita no histórico "recentemente acessados" (localStorage).
|
||||||
|
// Fora do Promise.all pra não bloquear renderização.
|
||||||
|
const p = detail.patient?.value;
|
||||||
|
if (p?.id) {
|
||||||
|
try { await registerPatientVisit(p); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.patientId, async (id) => {
|
watch(() => props.patientId, async (id) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user