AgendaEventDialog: props lockType + lockPatient + slot #headerLeft (additivos)
User escolheu caminho A: modificar AgendaEventDialog em vez de copiar.
Mudancas SAO ADITIVAS — comportamento atual dos 5 callsites legacy
(TherapistDashboard, PatientsListPage, MelissaAgenda,
MelissaAgendamentosRecebidos, MelissaLayout) preservado.
VALIDACAO: rodei os 7 spec files do agenda — 301 testes passaram.
Zero regressao.
ADICIONADO em src/features/agenda/components/AgendaEventDialog.vue
- Prop lockType (Boolean, default false): pula step 1 (escolha de tipo)
e vai direto pro form. Watch immediate em [lockType, modelValue]
forca step.value=2 quando lockType=true e dialog abre.
- Prop lockPatient (Boolean, default false): esconde botoes "trocar"/
"limpar" do paciente. Mostra icon de lock com tooltip "Paciente do
prontuario". Cobre o cenario "criar sessao pra paciente fixo" sem
precisar do isEdit que o patientLocked computed exige.
- Slot #headerLeft: substitui o conteudo esquerdo do header (default
era header-dot + headerTitle + previewRange). Permite callsites
customizar com icon+title+subtitle proprios sem mexer no resto do
header (X / actions).
- v-if no Step 1: "step === 1 && !lockType"
- v-if nos buttons trocar/limpar: "!patientLocked && !lockPatient"
- Lock icon: "patientLocked || lockPatient" + tooltip dinamico
MELISSAPACIENTE.VUE
- Reverte o inject-only do commit 88dff50.
- Mantem o inject(MELISSA_AGENDA_KEY) APENAS pra LER dados pesados
(commitmentOptions, workRules, allEvents, agendaSettings, feriados,
ownerId, tenantId) — evita re-fetch.
- State LOCAL pro dialog: sessaoDialogOpen, sessaoDialogEventRow,
sessaoDialogStartISO, sessaoDialogEndISO. Nao colide com o dialog
global do MelissaLayout que continua na Agenda.
- goAgendar(): inicializa eventRow com paciente_id fixo + tipo='sessao'
+ defaults razoaveis (proximo slot 15min + duracao da agenda),
abre o dialog local.
- Handlers onSessaoDialogSave / onSessaoDialogDelete delegam pros
handlers globais (M.onDialogSave/Delete) e ao final refetcham
sessions+recorrencias do paciente in-place.
- Render <AgendaEventDialog> com lock-type=true + lock-patient=true
+ slot #headerLeft custom (icon pi-calendar-plus em quadrado
primary 40x40 + "Nova sessão" + nome do paciente como subtitulo).
Resultado: prontuario tem o MESMO componente da Agenda (form completo
de sessao, frequencia com preview de ocorrencias + conflitos,
vinculacao de servicos/billing, edicao de serie, etc) mas pre-fixado
no contexto do paciente, com header proprio e single source of truth.
ESLint: 31 errors pre-existentes em ambos arquivos (variaveis declaradas
nao usadas — confirmado via git stash baseline). 0 errors da minha
mudanca.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -105,7 +105,24 @@ const props = defineProps({
|
||||
feriados: { type: Array, default: () => [] },
|
||||
|
||||
// Rota para cadastro completo de paciente (abre em nova aba)
|
||||
newPatientRoute: { type: String, default: '' }
|
||||
newPatientRoute: { type: String, default: '' },
|
||||
|
||||
// ── Locks aditivos (default false — comportamento atual) ──────────
|
||||
// Usados pelo MelissaPaciente.vue pra abrir esse dialog ja em
|
||||
// contexto de Sessao + paciente fixo do prontuario. Os 5 callsites
|
||||
// existentes (TherapistDashboard, PatientsListPage, MelissaAgenda,
|
||||
// MelissaAgendamentosRecebidos, MelissaLayout) seguem com defaults.
|
||||
//
|
||||
// lockType=true: pula o step de escolha de tipo (commitment cards) e
|
||||
// vai direto pro form. Espera que eventRow ja venha com tipo+commitment
|
||||
// resolvidos (no MelissaPaciente: tipo='sessao').
|
||||
//
|
||||
// lockPatient=true: esconde os botoes "trocar"/"limpar" do paciente
|
||||
// e mostra o icon de lock (mesma UX do patientLocked computed que
|
||||
// ja existia pra modo edit, agora cobre tambem cenarios "criar sessao
|
||||
// pra paciente fixo").
|
||||
lockType: { type: Boolean, default: false },
|
||||
lockPatient: { type: Boolean, default: false }
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'save', 'delete', 'updateSeriesEvent', 'editSeriesOccurrence', 'updated']);
|
||||
@@ -176,6 +193,17 @@ const {
|
||||
resetForm
|
||||
} = _composer;
|
||||
|
||||
// Lock de tipo: quando lockType=true (callsite no contexto de Sessao do
|
||||
// paciente, ex: MelissaPaciente), pula o step 1 e vai direto pro form.
|
||||
// Watch immediate cobre tanto open quanto re-open com novo eventRow.
|
||||
watch(
|
||||
() => [props.lockType, props.modelValue],
|
||||
([locked, open]) => {
|
||||
if (locked && open) step.value = 2;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// ── recorrência: opções estáticas (consts data — não migradas) ──
|
||||
const freqOpcoes = [
|
||||
{ value: 'avulsa', label: 'Avulsa' },
|
||||
@@ -718,13 +746,18 @@ const googleCalendarUrl = computed(() => {
|
||||
<Dialog v-model:visible="visible" modal :draggable="false" :dismissableMask="true" :style="{ width: '1000px', maxWidth: '96vw' }" :breakpoints="{ '960px': '96vw', '640px': '98vw' }" class="agenda-event-composer" pt:mask:class="backdrop-blur-xs">
|
||||
<template #header>
|
||||
<div class="w-full flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<div class="header-dot shrink-0" :style="selectedCommitment?.bg_color ? { background: `#${selectedCommitment.bg_color}` } : {}" />
|
||||
<div class="min-w-0">
|
||||
<div class="font-semibold truncate text-base">{{ headerTitle }}</div>
|
||||
<div v-if="step === 2" class="text-xs text-color-secondary truncate">{{ previewRange }}</div>
|
||||
<!-- Slot headerLeft (override de chamadas como MelissaPaciente
|
||||
que precisam icon+title+subtitle custom). Default: dot
|
||||
colorido + headerTitle + previewRange. -->
|
||||
<slot name="headerLeft">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<div class="header-dot shrink-0" :style="selectedCommitment?.bg_color ? { background: `#${selectedCommitment.bg_color}` } : {}" />
|
||||
<div class="min-w-0">
|
||||
<div class="font-semibold truncate text-base">{{ headerTitle }}</div>
|
||||
<div v-if="step === 2" class="text-xs text-color-secondary truncate">{{ previewRange }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
<!-- actions moved to footer -->
|
||||
</div>
|
||||
@@ -733,9 +766,9 @@ const googleCalendarUrl = computed(() => {
|
||||
<!-- ConfirmDialog renderizado na página pai para evitar conflito de z-index com o Dialog -->
|
||||
|
||||
<!-- ══════════════════════════════════════════════ -->
|
||||
<!-- STEP 1 — escolha o tipo -->
|
||||
<!-- STEP 1 — escolha o tipo (oculto se lockType) -->
|
||||
<!-- ══════════════════════════════════════════════ -->
|
||||
<div v-if="step === 1" class="p-2">
|
||||
<div v-if="step === 1 && !lockType" class="p-2">
|
||||
<div class="mb-4 text-sm text-color-secondary">Selecione o tipo de compromisso para começar.</div>
|
||||
|
||||
<Message v-if="isDayBlocked" severity="warn" class="mb-4" :closable="false">
|
||||
@@ -863,9 +896,9 @@ const googleCalendarUrl = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-1 shrink-0">
|
||||
<Button v-if="!patientLocked" icon="pi pi-pencil" severity="secondary" outlined size="small" class="rounded-full h-8 w-8" v-tooltip.top="'Trocar'" @click="openPacientePicker" />
|
||||
<Button v-if="!patientLocked" icon="pi pi-times" severity="secondary" text size="small" class="rounded-full h-8 w-8" v-tooltip.top="'Limpar'" @click="clearPaciente" />
|
||||
<span v-if="patientLocked" v-tooltip.top="'Paciente não pode ser alterado após criação'" class="flex items-center gap-1 text-xs text-color-secondary px-2"><i class="pi pi-lock" /></span>
|
||||
<Button v-if="!patientLocked && !lockPatient" icon="pi pi-pencil" severity="secondary" outlined size="small" class="rounded-full h-8 w-8" v-tooltip.top="'Trocar'" @click="openPacientePicker" />
|
||||
<Button v-if="!patientLocked && !lockPatient" icon="pi pi-times" severity="secondary" text size="small" class="rounded-full h-8 w-8" v-tooltip.top="'Limpar'" @click="clearPaciente" />
|
||||
<span v-if="patientLocked || lockPatient" v-tooltip.top="lockPatient ? 'Paciente do prontuário' : 'Paciente não pode ser alterado após criação'" class="flex items-center gap-1 text-xs text-color-secondary px-2"><i class="pi pi-lock" /></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import { useRouter } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConversationDrawerStore } from '@/stores/conversationDrawerStore';
|
||||
import { MELISSA_AGENDA_KEY } from './composables/useMelissaAgenda';
|
||||
import AgendaEventDialog from '@/features/agenda/components/AgendaEventDialog.vue';
|
||||
import DocumentsListPage from '@/features/documents/DocumentsListPage.vue';
|
||||
import PatientConversationsTab from '@/features/patients/prontuario/PatientConversationsTab.vue';
|
||||
import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue';
|
||||
@@ -458,8 +459,16 @@ function addFinancial() {
|
||||
novoLancOpen.value = true;
|
||||
}
|
||||
|
||||
// Abre o AgendaEventDialog GLOBAL (mesmo que MelissaAgenda usa) com
|
||||
// paciente pre-selecionado. O dialog vive no MelissaLayout via provide.
|
||||
// Abre AgendaEventDialog LOCAL (instancia propria) com paciente fixo
|
||||
// e tipo='sessao' travado. Dados pesados (commitmentOptions, workRules,
|
||||
// allEvents, agendaSettings, feriados, ownerId, tenantId) vem via inject
|
||||
// do MelissaLayout. State do dialog (open/eventRow/start/end) e LOCAL
|
||||
// pra nao colidir com o dialog global da Agenda.
|
||||
const sessaoDialogOpen = ref(false);
|
||||
const sessaoDialogEventRow = ref(null);
|
||||
const sessaoDialogStartISO = ref(null);
|
||||
const sessaoDialogEndISO = ref(null);
|
||||
|
||||
function goAgendar() {
|
||||
activeTab.value = 'agenda';
|
||||
if (isMobile.value) drawerOpen.value = false;
|
||||
@@ -467,7 +476,7 @@ function goAgendar() {
|
||||
toast.add({ severity: 'warn', summary: 'Paciente sem ID', life: 2500 });
|
||||
return;
|
||||
}
|
||||
if (!melissaAgenda || typeof melissaAgenda.onCreateEventoForPatient !== 'function') {
|
||||
if (!melissaAgenda?.ownerId?.value) {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'Agenda indisponível',
|
||||
@@ -476,21 +485,58 @@ function goAgendar() {
|
||||
});
|
||||
return;
|
||||
}
|
||||
melissaAgenda.onCreateEventoForPatient(props.patientId);
|
||||
// Defaults razoaveis: proximo slot 15min + duracao padrao da agenda.
|
||||
const durMin =
|
||||
melissaAgenda.settings?.value?.session_duration_min ??
|
||||
melissaAgenda.settings?.value?.duracao_padrao_minutos ??
|
||||
50;
|
||||
const base = new Date();
|
||||
base.setSeconds(0, 0);
|
||||
const remainder = base.getMinutes() % 15;
|
||||
if (remainder !== 0) base.setMinutes(base.getMinutes() + (15 - remainder));
|
||||
|
||||
sessaoDialogEventRow.value = {
|
||||
owner_id: melissaAgenda.ownerId.value,
|
||||
terapeuta_id: null,
|
||||
paciente_id: String(props.patientId),
|
||||
tipo: 'sessao',
|
||||
status: 'agendado',
|
||||
titulo: null,
|
||||
observacoes: null,
|
||||
visibility_scope: 'public',
|
||||
determined_commitment_id: null
|
||||
};
|
||||
sessaoDialogStartISO.value = base.toISOString();
|
||||
sessaoDialogEndISO.value = new Date(base.getTime() + durMin * 60000).toISOString();
|
||||
sessaoDialogOpen.value = true;
|
||||
}
|
||||
|
||||
// Watch o dialogOpen do AgendaEventDialog: quando ele fecha (true -> false)
|
||||
// refetcha sessoes + recorrencias do paciente. Cobre tanto save quanto
|
||||
// close-sem-salvar (idempotente; load() ja eh barato).
|
||||
if (melissaAgenda?.dialogOpen) {
|
||||
watch(melissaAgenda.dialogOpen, async (now, prev) => {
|
||||
if (prev && !now && props.patientId) {
|
||||
await Promise.all([
|
||||
sessionsHook.load(props.patientId),
|
||||
recorrenciasHook.load(props.patientId)
|
||||
]);
|
||||
}
|
||||
});
|
||||
// Handlers do dialog local — delegam pros handlers globais do useMelissaAgenda
|
||||
// (M.onDialogSave/Delete) que ja sabem mexer com agenda_eventos +
|
||||
// recurrence_rules + exceptions. Apos fechar, refetcha sessions+recorrencias.
|
||||
async function onSessaoDialogSave(payload) {
|
||||
if (typeof melissaAgenda?.onDialogSave === 'function') {
|
||||
await melissaAgenda.onDialogSave(payload);
|
||||
}
|
||||
sessaoDialogOpen.value = false;
|
||||
if (props.patientId) {
|
||||
await Promise.all([
|
||||
sessionsHook.load(props.patientId),
|
||||
recorrenciasHook.load(props.patientId)
|
||||
]);
|
||||
}
|
||||
}
|
||||
async function onSessaoDialogDelete(payload) {
|
||||
if (typeof melissaAgenda?.onDialogDelete === 'function') {
|
||||
await melissaAgenda.onDialogDelete(payload);
|
||||
}
|
||||
sessaoDialogOpen.value = false;
|
||||
if (props.patientId) {
|
||||
await Promise.all([
|
||||
sessionsHook.load(props.patientId),
|
||||
recorrenciasHook.load(props.patientId)
|
||||
]);
|
||||
}
|
||||
}
|
||||
async function salvarLancamento() {
|
||||
const f = novoLancForm.value;
|
||||
@@ -2204,11 +2250,53 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- Dialog Nova Sessao removido — agora usa o AgendaEventDialog
|
||||
GLOBAL do MelissaLayout (mesmo que a Agenda usa). MelissaPaciente
|
||||
dispara via melissaAgenda.onCreateEventoForPatient(patientId).
|
||||
Watch em melissaAgenda.dialogOpen refetcha sessions+recorrencias
|
||||
quando o dialog fecha (cobre save e cancel). -->
|
||||
<!-- AgendaEventDialog LOCAL do prontuario.
|
||||
- Reusa o componente da Agenda (sem duplicar codigo) com 2 props
|
||||
novas: lockType (pula step 1) + lockPatient (esconde trocar/limpar).
|
||||
- Slot #headerLeft sobrescreve o header padrao com icon + "Nova
|
||||
sessao" + nome do paciente.
|
||||
- State LOCAL (sessaoDialog*) — nao colide com o dialog global do
|
||||
MelissaLayout que continua na Agenda.
|
||||
- Dados pesados (commitmentOptions, workRules, allEvents,
|
||||
agendaSettings, feriados, ownerId, tenantId) vem via inject do
|
||||
MelissaLayout — evita re-fetch.
|
||||
- Save/Delete delegam pros handlers globais (M.onDialogSave/Delete)
|
||||
que ja sabem orquestrar agenda_eventos + recurrence_rules. -->
|
||||
<AgendaEventDialog
|
||||
v-if="melissaAgenda"
|
||||
v-model="sessaoDialogOpen"
|
||||
:event-row="sessaoDialogEventRow"
|
||||
:initial-start-i-s-o="sessaoDialogStartISO"
|
||||
:initial-end-i-s-o="sessaoDialogEndISO"
|
||||
:owner-id="melissaAgenda.ownerId?.value || ''"
|
||||
:tenant-id="melissaAgenda.clinicTenantId?.value || ''"
|
||||
:commitment-options="melissaAgenda.commitmentOptions?.value || []"
|
||||
:work-rules="melissaAgenda.workRules?.value || []"
|
||||
:blocked-dates="[]"
|
||||
:agenda-settings="melissaAgenda.settings?.value || null"
|
||||
:all-events="melissaAgenda.allEventsForDialog?.value || []"
|
||||
:pausas-semanais="melissaAgenda.settings?.value?.pausas_semanais || []"
|
||||
:feriados="melissaAgenda.feriados?.value || []"
|
||||
new-patient-route="/therapist/patients/cadastro"
|
||||
:lock-type="true"
|
||||
:lock-patient="true"
|
||||
@save="onSessaoDialogSave"
|
||||
@delete="onSessaoDialogDelete"
|
||||
@updateSeriesEvent="onSessaoDialogSave"
|
||||
@editSeriesOccurrence="onSessaoDialogSave"
|
||||
>
|
||||
<template #headerLeft>
|
||||
<div class="mpa-dlg-head">
|
||||
<div class="mpa-dlg-head__icon">
|
||||
<i class="pi pi-calendar-plus" />
|
||||
</div>
|
||||
<div class="mpa-dlg-head__text">
|
||||
<div class="mpa-dlg-head__title">Nova sessão</div>
|
||||
<div class="mpa-dlg-head__sub">{{ dash(nomeCompleto) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</AgendaEventDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user