Ajuste em Massa - Paciente, Terapeuta, Clinica e Admin - Inicio agenda
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user