Initial commit

This commit is contained in:
2025-08-04 16:33:07 +03:30
commit f798e8e35c
9595 changed files with 1208683 additions and 0 deletions

View File

@@ -0,0 +1,341 @@
<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
data-allow-mismatch
temporary
location="end"
:model-value="props.isDrawerOpen"
width="370"
:border="0"
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="tabler-trash"
/>
</IconBtn>
</template>
</AppDrawerHeaderSection>
<VDivider />
<PerfectScrollbar :options="{ wheelPropagation: false }">
<VCard flat>
<VCardText>
<!-- SECTION Form -->
<VForm
ref="refForm"
@submit.prevent="handleSubmit"
>
<VRow>
<!-- 👉 Title -->
<VCol cols="12">
<AppTextField
id="event-title"
v-model="event.title"
label="Title"
placeholder="Meeting with Jane"
:rules="[requiredValidator]"
/>
</VCol>
<!-- 👉 Calendar -->
<VCol cols="12">
<AppSelect
id="event-label"
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
:color="item.raw.color"
icon="tabler-circle-filled"
size="8"
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="tabler-circle-filled"
:color="item.raw.color"
/>
</template>
</VListItem>
</template>
</AppSelect>
</VCol>
<!-- 👉 Start date -->
<VCol cols="12">
<AppDateTimePicker
id="event-start-date"
: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
id="event-end-date"
: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
id="event-all-day"
v-model="event.allDay"
label="All day"
/>
</VCol>
<!-- 👉 Event URL -->
<VCol cols="12">
<AppTextField
id="event-url"
v-model="event.url"
label="Event URL"
placeholder="https://event.com/meeting"
:rules="[urlValidator]"
type="url"
/>
</VCol>
<!-- 👉 Guests -->
<VCol cols="12">
<AppSelect
id="event-guests"
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">
<AppTextField
id="event-location"
v-model="event.extendedProps.location"
label="Location"
placeholder="Meeting room"
/>
</VCol>
<!-- 👉 Description -->
<VCol cols="12">
<AppTextarea
id="event-description"
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>

View File

@@ -0,0 +1 @@
export {}

View File

@@ -0,0 +1,302 @@
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) => {
calendarApi.value = refCalendar.value.getApi()
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 => {
// Making API call using $api('', { method: ... })
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(() => {
nextTick(() => {
if (refCalendar.value)
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,
}
}

View 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',
})
},
},
})