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: () => [] },
|
feriados: { type: Array, default: () => [] },
|
||||||
|
|
||||||
// Rota para cadastro completo de paciente (abre em nova aba)
|
// 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']);
|
const emit = defineEmits(['update:modelValue', 'save', 'delete', 'updateSeriesEvent', 'editSeriesOccurrence', 'updated']);
|
||||||
@@ -176,6 +193,17 @@ const {
|
|||||||
resetForm
|
resetForm
|
||||||
} = _composer;
|
} = _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) ──
|
// ── recorrência: opções estáticas (consts data — não migradas) ──
|
||||||
const freqOpcoes = [
|
const freqOpcoes = [
|
||||||
{ value: 'avulsa', label: 'Avulsa' },
|
{ 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">
|
<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>
|
<template #header>
|
||||||
<div class="w-full flex items-center justify-between gap-3">
|
<div class="w-full flex items-center justify-between gap-3">
|
||||||
<div class="flex items-center gap-2 min-w-0">
|
<!-- Slot headerLeft (override de chamadas como MelissaPaciente
|
||||||
<div class="header-dot shrink-0" :style="selectedCommitment?.bg_color ? { background: `#${selectedCommitment.bg_color}` } : {}" />
|
que precisam icon+title+subtitle custom). Default: dot
|
||||||
<div class="min-w-0">
|
colorido + headerTitle + previewRange. -->
|
||||||
<div class="font-semibold truncate text-base">{{ headerTitle }}</div>
|
<slot name="headerLeft">
|
||||||
<div v-if="step === 2" class="text-xs text-color-secondary truncate">{{ previewRange }}</div>
|
<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>
|
||||||
</div>
|
</slot>
|
||||||
|
|
||||||
<!-- actions moved to footer -->
|
<!-- actions moved to footer -->
|
||||||
</div>
|
</div>
|
||||||
@@ -733,9 +766,9 @@ const googleCalendarUrl = computed(() => {
|
|||||||
<!-- ConfirmDialog renderizado na página pai para evitar conflito de z-index com o Dialog -->
|
<!-- 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>
|
<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">
|
<Message v-if="isDayBlocked" severity="warn" class="mb-4" :closable="false">
|
||||||
@@ -863,9 +896,9 @@ const googleCalendarUrl = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-1 shrink-0">
|
<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 && !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" icon="pi pi-times" severity="secondary" text size="small" class="rounded-full h-8 w-8" v-tooltip.top="'Limpar'" @click="clearPaciente" />
|
<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" 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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { useRouter } from 'vue-router';
|
|||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { useConversationDrawerStore } from '@/stores/conversationDrawerStore';
|
import { useConversationDrawerStore } from '@/stores/conversationDrawerStore';
|
||||||
import { MELISSA_AGENDA_KEY } from './composables/useMelissaAgenda';
|
import { MELISSA_AGENDA_KEY } from './composables/useMelissaAgenda';
|
||||||
|
import AgendaEventDialog from '@/features/agenda/components/AgendaEventDialog.vue';
|
||||||
import DocumentsListPage from '@/features/documents/DocumentsListPage.vue';
|
import DocumentsListPage from '@/features/documents/DocumentsListPage.vue';
|
||||||
import PatientConversationsTab from '@/features/patients/prontuario/PatientConversationsTab.vue';
|
import PatientConversationsTab from '@/features/patients/prontuario/PatientConversationsTab.vue';
|
||||||
import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue';
|
import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue';
|
||||||
@@ -458,8 +459,16 @@ function addFinancial() {
|
|||||||
novoLancOpen.value = true;
|
novoLancOpen.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Abre o AgendaEventDialog GLOBAL (mesmo que MelissaAgenda usa) com
|
// Abre AgendaEventDialog LOCAL (instancia propria) com paciente fixo
|
||||||
// paciente pre-selecionado. O dialog vive no MelissaLayout via provide.
|
// 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() {
|
function goAgendar() {
|
||||||
activeTab.value = 'agenda';
|
activeTab.value = 'agenda';
|
||||||
if (isMobile.value) drawerOpen.value = false;
|
if (isMobile.value) drawerOpen.value = false;
|
||||||
@@ -467,7 +476,7 @@ function goAgendar() {
|
|||||||
toast.add({ severity: 'warn', summary: 'Paciente sem ID', life: 2500 });
|
toast.add({ severity: 'warn', summary: 'Paciente sem ID', life: 2500 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!melissaAgenda || typeof melissaAgenda.onCreateEventoForPatient !== 'function') {
|
if (!melissaAgenda?.ownerId?.value) {
|
||||||
toast.add({
|
toast.add({
|
||||||
severity: 'warn',
|
severity: 'warn',
|
||||||
summary: 'Agenda indisponível',
|
summary: 'Agenda indisponível',
|
||||||
@@ -476,21 +485,58 @@ function goAgendar() {
|
|||||||
});
|
});
|
||||||
return;
|
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)
|
// Handlers do dialog local — delegam pros handlers globais do useMelissaAgenda
|
||||||
// refetcha sessoes + recorrencias do paciente. Cobre tanto save quanto
|
// (M.onDialogSave/Delete) que ja sabem mexer com agenda_eventos +
|
||||||
// close-sem-salvar (idempotente; load() ja eh barato).
|
// recurrence_rules + exceptions. Apos fechar, refetcha sessions+recorrencias.
|
||||||
if (melissaAgenda?.dialogOpen) {
|
async function onSessaoDialogSave(payload) {
|
||||||
watch(melissaAgenda.dialogOpen, async (now, prev) => {
|
if (typeof melissaAgenda?.onDialogSave === 'function') {
|
||||||
if (prev && !now && props.patientId) {
|
await melissaAgenda.onDialogSave(payload);
|
||||||
await Promise.all([
|
}
|
||||||
sessionsHook.load(props.patientId),
|
sessaoDialogOpen.value = false;
|
||||||
recorrenciasHook.load(props.patientId)
|
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() {
|
async function salvarLancamento() {
|
||||||
const f = novoLancForm.value;
|
const f = novoLancForm.value;
|
||||||
@@ -2204,11 +2250,53 @@ onBeforeUnmount(() => {
|
|||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<!-- Dialog Nova Sessao removido — agora usa o AgendaEventDialog
|
<!-- AgendaEventDialog LOCAL do prontuario.
|
||||||
GLOBAL do MelissaLayout (mesmo que a Agenda usa). MelissaPaciente
|
- Reusa o componente da Agenda (sem duplicar codigo) com 2 props
|
||||||
dispara via melissaAgenda.onCreateEventoForPatient(patientId).
|
novas: lockType (pula step 1) + lockPatient (esconde trocar/limpar).
|
||||||
Watch em melissaAgenda.dialogOpen refetcha sessions+recorrencias
|
- Slot #headerLeft sobrescreve o header padrao com icon + "Nova
|
||||||
quando o dialog fecha (cobre save e cancel). -->
|
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>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
Reference in New Issue
Block a user