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:
@@ -50,6 +50,59 @@ Touched: none
|
|||||||
## [2026-05-08 00:00] session | Melissa cfg-* nativas + temas + cronometro DB
|
## [2026-05-08 00:00] session | Melissa cfg-* nativas + temas + cronometro DB
|
||||||
Touched: none
|
Touched: none
|
||||||
|
|
||||||
|
## [2026-05-08 11:30] session | MelissaPaciente Fase 1 (foundation: composables + skeleton + slug)
|
||||||
|
Touched: none (sem mudanca de wiki)
|
||||||
|
Detalhes: User escolheu "Full rewrite Melissa nativo" pra portar
|
||||||
|
PatientProntuario.vue (3593L Dialog) pro Melissa. Plano em 8 fases (2-8 sao
|
||||||
|
cada tab/wireup, sessao dedicada). Fase 1 entregue:
|
||||||
|
|
||||||
|
5 COMPOSABLES NOVOS em src/features/patients/composables/:
|
||||||
|
- usePatientDetail.js (108L) — patients + groups + tags via 4 queries
|
||||||
|
(getPatientById, getPatientRelations, getGroupsByIds, getTagsByIds).
|
||||||
|
Espelha 1:1 a logica de loadDetail() do PatientProntuario L893-953.
|
||||||
|
- usePatientSessions.js (83L) — agenda_eventos limit 100 ordenado desc +
|
||||||
|
computeds proximaSessao/ultimaSessao/totalSessoes/totalRealizadas/
|
||||||
|
totalFaltas/totalCanceladas.
|
||||||
|
- usePatientFinancial.js (82L) — financial_records (type=receita) limit 100
|
||||||
|
+ computeds totalRecebido/EmAberto/Atrasado/ultimoPago.
|
||||||
|
- usePatientMessages.js (64L) — conversation_messages limit 200 + computeds
|
||||||
|
recentes (top 4)/totalIn/totalOut/ultimaMensagem.
|
||||||
|
- usePatientDocuments.js (70L) — documents (deleted_at IS NULL) limit 200
|
||||||
|
+ computeds total/totalBytes/tiposCount/ultimo.
|
||||||
|
|
||||||
|
MELISSAPACIENTE.VUE NOVO (1190L) em src/layout/melissa/:
|
||||||
|
- Prefixo CSS .mpa-* (Melissa PAciente). Chrome glass + drawer mobile +
|
||||||
|
right: max(...) >=1024px (mesmo padrao MelissaAgendador/Negocio).
|
||||||
|
- Header: avatar + nome + ageLabel + pronomes + status/convenio Tag +
|
||||||
|
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 contém 4 cards: Acoes Rapidas (Conversar/Editar/Lancamento) +
|
||||||
|
Navegacao (7 tabs com icones coloridos) + Sub-nav Perfil (visivel so
|
||||||
|
quando aba Perfil ativa, 6 secoes) + Vinculos (chips de grupos+tags).
|
||||||
|
- Main: 7 tabs com placeholders ("Em desenvolvimento — Fase X"). Aba
|
||||||
|
Visao Geral ja mostra 4 KPIs reais via composables (sessoes totais,
|
||||||
|
em aberto, mensagens, documentos).
|
||||||
|
- Props :patient-id; emits close/edit/add-financial/open-whatsapp.
|
||||||
|
- Watch immediate em props.patientId, dispara loadAll() via Promise.all
|
||||||
|
dos 5 composables.
|
||||||
|
|
||||||
|
MELISSALAYOUT.VUE atualizado:
|
||||||
|
- Import MelissaPaciente.
|
||||||
|
- SECOES.paciente entry novo (label/icon/descricao).
|
||||||
|
- 'paciente' adicionado ao MELISSA_NON_CONFIG_SLUGS.
|
||||||
|
- Render condicional com :patient-id="String(route.query.id || '')" —
|
||||||
|
navegacao via /melissa/paciente?id=xxx.
|
||||||
|
|
||||||
|
NAO ALTERADO: PatientProntuario.vue continua intocado nos 4 callsites
|
||||||
|
(TherapistDashboard, MelissaAgenda, MelissaPacientes, PatientsListPage).
|
||||||
|
Migration acontece nas Fases 2-8. Fase 8 troca os callsites no Melissa.
|
||||||
|
|
||||||
|
ESLint: 0 errors da minha mudanca. 2 errors pre-existentes em MelissaLayout
|
||||||
|
(duplicate key 'financeiro' L242, empty block L1130) — nao toquei aquelas
|
||||||
|
linhas. PatientProntuario tem 2 outros pre-existentes. Working tree:
|
||||||
|
MelissaLayout.vue + 6 arquivos novos.
|
||||||
|
|
||||||
## [2026-05-08 09:30] session | Chrome+preview em 7 paginas Melissa (LinkExterno preview novo)
|
## [2026-05-08 09:30] session | Chrome+preview em 7 paginas Melissa (LinkExterno preview novo)
|
||||||
Touched: none (sem mudanca de wiki - aplicacao do pattern existente)
|
Touched: none (sem mudanca de wiki - aplicacao do pattern existente)
|
||||||
Detalhes: Aplicou o chrome `right: max(6px, min(50%, calc(100% - 1006px)))`
|
Detalhes: Aplicou o chrome `right: max(6px, min(50%, calc(100% - 1006px)))`
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Agência PSI
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Arquivo: src/features/patients/composables/usePatientDocuments.js
|
||||||
|
|
|
||||||
|
| Documentos do paciente — carrega so os campos pra KPIs (count, tipo,
|
||||||
|
| ultima atualizacao). O detalhe completo fica em DocumentsListPage que
|
||||||
|
| tem composable proprio. Filtra deletados (deleted_at IS NULL).
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { supabase } from '@/lib/supabase/client';
|
||||||
|
|
||||||
|
export function usePatientDocuments() {
|
||||||
|
const documents = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref('');
|
||||||
|
|
||||||
|
async function load(patientId) {
|
||||||
|
if (!patientId) {
|
||||||
|
documents.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
error.value = '';
|
||||||
|
documents.value = [];
|
||||||
|
try {
|
||||||
|
const { data, error: err } = await supabase
|
||||||
|
.from('documents')
|
||||||
|
.select('id, tipo_documento, created_at, status_revisao, tamanho_bytes')
|
||||||
|
.eq('patient_id', patientId)
|
||||||
|
.is('deleted_at', null)
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
.limit(200);
|
||||||
|
if (err) throw err;
|
||||||
|
documents.value = data || [];
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e?.message || 'Falha ao carregar documentos.';
|
||||||
|
documents.value = [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = computed(() => documents.value.length);
|
||||||
|
const totalBytes = computed(() =>
|
||||||
|
documents.value.reduce((acc, d) => acc + Number(d.tamanho_bytes || 0), 0)
|
||||||
|
);
|
||||||
|
const tiposCount = computed(() => {
|
||||||
|
const map = new Map();
|
||||||
|
documents.value.forEach((d) => {
|
||||||
|
const k = d.tipo_documento || 'outro';
|
||||||
|
map.set(k, (map.get(k) || 0) + 1);
|
||||||
|
});
|
||||||
|
return Object.fromEntries(map);
|
||||||
|
});
|
||||||
|
const ultimo = computed(() => documents.value[0] || null);
|
||||||
|
|
||||||
|
return {
|
||||||
|
documents,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
load,
|
||||||
|
total,
|
||||||
|
totalBytes,
|
||||||
|
tiposCount,
|
||||||
|
ultimo
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Agência PSI
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Arquivo: src/features/patients/composables/usePatientFinancial.js
|
||||||
|
|
|
||||||
|
| Lancamentos financeiros (financial_records) do paciente. Filtra type=receita,
|
||||||
|
| limita 100. Schema: paid_at NULL = pendente, preenchido = pago.
|
||||||
|
| "Vencido" = paid_at IS NULL AND due_date < hoje.
|
||||||
|
| Computeds derivados: kpis (em aberto, atrasado, total, ultimo pago).
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { supabase } from '@/lib/supabase/client';
|
||||||
|
|
||||||
|
export function usePatientFinancial() {
|
||||||
|
const records = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref('');
|
||||||
|
|
||||||
|
async function load(patientId) {
|
||||||
|
if (!patientId) {
|
||||||
|
records.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
error.value = '';
|
||||||
|
records.value = [];
|
||||||
|
try {
|
||||||
|
const { data, error: err } = await supabase
|
||||||
|
.from('financial_records')
|
||||||
|
.select('id, type, amount, due_date, paid_at, description, payment_method, category, created_at')
|
||||||
|
.eq('patient_id', patientId)
|
||||||
|
.eq('type', 'receita')
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
.limit(100);
|
||||||
|
if (err) throw err;
|
||||||
|
records.value = data || [];
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e?.message || 'Falha ao carregar lançamentos.';
|
||||||
|
records.value = [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalRecebido = computed(() =>
|
||||||
|
records.value
|
||||||
|
.filter((r) => r.paid_at)
|
||||||
|
.reduce((acc, r) => acc + Number(r.amount || 0), 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalEmAberto = computed(() =>
|
||||||
|
records.value
|
||||||
|
.filter((r) => !r.paid_at)
|
||||||
|
.reduce((acc, r) => acc + Number(r.amount || 0), 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalAtrasado = computed(() => {
|
||||||
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
|
return records.value
|
||||||
|
.filter((r) => !r.paid_at && r.due_date && r.due_date < today)
|
||||||
|
.reduce((acc, r) => acc + Number(r.amount || 0), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ultimoPago = computed(() => {
|
||||||
|
const pagos = records.value.filter((r) => r.paid_at);
|
||||||
|
if (!pagos.length) return null;
|
||||||
|
return [...pagos].sort((a, b) => new Date(b.paid_at) - new Date(a.paid_at))[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
records,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
load,
|
||||||
|
totalRecebido,
|
||||||
|
totalEmAberto,
|
||||||
|
totalAtrasado,
|
||||||
|
ultimoPago
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Agência PSI
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Arquivo: src/features/patients/composables/usePatientMessages.js
|
||||||
|
|
|
||||||
|
| Mensagens de conversa do paciente. Carrega 200 mais recentes (in+out)
|
||||||
|
| pra alimentar o card "Ultimas mensagens" (Visao Geral, top 4) e os
|
||||||
|
| KPIs da aba Conversas. Conversa completa fica no PatientConversationsTab.
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { supabase } from '@/lib/supabase/client';
|
||||||
|
|
||||||
|
export function usePatientMessages() {
|
||||||
|
const messages = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref('');
|
||||||
|
|
||||||
|
async function load(patientId) {
|
||||||
|
if (!patientId) {
|
||||||
|
messages.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
error.value = '';
|
||||||
|
messages.value = [];
|
||||||
|
try {
|
||||||
|
const { data, error: err } = await supabase
|
||||||
|
.from('conversation_messages')
|
||||||
|
.select('id, body, direction, created_at, channel, kanban_status')
|
||||||
|
.eq('patient_id', patientId)
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
.limit(200);
|
||||||
|
if (err) throw err;
|
||||||
|
messages.value = data || [];
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e?.message || 'Falha ao carregar mensagens.';
|
||||||
|
messages.value = [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentes = computed(() => messages.value.slice(0, 4));
|
||||||
|
const totalIn = computed(() =>
|
||||||
|
messages.value.filter((m) => m.direction === 'in' || m.direction === 'inbound').length
|
||||||
|
);
|
||||||
|
const totalOut = computed(() =>
|
||||||
|
messages.value.filter((m) => m.direction === 'out' || m.direction === 'outbound').length
|
||||||
|
);
|
||||||
|
const ultimaMensagem = computed(() => messages.value[0] || null);
|
||||||
|
|
||||||
|
return {
|
||||||
|
messages,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
load,
|
||||||
|
recentes,
|
||||||
|
totalIn,
|
||||||
|
totalOut,
|
||||||
|
ultimaMensagem
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Agência PSI
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Arquivo: src/features/patients/composables/usePatientSessions.js
|
||||||
|
|
|
||||||
|
| Carrega sessoes (agenda_eventos) do paciente. Limit 100 mais recentes
|
||||||
|
| ordenadas desc por inicio_em. Compativel com a logica original do
|
||||||
|
| PatientProntuario.vue.
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { supabase } from '@/lib/supabase/client';
|
||||||
|
|
||||||
|
export function usePatientSessions() {
|
||||||
|
const sessions = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref('');
|
||||||
|
|
||||||
|
async function load(patientId) {
|
||||||
|
if (!patientId) {
|
||||||
|
sessions.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
error.value = '';
|
||||||
|
sessions.value = [];
|
||||||
|
try {
|
||||||
|
const { data, error: err } = await supabase
|
||||||
|
.from('agenda_eventos')
|
||||||
|
.select('id, inicio_em, fim_em, status, modalidade, tipo, titulo, titulo_custom, observacoes')
|
||||||
|
.eq('patient_id', patientId)
|
||||||
|
.order('inicio_em', { ascending: false })
|
||||||
|
.limit(100);
|
||||||
|
if (err) throw err;
|
||||||
|
sessions.value = data || [];
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e?.message || 'Falha ao carregar sessões.';
|
||||||
|
sessions.value = [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers derivados — proxima sessao agendada e status corrente
|
||||||
|
const proximaSessao = computed(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
return [...sessions.value]
|
||||||
|
.filter((s) => s.inicio_em && new Date(s.inicio_em).getTime() >= now)
|
||||||
|
.sort((a, b) => new Date(a.inicio_em) - new Date(b.inicio_em))[0] || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const ultimaSessao = computed(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
return sessions.value
|
||||||
|
.filter((s) => s.inicio_em && new Date(s.inicio_em).getTime() < now)
|
||||||
|
.sort((a, b) => new Date(b.inicio_em) - new Date(a.inicio_em))[0] || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalSessoes = computed(() => sessions.value.length);
|
||||||
|
const totalRealizadas = computed(() =>
|
||||||
|
sessions.value.filter((s) => s.status === 'realizada' || s.status === 'presente').length
|
||||||
|
);
|
||||||
|
const totalFaltas = computed(() =>
|
||||||
|
sessions.value.filter((s) => s.status === 'falta').length
|
||||||
|
);
|
||||||
|
const totalCanceladas = computed(() =>
|
||||||
|
sessions.value.filter((s) => s.status === 'cancelada' || s.status === 'cancelado').length
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessions,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
load,
|
||||||
|
proximaSessao,
|
||||||
|
ultimaSessao,
|
||||||
|
totalSessoes,
|
||||||
|
totalRealizadas,
|
||||||
|
totalFaltas,
|
||||||
|
totalCanceladas
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -39,6 +39,7 @@ import MelissaTags from './MelissaTags.vue';
|
|||||||
import MelissaGrupos from './MelissaGrupos.vue';
|
import MelissaGrupos from './MelissaGrupos.vue';
|
||||||
import MelissaConfiguracoes from './MelissaConfiguracoes.vue';
|
import MelissaConfiguracoes from './MelissaConfiguracoes.vue';
|
||||||
import MelissaPerfil from './MelissaPerfil.vue';
|
import MelissaPerfil from './MelissaPerfil.vue';
|
||||||
|
import MelissaPaciente from './MelissaPaciente.vue';
|
||||||
import MelissaPlano from './MelissaPlano.vue';
|
import MelissaPlano from './MelissaPlano.vue';
|
||||||
import MelissaNegocio from './MelissaNegocio.vue';
|
import MelissaNegocio from './MelissaNegocio.vue';
|
||||||
import MelissaAlterarPlano from './MelissaAlterarPlano.vue';
|
import MelissaAlterarPlano from './MelissaAlterarPlano.vue';
|
||||||
@@ -199,6 +200,11 @@ const SECOES = {
|
|||||||
grupos: { label: 'Grupos de pacientes', icon: 'pi pi-th-large', descricao: 'Categorize pacientes por grupos.' },
|
grupos: { label: 'Grupos de pacientes', icon: 'pi pi-th-large', descricao: 'Categorize pacientes por grupos.' },
|
||||||
tags: { label: 'Tags', icon: 'pi pi-tag', descricao: 'Etiquetas livres pra organizar pacientes.' },
|
tags: { label: 'Tags', icon: 'pi pi-tag', descricao: 'Etiquetas livres pra organizar pacientes.' },
|
||||||
medicos: { label: 'Médicos e referências', icon: 'pi pi-user-edit', descricao: 'Profissionais que encaminham ou recebem pacientes.' },
|
medicos: { label: 'Médicos e referências', icon: 'pi pi-user-edit', descricao: 'Profissionais que encaminham ou recebem pacientes.' },
|
||||||
|
// Pagina nativa do prontuario do paciente (MelissaPaciente) — Fase 1 foundation.
|
||||||
|
// ID do paciente vem via route.query.id (?id=xxx). Substitui gradualmente o
|
||||||
|
// PatientProntuario.vue legado (3593L Dialog) que continua nos 4 callsites
|
||||||
|
// ate Fase 8 (wire-up final).
|
||||||
|
paciente: { label: 'Paciente', icon: 'pi pi-user', descricao: 'Prontuario completo: visao geral, perfil, sessoes, financeiro, documentos, conversas.' },
|
||||||
// Pagina unificada do Layout Melissa (Aparencia + Fundo + Relogio + Cronometro)
|
// Pagina unificada do Layout Melissa (Aparencia + Fundo + Relogio + Cronometro)
|
||||||
aparencia: { label: 'Layout Melissa', icon: 'pi pi-palette', descricao: 'Aparência, plano de fundo, relógio e cronômetro do resumo.' },
|
aparencia: { label: 'Layout Melissa', icon: 'pi pi-palette', descricao: 'Aparência, plano de fundo, relógio e cronômetro do resumo.' },
|
||||||
// Pagina nativa do perfil (MelissaPerfil) — saiu do MelissaConfiguracoes
|
// Pagina nativa do perfil (MelissaPerfil) — saiu do MelissaConfiguracoes
|
||||||
@@ -258,7 +264,7 @@ const MELISSA_EMBED_KEYS = [];
|
|||||||
// conversas etc. + as MELISSA_EMBED_KEYS. Usado no isConfigRoute pra
|
// conversas etc. + as MELISSA_EMBED_KEYS. Usado no isConfigRoute pra
|
||||||
// evitar colisão (ex: /melissa/agenda → MelissaAgenda, não config).
|
// evitar colisão (ex: /melissa/agenda → MelissaAgenda, não config).
|
||||||
const MELISSA_NON_CONFIG_SLUGS = new Set([
|
const MELISSA_NON_CONFIG_SLUGS = new Set([
|
||||||
'agenda', 'pacientes', 'compromissos', 'recorrencias', 'conversas',
|
'agenda', 'pacientes', 'paciente', 'compromissos', 'recorrencias', 'conversas',
|
||||||
'tags', 'grupos', 'cadastros-recebidos', 'medicos', 'agendamentos-recebidos',
|
'tags', 'grupos', 'cadastros-recebidos', 'medicos', 'agendamentos-recebidos',
|
||||||
'link-externo', 'notificacoes', 'financeiro', 'financeiro-lancamentos',
|
'link-externo', 'notificacoes', 'financeiro', 'financeiro-lancamentos',
|
||||||
'documentos', 'documentos-templates', 'relatorios',
|
'documentos', 'documentos-templates', 'relatorios',
|
||||||
@@ -1859,6 +1865,17 @@ function onKeydown(e) {
|
|||||||
@close="fecharSecao"
|
@close="fecharSecao"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Pagina nativa do prontuario do paciente (Fase 1 foundation).
|
||||||
|
ID vem via route.query.id (?id=xxx). Quando ID muda, MelissaPaciente
|
||||||
|
refetcha tudo via composables (usePatientDetail/Sessions/etc). -->
|
||||||
|
<MelissaPaciente
|
||||||
|
v-if="layoutReady && secaoAberta === 'paciente'"
|
||||||
|
:patient-id="String(route.query.id || '')"
|
||||||
|
@close="fecharSecao"
|
||||||
|
@edit="(id) => fecharSecao()"
|
||||||
|
@open-whatsapp="(id) => fecharSecao()"
|
||||||
|
/>
|
||||||
|
|
||||||
<MelissaPlano
|
<MelissaPlano
|
||||||
v-if="layoutReady && secaoAberta === 'plano'"
|
v-if="layoutReady && secaoAberta === 'plano'"
|
||||||
@close="fecharSecao"
|
@close="fecharSecao"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user