fix: added attendee, color, link in event modal and fixed existing bug
This commit is contained in:
parent
52d99ebf20
commit
8031964d3d
@ -25,8 +25,7 @@
|
|||||||
<EventModal
|
<EventModal
|
||||||
v-if="showEventModal"
|
v-if="showEventModal"
|
||||||
v-model="showEventModal"
|
v-model="showEventModal"
|
||||||
v-model:events="events"
|
:event="activeEvent"
|
||||||
:event="event"
|
|
||||||
:doctype="doctype"
|
:doctype="doctype"
|
||||||
:docname="doc?.name"
|
:docname="doc?.name"
|
||||||
/>
|
/>
|
||||||
@ -36,6 +35,7 @@ import TaskModal from '@/components/Modals/TaskModal.vue'
|
|||||||
import NoteModal from '@/components/Modals/NoteModal.vue'
|
import NoteModal from '@/components/Modals/NoteModal.vue'
|
||||||
import CallLogModal from '@/components/Modals/CallLogModal.vue'
|
import CallLogModal from '@/components/Modals/CallLogModal.vue'
|
||||||
import EventModal from '@/components/Modals/EventModal.vue'
|
import EventModal from '@/components/Modals/EventModal.vue'
|
||||||
|
import { showEventModal, activeEvent } from '@/composables/event'
|
||||||
import { call } from 'frappe-ui'
|
import { call } from 'frappe-ui'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
@ -46,22 +46,11 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const activities = defineModel()
|
const activities = defineModel()
|
||||||
const events = defineModel('events')
|
|
||||||
|
|
||||||
const showEventModal = ref(false)
|
|
||||||
const event = ref({})
|
|
||||||
|
|
||||||
|
// Event
|
||||||
function showEvent(e) {
|
function showEvent(e) {
|
||||||
event.value = e || {
|
|
||||||
subject: '',
|
|
||||||
description: '',
|
|
||||||
starts_on: '',
|
|
||||||
ends_on: '',
|
|
||||||
all_day: false,
|
|
||||||
event_type: 'Public',
|
|
||||||
color: 'green',
|
|
||||||
}
|
|
||||||
showEventModal.value = true
|
showEventModal.value = true
|
||||||
|
activeEvent.value = e
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tasks
|
// Tasks
|
||||||
|
|||||||
@ -58,31 +58,25 @@
|
|||||||
<div
|
<div
|
||||||
class="flex justify-between gap-2 items-center text-ink-gray-6"
|
class="flex justify-between gap-2 items-center text-ink-gray-6"
|
||||||
>
|
>
|
||||||
<div>{{ formattedDateTime(event) }}</div>
|
<div>
|
||||||
<div>{{ formattedDate(event) }}</div>
|
{{
|
||||||
|
startEndTime(event.starts_on, event.ends_on, event.all_day)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div>{{ startDate(event.starts_on) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<EventModal
|
|
||||||
v-if="showEventModal"
|
|
||||||
v-model="showEventModal"
|
|
||||||
v-model:events="eventsResource"
|
|
||||||
:event="event"
|
|
||||||
:doctype="doctype"
|
|
||||||
:docname="docname"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import CalendarIcon from '@/components/Icons/CalendarIcon.vue'
|
import CalendarIcon from '@/components/Icons/CalendarIcon.vue'
|
||||||
import EventModal from '@/components/Modals/EventModal.vue'
|
|
||||||
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||||
import { usersStore } from '@/stores/users'
|
import { useEvent, showEventModal, activeEvent } from '@/composables/event'
|
||||||
import { formatDate, timeAgo } from '@/utils'
|
import { formatDate, timeAgo } from '@/utils'
|
||||||
import { Tooltip, Avatar, dayjs, createListResource } from 'frappe-ui'
|
import { Tooltip, Avatar } from 'frappe-ui'
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
doctype: {
|
doctype: {
|
||||||
@ -95,103 +89,13 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { getUser } = usersStore()
|
|
||||||
|
|
||||||
const eventsResource = createListResource({
|
|
||||||
doctype: 'Event',
|
|
||||||
cache: ['calendar', props.docname],
|
|
||||||
fields: [
|
|
||||||
'name',
|
|
||||||
'status',
|
|
||||||
'subject',
|
|
||||||
'description',
|
|
||||||
'starts_on',
|
|
||||||
'ends_on',
|
|
||||||
'all_day',
|
|
||||||
'event_type',
|
|
||||||
'color',
|
|
||||||
'owner',
|
|
||||||
'reference_doctype',
|
|
||||||
'reference_docname',
|
|
||||||
'creation',
|
|
||||||
],
|
|
||||||
filters: {
|
|
||||||
reference_doctype: props.doctype,
|
|
||||||
reference_docname: props.docname,
|
|
||||||
},
|
|
||||||
auto: true,
|
|
||||||
orderBy: 'creation desc',
|
|
||||||
onSuccess: (d) => {
|
|
||||||
console.log(d)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const eventParticipants = createListResource({
|
|
||||||
doctype: 'Event Participants',
|
|
||||||
cache: ['Event Participants', props.docname],
|
|
||||||
fields: ['*'],
|
|
||||||
parent: 'Event',
|
|
||||||
})
|
|
||||||
|
|
||||||
const events = computed(() => {
|
|
||||||
if (!eventsResource.data) return []
|
|
||||||
|
|
||||||
if (!eventParticipants.data?.length) {
|
|
||||||
eventParticipants.update({
|
|
||||||
filters: {
|
|
||||||
parenttype: 'Event',
|
|
||||||
parentfield: 'event_participants',
|
|
||||||
parent: ['in', eventsResource.data.map((e) => e.name)],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
!eventParticipants.list.loading && eventParticipants.reload()
|
|
||||||
} else {
|
|
||||||
eventsResource.data.forEach((event) => {
|
|
||||||
if (typeof event.owner !== 'object') {
|
|
||||||
event.owner = {
|
|
||||||
label: getUser(event.owner).full_name,
|
|
||||||
image: getUser(event.owner).user_image,
|
|
||||||
name: event.owner,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
event.participants = [
|
|
||||||
event.owner,
|
|
||||||
...eventParticipants.data
|
|
||||||
.filter((participant) => participant.parent === event.name)
|
|
||||||
.map((participant) => ({
|
|
||||||
label: getUser(participant.email).full_name || participant.email,
|
|
||||||
image: getUser(participant.email).user_image || '',
|
|
||||||
name: participant.email,
|
|
||||||
})),
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return eventsResource.data
|
|
||||||
})
|
|
||||||
|
|
||||||
const formattedDateTime = (e) => {
|
|
||||||
const start = dayjs(e.starts_on)
|
|
||||||
const end = dayjs(e.ends_on)
|
|
||||||
|
|
||||||
if (e.all_day) {
|
|
||||||
return __('All day')
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${start.format('h:mm a')} - ${end.format('h:mm a')}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const formattedDate = (e) => {
|
|
||||||
const start = dayjs(e.starts_on)
|
|
||||||
return start.format('ddd, D MMM YYYY')
|
|
||||||
}
|
|
||||||
|
|
||||||
const showEventModal = ref(false)
|
|
||||||
const event = ref(null)
|
|
||||||
|
|
||||||
function showEvent(e) {
|
function showEvent(e) {
|
||||||
showEventModal.value = true
|
showEventModal.value = true
|
||||||
event.value = e
|
activeEvent.value = e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { events, startEndTime, startDate } = useEvent(
|
||||||
|
props.doctype,
|
||||||
|
props.docname,
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -42,15 +42,29 @@
|
|||||||
<div class="text-base text-ink-gray-7 w-3/12">
|
<div class="text-base text-ink-gray-7 w-3/12">
|
||||||
{{ __('Title') }}
|
{{ __('Title') }}
|
||||||
</div>
|
</div>
|
||||||
<TextInput
|
<div class="flex gap-1 w-9/12">
|
||||||
ref="title"
|
<Dropdown class="" :options="colors">
|
||||||
class="w-9/12"
|
<div
|
||||||
size="md"
|
class="flex items-center justify-center size-7 shrink-0 border border-outline-gray-2 bg-surface-white hover:border-outline-gray-3 hover:shadow-sm rounded cursor-pointer"
|
||||||
v-model="_event.title"
|
>
|
||||||
:placeholder="__('Call with John Doe')"
|
<div
|
||||||
variant="outline"
|
class="size-2.5 rounded-full cursor-pointer"
|
||||||
required
|
:style="{
|
||||||
/>
|
backgroundColor: _event.color || '#30A66D',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
|
<TextInput
|
||||||
|
class="w-full"
|
||||||
|
ref="title"
|
||||||
|
size="sm"
|
||||||
|
v-model="_event.title"
|
||||||
|
:placeholder="__('Call with John Doe')"
|
||||||
|
variant="outline"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="text-base text-ink-gray-7 w-3/12">
|
<div class="text-base text-ink-gray-7 w-3/12">
|
||||||
@ -83,7 +97,7 @@
|
|||||||
</DatePicker>
|
</DatePicker>
|
||||||
<TimePicker
|
<TimePicker
|
||||||
v-if="!_event.isFullDay"
|
v-if="!_event.isFullDay"
|
||||||
class="max-w-[105px]"
|
class="max-w-[112px]"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:modelValue="_event.fromTime"
|
:modelValue="_event.fromTime"
|
||||||
:placeholder="__('Start Time')"
|
:placeholder="__('Start Time')"
|
||||||
@ -91,7 +105,7 @@
|
|||||||
/>
|
/>
|
||||||
<TimePicker
|
<TimePicker
|
||||||
v-if="!_event.isFullDay"
|
v-if="!_event.isFullDay"
|
||||||
class="max-w-[105px]"
|
class="max-w-[112px]"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:modelValue="_event.toTime"
|
:modelValue="_event.toTime"
|
||||||
:options="toOptions"
|
:options="toOptions"
|
||||||
@ -101,6 +115,50 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="text-base text-ink-gray-7 w-3/12">
|
||||||
|
{{ __('Link') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 w-9/12">
|
||||||
|
<FormControl
|
||||||
|
:class="_event.referenceDoctype ? 'w-20' : 'w-full'"
|
||||||
|
type="select"
|
||||||
|
:options="linkDoctypeOptions"
|
||||||
|
v-model="_event.referenceDoctype"
|
||||||
|
variant="outline"
|
||||||
|
:placeholder="__('Add Lead or Deal')"
|
||||||
|
@change="() => (_event.referenceDocname = '')"
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
v-if="_event.referenceDoctype"
|
||||||
|
class="w-full"
|
||||||
|
v-model="_event.referenceDocname"
|
||||||
|
:doctype="_event.referenceDoctype"
|
||||||
|
variant="outline"
|
||||||
|
:placeholder="
|
||||||
|
__('Select {0}', [
|
||||||
|
_event.referenceDoctype == 'CRM Lead'
|
||||||
|
? __('Lead')
|
||||||
|
: __('Deal'),
|
||||||
|
])
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="text-base text-ink-gray-7 mt-1.5 w-3/12">
|
||||||
|
{{ __('Attendees') }}
|
||||||
|
</div>
|
||||||
|
<div class="w-9/12">
|
||||||
|
<Attendee
|
||||||
|
v-model="peoples"
|
||||||
|
:validate="validateEmail"
|
||||||
|
:error-message="
|
||||||
|
(value) => __('{0} is an invalid email address', [value])
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="mt-2 text-base text-ink-gray-7 w-3/12">
|
<div class="mt-2 text-base text-ink-gray-7 w-3/12">
|
||||||
{{ __('Description') }}
|
{{ __('Description') }}
|
||||||
@ -119,7 +177,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<div class="flex gap-2 justify-end">
|
<div v-if="eventsResource" class="flex gap-2 justify-end">
|
||||||
<Button :label="__('Cancel')" @click="show = false" />
|
<Button :label="__('Cancel')" @click="show = false" />
|
||||||
<Button
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
@ -131,7 +189,9 @@
|
|||||||
: __('Create')
|
: __('Create')
|
||||||
"
|
"
|
||||||
:loading="
|
:loading="
|
||||||
mode === 'edit' ? events.setValue.loading : events.insert.loading
|
mode === 'edit'
|
||||||
|
? eventsResource.setValue.loading
|
||||||
|
: eventsResource.insert.loading
|
||||||
"
|
"
|
||||||
@click="update"
|
@click="update"
|
||||||
/>
|
/>
|
||||||
@ -140,6 +200,8 @@
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
import Attendee from '@/components/Calendar/Attendee.vue'
|
||||||
import {
|
import {
|
||||||
Switch,
|
Switch,
|
||||||
TextEditor,
|
TextEditor,
|
||||||
@ -148,9 +210,15 @@ import {
|
|||||||
DatePicker,
|
DatePicker,
|
||||||
TimePicker,
|
TimePicker,
|
||||||
dayjs,
|
dayjs,
|
||||||
|
Dropdown,
|
||||||
|
FormControl,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { globalStore } from '@/stores/global'
|
import { globalStore } from '@/stores/global'
|
||||||
import { onMounted, ref, computed } from 'vue'
|
import { validateEmail } from '@/utils'
|
||||||
|
import { allTimeSlots } from '@/components/Calendar/utils'
|
||||||
|
import { useEvent } from '@/composables/event'
|
||||||
|
import { CalendarColorMap as colorMap } from 'frappe-ui'
|
||||||
|
import { onMounted, ref, computed, h } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
event: {
|
event: {
|
||||||
@ -170,7 +238,8 @@ const props = defineProps({
|
|||||||
const { $dialog } = globalStore()
|
const { $dialog } = globalStore()
|
||||||
|
|
||||||
const show = defineModel()
|
const show = defineModel()
|
||||||
const events = defineModel('events')
|
|
||||||
|
const { eventsResource } = useEvent(props.doctype, props.docname)
|
||||||
|
|
||||||
const title = ref(null)
|
const title = ref(null)
|
||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
@ -192,6 +261,29 @@ const _event = ref({
|
|||||||
isFullDay: false,
|
isFullDay: false,
|
||||||
eventType: 'Public',
|
eventType: 'Public',
|
||||||
color: 'green',
|
color: 'green',
|
||||||
|
referenceDoctype: '',
|
||||||
|
referenceDocname: '',
|
||||||
|
event_participants: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const peoples = computed({
|
||||||
|
get() {
|
||||||
|
return _event.value.event_participants || []
|
||||||
|
},
|
||||||
|
set(list) {
|
||||||
|
const seen = new Set()
|
||||||
|
const out = []
|
||||||
|
for (const a of list || []) {
|
||||||
|
if (!a?.email || seen.has(a.email)) continue
|
||||||
|
seen.add(a.email)
|
||||||
|
out.push({
|
||||||
|
email: a.email,
|
||||||
|
reference_doctype: a.reference_doctype || 'Contact',
|
||||||
|
reference_docname: a.reference_docname || '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_event.value.event_participants = out
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -215,6 +307,9 @@ onMounted(() => {
|
|||||||
isFullDay: props.event.all_day,
|
isFullDay: props.event.all_day,
|
||||||
eventType: props.event.event_type,
|
eventType: props.event.event_type,
|
||||||
color: props.event.color,
|
color: props.event.color,
|
||||||
|
referenceDoctype: props.event.reference_doctype,
|
||||||
|
referenceDocname: props.event.reference_docname,
|
||||||
|
event_participants: props.event.event_participants || [],
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => title.value?.el?.focus(), 100)
|
setTimeout(() => title.value?.el?.focus(), 100)
|
||||||
@ -235,24 +330,35 @@ function updateTime(t, fromTime = false) {
|
|||||||
if (!_event.value.toTime) {
|
if (!_event.value.toTime) {
|
||||||
const hour = parseInt(t.split(':')[0])
|
const hour = parseInt(t.split(':')[0])
|
||||||
const minute = parseInt(t.split(':')[1])
|
const minute = parseInt(t.split(':')[1])
|
||||||
_event.value.toTime = `${hour + 1}:${minute}`
|
let nh = hour + 1
|
||||||
|
let nm = minute
|
||||||
|
if (nh >= 24) {
|
||||||
|
nh = 23
|
||||||
|
nm = 59
|
||||||
|
}
|
||||||
|
_event.value.toTime = `${String(nh).padStart(2, '0')}:${String(nm).padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_event.value.toTime = t
|
_event.value.toTime = t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateFromToTime(oldTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateFromToTime(oldTo) {
|
||||||
|
if (_event.value.isFullDay) return true
|
||||||
if (_event.value.toTime && _event.value.fromTime) {
|
if (_event.value.toTime && _event.value.fromTime) {
|
||||||
const diff = dayjs(_event.value.toDate + ' ' + _event.value.toTime).diff(
|
const diff = dayjs(_event.value.fromDate + ' ' + _event.value.toTime).diff(
|
||||||
dayjs(_event.value.fromDate + ' ' + _event.value.fromTime),
|
dayjs(_event.value.fromDate + ' ' + _event.value.fromTime),
|
||||||
'minute',
|
'minute',
|
||||||
)
|
)
|
||||||
|
|
||||||
if (diff <= 0) {
|
if (diff <= 0) {
|
||||||
_event.value.toTime = oldTo
|
_event.value.toTime = oldTo
|
||||||
error.value = __('End time should be after start time')
|
error.value = __('End time should be after start time')
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
@ -263,11 +369,17 @@ function update() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_event.value.id ? updateEvent() : createEvent()
|
validateFromToTime(_event.value.toTime)
|
||||||
|
|
||||||
|
if (_event.value.id && _event.value.id !== 'duplicate') {
|
||||||
|
updateEvent()
|
||||||
|
} else {
|
||||||
|
createEvent()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEvent() {
|
function createEvent() {
|
||||||
events.value.insert.submit(
|
eventsResource.insert.submit(
|
||||||
{
|
{
|
||||||
subject: _event.value.title,
|
subject: _event.value.title,
|
||||||
description: _event.value.description,
|
description: _event.value.description,
|
||||||
@ -278,10 +390,13 @@ function createEvent() {
|
|||||||
color: _event.value.color,
|
color: _event.value.color,
|
||||||
reference_doctype: props.doctype,
|
reference_doctype: props.doctype,
|
||||||
reference_docname: props.docname,
|
reference_docname: props.docname,
|
||||||
|
reference_doctype: _event.value.referenceDoctype || props.doctype,
|
||||||
|
reference_docname: _event.value.referenceDocname || props.docname,
|
||||||
|
event_participants: _event.value.event_participants,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await events.value.reload()
|
await eventsResource.reload()
|
||||||
show.value = false
|
show.value = false
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -294,7 +409,7 @@ function updateEvent() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
events.value.setValue.submit(
|
eventsResource.setValue.submit(
|
||||||
{
|
{
|
||||||
name: _event.value.id,
|
name: _event.value.id,
|
||||||
subject: _event.value.title,
|
subject: _event.value.title,
|
||||||
@ -306,10 +421,13 @@ function updateEvent() {
|
|||||||
color: _event.value.color,
|
color: _event.value.color,
|
||||||
reference_doctype: props.doctype,
|
reference_doctype: props.doctype,
|
||||||
reference_docname: props.docname,
|
reference_docname: props.docname,
|
||||||
|
reference_doctype: _event.value.referenceDoctype || props.doctype,
|
||||||
|
reference_docname: _event.value.referenceDocname || props.docname,
|
||||||
|
event_participants: _event.value.event_participants,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await events.value.reload()
|
await eventsResource.reload()
|
||||||
show.value = false
|
show.value = false
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -336,9 +454,9 @@ function deleteEvent() {
|
|||||||
variant: 'solid',
|
variant: 'solid',
|
||||||
theme: 'red',
|
theme: 'red',
|
||||||
onClick: (close) => {
|
onClick: (close) => {
|
||||||
events.value.delete.submit(_event.value.id, {
|
eventsResource.delete.submit(_event.value.id, {
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await events.value.reload()
|
await eventsResource.reload()
|
||||||
show.value = false
|
show.value = false
|
||||||
close()
|
close()
|
||||||
},
|
},
|
||||||
@ -386,20 +504,19 @@ const toOptions = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
function allTimeSlots() {
|
const linkDoctypeOptions = [
|
||||||
const out = []
|
{ label: '', value: '' },
|
||||||
for (let h = 0; h < 24; h++) {
|
{ label: __('Lead'), value: 'CRM Lead' },
|
||||||
for (const m of [0, 15, 30, 45]) {
|
{ label: __('Deal'), value: 'CRM Deal' },
|
||||||
const hh = String(h).padStart(2, '0')
|
]
|
||||||
const mm = String(m).padStart(2, '0')
|
|
||||||
const ampm = h >= 12 ? 'pm' : 'am'
|
const colors = Object.keys(colorMap).map((c) => ({
|
||||||
const hour12 = h % 12 === 0 ? 12 : h % 12
|
label: c.charAt(0).toUpperCase() + c.slice(1),
|
||||||
out.push({
|
value: colorMap[c].color,
|
||||||
value: `${hh}:${mm}`,
|
icon: h('div', {
|
||||||
label: `${hour12}:${mm} ${ampm}`,
|
class: '!size-2.5 rounded-full',
|
||||||
})
|
style: { backgroundColor: colorMap[c].color },
|
||||||
}
|
}),
|
||||||
}
|
onClick: () => (_event.value.color = colorMap[c].color),
|
||||||
return out
|
}))
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
138
frontend/src/composables/event.js
Normal file
138
frontend/src/composables/event.js
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import { usersStore } from '@/stores/users'
|
||||||
|
import { dayjs, createListResource } from 'frappe-ui'
|
||||||
|
import { sameArrayContents } from '@/utils'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
export const showEventModal = ref(false)
|
||||||
|
export const activeEvent = ref(null)
|
||||||
|
|
||||||
|
export function useEvent(doctype, docname) {
|
||||||
|
const { getUser } = usersStore()
|
||||||
|
|
||||||
|
const eventsResource = createListResource({
|
||||||
|
doctype: 'Event',
|
||||||
|
cache: ['calendar', docname],
|
||||||
|
fields: [
|
||||||
|
'name',
|
||||||
|
'status',
|
||||||
|
'subject',
|
||||||
|
'description',
|
||||||
|
'starts_on',
|
||||||
|
'ends_on',
|
||||||
|
'all_day',
|
||||||
|
'event_type',
|
||||||
|
'color',
|
||||||
|
'owner',
|
||||||
|
'reference_doctype',
|
||||||
|
'reference_docname',
|
||||||
|
'creation',
|
||||||
|
],
|
||||||
|
filters: {
|
||||||
|
reference_doctype: doctype,
|
||||||
|
reference_docname: docname,
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
orderBy: 'creation desc',
|
||||||
|
onSuccess: (d) => {
|
||||||
|
console.log(d)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const eventParticipantsResource = createListResource({
|
||||||
|
doctype: 'Event Participants',
|
||||||
|
fields: ['*'],
|
||||||
|
parent: 'Event',
|
||||||
|
})
|
||||||
|
|
||||||
|
const events = computed(() => {
|
||||||
|
if (!eventsResource.data) return []
|
||||||
|
const eventNames = eventsResource.data.map((e) => e.name)
|
||||||
|
if (
|
||||||
|
!eventParticipantsResource.data?.length ||
|
||||||
|
eventsParticipantIsUpdated(eventNames)
|
||||||
|
) {
|
||||||
|
eventParticipantsResource.update({
|
||||||
|
filters: {
|
||||||
|
parenttype: 'Event',
|
||||||
|
parentfield: 'event_participants',
|
||||||
|
parent: ['in', eventNames],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
!eventParticipantsResource.list.loading &&
|
||||||
|
eventParticipantsResource.reload()
|
||||||
|
} else {
|
||||||
|
eventsResource.data.forEach((event) => {
|
||||||
|
if (typeof event.owner !== 'object') {
|
||||||
|
event.owner = {
|
||||||
|
label: getUser(event.owner).full_name,
|
||||||
|
image: getUser(event.owner).user_image,
|
||||||
|
name: event.owner,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.event_participants = [
|
||||||
|
...eventParticipantsResource.data.filter(
|
||||||
|
(participant) => participant.parent === event.name,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
event.participants = [
|
||||||
|
event.owner,
|
||||||
|
...eventParticipantsResource.data
|
||||||
|
.filter((participant) => participant.parent === event.name)
|
||||||
|
.map((participant) => ({
|
||||||
|
label: getUser(participant.email).full_name || participant.email,
|
||||||
|
image: getUser(participant.email).user_image || '',
|
||||||
|
name: participant.email,
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventsResource.data
|
||||||
|
})
|
||||||
|
|
||||||
|
function eventsParticipantIsUpdated(eventNames) {
|
||||||
|
const parentFilter = eventParticipantsResource.filters?.parent?.[1]
|
||||||
|
|
||||||
|
if (eventNames.length && !sameArrayContents(parentFilter, eventNames))
|
||||||
|
return true
|
||||||
|
|
||||||
|
let d = eventsResource.setValue.data
|
||||||
|
if (!d) return false
|
||||||
|
|
||||||
|
let newParticipants = d.event_participants.map((p) => p.name)
|
||||||
|
let oldParticipants = eventParticipantsResource.data
|
||||||
|
.filter((p) => p.parent === d.name)
|
||||||
|
.map((p) => p.name)
|
||||||
|
|
||||||
|
return !sameArrayContents(newParticipants, oldParticipants)
|
||||||
|
}
|
||||||
|
|
||||||
|
const startEndTime = (
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
isFullDay = false,
|
||||||
|
format = 'h:mm a',
|
||||||
|
) => {
|
||||||
|
const start = dayjs(startTime)
|
||||||
|
const end = dayjs(endTime)
|
||||||
|
|
||||||
|
if (isFullDay) return __('All day')
|
||||||
|
|
||||||
|
return `${start.format(format)} - ${end.format(format)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const startDate = (startTime, format = 'ddd, D MMM YYYY') => {
|
||||||
|
const start = dayjs(startTime)
|
||||||
|
return start.format(format)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
eventsResource,
|
||||||
|
eventParticipantsResource,
|
||||||
|
events,
|
||||||
|
startEndTime,
|
||||||
|
startDate,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -689,3 +689,33 @@ export function validateConditions(conditions) {
|
|||||||
|
|
||||||
return conditions.length > 0
|
return conditions.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sameArrayContents: returns true if both arrays have exactly the same elements
|
||||||
|
// (including duplicate counts) irrespective of order.
|
||||||
|
// Non-arrays or arrays of different length return false.
|
||||||
|
export function sameArrayContents(a, b) {
|
||||||
|
if (a === b) return true
|
||||||
|
if (!Array.isArray(a) || !Array.isArray(b)) return false
|
||||||
|
if (a.length !== b.length) return false
|
||||||
|
if (a.length === 0) return true
|
||||||
|
const counts = new Map()
|
||||||
|
for (const v of a) {
|
||||||
|
counts.set(v, (counts.get(v) || 0) + 1)
|
||||||
|
}
|
||||||
|
for (const v of b) {
|
||||||
|
const c = counts.get(v)
|
||||||
|
if (!c) return false
|
||||||
|
if (c === 1) counts.delete(v)
|
||||||
|
else counts.set(v, c - 1)
|
||||||
|
}
|
||||||
|
return counts.size === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// orderSensitiveEqual: returns true only if arrays are strictly equal index-wise
|
||||||
|
export function orderSensitiveEqual(a, b) {
|
||||||
|
if (a === b) return true
|
||||||
|
if (!Array.isArray(a) || !Array.isArray(b)) return false
|
||||||
|
if (a.length !== b.length) return false
|
||||||
|
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user