Files
agenciapsilmno/src/features/agenda/components/AgendaCalendar.vue

209 lines
5.0 KiB
Vue

<!-- src/features/agenda/components/AgendaCalendar.vue -->
<script setup>
import { computed, ref, watch, onMounted } from 'vue'
import FullCalendar from '@fullcalendar/vue3'
import timeGridPlugin from '@fullcalendar/timegrid'
import interactionPlugin from '@fullcalendar/interaction'
import dayGridPlugin from '@fullcalendar/daygrid'
import ProgressSpinner from 'primevue/progressspinner'
const props = defineProps({
// UI
view: { type: String, default: 'day' }, // 'day' | 'week'
mode: { type: String, default: 'work_hours' }, // 'full_24h' | 'work_hours'
// calendar behavior
timezone: { type: String, default: 'America/Sao_Paulo' },
slotDuration: { type: String, default: '00:30:00' },
slotMinTime: { type: String, default: '06:00:00' },
slotMaxTime: { type: String, default: '22:00:00' },
businessHours: { type: [Array, Object], default: () => [] },
// data
events: { type: Array, default: () => [] },
loading: { type: Boolean, default: false }
})
const emit = defineEmits([
'rangeChange',
'selectTime',
'eventClick',
'eventDrop',
'eventResize'
])
const fcRef = ref(null)
const initialView = computed(() => (props.view === 'week' ? 'timeGridWeek' : 'timeGridDay'))
function getApi () {
const inst = fcRef.value
return inst?.getApi?.() || null
}
function emitRange () {
const api = getApi()
if (!api) return
const v = api.view
emit('rangeChange', { start: v.activeStart, end: v.activeEnd })
}
// -----------------------------
// Calendar options
// -----------------------------
const calendarOptions = computed(() => {
const isWorkHours = props.mode !== 'full_24h'
// No modo 24h, não recorta — mas mantemos min/max caso você queira ainda controlar
const minTime = isWorkHours ? props.slotMinTime : '00:00:00'
// FullCalendar timeGrid costuma aceitar 24:00:00, mas 23:59:59 evita edge-case
const maxTime = isWorkHours ? props.slotMaxTime : '23:59:59'
return {
plugins: [timeGridPlugin, interactionPlugin, dayGridPlugin],
initialView: initialView.value,
timeZone: props.timezone,
// Header desativado (você controla no Toolbar)
headerToolbar: false,
// Visão "produto": blocos com linhas suaves
nowIndicator: true,
allDaySlot: false,
expandRows: true,
height: 'auto',
// Seleção / DnD / Resize
selectable: true,
selectMirror: true,
editable: true,
eventStartEditable: true,
eventDurationEditable: true,
// Intervalos visuais
slotDuration: props.slotDuration,
slotMinTime: minTime,
slotMaxTime: maxTime,
slotLabelFormat: {
hour: '2-digit',
minute: '2-digit',
hour12: false
},
// Horário "verdadeiro" de funcionamento (se você usar)
businessHours: props.businessHours,
// Dados
events: props.events,
// Melhor UX
weekends: true,
firstDay: 1, // segunda
// Callbacks
datesSet: () => {
// dispara quando muda o intervalo exibido (prev/next/today/view)
emitRange()
},
select: (selection) => {
// selection: { start, end, allDay, ... }
emit('selectTime', selection)
},
eventClick: (info) => emit('eventClick', info),
eventDrop: (info) => emit('eventDrop', info),
eventResize: (info) => emit('eventResize', info)
}
})
// -----------------------------
// Exposed methods (para Toolbar/Page)
// -----------------------------
function goToday () { getApi()?.today?.() }
function prev () { getApi()?.prev?.() }
function next () { getApi()?.next?.() }
function setView (v) {
const api = getApi()
if (!api) return
api.changeView(v === 'week' ? 'timeGridWeek' : 'timeGridDay')
}
defineExpose({ goToday, prev, next, setView })
// Se a prop view mudar, sincroniza
watch(
() => props.view,
(v) => setView(v)
)
// Emite o range inicial assim que montar
onMounted(() => {
// garante que o FullCalendar já criou a view
setTimeout(() => emitRange(), 0)
})
</script>
<template>
<div class="agenda-calendar-wrap">
<div v-if="loading" class="agenda-calendar-loading">
<ProgressSpinner strokeWidth="3" />
<div class="text-sm mt-2" style="color: var(--text-color-secondary);">
Carregando agenda
</div>
</div>
<FullCalendar
ref="fcRef"
:options="calendarOptions"
/>
</div>
</template>
<style scoped>
.agenda-calendar-wrap{
position: relative;
width: 100%;
}
.agenda-calendar-loading{
position: absolute;
inset: 0;
z-index: 20;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
backdrop-filter: blur(3px);
background: color-mix(in srgb, var(--surface-card) 85%, transparent);
border-radius: 16px;
}
/* Deixa o calendário "respirar" dentro de cards/layouts */
:deep(.fc){
font-size: 0.95rem;
}
:deep(.fc .fc-timegrid-slot){
height: 2.2rem;
}
:deep(.fc .fc-timegrid-now-indicator-line){
opacity: .75;
}
:deep(.fc .fc-scrollgrid){
border-radius: 16px;
overflow: hidden;
border: 1px solid var(--surface-border);
}
:deep(.fc .fc-timegrid-axis-cushion),
:deep(.fc .fc-timegrid-slot-label-cushion){
color: var(--text-color-secondary);
}
</style>