From 8f4e6679eb9c491b66940de1ede5dbb2f5b3f896 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Mon, 11 May 2026 10:45:57 -0300 Subject: [PATCH] agenda: pacientes arquivados/inativos visiveis e bloqueados no picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AgendaEventDialogV2.filteredPatients agora mostra TODOS os pacientes (antes filtrava status='Ativo' silenciosamente), ordenados Ativo > Inativo > Arquivado. Items nao-Ativo vem com Tag colorida + disabled + tooltip explicativo — UX clara: o paciente aparece (user nao "perde" no search) mas nao da pra agendar. selectPaciente bloqueia non-Ativo (defesa em camadas: template ja marca disabled, mas se alguem chamar a funcao programaticamente por cache stale etc, a regra continua valendo). Copia status pro form pra canSave aplicar getPatientAgendaPermissions corretamente. 3 specs novas em useAgendaEventPickerBilling.spec cobrem o bloqueio + copia do status. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../agenda/components/AgendaEventDialogV2.vue | 33 ++++++++++++++----- .../useAgendaEventPickerBilling.spec.js | 27 +++++++++++++++ .../useAgendaEventPickerBilling.js | 10 ++++++ 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/src/features/agenda/components/AgendaEventDialogV2.vue b/src/features/agenda/components/AgendaEventDialogV2.vue index 837db0b..28b9e6b 100644 --- a/src/features/agenda/components/AgendaEventDialogV2.vue +++ b/src/features/agenda/components/AgendaEventDialogV2.vue @@ -270,16 +270,22 @@ function onPatientCreatedRapido(p) { cadRapidoOpen.value = false; } +// Mostra TODOS os pacientes (inclusive Inativo/Arquivado) — UX intencional: +// nao-Ativos vem com badge + disabled no template, ordenados pro final. +// selectPaciente bloqueia o clique se status !== 'Ativo'. +const _aev2StatusRank = { Ativo: 0, Inativo: 1, Arquivado: 2 }; const filteredPatients = computed(() => { const q = String(pacienteSearch.value || '').trim().toLowerCase(); - const list = (patients.value || []).filter((p) => p.status === 'Ativo'); - if (!q) return list; - return list.filter((p) => { - const nome = String(p.nome || '').toLowerCase(); - const email = String(p.email || '').toLowerCase(); - const tel = String(p.telefone || '').toLowerCase(); - return nome.includes(q) || email.includes(q) || tel.includes(q); - }); + const list = patients.value || []; + const matched = !q + ? [...list] + : list.filter((p) => { + const nome = String(p.nome || '').toLowerCase(); + const email = String(p.email || '').toLowerCase(); + const tel = String(p.telefone || '').toLowerCase(); + return nome.includes(q) || email.includes(q) || tel.includes(q); + }); + return matched.sort((a, b) => (_aev2StatusRank[a.status] ?? 3) - (_aev2StatusRank[b.status] ?? 3)); }); function goToAgendamentosRecebidos() { @@ -924,6 +930,9 @@ const heroDateText = computed(() => { :key="p.id" type="button" class="aev2-picker-item" + :class="{ 'aev2-picker-item--blocked': p.status && p.status !== 'Ativo' }" + :disabled="p.status && p.status !== 'Ativo'" + :title="p.status === 'Arquivado' ? 'Paciente arquivado — não é possível agendar' : p.status === 'Inativo' ? 'Paciente inativo — não é possível agendar' : ''" @click="selectPaciente(p)" > @@ -932,6 +941,8 @@ const heroDateText = computed(() => {
{{ p.nome }}
{{ p.email || p.telefone }}
+ + @@ -946,6 +957,7 @@ const heroDateText = computed(() => { email-field="email_principal" phone-field="telefone" :extra-payload="{ status: 'Ativo' }" + hide-view-list-button @created="onPatientCreatedRapido" /> @@ -1477,4 +1489,9 @@ const heroDateText = computed(() => { transition: background .15s; } .aev2-picker-item:hover { background: var(--aev2-pill-bg); } +.aev2-picker-item--blocked { + opacity: .6; + cursor: not-allowed; +} +.aev2-picker-item--blocked:hover { background: transparent; } diff --git a/src/features/agenda/composables/__tests__/useAgendaEventPickerBilling.spec.js b/src/features/agenda/composables/__tests__/useAgendaEventPickerBilling.spec.js index 3bf5170..b5f433e 100644 --- a/src/features/agenda/composables/__tests__/useAgendaEventPickerBilling.spec.js +++ b/src/features/agenda/composables/__tests__/useAgendaEventPickerBilling.spec.js @@ -333,6 +333,33 @@ describe('selectPaciente / clearPaciente', () => { expect(composer.form.value.paciente_id).toBe(null); }); + it('selectPaciente copia status pro form quando Ativo', () => { + const composer = makeComposer(); + const { selectPaciente } = setup({ composer }); + selectPaciente({ id: 'p-1', nome: 'Ana', status: 'Ativo' }); + expect(composer.form.value.paciente_id).toBe('p-1'); + expect(composer.form.value.paciente_status).toBe('Ativo'); + }); + + it('selectPaciente bloqueia paciente Arquivado (defesa em camadas)', () => { + const composer = makeComposer(); + const { selectPaciente, pacientePickerOpen } = setup({ composer }); + pacientePickerOpen.value = true; + selectPaciente({ id: 'p-arq', nome: 'Ana', status: 'Arquivado' }); + // Form nao deve ter sido tocado + expect(composer.form.value.paciente_id).toBe(null); + expect(composer.form.value.paciente_status).toBeFalsy(); + // Picker permanece aberto pro user escolher outro + expect(pacientePickerOpen.value).toBe(true); + }); + + it('selectPaciente bloqueia paciente Inativo', () => { + const composer = makeComposer(); + const { selectPaciente } = setup({ composer }); + selectPaciente({ id: 'p-ina', nome: 'Bruno', status: 'Inativo' }); + expect(composer.form.value.paciente_id).toBe(null); + }); + it('clearPaciente limpa form + samePatientConflict', () => { const composer = makeComposer({ formExtra: { paciente_id: 'p-1', paciente_nome: 'Ana', paciente_avatar: 'url' } diff --git a/src/features/agenda/composables/useAgendaEventPickerBilling.js b/src/features/agenda/composables/useAgendaEventPickerBilling.js index 26915bf..b445903 100644 --- a/src/features/agenda/composables/useAgendaEventPickerBilling.js +++ b/src/features/agenda/composables/useAgendaEventPickerBilling.js @@ -286,9 +286,18 @@ export function useAgendaEventPickerBilling({ function selectPaciente(p) { if (!p?.id) return; + // Bloqueia clique em paciente arquivado/inativo — defesa em camadas: + // o template do picker ja marca esses items como disabled, mas se + // alguem chamar selectPaciente programaticamente (cache stale, etc), + // a regra precisa valer. + if (p.status && p.status !== 'Ativo') return; composer.form.value.paciente_id = p.id; composer.form.value.paciente_nome = p.nome || ''; composer.form.value.paciente_avatar = p.avatar_url || ''; + // Sem isso, form.paciente_status fica '' e canSave nao consegue + // aplicar getPatientAgendaPermissions — qualquer falha do filtro + // acima vira sessao criavel com paciente fora do escopo. + composer.form.value.paciente_status = p.status || ''; pacientePickerOpen.value = false; } @@ -296,6 +305,7 @@ export function useAgendaEventPickerBilling({ composer.form.value.paciente_id = null; composer.form.value.paciente_nome = ''; composer.form.value.paciente_avatar = ''; + composer.form.value.paciente_status = ''; actions.samePatientConflict.value = null; }