Preficicação, Convenio, Ajustes Agenda, Configurações Excessões
This commit is contained in:
@@ -524,6 +524,7 @@ import { logEvent, logError } from '@/support/supportLogger'
|
||||
import { useAgendaSettings } from '@/features/agenda/composables/useAgendaSettings'
|
||||
import { useAgendaEvents } from '@/features/agenda/composables/useAgendaEvents'
|
||||
import { useRecurrence } from '@/features/agenda/composables/useRecurrence'
|
||||
import { useCommitmentServices } from '@/features/agenda/composables/useCommitmentServices'
|
||||
import { useDeterminedCommitments } from '@/features/agenda/composables/useDeterminedCommitments'
|
||||
import { useFeriados } from '@/composables/useFeriados'
|
||||
|
||||
@@ -538,7 +539,8 @@ const route = useRoute()
|
||||
|
||||
// Data inicial via query param ?date=YYYY-MM-DD (vindo de "Ver na agenda")
|
||||
const _queryDate = route.query.date ? new Date(route.query.date + 'T12:00:00') : null
|
||||
const toast = useToast()
|
||||
const toast = useToast()
|
||||
const confirm = useConfirm()
|
||||
|
||||
// ── Suporte técnico SaaS ────────────────────────────────────────────────────
|
||||
const supportStore = useSupportDebugStore()
|
||||
@@ -636,6 +638,8 @@ const {
|
||||
upsertException,
|
||||
} = useRecurrence()
|
||||
|
||||
const { saveRuleItems, propagateToSerie } = useCommitmentServices()
|
||||
|
||||
const ownerId = computed(() => settings.value?.owner_id || '')
|
||||
|
||||
// -----------------------------
|
||||
@@ -946,7 +950,7 @@ async function loadMonthSearchRows () {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('agenda_eventos')
|
||||
.select('id, owner_id, tipo, status, titulo, inicio_em, fim_em, observacoes, modalidade, determined_commitment_id, patients!agenda_eventos_patient_id_fkey(nome_completo)')
|
||||
.select('id, owner_id, tipo, status, titulo, inicio_em, fim_em, observacoes, modalidade, determined_commitment_id, insurance_plan_id, insurance_guide_number, insurance_value, patients!agenda_eventos_patient_id_fkey(nome_completo)')
|
||||
.eq('owner_id', uid)
|
||||
.is('mirror_of_event_id', null)
|
||||
.gte('inicio_em', start)
|
||||
@@ -975,8 +979,13 @@ watch(currentDate, (newD, oldD) => {
|
||||
})
|
||||
|
||||
const calendarEvents = computed(() => {
|
||||
// calendarRows já filtra onlySessions e une reais + virtuais
|
||||
const base = mapAgendaEventosToCalendarEvents(calendarRows.value)
|
||||
// separa reais e virtuais para aplicar mapAgendaEventosToCalendarEvents
|
||||
// em cada grupo — as virtuais precisam do mesmo tratamento de cores
|
||||
const realRows = calendarRows.value.filter(r => !r.is_occurrence)
|
||||
const occRows = calendarRows.value.filter(r => r.is_occurrence)
|
||||
|
||||
const base = mapAgendaEventosToCalendarEvents(realRows)
|
||||
const occEvents = mapAgendaEventosToCalendarEvents(occRows)
|
||||
|
||||
const breaks =
|
||||
settings.value && currentRange.value.start && currentRange.value.end
|
||||
@@ -987,7 +996,7 @@ const calendarEvents = computed(() => {
|
||||
)
|
||||
: []
|
||||
|
||||
return [...base, ...breaks, ...feriadoFcEvents.value]
|
||||
return [...base, ...occEvents, ...breaks, ...feriadoFcEvents.value]
|
||||
})
|
||||
|
||||
const visibleTitle = computed(() => {
|
||||
@@ -1069,12 +1078,13 @@ const fcOptions = computed(() => ({
|
||||
snapDuration,
|
||||
slotLabelInterval,
|
||||
slotLabelContent,
|
||||
expandRows: true,
|
||||
expandRows: false,
|
||||
height: 'auto',
|
||||
slotMinHeight: 14,
|
||||
|
||||
dayMaxEvents: true,
|
||||
weekends: true,
|
||||
eventMinHeight: 28,
|
||||
eventMinHeight: 14,
|
||||
|
||||
businessHours: businessHours.value,
|
||||
events: calendarEvents.value,
|
||||
@@ -1095,8 +1105,17 @@ const fcOptions = computed(() => ({
|
||||
const start = arg?.start
|
||||
const end = arg?.end
|
||||
if (start && end) {
|
||||
const prevStart = currentRange.value.start?.toString()
|
||||
const prevEnd = currentRange.value.end?.toString()
|
||||
currentRange.value = { start, end }
|
||||
await _reloadRange()
|
||||
// Recarrega sempre que o range mudar OU quando _occurrenceRows estiver vazio
|
||||
if (
|
||||
start.toString() !== prevStart ||
|
||||
end.toString() !== prevEnd ||
|
||||
_occurrenceRows.value.length === 0
|
||||
) {
|
||||
await _reloadRange()
|
||||
}
|
||||
if (eventsError.value) toast.add({ severity: 'warn', summary: 'Compromissos', detail: eventsError.value, life: 4500 })
|
||||
}
|
||||
},
|
||||
@@ -1273,7 +1292,7 @@ async function loadMiniMonthEvents (refDate) {
|
||||
}
|
||||
|
||||
// 2. Ocorrências virtuais de recorrência (não existem no banco)
|
||||
const occRows = await loadAndExpand(ownerId.value, start, end, [], clinicTenantId.value)
|
||||
const occRows = await loadAndExpand(ownerId.value, start, end, rows.value, clinicTenantId.value)
|
||||
for (const r of occRows || []) {
|
||||
if (!r.inicio_em || !r.is_occurrence) continue
|
||||
const ev = new Date(r.inicio_em)
|
||||
@@ -1309,7 +1328,7 @@ watch(
|
||||
() => loadMiniMonthEvents(miniDate.value),
|
||||
{ immediate: true }
|
||||
)
|
||||
watch(() => rows.value?.length, () => loadMiniMonthEvents(miniDate.value))
|
||||
watch(rows, () => loadMiniMonthEvents(miniDate.value))
|
||||
// Fix persistência: recarrega quando ownerId fica disponível (settings são async)
|
||||
watch(ownerId, (v) => { if (v) loadMiniMonthEvents(miniDate.value) })
|
||||
|
||||
@@ -1555,7 +1574,10 @@ function onEventClick (info) {
|
||||
determined_commitment_id: ep.determined_commitment_id ?? null,
|
||||
titulo_custom: ep.titulo_custom ?? null,
|
||||
extra_fields: ep.extra_fields ?? null,
|
||||
price: ep.price != null ? Number(ep.price) : null,
|
||||
price: ep.price != null ? Number(ep.price) : null,
|
||||
insurance_plan_id: ep.insurance_plan_id ?? null,
|
||||
insurance_guide_number: ep.insurance_guide_number ?? null,
|
||||
insurance_value: ep.insurance_value != null ? Number(ep.insurance_value) : null,
|
||||
// ── recorrência (nova arquitetura) ──────────────────────────
|
||||
recurrence_id: ep.recurrence_id ?? ep.recurrenceId ?? ep.serie_id ?? null,
|
||||
original_date: ep.original_date ?? ep.originalDate ?? ep.recurrence_date ?? null,
|
||||
@@ -1647,6 +1669,9 @@ function pickDbFields (obj) {
|
||||
'recurrence_id', 'recurrence_date',
|
||||
// financeiro
|
||||
'price',
|
||||
'insurance_plan_id',
|
||||
'insurance_guide_number',
|
||||
'insurance_value',
|
||||
]
|
||||
const out = {}
|
||||
for (const k of allowed) {
|
||||
@@ -1711,6 +1736,49 @@ async function onUpdateSeriesEvent ({ id, status, recurrence_date, inicio_em, fi
|
||||
}
|
||||
}
|
||||
|
||||
// Opção C — oferece geração de billing_contract após criar série recorrente com serviços.
|
||||
// Chamada APÓS fechar o dialog principal para não bloquear o fluxo principal.
|
||||
async function _offerBillingContract (normalized, recorrencia, tenantId) {
|
||||
const n = recorrencia.qtdSessoes
|
||||
const items = recorrencia.commitmentItems || []
|
||||
const totalPorSessao = items.reduce((s, i) => s + (i.final_price ?? 0), 0)
|
||||
const pacoteFechado = recorrencia.serieValorMode === 'dividir'
|
||||
const packagePrice = pacoteFechado ? totalPorSessao : totalPorSessao * n
|
||||
const perSessao = pacoteFechado ? totalPorSessao / n : totalPorSessao
|
||||
const fmtB = v => Number(v).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })
|
||||
return new Promise(resolve => {
|
||||
confirm.require({
|
||||
header: 'Gerar contrato de cobrança?',
|
||||
message: `${n} sessões — ${fmtB(perSessao)} por sessão. Total da série: ${fmtB(packagePrice)}.`,
|
||||
icon: 'pi pi-file',
|
||||
acceptLabel: 'Sim, gerar contrato',
|
||||
rejectLabel: 'Agora não',
|
||||
accept: async () => {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('billing_contracts')
|
||||
.insert({
|
||||
owner_id: normalized.owner_id,
|
||||
tenant_id: tenantId,
|
||||
patient_id: normalized.paciente_id,
|
||||
type: 'package',
|
||||
total_sessions: n,
|
||||
sessions_used: 0,
|
||||
package_price: packagePrice,
|
||||
status: 'active',
|
||||
})
|
||||
if (error) throw error
|
||||
toast.add({ severity: 'success', summary: 'Contrato gerado', detail: `Pacote de ${n} sessões: ${fmtB(packagePrice)}.`, life: 3000 })
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'warn', summary: 'Erro ao gerar contrato', detail: e?.message, life: 4000 })
|
||||
}
|
||||
resolve()
|
||||
},
|
||||
reject: () => resolve(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function onDialogSave (arg) {
|
||||
let normalized = null
|
||||
|
||||
@@ -1826,34 +1894,95 @@ async function onDialogSave (arg) {
|
||||
if (exErr) logError('AgendaTerapeutaPage', 'onDialogSave: erro ao inserir exceptions', exErr)
|
||||
}
|
||||
|
||||
// Opção C — salvar template de serviços da regra
|
||||
if (createdRule?.id && recorrencia.commitmentItems?.length) {
|
||||
await saveRuleItems(createdRule.id, recorrencia.commitmentItems)
|
||||
}
|
||||
|
||||
const detail = recorrencia.qtdSessoes
|
||||
? `${recorrencia.qtdSessoes} sessões criadas`
|
||||
: 'Série recorrente criada'
|
||||
toast.add({ severity: 'success', summary: 'Série criada', detail, life: 3000 })
|
||||
dialogOpen.value = false
|
||||
await _reloadRange()
|
||||
|
||||
// Opção C — oferecer billing_contract após fechar o dialog (só com serviços + nº definido + paciente)
|
||||
if (createdRule?.id && recorrencia.commitmentItems?.length && recorrencia.qtdSessoes && normalized.paciente_id) {
|
||||
await _offerBillingContract(normalized, recorrencia, clinicId)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ── CASO D: edição "somente_este" de ocorrência de série ───────────────
|
||||
if (recurrenceId && editMode === 'somente_este') {
|
||||
if (originalDate) {
|
||||
let eventId = id ?? null
|
||||
|
||||
if (id) {
|
||||
// Evento já materializado: atualiza campos + mantém exceção sincronizada
|
||||
await update(id, pickDbFields(normalized))
|
||||
if (originalDate) {
|
||||
await upsertException({
|
||||
recurrence_id: recurrenceId,
|
||||
tenant_id: clinicId,
|
||||
original_date: originalDate,
|
||||
type: 'reschedule_session',
|
||||
new_date: normalized.inicio_em?.slice(0, 10),
|
||||
new_start_time: normalized.inicio_em ? new Date(normalized.inicio_em).toTimeString().slice(0, 8) : null,
|
||||
new_end_time: normalized.fim_em ? new Date(normalized.fim_em).toTimeString().slice(0, 8) : null,
|
||||
modalidade: normalized.modalidade ?? null,
|
||||
titulo_custom: normalized.titulo_custom ?? null,
|
||||
observacoes: normalized.observacoes ?? null,
|
||||
extra_fields: normalized.extra_fields ?? null,
|
||||
})
|
||||
}
|
||||
} else if (originalDate) {
|
||||
// Ocorrência ainda virtual: cria exceção + materializa para salvar commitment_services
|
||||
await upsertException({
|
||||
recurrence_id: recurrenceId,
|
||||
tenant_id: clinicId,
|
||||
original_date: originalDate,
|
||||
type: 'reschedule_session',
|
||||
new_date: normalized.inicio_em?.slice(0, 10),
|
||||
recurrence_id: recurrenceId,
|
||||
tenant_id: clinicId,
|
||||
original_date: originalDate,
|
||||
type: 'reschedule_session',
|
||||
new_date: normalized.inicio_em?.slice(0, 10),
|
||||
new_start_time: normalized.inicio_em ? new Date(normalized.inicio_em).toTimeString().slice(0, 8) : null,
|
||||
new_end_time: normalized.fim_em ? new Date(normalized.fim_em).toTimeString().slice(0, 8) : null,
|
||||
modalidade: normalized.modalidade ?? null,
|
||||
titulo_custom: normalized.titulo_custom ?? null,
|
||||
observacoes: normalized.observacoes ?? null,
|
||||
extra_fields: normalized.extra_fields ?? null,
|
||||
modalidade: normalized.modalidade ?? null,
|
||||
titulo_custom: normalized.titulo_custom ?? null,
|
||||
observacoes: normalized.observacoes ?? null,
|
||||
extra_fields: normalized.extra_fields ?? null,
|
||||
})
|
||||
} else if (id) {
|
||||
await update(id, pickDbFields(normalized))
|
||||
if (arg.onSaved) {
|
||||
const { data: existing } = await supabase
|
||||
.from('agenda_eventos').select('id')
|
||||
.eq('recurrence_id', recurrenceId).eq('recurrence_date', originalDate)
|
||||
.maybeSingle()
|
||||
if (existing?.id) {
|
||||
eventId = existing.id
|
||||
} else {
|
||||
const mat = await create({
|
||||
owner_id: normalized.owner_id,
|
||||
tenant_id: clinicId,
|
||||
recurrence_id: recurrenceId,
|
||||
recurrence_date: originalDate,
|
||||
tipo: normalized.tipo,
|
||||
status: normalized.status,
|
||||
inicio_em: normalized.inicio_em,
|
||||
fim_em: normalized.fim_em,
|
||||
titulo: normalized.titulo,
|
||||
patient_id: normalized.patient_id,
|
||||
determined_commitment_id: normalized.determined_commitment_id,
|
||||
modalidade: normalized.modalidade ?? 'presencial',
|
||||
observacoes: normalized.observacoes ?? null,
|
||||
extra_fields: normalized.extra_fields ?? null,
|
||||
})
|
||||
eventId = mat.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Opção C — salvar serviços e marcar esta ocorrência como customizada
|
||||
if (eventId) await arg.onSaved?.(eventId, { markCustomized: true })
|
||||
|
||||
toast.add({ severity: 'success', summary: 'Salvo', detail: 'Sessão atualizada individualmente.', life: 2500 })
|
||||
dialogOpen.value = false
|
||||
await _reloadRange()
|
||||
@@ -1874,6 +2003,15 @@ async function onDialogSave (arg) {
|
||||
observacoes: normalized.observacoes ?? null,
|
||||
extra_fields: normalized.extra_fields ?? null,
|
||||
})
|
||||
|
||||
// Opção C — atualizar template e propagar para a nova sub-série
|
||||
const serviceItemsE = arg.serviceItems
|
||||
if (newRuleId && serviceItemsE?.length) {
|
||||
await saveRuleItems(newRuleId, serviceItemsE)
|
||||
await propagateToSerie(newRuleId, serviceItemsE, { fromDate: originalDate })
|
||||
}
|
||||
if (id) await arg.onSaved?.(id)
|
||||
|
||||
toast.add({ severity: 'success', summary: 'Salvo', detail: 'Esta sessão e as seguintes foram atualizadas.', life: 2500 })
|
||||
dialogOpen.value = false
|
||||
await _reloadRange()
|
||||
@@ -1893,20 +2031,66 @@ async function onDialogSave (arg) {
|
||||
observacoes: normalized.observacoes ?? null,
|
||||
extra_fields: normalized.extra_fields ?? null,
|
||||
})
|
||||
|
||||
// Opção C — atualizar template e propagar para toda a série
|
||||
const serviceItemsF = arg.serviceItems
|
||||
if (recurrenceId && serviceItemsF?.length) {
|
||||
await saveRuleItems(recurrenceId, serviceItemsF)
|
||||
await propagateToSerie(recurrenceId, serviceItemsF)
|
||||
}
|
||||
if (id) await arg.onSaved?.(id)
|
||||
|
||||
toast.add({ severity: 'success', summary: 'Salvo', detail: 'Todas as sessões da série foram atualizadas.', life: 2500 })
|
||||
dialogOpen.value = false
|
||||
await _reloadRange()
|
||||
return
|
||||
}
|
||||
|
||||
// ── CASO G: edição "todos sem exceção" — sobrescreve TUDO incluindo customizadas ──
|
||||
if (recurrenceId && editMode === 'todos_sem_excecao') {
|
||||
const startDate = new Date(normalized.inicio_em)
|
||||
await updateRule(recurrenceId, {
|
||||
weekdays: [startDate.getDay()],
|
||||
start_time: startDate.toTimeString().slice(0, 8),
|
||||
end_time: normalized.fim_em ? new Date(normalized.fim_em).toTimeString().slice(0, 8) : undefined,
|
||||
duration_min: recorrencia?.duracaoMin ?? 50,
|
||||
modalidade: normalized.modalidade ?? 'presencial',
|
||||
titulo_custom: normalized.titulo_custom ?? null,
|
||||
observacoes: normalized.observacoes ?? null,
|
||||
extra_fields: normalized.extra_fields ?? null,
|
||||
})
|
||||
|
||||
// Propaga para todos — incluindo services_customized=true — e reseta o flag
|
||||
const serviceItemsG = arg.serviceItems
|
||||
if (recurrenceId && serviceItemsG?.length) {
|
||||
await saveRuleItems(recurrenceId, serviceItemsG)
|
||||
await propagateToSerie(recurrenceId, serviceItemsG, { ignoreCustomized: true })
|
||||
}
|
||||
|
||||
// Reseta services_customized para false em todos os eventos da série
|
||||
await supabase
|
||||
.from('agenda_eventos')
|
||||
.update({ services_customized: false })
|
||||
.eq('recurrence_id', recurrenceId)
|
||||
|
||||
if (id) await arg.onSaved?.(id)
|
||||
|
||||
toast.add({ severity: 'success', summary: 'Salvo', detail: 'Todas as sessões da série foram atualizadas (sem exceção).', life: 2500 })
|
||||
dialogOpen.value = false
|
||||
await _reloadRange()
|
||||
return
|
||||
}
|
||||
|
||||
// ── CASO A/B: evento avulso ou sessão única ────────────────────────────
|
||||
const dbPayload = pickDbFields(normalized)
|
||||
|
||||
if (id) {
|
||||
await update(id, dbPayload)
|
||||
await arg.onSaved?.(id)
|
||||
toast.add({ severity: 'success', summary: 'Salvo', detail: 'Compromisso atualizado.', life: 2500 })
|
||||
} else {
|
||||
await create(dbPayload)
|
||||
const created = await create(dbPayload)
|
||||
await arg.onSaved?.(created.id)
|
||||
toast.add({ severity: 'success', summary: 'Criado', detail: 'Compromisso criado.', life: 2500 })
|
||||
}
|
||||
|
||||
@@ -2219,6 +2403,15 @@ onMounted(async () => {
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* ── Altura mínima dos slots ───────────────────────────── */
|
||||
.fc-timegrid-slot {
|
||||
height: 14px !important;
|
||||
}
|
||||
.fc-timegrid-slot-label {
|
||||
font-size: 10px !important;
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
/* ── Slot labels customizados ──────────────────────────── */
|
||||
.fc-slot-label-hour {
|
||||
display: inline-block;
|
||||
|
||||
Reference in New Issue
Block a user