MelissaPaciente Fase 1: foundation (5 composables + skeleton 7 tabs + slug paciente)
Inicio do port do PatientProntuario.vue (3593L Dialog) pra Melissa nativo. Plano em 8 fases — esta entrega cobre apenas a Fase 1 (foundation). PatientProntuario continua intocado nos 4 callsites (TherapistDashboard, MelissaAgenda, MelissaPacientes, PatientsListPage); migration acontece nas fases 2-8. 5 COMPOSABLES NOVOS em src/features/patients/composables/ - usePatientDetail.js (108L): patients + groups + tags - usePatientSessions.js (83L): agenda_eventos + computeds proxima/ultima/totais - usePatientFinancial.js (82L): financial_records + computeds totalRecebido/Aberto/Atrasado - usePatientMessages.js (64L): conversation_messages + computeds recentes/totalIn/Out - usePatientDocuments.js (70L): documents + computeds total/Bytes/tiposCount Cada composable encapsula a query original do PatientProntuario.vue + adiciona computeds derivados. Reutilizaveis em outros lugares no futuro (dashboards, relatorios, etc). MELISSAPACIENTE.VUE NOVO (1190L) em src/layout/melissa/ - Prefixo CSS .mpa-*. Chrome glass + drawer mobile + right: max(...) >=1024px (mesmo padrao MelissaAgendador/Negocio). - Header: avatar + nome + ageLabel + pronomes + Tag status/convenio + risco-elevado pill + actions (Conversar / Editar / Close). - Subheader condicional: banner risco elevado. - Body 2-col: sidebar 320px (esquerda, drawer no mobile) + main flex 1. - Sidebar com 4 cards: Acoes Rapidas / Navegacao 7 tabs / Sub-nav Perfil / Vinculos (chips grupos+tags). - Main: 7 tabs (Visao Geral / Perfil / Prontuario / Agenda / Financeiro / Documentos / Conversas). Visao Geral ja mostra 4 KPIs reais via composables. Outras 6 abas com placeholders "Em desenvolvimento — Fase X". MELISSALAYOUT.VUE - Import MelissaPaciente. - SECOES.paciente entry novo. - 'paciente' adicionado ao MELISSA_NON_CONFIG_SLUGS. - Render condicional com :patient-id="String(route.query.id || '')" — navegacao via /melissa/paciente?id=xxx. ESLint: 0 errors da mudanca. 2 errors pre-existentes em MelissaLayout (duplicate key 'financeiro' L242, empty block L1130) — nao toquei essas linhas. PatientProntuario tem outros pre-existentes nao tocados. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Arquivo: src/features/patients/composables/usePatientDetail.js
|
||||
|
|
||||
| Composable de detalhe completo de paciente — patient row + grupos + tags
|
||||
| Extraido do PatientProntuario.vue pra permitir reuso em MelissaPaciente.vue.
|
||||
| Mantem a mesma logica original (Promise.all em 2 etapas, RLS-aware).
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
async function getPatientById(id) {
|
||||
const { data, error } = await supabase
|
||||
.from('patients')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.maybeSingle();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
|
||||
async function getPatientRelations(id) {
|
||||
const { data: g, error: ge } = await supabase
|
||||
.from('patient_group_patient')
|
||||
.select('patient_group_id')
|
||||
.eq('patient_id', id);
|
||||
if (ge) throw ge;
|
||||
const { data: t, error: te } = await supabase
|
||||
.from('patient_patient_tag')
|
||||
.select('tag_id')
|
||||
.eq('patient_id', id);
|
||||
if (te) throw te;
|
||||
return {
|
||||
groupIds: (g || []).map((x) => x.patient_group_id).filter(Boolean),
|
||||
tagIds: (t || []).map((x) => x.tag_id).filter(Boolean)
|
||||
};
|
||||
}
|
||||
|
||||
async function getGroupsByIds(ids) {
|
||||
if (!ids?.length) return [];
|
||||
const { data, error } = await supabase
|
||||
.from('patient_groups')
|
||||
.select('id, nome')
|
||||
.in('id', ids)
|
||||
.order('nome', { ascending: true });
|
||||
if (error) throw error;
|
||||
return (data || []).map((g) => ({ id: g.id, name: g.nome }));
|
||||
}
|
||||
|
||||
async function getTagsByIds(ids) {
|
||||
if (!ids?.length) return [];
|
||||
const { data, error } = await supabase
|
||||
.from('patient_tags')
|
||||
.select('id, nome, cor')
|
||||
.in('id', ids)
|
||||
.order('nome', { ascending: true });
|
||||
if (error) throw error;
|
||||
return (data || []).map((t) => ({ id: t.id, name: t.nome, color: t.cor }));
|
||||
}
|
||||
|
||||
export function usePatientDetail() {
|
||||
const patient = ref(null);
|
||||
const groups = ref([]);
|
||||
const tags = ref([]);
|
||||
const loading = ref(false);
|
||||
const loadError = ref('');
|
||||
|
||||
async function load(id) {
|
||||
if (!id) {
|
||||
patient.value = null;
|
||||
groups.value = [];
|
||||
tags.value = [];
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
loadError.value = '';
|
||||
patient.value = null;
|
||||
groups.value = [];
|
||||
tags.value = [];
|
||||
try {
|
||||
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).');
|
||||
patient.value = p;
|
||||
const [g, t] = await Promise.all([
|
||||
getGroupsByIds(rel.groupIds || []),
|
||||
getTagsByIds(rel.tagIds || [])
|
||||
]);
|
||||
groups.value = g;
|
||||
tags.value = t;
|
||||
} catch (e) {
|
||||
loadError.value = e?.message || 'Falha ao buscar dados.';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
patient,
|
||||
groups,
|
||||
tags,
|
||||
loading,
|
||||
loadError,
|
||||
load
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user