MelissaPaciente: dialog nova sessao integra useRecurrence (recorrencia semanal)
User apontou que ja existe sistema de recorrencia pronto (useRecurrence.js
+ tabela recurrence_rules + MelissaRecorrencias). Integrei no dialog de
nova sessao.
NOVO no dialog:
- Checkbox "Repetir semanalmente" + texto explicativo (cria serie no
mesmo dia da semana e horario)
- Quando ativado, mostra 3 opcoes radio:
- "Sem data de fim" (open-ended — continua ate cancelar)
- "Apos N sessoes" (max_occurrences)
- "Ate <data>" (end_date)
- Cada opcao com input inline disabled quando nao selecionada
- Label do botao salvar muda dinamicamente: "Agendar sessao" -> "Criar
recorrencia"
LOGICA salvarSessao() ramificada:
- Se repetir = false: caminho original (createSession + INSERT em
agenda_eventos)
- Se repetir = true: caminho NOVO via useRecurrence.createRule:
- type: 'weekly', interval: 1
- weekdays: [inicio.getDay()] (calculado do dia da semana selecionado)
- start_date: f.data
- end_date / max_occurrences conforme fim_tipo
- start_time: f.hora
- duration_min, modalidade, titulo_custom, observacoes, status: 'ativo'
- Insere row em recurrence_rules; ocorrencias sao geradas dinamicamente
pelo expandRules() do composable. Sessoes confirmadas/realizadas
viram rows reais sob demanda.
Validacoes adicionais:
- fim_tipo='data' exige fim_data preenchido (toast warn)
- fim_tipo='count' exige fim_count >= 1 (toast warn)
Reload das sessoes ao final pra refletir caso start_date seja hoje
(occurrence ja entra na timeline).
Toast de sucesso aponta pra "Recorrencias" como destino pra gerenciar
a serie.
ESLint: 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConversationDrawerStore } from '@/stores/conversationDrawerStore';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import DocumentsListPage from '@/features/documents/DocumentsListPage.vue';
|
||||
import PatientConversationsTab from '@/features/patients/prontuario/PatientConversationsTab.vue';
|
||||
import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue';
|
||||
@@ -30,6 +31,7 @@ import { usePatientSessions } from '@/features/patients/composables/usePatientSe
|
||||
import { usePatientFinancial } from '@/features/patients/composables/usePatientFinancial';
|
||||
import { usePatientMessages } from '@/features/patients/composables/usePatientMessages';
|
||||
import { usePatientDocuments } from '@/features/patients/composables/usePatientDocuments';
|
||||
import { useRecurrence } from '@/features/agenda/composables/useRecurrence';
|
||||
import {
|
||||
pickField,
|
||||
calcAge,
|
||||
@@ -70,6 +72,7 @@ const sessionsHook = usePatientSessions();
|
||||
const financialHook = usePatientFinancial();
|
||||
const messagesHook = usePatientMessages();
|
||||
const documentsHook = usePatientDocuments();
|
||||
const recurrenceHook = useRecurrence();
|
||||
|
||||
// ── Breakpoints + drawer ───────────────────────────────────
|
||||
const drawerOpen = ref(false);
|
||||
@@ -425,7 +428,12 @@ const novaSessaoForm = ref({
|
||||
duracao_min: 50,
|
||||
modalidade: 'presencial',
|
||||
titulo_custom: '',
|
||||
observacoes: ''
|
||||
observacoes: '',
|
||||
// Recorrencia (integra com useRecurrence — schema recurrence_rules)
|
||||
repetir: false,
|
||||
fim_tipo: 'open', // open | data | count
|
||||
fim_data: '',
|
||||
fim_count: 12
|
||||
});
|
||||
const SESSAO_TIPOS = [
|
||||
{ label: 'Sessão', value: 'sessao' },
|
||||
@@ -465,7 +473,11 @@ function goAgendar() {
|
||||
duracao_min: 50,
|
||||
modalidade: 'presencial',
|
||||
titulo_custom: '',
|
||||
observacoes: ''
|
||||
observacoes: '',
|
||||
repetir: false,
|
||||
fim_tipo: 'open',
|
||||
fim_data: '',
|
||||
fim_count: 12
|
||||
};
|
||||
novaSessaoOpen.value = true;
|
||||
}
|
||||
@@ -479,9 +491,6 @@ async function salvarSessao() {
|
||||
toast.add({ severity: 'warn', summary: 'Hora obrigatória', life: 2500 });
|
||||
return;
|
||||
}
|
||||
// Monta inicio_em + fim_em a partir de data + hora + duracao_min.
|
||||
// Local time -> ISO. Sem timezone explicit, usa o do browser (consistente
|
||||
// com o pattern do AgendaEventDialog e MelissaAgenda).
|
||||
const [y, m, d] = f.data.split('-').map(Number);
|
||||
const [hh, mm] = f.hora.split(':').map(Number);
|
||||
const inicio = new Date(y, (m || 1) - 1, d || 1, hh || 0, mm || 0, 0);
|
||||
@@ -489,8 +498,61 @@ async function salvarSessao() {
|
||||
toast.add({ severity: 'warn', summary: 'Data/hora inválida', life: 2500 });
|
||||
return;
|
||||
}
|
||||
const fim = new Date(inicio.getTime() + (Number(f.duracao_min) || 50) * 60 * 1000);
|
||||
|
||||
// Caminho RECORRENTE: cria regra em recurrence_rules via useRecurrence.
|
||||
// Ocorrencias sao geradas dinamicamente — nao precisa popular agenda_eventos
|
||||
// futuros (sessoes confirmadas/realizadas viram rows reais sob demanda).
|
||||
if (f.repetir) {
|
||||
if (f.fim_tipo === 'data' && !f.fim_data) {
|
||||
toast.add({ severity: 'warn', summary: 'Informe a data de fim', life: 2500 });
|
||||
return;
|
||||
}
|
||||
if (f.fim_tipo === 'count' && (!f.fim_count || Number(f.fim_count) < 1)) {
|
||||
toast.add({ severity: 'warn', summary: 'Informe o número de ocorrências', life: 2500 });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { data: userData } = await supabase.auth.getUser();
|
||||
const ownerId = userData?.user?.id;
|
||||
const rule = {
|
||||
patient_id: props.patientId,
|
||||
owner_id: ownerId,
|
||||
type: 'weekly',
|
||||
interval: 1,
|
||||
weekdays: [inicio.getDay()],
|
||||
start_date: f.data,
|
||||
end_date: f.fim_tipo === 'data' ? f.fim_data : null,
|
||||
max_occurrences: f.fim_tipo === 'count' ? Number(f.fim_count) : null,
|
||||
start_time: f.hora,
|
||||
duration_min: Number(f.duracao_min) || 50,
|
||||
modalidade: f.modalidade,
|
||||
titulo_custom: String(f.titulo_custom || '').trim() || null,
|
||||
observacoes: String(f.observacoes || '').trim() || null,
|
||||
status: 'ativo'
|
||||
};
|
||||
await recurrenceHook.createRule(rule);
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Recorrência criada',
|
||||
detail: 'A série semanal está ativa. Veja em "Recorrências".',
|
||||
life: 3000
|
||||
});
|
||||
novaSessaoOpen.value = false;
|
||||
// Recarrega sessoes do paciente (caso start_date seja hoje).
|
||||
await sessionsHook.load(props.patientId);
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Falha ao criar recorrência',
|
||||
detail: e?.message || 'Erro inesperado',
|
||||
life: 4000
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Caminho SESSAO UNICA: insert direto em agenda_eventos.
|
||||
const fim = new Date(inicio.getTime() + (Number(f.duracao_min) || 50) * 60 * 1000);
|
||||
const result = await sessionsHook.createSession(props.patientId, {
|
||||
inicio_em: inicio.toISOString(),
|
||||
fim_em: fim.toISOString(),
|
||||
@@ -2197,6 +2259,49 @@ onBeforeUnmount(() => {
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Bloco de recorrencia (cria recurrence_rules em vez de agenda_eventos) -->
|
||||
<div class="mpa-recur">
|
||||
<label class="mpa-recur__toggle">
|
||||
<Checkbox v-model="novaSessaoForm.repetir" :binary="true" />
|
||||
<div class="mpa-recur__toggle-text">
|
||||
<strong>Repetir semanalmente</strong>
|
||||
<span>Cria uma série recorrente neste mesmo dia da semana e horário</span>
|
||||
</div>
|
||||
</label>
|
||||
<div v-if="novaSessaoForm.repetir" class="mpa-recur__opts">
|
||||
<label class="mpa-recur__opt">
|
||||
<RadioButton v-model="novaSessaoForm.fim_tipo" value="open" />
|
||||
<span>Sem data de fim — continua até cancelar</span>
|
||||
</label>
|
||||
<label class="mpa-recur__opt">
|
||||
<RadioButton v-model="novaSessaoForm.fim_tipo" value="count" />
|
||||
<span>
|
||||
Após
|
||||
<InputText
|
||||
v-model="novaSessaoForm.fim_count"
|
||||
type="number"
|
||||
min="1"
|
||||
class="mpa-recur__inline"
|
||||
:disabled="novaSessaoForm.fim_tipo !== 'count'"
|
||||
/>
|
||||
sessões
|
||||
</span>
|
||||
</label>
|
||||
<label class="mpa-recur__opt">
|
||||
<RadioButton v-model="novaSessaoForm.fim_tipo" value="data" />
|
||||
<span>
|
||||
Até
|
||||
<InputText
|
||||
v-model="novaSessaoForm.fim_data"
|
||||
type="date"
|
||||
class="mpa-recur__inline mpa-recur__inline--date"
|
||||
:disabled="novaSessaoForm.fim_tipo !== 'data'"
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<button class="mpa-quick-btn" @click="novaSessaoOpen = false">
|
||||
@@ -2209,7 +2314,7 @@ onBeforeUnmount(() => {
|
||||
@click="salvarSessao"
|
||||
>
|
||||
<i :class="sessionsHook.busy.value ? 'pi pi-spin pi-spinner' : 'pi pi-check'" :style="{ color: '#10b981' }" />
|
||||
<span>Agendar sessão</span>
|
||||
<span>{{ novaSessaoForm.repetir ? 'Criar recorrência' : 'Agendar sessão' }}</span>
|
||||
</button>
|
||||
</template>
|
||||
</Dialog>
|
||||
@@ -3242,6 +3347,71 @@ onBeforeUnmount(() => {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
/* Bloco de recorrencia (toggle + opcoes radio) dentro do dialog Nova Sessao */
|
||||
.mpa-recur {
|
||||
margin-top: 4px;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
background: var(--m-bg-medium);
|
||||
border: 1px solid var(--m-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.mpa-recur__toggle {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.mpa-recur__toggle-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.mpa-recur__toggle-text strong {
|
||||
font-size: 0.85rem;
|
||||
color: var(--m-text);
|
||||
font-weight: 700;
|
||||
}
|
||||
.mpa-recur__toggle-text span {
|
||||
font-size: 0.74rem;
|
||||
color: var(--m-text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.mpa-recur__opts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding-left: 4px;
|
||||
padding-top: 4px;
|
||||
border-top: 1px dashed var(--m-border);
|
||||
padding-top: 10px;
|
||||
}
|
||||
.mpa-recur__opt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 0.82rem;
|
||||
color: var(--m-text);
|
||||
}
|
||||
.mpa-recur__opt > span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.mpa-recur__inline {
|
||||
width: 70px !important;
|
||||
text-align: center;
|
||||
}
|
||||
.mpa-recur__inline--date {
|
||||
width: 150px !important;
|
||||
}
|
||||
|
||||
/* ═══════ Tab Conversas (Fase 7) — CTA pra drawer ═══════ */
|
||||
.mpa-conv-cta {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user