first commit
This commit is contained in:
327
resources/js/views/apps/calendar/CalendarEventHandler.vue
Normal file
327
resources/js/views/apps/calendar/CalendarEventHandler.vue
Normal file
@@ -0,0 +1,327 @@
|
||||
<script setup>
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
|
||||
import { VForm } from 'vuetify/components/VForm'
|
||||
import { useCalendarStore } from './useCalendarStore'
|
||||
import avatar1 from '@images/avatars/avatar-1.png'
|
||||
import avatar2 from '@images/avatars/avatar-2.png'
|
||||
import avatar3 from '@images/avatars/avatar-3.png'
|
||||
import avatar5 from '@images/avatars/avatar-5.png'
|
||||
import avatar6 from '@images/avatars/avatar-6.png'
|
||||
import avatar7 from '@images/avatars/avatar-7.png'
|
||||
|
||||
// 👉 store
|
||||
const props = defineProps({
|
||||
isDrawerOpen: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
event: {
|
||||
type: null,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:isDrawerOpen',
|
||||
'addEvent',
|
||||
'updateEvent',
|
||||
'removeEvent',
|
||||
])
|
||||
|
||||
const store = useCalendarStore()
|
||||
const refForm = ref()
|
||||
|
||||
// 👉 Event
|
||||
const event = ref(JSON.parse(JSON.stringify(props.event)))
|
||||
|
||||
const resetEvent = () => {
|
||||
event.value = JSON.parse(JSON.stringify(props.event))
|
||||
nextTick(() => {
|
||||
refForm.value?.resetValidation()
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => props.isDrawerOpen, resetEvent)
|
||||
|
||||
const removeEvent = () => {
|
||||
emit('removeEvent', String(event.value.id))
|
||||
|
||||
// Close drawer
|
||||
emit('update:isDrawerOpen', false)
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
refForm.value?.validate().then(({ valid }) => {
|
||||
if (valid) {
|
||||
|
||||
// If id exist on id => Update event
|
||||
if ('id' in event.value)
|
||||
emit('updateEvent', event.value)
|
||||
|
||||
// Else => add new event
|
||||
else
|
||||
emit('addEvent', event.value)
|
||||
|
||||
// Close drawer
|
||||
emit('update:isDrawerOpen', false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const guestsOptions = [
|
||||
{
|
||||
avatar: avatar1,
|
||||
name: 'Jane Foster',
|
||||
},
|
||||
{
|
||||
avatar: avatar3,
|
||||
name: 'Donna Frank',
|
||||
},
|
||||
{
|
||||
avatar: avatar5,
|
||||
name: 'Gabrielle Robertson',
|
||||
},
|
||||
{
|
||||
avatar: avatar7,
|
||||
name: 'Lori Spears',
|
||||
},
|
||||
{
|
||||
avatar: avatar6,
|
||||
name: 'Sandy Vega',
|
||||
},
|
||||
{
|
||||
avatar: avatar2,
|
||||
name: 'Cheryl May',
|
||||
},
|
||||
]
|
||||
|
||||
// 👉 Form
|
||||
const onCancel = () => {
|
||||
|
||||
// Close drawer
|
||||
emit('update:isDrawerOpen', false)
|
||||
nextTick(() => {
|
||||
refForm.value?.reset()
|
||||
resetEvent()
|
||||
refForm.value?.resetValidation()
|
||||
})
|
||||
}
|
||||
|
||||
const startDateTimePickerConfig = computed(() => {
|
||||
const config = {
|
||||
enableTime: !event.value.allDay,
|
||||
dateFormat: `Y-m-d${ event.value.allDay ? '' : ' H:i' }`,
|
||||
}
|
||||
|
||||
if (event.value.end)
|
||||
config.maxDate = event.value.end
|
||||
|
||||
return config
|
||||
})
|
||||
|
||||
const endDateTimePickerConfig = computed(() => {
|
||||
const config = {
|
||||
enableTime: !event.value.allDay,
|
||||
dateFormat: `Y-m-d${ event.value.allDay ? '' : ' H:i' }`,
|
||||
}
|
||||
|
||||
if (event.value.start)
|
||||
config.minDate = event.value.start
|
||||
|
||||
return config
|
||||
})
|
||||
|
||||
const dialogModelValueUpdate = val => {
|
||||
emit('update:isDrawerOpen', val)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VNavigationDrawer
|
||||
temporary
|
||||
location="end"
|
||||
:model-value="props.isDrawerOpen"
|
||||
width="420"
|
||||
class="scrollable-content"
|
||||
@update:model-value="dialogModelValueUpdate"
|
||||
>
|
||||
<!-- 👉 Header -->
|
||||
<AppDrawerHeaderSection
|
||||
:title="event.id ? 'Update Event' : 'Add Event'"
|
||||
@cancel="$emit('update:isDrawerOpen', false)"
|
||||
>
|
||||
<template #beforeClose>
|
||||
<IconBtn
|
||||
v-show="event.id"
|
||||
@click="removeEvent"
|
||||
>
|
||||
<VIcon
|
||||
size="18"
|
||||
icon="ri-delete-bin-7-line"
|
||||
/>
|
||||
</IconBtn>
|
||||
</template>
|
||||
</AppDrawerHeaderSection>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<PerfectScrollbar :options="{ wheelPropagation: false }">
|
||||
<VCard flat>
|
||||
<VCardText>
|
||||
<!-- SECTION Form -->
|
||||
<VForm
|
||||
ref="refForm"
|
||||
@submit.prevent="handleSubmit"
|
||||
>
|
||||
<VRow>
|
||||
<!-- 👉 Title -->
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="event.title"
|
||||
label="Title"
|
||||
placeholder="Meeting with Jane"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Calendar -->
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
v-model="event.extendedProps.calendar"
|
||||
label="Label"
|
||||
placeholder="Select Event Label"
|
||||
:rules="[requiredValidator]"
|
||||
:items="store.availableCalendars"
|
||||
:item-title="item => item.label"
|
||||
:item-value="item => item.label"
|
||||
>
|
||||
<template #selection="{ item }">
|
||||
<div
|
||||
v-show="event.extendedProps.calendar"
|
||||
class="align-center"
|
||||
:class="event.extendedProps.calendar ? 'd-flex' : ''"
|
||||
>
|
||||
<VIcon
|
||||
size="8"
|
||||
icon="ri-circle-fill"
|
||||
:color="item.raw.color"
|
||||
class="me-2"
|
||||
/>
|
||||
<span>{{ item.raw.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item="{ item, props: itemProps }">
|
||||
<VListItem v-bind="itemProps">
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
size="8"
|
||||
icon="ri-circle-fill"
|
||||
:color="item.raw.color"
|
||||
/>
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
</VSelect>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Start date -->
|
||||
<VCol cols="12">
|
||||
<AppDateTimePicker
|
||||
:key="JSON.stringify(startDateTimePickerConfig)"
|
||||
v-model="event.start"
|
||||
:rules="[requiredValidator]"
|
||||
label="Start date"
|
||||
placeholder="Select Date"
|
||||
:config="startDateTimePickerConfig"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 End date -->
|
||||
<VCol cols="12">
|
||||
<AppDateTimePicker
|
||||
:key="JSON.stringify(endDateTimePickerConfig)"
|
||||
v-model="event.end"
|
||||
:rules="[requiredValidator]"
|
||||
label="End date"
|
||||
placeholder="Select End Date"
|
||||
:config="endDateTimePickerConfig"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 All day -->
|
||||
<VCol cols="12">
|
||||
<VSwitch
|
||||
v-model="event.allDay"
|
||||
label="All day"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Event URL -->
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="event.url"
|
||||
label="Event URL"
|
||||
placeholder="https://event.com/meeting"
|
||||
:rules="[urlValidator]"
|
||||
type="url"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Guests -->
|
||||
<VCol cols="12">
|
||||
<VSelect
|
||||
v-model="event.extendedProps.guests"
|
||||
label="Guests"
|
||||
placeholder="Select guests"
|
||||
:items="guestsOptions"
|
||||
:item-title="item => item.name"
|
||||
:item-value="item => item.name"
|
||||
chips
|
||||
multiple
|
||||
eager
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Location -->
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="event.extendedProps.location"
|
||||
label="Location"
|
||||
placeholder="Meeting room"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Description -->
|
||||
<VCol cols="12">
|
||||
<VTextarea
|
||||
v-model="event.extendedProps.description"
|
||||
label="Description"
|
||||
placeholder="Meeting description"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- 👉 Form buttons -->
|
||||
<VCol cols="12">
|
||||
<VBtn
|
||||
type="submit"
|
||||
class="me-3"
|
||||
>
|
||||
Submit
|
||||
</VBtn>
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
@click="onCancel"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
<!-- !SECTION -->
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</PerfectScrollbar>
|
||||
</VNavigationDrawer>
|
||||
</template>
|
1
resources/js/views/apps/calendar/types.js
Normal file
1
resources/js/views/apps/calendar/types.js
Normal file
@@ -0,0 +1 @@
|
||||
export {}
|
295
resources/js/views/apps/calendar/useCalendar.js
Normal file
295
resources/js/views/apps/calendar/useCalendar.js
Normal file
@@ -0,0 +1,295 @@
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||
import interactionPlugin from '@fullcalendar/interaction'
|
||||
import listPlugin from '@fullcalendar/list'
|
||||
import timeGridPlugin from '@fullcalendar/timegrid'
|
||||
import { useConfigStore } from '@core/stores/config'
|
||||
import { useCalendarStore } from '@/views/apps/calendar/useCalendarStore'
|
||||
|
||||
export const blankEvent = {
|
||||
title: '',
|
||||
start: '',
|
||||
end: '',
|
||||
allDay: false,
|
||||
url: '',
|
||||
extendedProps: {
|
||||
/*
|
||||
ℹ️ We have to use undefined here because if we have blank string as value then select placeholder will be active (moved to top).
|
||||
Hence, we need to set it to undefined or null
|
||||
*/
|
||||
calendar: undefined,
|
||||
guests: [],
|
||||
location: '',
|
||||
description: '',
|
||||
},
|
||||
}
|
||||
export const useCalendar = (event, isEventHandlerSidebarActive, isLeftSidebarOpen) => {
|
||||
const configStore = useConfigStore()
|
||||
|
||||
// 👉 Store
|
||||
const store = useCalendarStore()
|
||||
|
||||
// 👉 Calendar template ref
|
||||
const refCalendar = ref()
|
||||
|
||||
|
||||
// 👉 Calendar colors
|
||||
const calendarsColor = {
|
||||
Business: 'primary',
|
||||
Holiday: 'success',
|
||||
Personal: 'error',
|
||||
Family: 'warning',
|
||||
ETC: 'info',
|
||||
}
|
||||
|
||||
|
||||
// ℹ️ Extract event data from event API
|
||||
const extractEventDataFromEventApi = eventApi => {
|
||||
const { id, title, start, end, url, extendedProps: { calendar, guests, location, description }, allDay } = eventApi
|
||||
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
start,
|
||||
end,
|
||||
url,
|
||||
extendedProps: {
|
||||
calendar,
|
||||
guests,
|
||||
location,
|
||||
description,
|
||||
},
|
||||
allDay,
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof process !== 'undefined' && process.server)
|
||||
store.fetchEvents()
|
||||
|
||||
|
||||
// 👉 Fetch events
|
||||
const fetchEvents = (info, successCallback) => {
|
||||
// If there's no info => Don't make useless API call
|
||||
if (!info)
|
||||
return
|
||||
store.fetchEvents()
|
||||
.then(r => {
|
||||
successCallback(r.map(e => ({
|
||||
...e,
|
||||
|
||||
// Convert string representation of date to Date object
|
||||
start: new Date(e.start),
|
||||
end: new Date(e.end),
|
||||
})))
|
||||
})
|
||||
.catch(e => {
|
||||
console.error('Error occurred while fetching calendar events', e)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 👉 Calendar API
|
||||
const calendarApi = ref(null)
|
||||
|
||||
|
||||
// 👉 Update event in calendar [UI]
|
||||
const updateEventInCalendar = (updatedEventData, propsToUpdate, extendedPropsToUpdate) => {
|
||||
const existingEvent = calendarApi.value?.getEventById(String(updatedEventData.id))
|
||||
if (!existingEvent) {
|
||||
console.warn('Can\'t found event in calendar to update')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ---Set event properties except date related
|
||||
// Docs: https://fullcalendar.io/docs/Event-setProp
|
||||
// dateRelatedProps => ['start', 'end', 'allDay']
|
||||
for (let index = 0; index < propsToUpdate.length; index++) {
|
||||
const propName = propsToUpdate[index]
|
||||
|
||||
existingEvent.setProp(propName, updatedEventData[propName])
|
||||
}
|
||||
|
||||
// --- Set date related props
|
||||
// ? Docs: https://fullcalendar.io/docs/Event-setDates
|
||||
existingEvent.setDates(updatedEventData.start, updatedEventData.end, { allDay: updatedEventData.allDay })
|
||||
|
||||
// --- Set event's extendedProps
|
||||
// ? Docs: https://fullcalendar.io/docs/Event-setExtendedProp
|
||||
for (let index = 0; index < extendedPropsToUpdate.length; index++) {
|
||||
const propName = extendedPropsToUpdate[index]
|
||||
|
||||
existingEvent.setExtendedProp(propName, updatedEventData.extendedProps[propName])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 👉 Remove event in calendar [UI]
|
||||
const removeEventInCalendar = eventId => {
|
||||
const _event = calendarApi.value?.getEventById(eventId)
|
||||
if (_event)
|
||||
_event.remove()
|
||||
}
|
||||
|
||||
|
||||
// 👉 refetch events
|
||||
const refetchEvents = () => {
|
||||
calendarApi.value?.refetchEvents()
|
||||
}
|
||||
|
||||
watch(() => store.selectedCalendars, refetchEvents)
|
||||
|
||||
|
||||
// 👉 Add event
|
||||
const addEvent = _event => {
|
||||
store.addEvent(_event)
|
||||
.then(() => {
|
||||
refetchEvents()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 👉 Update event
|
||||
const updateEvent = _event => {
|
||||
store.updateEvent(_event)
|
||||
.then(r => {
|
||||
const propsToUpdate = ['id', 'title', 'url']
|
||||
const extendedPropsToUpdate = ['calendar', 'guests', 'location', 'description']
|
||||
|
||||
updateEventInCalendar(r, propsToUpdate, extendedPropsToUpdate)
|
||||
})
|
||||
refetchEvents()
|
||||
}
|
||||
|
||||
|
||||
// 👉 Remove event
|
||||
const removeEvent = eventId => {
|
||||
store.removeEvent(eventId).then(() => {
|
||||
removeEventInCalendar(eventId)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 👉 Calendar options
|
||||
const calendarOptions = {
|
||||
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin, listPlugin],
|
||||
initialView: 'dayGridMonth',
|
||||
headerToolbar: {
|
||||
start: 'drawerToggler,prev,next title',
|
||||
end: 'dayGridMonth,timeGridWeek,timeGridDay,listMonth',
|
||||
},
|
||||
events: fetchEvents,
|
||||
|
||||
// ❗ We need this to be true because when its false and event is allDay event and end date is same as start data then Full calendar will set end to null
|
||||
forceEventDuration: true,
|
||||
|
||||
/*
|
||||
Enable dragging and resizing event
|
||||
Docs: https://fullcalendar.io/docs/editable
|
||||
*/
|
||||
editable: true,
|
||||
|
||||
/*
|
||||
Enable resizing event from start
|
||||
Docs: https://fullcalendar.io/docs/eventResizableFromStart
|
||||
*/
|
||||
eventResizableFromStart: true,
|
||||
|
||||
/*
|
||||
Automatically scroll the scroll-containers during event drag-and-drop and date selecting
|
||||
Docs: https://fullcalendar.io/docs/dragScroll
|
||||
*/
|
||||
dragScroll: true,
|
||||
|
||||
/*
|
||||
Max number of events within a given day
|
||||
Docs: https://fullcalendar.io/docs/dayMaxEvents
|
||||
*/
|
||||
dayMaxEvents: 2,
|
||||
|
||||
/*
|
||||
Determines if day names and week names are clickable
|
||||
Docs: https://fullcalendar.io/docs/navLinks
|
||||
*/
|
||||
navLinks: true,
|
||||
eventClassNames({ event: calendarEvent }) {
|
||||
const colorName = calendarsColor[calendarEvent._def.extendedProps.calendar]
|
||||
|
||||
return [
|
||||
// Background Color
|
||||
`bg-light-${colorName} text-${colorName}`,
|
||||
]
|
||||
},
|
||||
eventClick({ event: clickedEvent, jsEvent }) {
|
||||
// Prevent the default action
|
||||
jsEvent.preventDefault()
|
||||
if (clickedEvent.url) {
|
||||
// Open the URL in a new tab
|
||||
window.open(clickedEvent.url, '_blank')
|
||||
}
|
||||
|
||||
// * Only grab required field otherwise it goes in infinity loop
|
||||
// ! Always grab all fields rendered by form (even if it get `undefined`) otherwise due to Vue3/Composition API you might get: "object is not extensible"
|
||||
event.value = extractEventDataFromEventApi(clickedEvent)
|
||||
isEventHandlerSidebarActive.value = true
|
||||
},
|
||||
|
||||
// customButtons
|
||||
dateClick(info) {
|
||||
event.value = { ...event.value, start: info.date }
|
||||
isEventHandlerSidebarActive.value = true
|
||||
},
|
||||
|
||||
/*
|
||||
Handle event drop (Also include dragged event)
|
||||
Docs: https://fullcalendar.io/docs/eventDrop
|
||||
We can use `eventDragStop` but it doesn't return updated event so we have to use `eventDrop` which returns updated event
|
||||
*/
|
||||
eventDrop({ event: droppedEvent }) {
|
||||
updateEvent(extractEventDataFromEventApi(droppedEvent))
|
||||
},
|
||||
|
||||
/*
|
||||
Handle event resize
|
||||
Docs: https://fullcalendar.io/docs/eventResize
|
||||
*/
|
||||
eventResize({ event: resizedEvent }) {
|
||||
if (resizedEvent.start && resizedEvent.end)
|
||||
updateEvent(extractEventDataFromEventApi(resizedEvent))
|
||||
},
|
||||
customButtons: {
|
||||
drawerToggler: {
|
||||
text: 'calendarDrawerToggler',
|
||||
click() {
|
||||
isLeftSidebarOpen.value = true
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
// 👉 onMounted
|
||||
onMounted(() => {
|
||||
calendarApi.value = refCalendar.value.getApi()
|
||||
})
|
||||
|
||||
|
||||
// 👉 Jump to date on sidebar(inline) calendar change
|
||||
const jumpToDate = currentDate => {
|
||||
calendarApi.value?.gotoDate(new Date(currentDate))
|
||||
}
|
||||
|
||||
watch(() => configStore.isAppRTL, val => {
|
||||
calendarApi.value?.setOption('direction', val ? 'rtl' : 'ltr')
|
||||
}, { immediate: true })
|
||||
|
||||
return {
|
||||
refCalendar,
|
||||
calendarOptions,
|
||||
refetchEvents,
|
||||
fetchEvents,
|
||||
addEvent,
|
||||
updateEvent,
|
||||
removeEvent,
|
||||
jumpToDate,
|
||||
}
|
||||
}
|
59
resources/js/views/apps/calendar/useCalendarStore.js
Normal file
59
resources/js/views/apps/calendar/useCalendarStore.js
Normal file
@@ -0,0 +1,59 @@
|
||||
export const useCalendarStore = defineStore('calendar', {
|
||||
// arrow function recommended for full type inference
|
||||
state: () => ({
|
||||
availableCalendars: [
|
||||
{
|
||||
color: 'error',
|
||||
label: 'Personal',
|
||||
},
|
||||
{
|
||||
color: 'primary',
|
||||
label: 'Business',
|
||||
},
|
||||
{
|
||||
color: 'warning',
|
||||
label: 'Family',
|
||||
},
|
||||
{
|
||||
color: 'success',
|
||||
label: 'Holiday',
|
||||
},
|
||||
{
|
||||
color: 'info',
|
||||
label: 'ETC',
|
||||
},
|
||||
],
|
||||
selectedCalendars: ['Personal', 'Business', 'Family', 'Holiday', 'ETC'],
|
||||
}),
|
||||
actions: {
|
||||
async fetchEvents() {
|
||||
const { data, error } = await useApi(createUrl('/apps/calendar', {
|
||||
query: {
|
||||
calendars: this.selectedCalendars,
|
||||
},
|
||||
}))
|
||||
|
||||
if (error.value)
|
||||
return error.value
|
||||
|
||||
return data.value
|
||||
},
|
||||
async addEvent(event) {
|
||||
await $api('/apps/calendar', {
|
||||
method: 'POST',
|
||||
body: event,
|
||||
})
|
||||
},
|
||||
async updateEvent(event) {
|
||||
return await $api(`/apps/calendar/${event.id}`, {
|
||||
method: 'PUT',
|
||||
body: event,
|
||||
})
|
||||
},
|
||||
async removeEvent(eventId) {
|
||||
return await $api(`/apps/calendar/${eventId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
Reference in New Issue
Block a user