MelissaPaciente: dialog Nova Sessao usa "Frequencia" estilo AgendaEventDialog
User pediu pra trocar o checkbox "Repetir semanalmente" + radios pelo
mesmo widget de Frequencia que existe no AgendaEventDialog. Replicado
1:1 o pattern (chips + qtd sessoes).
REMOVIDO
- Checkbox "Repetir semanalmente"
- 3 radios de fim_tipo (open/count/data)
- Inputs inline associados (fim_count, fim_data)
ADICIONADO no form
- novaSessaoForm.freq: 'avulsa' (default) | 'semanal' | 'quinzenal' |
'diasEspecificos'
- novaSessaoForm.diasSelecionados: array<int> (so usado em
diasEspecificos)
- novaSessaoForm.qtdMode: '4' | '8' | '12' | 'personalizar'
- novaSessaoForm.qtdCustom: number (so usado em personalizar)
ADICIONADO catalogos (FREQ_OPCOES, DIAS_SEMANA_OPCOES, QTD_SESSOES_OPCOES)
e helpers (toggleDiaSelecionado, qtdSessoesEfetiva computed,
novaSessaoCtaLabel computed).
ADICIONADO no template:
- Chips horizontais "Avulsa / Semanal / Quinzenal / Dias específicos"
(estilo .mpa-freq-chip — pill arredondado, primary quando active)
- Preview com icon refresh: "Toda quarta, às 14:00" / "A cada 2 semanas,
toda quarta..."
- Grid de dias da semana (Seg Ter Qua Qui Sex Sab Dom) so quando
diasEspecificos
- Quantidade de sessoes: chips "4 sessoes / 8 / 12 / Personalizar"
+ InputNumber show-buttons quando personalizar
- Label dinamica do CTA: "Agendar sessão" (avulsa) / "Criar recorrência"
LOGICA salvarSessao mapeia freq -> recurrence_rules:
- avulsa: caminho original (createSession + INSERT agenda_eventos)
- semanal: type='weekly', interval=1, weekdays=[dow]
- quinzenal: type='biweekly', interval=2, weekdays=[dow]
- diasEspecificos: type='custom_weekdays', interval=1, weekdays=[selecionados]
Sempre com max_occurrences (qtd efetiva) — sem mais open-ended por
default. Toast detalha "{N} sessoes previstas".
Validacoes:
- diasEspecificos exige >=1 dia selecionado (toast warn)
- qtd efetiva >= 1 (cobrindo personalizar invalido)
CSS: ~120L (substitui o bloco .mpa-recur antigo). Usa accent var
--p-primary-color pra match do app theme. .mpa-freq-chip / .mpa-dia-chip
hover/active states. .mpa-freq-preview com bg color-mix do primary.
ESLint: 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,7 @@ import {
|
||||
fmtDayShort,
|
||||
fmtRecurrenceLabel,
|
||||
fmtRecurrenceFim,
|
||||
WEEKDAY_LABEL as WEEKDAY_LABEL_BLOCK,
|
||||
fmtCPF,
|
||||
fmtRG,
|
||||
fmtGender,
|
||||
@@ -456,6 +457,9 @@ function addFinancial() {
|
||||
}
|
||||
|
||||
// Atalho: navega pra aba Agenda + abre dialog de nova sessao.
|
||||
// Frequencia espelha AgendaEventDialog: avulsa | semanal | quinzenal |
|
||||
// diasEspecificos. Avulsa cria 1 row em agenda_eventos; demais criam
|
||||
// regra em recurrence_rules via useRecurrence.
|
||||
const novaSessaoOpen = ref(false);
|
||||
const novaSessaoForm = ref({
|
||||
tipo: 'sessao',
|
||||
@@ -465,11 +469,50 @@ const novaSessaoForm = ref({
|
||||
modalidade: 'presencial',
|
||||
titulo_custom: '',
|
||||
observacoes: '',
|
||||
// Recorrencia (integra com useRecurrence — schema recurrence_rules)
|
||||
repetir: false,
|
||||
fim_tipo: 'open', // open | data | count
|
||||
fim_data: '',
|
||||
fim_count: 12
|
||||
freq: 'avulsa',
|
||||
diasSelecionados: [],
|
||||
qtdMode: '12',
|
||||
qtdCustom: 12
|
||||
});
|
||||
const FREQ_OPCOES = [
|
||||
{ value: 'avulsa', label: 'Avulsa' },
|
||||
{ value: 'semanal', label: 'Semanal' },
|
||||
{ value: 'quinzenal', label: 'Quinzenal' },
|
||||
{ value: 'diasEspecificos', label: 'Dias específicos' }
|
||||
];
|
||||
const DIAS_SEMANA_OPCOES = [
|
||||
{ value: 1, short: 'Seg' },
|
||||
{ value: 2, short: 'Ter' },
|
||||
{ value: 3, short: 'Qua' },
|
||||
{ value: 4, short: 'Qui' },
|
||||
{ value: 5, short: 'Sex' },
|
||||
{ value: 6, short: 'Sáb' },
|
||||
{ value: 0, short: 'Dom' }
|
||||
];
|
||||
const QTD_SESSOES_OPCOES = [
|
||||
{ value: '4', label: '4 sessões' },
|
||||
{ value: '8', label: '8 sessões' },
|
||||
{ value: '12', label: '12 sessões' },
|
||||
{ value: 'personalizar', label: 'Personalizar' }
|
||||
];
|
||||
|
||||
function toggleDiaSelecionado(d) {
|
||||
const arr = [...(novaSessaoForm.value.diasSelecionados || [])];
|
||||
const idx = arr.indexOf(d);
|
||||
if (idx === -1) arr.push(d);
|
||||
else arr.splice(idx, 1);
|
||||
novaSessaoForm.value.diasSelecionados = arr;
|
||||
}
|
||||
|
||||
const qtdSessoesEfetiva = computed(() => {
|
||||
const f = novaSessaoForm.value;
|
||||
if (f.qtdMode === 'personalizar') return Number(f.qtdCustom) || null;
|
||||
return Number(f.qtdMode) || null;
|
||||
});
|
||||
|
||||
// Label dinamico do botao "Salvar"
|
||||
const novaSessaoCtaLabel = computed(() => {
|
||||
return novaSessaoForm.value.freq === 'avulsa' ? 'Agendar sessão' : 'Criar recorrência';
|
||||
});
|
||||
const SESSAO_TIPOS = [
|
||||
{ label: 'Sessão', value: 'sessao' },
|
||||
@@ -510,10 +553,10 @@ function goAgendar() {
|
||||
modalidade: 'presencial',
|
||||
titulo_custom: '',
|
||||
observacoes: '',
|
||||
repetir: false,
|
||||
fim_tipo: 'open',
|
||||
fim_data: '',
|
||||
fim_count: 12
|
||||
freq: 'avulsa',
|
||||
diasSelecionados: [],
|
||||
qtdMode: '12',
|
||||
qtdCustom: 12
|
||||
};
|
||||
novaSessaoOpen.value = true;
|
||||
}
|
||||
@@ -536,15 +579,30 @@ async function salvarSessao() {
|
||||
}
|
||||
|
||||
// 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;
|
||||
// Ocorrencias sao geradas dinamicamente.
|
||||
if (f.freq !== 'avulsa') {
|
||||
// Mapeamento freq -> { type, interval, weekdays }
|
||||
let type, interval, weekdays;
|
||||
if (f.freq === 'semanal') {
|
||||
type = 'weekly';
|
||||
interval = 1;
|
||||
weekdays = [inicio.getDay()];
|
||||
} else if (f.freq === 'quinzenal') {
|
||||
type = 'biweekly';
|
||||
interval = 2;
|
||||
weekdays = [inicio.getDay()];
|
||||
} else if (f.freq === 'diasEspecificos') {
|
||||
if (!f.diasSelecionados.length) {
|
||||
toast.add({ severity: 'warn', summary: 'Selecione ao menos um dia da semana', life: 3000 });
|
||||
return;
|
||||
}
|
||||
type = 'custom_weekdays';
|
||||
interval = 1;
|
||||
weekdays = [...f.diasSelecionados].sort();
|
||||
}
|
||||
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 });
|
||||
const max = qtdSessoesEfetiva.value;
|
||||
if (!max || max < 1) {
|
||||
toast.add({ severity: 'warn', summary: 'Quantidade de sessões inválida', life: 2500 });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -553,12 +611,12 @@ async function salvarSessao() {
|
||||
const rule = {
|
||||
patient_id: props.patientId,
|
||||
owner_id: ownerId,
|
||||
type: 'weekly',
|
||||
interval: 1,
|
||||
weekdays: [inicio.getDay()],
|
||||
type,
|
||||
interval,
|
||||
weekdays,
|
||||
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,
|
||||
end_date: null,
|
||||
max_occurrences: max,
|
||||
start_time: f.hora,
|
||||
duration_min: Number(f.duracao_min) || 50,
|
||||
modalidade: f.modalidade,
|
||||
@@ -570,12 +628,10 @@ async function salvarSessao() {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Recorrência criada',
|
||||
detail: 'A série semanal está ativa. Veja em "Recorrências".',
|
||||
detail: `${max} sessões previstas. Veja em "Recorrências".`,
|
||||
life: 3000
|
||||
});
|
||||
novaSessaoOpen.value = false;
|
||||
// Recarrega sessoes (caso start_date seja hoje) + recorrencias
|
||||
// (a regra recem-criada precisa aparecer no bloco da Tab Agenda).
|
||||
await Promise.all([
|
||||
sessionsHook.load(props.patientId),
|
||||
recorrenciasHook.load(props.patientId)
|
||||
@@ -2404,46 +2460,77 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Bloco de recorrencia (cria recurrence_rules em vez de agenda_eventos) -->
|
||||
<!-- Bloco de frequencia (espelha AgendaEventDialog: chips + qtd) -->
|
||||
<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>
|
||||
<label class="mpa-novo-lanc__label">Frequência</label>
|
||||
<div class="mpa-freq-chips">
|
||||
<button
|
||||
v-for="opt in FREQ_OPCOES"
|
||||
:key="opt.value"
|
||||
type="button"
|
||||
class="mpa-freq-chip"
|
||||
:class="{ 'is-active': novaSessaoForm.freq === opt.value }"
|
||||
@click="novaSessaoForm.freq = opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Preview semanal/quinzenal -->
|
||||
<div v-if="novaSessaoForm.freq === 'semanal' || novaSessaoForm.freq === 'quinzenal'" class="mpa-freq-preview">
|
||||
<i class="pi pi-refresh" />
|
||||
<span>
|
||||
{{ novaSessaoForm.freq === 'quinzenal' ? 'A cada 2 semanas, toda' : 'Toda' }}
|
||||
{{ novaSessaoForm.data ? (WEEKDAY_LABEL_BLOCK[new Date(novaSessaoForm.data + 'T00:00:00').getDay()] || '...').toLowerCase() : '...' }},
|
||||
às {{ novaSessaoForm.hora || '—' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Dias da semana (diasEspecificos) -->
|
||||
<div v-if="novaSessaoForm.freq === 'diasEspecificos'" class="mpa-freq-dias">
|
||||
<label class="mpa-novo-lanc__label">Dias da semana</label>
|
||||
<div class="mpa-dias-grid">
|
||||
<button
|
||||
v-for="d in DIAS_SEMANA_OPCOES"
|
||||
:key="d.value"
|
||||
type="button"
|
||||
class="mpa-dia-chip"
|
||||
:class="{ 'is-active': novaSessaoForm.diasSelecionados.includes(d.value) }"
|
||||
@click="toggleDiaSelecionado(d.value)"
|
||||
>
|
||||
{{ d.short }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quantidade de sessoes (so quando nao avulsa) -->
|
||||
<div v-if="novaSessaoForm.freq !== 'avulsa'" class="mpa-freq-qtd">
|
||||
<label class="mpa-novo-lanc__label">Quantidade de sessões</label>
|
||||
<div class="mpa-freq-chips">
|
||||
<button
|
||||
v-for="opt in QTD_SESSOES_OPCOES"
|
||||
:key="opt.value"
|
||||
type="button"
|
||||
class="mpa-freq-chip mpa-freq-chip--sm"
|
||||
:class="{ 'is-active': novaSessaoForm.qtdMode === opt.value }"
|
||||
@click="novaSessaoForm.qtdMode = opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="novaSessaoForm.qtdMode === 'personalizar'" class="mpa-freq-qtd-custom">
|
||||
<InputNumber
|
||||
v-model="novaSessaoForm.qtdCustom"
|
||||
:min="1"
|
||||
:max="200"
|
||||
show-buttons
|
||||
button-layout="horizontal"
|
||||
fluid
|
||||
>
|
||||
<template #decrementbuttonicon><i class="pi pi-minus" /></template>
|
||||
<template #incrementbuttonicon><i class="pi pi-plus" /></template>
|
||||
</InputNumber>
|
||||
</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>
|
||||
@@ -2458,7 +2545,7 @@ onBeforeUnmount(() => {
|
||||
@click="salvarSessao"
|
||||
>
|
||||
<i :class="sessionsHook.busy.value ? 'pi pi-spin pi-spinner' : 'pi pi-check'" :style="{ color: '#10b981' }" />
|
||||
<span>{{ novaSessaoForm.repetir ? 'Criar recorrência' : 'Agendar sessão' }}</span>
|
||||
<span>{{ novaSessaoCtaLabel }}</span>
|
||||
</button>
|
||||
</template>
|
||||
</Dialog>
|
||||
@@ -3491,7 +3578,7 @@ onBeforeUnmount(() => {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
/* Bloco de recorrencia (toggle + opcoes radio) dentro do dialog Nova Sessao */
|
||||
/* Bloco de frequencia dentro do dialog Nova Sessao (espelha AgendaEventDialog) */
|
||||
.mpa-recur {
|
||||
margin-top: 4px;
|
||||
padding: 12px;
|
||||
@@ -3500,60 +3587,107 @@ onBeforeUnmount(() => {
|
||||
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 {
|
||||
|
||||
/* Chips de frequencia + qtd sessoes */
|
||||
.mpa-freq-chips {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.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;
|
||||
.mpa-freq-chip {
|
||||
padding: 5px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
border: 1px solid var(--m-border);
|
||||
background: transparent;
|
||||
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);
|
||||
transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
|
||||
white-space: nowrap;
|
||||
font-family: inherit;
|
||||
}
|
||||
.mpa-recur__opt > span {
|
||||
.mpa-freq-chip:hover {
|
||||
border-color: var(--p-primary-color);
|
||||
color: var(--p-primary-color);
|
||||
}
|
||||
.mpa-freq-chip.is-active {
|
||||
background: var(--p-primary-color);
|
||||
border-color: var(--p-primary-color);
|
||||
color: var(--p-primary-contrast-color, #fff);
|
||||
}
|
||||
.mpa-freq-chip--sm {
|
||||
padding: 4px 10px;
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
|
||||
/* Preview semanal/quinzenal */
|
||||
.mpa-freq-preview {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--p-primary-color) 8%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--p-primary-color) 28%, transparent);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 500;
|
||||
color: var(--m-text);
|
||||
}
|
||||
.mpa-freq-preview > i {
|
||||
color: var(--p-primary-color);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Dias da semana grid (diasEspecificos) */
|
||||
.mpa-freq-dias {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.mpa-dias-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
.mpa-recur__inline {
|
||||
width: 70px !important;
|
||||
.mpa-dia-chip {
|
||||
min-width: 44px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.74rem;
|
||||
font-weight: 700;
|
||||
border: 1px solid var(--m-border);
|
||||
background: transparent;
|
||||
color: var(--m-text-muted);
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
.mpa-recur__inline--date {
|
||||
width: 150px !important;
|
||||
.mpa-dia-chip:hover {
|
||||
border-color: var(--p-primary-color);
|
||||
color: var(--p-primary-color);
|
||||
}
|
||||
.mpa-dia-chip.is-active {
|
||||
background: var(--p-primary-color);
|
||||
border-color: var(--p-primary-color);
|
||||
color: var(--p-primary-contrast-color, #fff);
|
||||
}
|
||||
|
||||
/* Qtd sessoes */
|
||||
.mpa-freq-qtd {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.mpa-freq-qtd-custom {
|
||||
margin-top: 4px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
background: var(--m-bg-soft);
|
||||
border: 1px solid var(--m-border);
|
||||
}
|
||||
|
||||
/* ═══════ Tab Conversas (Fase 7) — CTA pra drawer ═══════ */
|
||||
|
||||
Reference in New Issue
Block a user