agenda: pacientes arquivados/inativos visiveis e bloqueados no picker
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) <noreply@anthropic.com>
This commit is contained in:
@@ -270,16 +270,22 @@ function onPatientCreatedRapido(p) {
|
|||||||
cadRapidoOpen.value = false;
|
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 filteredPatients = computed(() => {
|
||||||
const q = String(pacienteSearch.value || '').trim().toLowerCase();
|
const q = String(pacienteSearch.value || '').trim().toLowerCase();
|
||||||
const list = (patients.value || []).filter((p) => p.status === 'Ativo');
|
const list = patients.value || [];
|
||||||
if (!q) return list;
|
const matched = !q
|
||||||
return list.filter((p) => {
|
? [...list]
|
||||||
const nome = String(p.nome || '').toLowerCase();
|
: list.filter((p) => {
|
||||||
const email = String(p.email || '').toLowerCase();
|
const nome = String(p.nome || '').toLowerCase();
|
||||||
const tel = String(p.telefone || '').toLowerCase();
|
const email = String(p.email || '').toLowerCase();
|
||||||
return nome.includes(q) || email.includes(q) || tel.includes(q);
|
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() {
|
function goToAgendamentosRecebidos() {
|
||||||
@@ -924,6 +930,9 @@ const heroDateText = computed(() => {
|
|||||||
:key="p.id"
|
:key="p.id"
|
||||||
type="button"
|
type="button"
|
||||||
class="aev2-picker-item"
|
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)"
|
@click="selectPaciente(p)"
|
||||||
>
|
>
|
||||||
<Avatar v-if="p.avatar_url" :image="p.avatar_url" shape="circle" size="normal" />
|
<Avatar v-if="p.avatar_url" :image="p.avatar_url" shape="circle" size="normal" />
|
||||||
@@ -932,6 +941,8 @@ const heroDateText = computed(() => {
|
|||||||
<div class="font-semibold truncate">{{ p.nome }}</div>
|
<div class="font-semibold truncate">{{ p.nome }}</div>
|
||||||
<div class="text-xs opacity-60 truncate">{{ p.email || p.telefone }}</div>
|
<div class="text-xs opacity-60 truncate">{{ p.email || p.telefone }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Tag v-if="p.status === 'Arquivado'" value="Arquivado" severity="danger" class="shrink-0" />
|
||||||
|
<Tag v-else-if="p.status === 'Inativo'" value="Inativo" severity="warn" class="shrink-0" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -946,6 +957,7 @@ const heroDateText = computed(() => {
|
|||||||
email-field="email_principal"
|
email-field="email_principal"
|
||||||
phone-field="telefone"
|
phone-field="telefone"
|
||||||
:extra-payload="{ status: 'Ativo' }"
|
:extra-payload="{ status: 'Ativo' }"
|
||||||
|
hide-view-list-button
|
||||||
@created="onPatientCreatedRapido"
|
@created="onPatientCreatedRapido"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -1477,4 +1489,9 @@ const heroDateText = computed(() => {
|
|||||||
transition: background .15s;
|
transition: background .15s;
|
||||||
}
|
}
|
||||||
.aev2-picker-item:hover { background: var(--aev2-pill-bg); }
|
.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; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -333,6 +333,33 @@ describe('selectPaciente / clearPaciente', () => {
|
|||||||
expect(composer.form.value.paciente_id).toBe(null);
|
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', () => {
|
it('clearPaciente limpa form + samePatientConflict', () => {
|
||||||
const composer = makeComposer({
|
const composer = makeComposer({
|
||||||
formExtra: { paciente_id: 'p-1', paciente_nome: 'Ana', paciente_avatar: 'url' }
|
formExtra: { paciente_id: 'p-1', paciente_nome: 'Ana', paciente_avatar: 'url' }
|
||||||
|
|||||||
@@ -286,9 +286,18 @@ export function useAgendaEventPickerBilling({
|
|||||||
|
|
||||||
function selectPaciente(p) {
|
function selectPaciente(p) {
|
||||||
if (!p?.id) return;
|
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_id = p.id;
|
||||||
composer.form.value.paciente_nome = p.nome || '';
|
composer.form.value.paciente_nome = p.nome || '';
|
||||||
composer.form.value.paciente_avatar = p.avatar_url || '';
|
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;
|
pacientePickerOpen.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,6 +305,7 @@ export function useAgendaEventPickerBilling({
|
|||||||
composer.form.value.paciente_id = null;
|
composer.form.value.paciente_id = null;
|
||||||
composer.form.value.paciente_nome = '';
|
composer.form.value.paciente_nome = '';
|
||||||
composer.form.value.paciente_avatar = '';
|
composer.form.value.paciente_avatar = '';
|
||||||
|
composer.form.value.paciente_status = '';
|
||||||
actions.samePatientConflict.value = null;
|
actions.samePatientConflict.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user