Ajuste em Massa - Paciente, Terapeuta, Clinica e Admin - Inicio agenda

This commit is contained in:
Leonardo
2026-02-22 17:56:01 -03:00
parent 6eff67bf22
commit 89b4ecaba1
77 changed files with 9433 additions and 1995 deletions
@@ -0,0 +1,288 @@
<script setup>
import { computed, onMounted, ref } from 'vue'
import Toast from 'primevue/toast'
import { useToast } from 'primevue/usetoast'
import AgendaToolbar from '../components/AgendaToolbar.vue'
import AgendaClinicMosaic from '../components/AgendaClinicMosaic.vue'
import AgendaEventDialog from '../components/AgendaEventDialog.vue'
import { useAgendaEvents } from '../composables/useAgendaEvents.js'
import { useAgendaClinicStaff } from '../composables/useAgendaClinicStaff.js'
import { mapAgendaEventosToCalendarEvents } from '../services/agendaMappers.js'
import { useTenantStore } from '@/stores/tenantStore'
const toast = useToast()
const tenantStore = useTenantStore()
// -------------------- UI state --------------------
const view = ref('day')
const mode = ref('work_hours')
const calendarRef = ref(null)
const dialogOpen = ref(false)
const dialogEventRow = ref(null)
const dialogStartISO = ref('')
const dialogEndISO = ref('')
// guardamos o range atual (para recarregar depois)
const currentRange = ref({ start: null, end: null })
// -------------------- data --------------------
const { loading: loadingStaff, error: staffError, staff, load: loadStaff } = useAgendaClinicStaff()
// ✅ agora já pega também create/update/remove (se você já atualizou o composable)
const {
loading: loadingEvents,
error: eventsError,
rows,
loadClinicRange,
create,
update,
remove
} = useAgendaEvents()
const tenantId = computed(() => {
const t = tenantStore.activeTenantId
if (!t) return null
if (t === 'null' || t === 'undefined') return null
return t
})
function isUuid (v) {
return typeof v === 'string' &&
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(v)
}
const staffCols = computed(() => {
return (staff.value || [])
.filter(s => isUuid(s.user_id))
.map(s => ({
id: s.user_id,
title: s.full_name || s.nome || s.name || s.email || 'Profissional'
}))
})
// ✅ AQUI está o que faltava: ownerIds
const ownerIds = computed(() => staffCols.value.map(p => p.id))
const allEvents = computed(() => mapAgendaEventosToCalendarEvents(rows.value || []))
const ownerOptions = computed(() =>
staffCols.value.map(p => ({ label: p.title, value: p.id }))
)
// -------------------- lifecycle --------------------
onMounted(async () => {
if (!tenantId.value) {
toast.add({ severity: 'warn', summary: 'Clínica', detail: 'Nenhum tenant ativo.', life: 4500 })
return
}
await loadStaff(tenantId.value)
if (staffError.value) {
toast.add({ severity: 'warn', summary: 'Profissionais', detail: staffError.value, life: 4500 })
}
})
// -------------------- toolbar actions --------------------
function onToday () { calendarRef.value?.goToday?.() }
function onPrev () { calendarRef.value?.prev?.() }
function onNext () { calendarRef.value?.next?.() }
function onChangeView (v) {
view.value = v
calendarRef.value?.setView?.(v)
}
function onToggleMode (m) {
mode.value = m
}
// -------------------- calendar callbacks --------------------
async function onRangeChange ({ start, end }) {
currentRange.value = { start, end }
const ids = ownerIds.value
if (!ids.length) return
await loadClinicRange(ids, new Date(start).toISOString(), new Date(end).toISOString())
if (eventsError.value) {
toast.add({ severity: 'warn', summary: 'Eventos', detail: eventsError.value, life: 4500 })
}
}
function onEventClick (info) {
const ev = info?.event
if (!ev) return
dialogEventRow.value = {
id: ev.id,
owner_id: ev.extendedProps?.owner_id,
terapeuta_id: ev.extendedProps?.terapeuta_id ?? null,
paciente_id: ev.extendedProps?.paciente_id ?? null,
tipo: ev.extendedProps?.tipo,
status: ev.extendedProps?.status,
titulo: ev.title,
observacoes: ev.extendedProps?.observacoes ?? null,
inicio_em: ev.start?.toISOString?.() || ev.startStr,
fim_em: ev.end?.toISOString?.() || ev.endStr
}
dialogStartISO.value = ''
dialogEndISO.value = ''
dialogOpen.value = true
}
async function persistMoveOrResize (info, actionLabel) {
try {
const ev = info?.event
if (!ev) return
const id = ev.id
const startISO = ev.start ? ev.start.toISOString() : null
const endISO = ev.end ? ev.end.toISOString() : null
if (!startISO || !endISO) throw new Error('Evento sem start/end.')
await update(id, { inicio_em: startISO, fim_em: endISO })
toast.add({
severity: 'success',
summary: actionLabel,
detail: 'Alteração salva.',
life: 1800
})
} catch (e) {
// desfaz no calendário
info?.revert?.()
toast.add({
severity: 'warn',
summary: 'Erro',
detail: eventsError.value || e?.message || 'Falha ao salvar alteração.',
life: 4500
})
}
}
function onEventDrop (info) {
persistMoveOrResize(info, 'Movido')
}
function onEventResize (info) {
persistMoveOrResize(info, 'Redimensionado')
}
// -------------------- dialog actions (mínimo funcional) --------------------
function onCreateClinicEvent () {
// cria evento base (depois você troca para "selecionar no calendário", mas aqui é bom pra começar)
const start = new Date()
const end = new Date(Date.now() + 50 * 60000)
dialogEventRow.value = null
dialogStartISO.value = start.toISOString()
dialogEndISO.value = end.toISOString()
dialogOpen.value = true
}
async function onDialogSave ({ id, payload }) {
try {
if (id) await update(id, payload)
else await create(payload)
dialogOpen.value = false
// recarrega range atual se existir
if (currentRange.value.start && currentRange.value.end) {
await loadClinicRange(
ownerIds.value,
new Date(currentRange.value.start).toISOString(),
new Date(currentRange.value.end).toISOString()
)
}
toast.add({ severity: 'success', summary: 'Salvo', detail: 'Evento salvo.', life: 2500 })
} catch (e) {
toast.add({ severity: 'warn', summary: 'Erro', detail: eventsError.value || 'Falha ao salvar.', life: 4500 })
}
}
async function onDialogDelete (id) {
try {
await remove(id)
dialogOpen.value = false
if (currentRange.value.start && currentRange.value.end) {
await loadClinicRange(
ownerIds.value,
new Date(currentRange.value.start).toISOString(),
new Date(currentRange.value.end).toISOString()
)
}
toast.add({ severity: 'success', summary: 'Excluído', detail: 'Evento removido.', life: 2500 })
} catch (e) {
toast.add({ severity: 'warn', summary: 'Erro', detail: eventsError.value || 'Falha ao excluir.', life: 4500 })
}
}
function onSlotSelect ({ ownerId, start, end }) {
dialogEventRow.value = null
dialogStartISO.value = new Date(start).toISOString()
dialogEndISO.value = new Date(end).toISOString()
// aqui você pode setar o owner default do dialog via ownerId
// o Dialog já tem dropdown, mas você pode passar ownerId no payload quando salvar
dialogOpen.value = true
// opcional: guardar pra preselecionar no dialog (se você implementar isso)
// dialogOwnerId.value = ownerId
}
</script>
<template>
<div class="p-4 md:p-6">
<Toast />
<AgendaToolbar
title="Agenda da clínica"
:view="view"
:mode="mode"
@today="onToday"
@prev="onPrev"
@next="onNext"
@changeView="onChangeView"
@toggleMode="onToggleMode"
@createSession="onCreateClinicEvent"
@createBlock="() => toast.add({ severity: 'info', summary: 'Bloqueio', detail: 'Próximo passo: bloqueio da clínica.', life: 2500 })"
/>
<AgendaClinicMosaic
ref="calendarRef"
:view="view"
:mode="mode"
:staff="staffCols"
:events="allEvents"
:loading="loadingStaff || loadingEvents"
@rangeChange="onRangeChange"
@slotSelect="onSlotSelect"
@eventClick="onEventClick"
@eventDrop="onEventDrop"
@eventResize="onEventResize"
/>
<AgendaEventDialog
v-model="dialogOpen"
:eventRow="dialogEventRow"
:initialStartISO="dialogStartISO"
:initialEndISO="dialogEndISO"
:ownerId="staffCols?.[0]?.id || ''"
:allowOwnerEdit="true"
:ownerOptions="ownerOptions"
@save="onDialogSave"
@delete="onDialogDelete"
/>
</div>
</template>