feat: added events tab in lead/deal page
create & update events from lead/deal page link events with lead or deal
This commit is contained in:
parent
b6e8d83c3b
commit
ea644c22f1
3
frontend/components.d.ts
vendored
3
frontend/components.d.ts
vendored
@ -128,6 +128,9 @@ declare module 'vue' {
|
|||||||
ERPNextIcon: typeof import('./src/components/Icons/ERPNextIcon.vue')['default']
|
ERPNextIcon: typeof import('./src/components/Icons/ERPNextIcon.vue')['default']
|
||||||
ERPNextSettings: typeof import('./src/components/Settings/ERPNextSettings.vue')['default']
|
ERPNextSettings: typeof import('./src/components/Settings/ERPNextSettings.vue')['default']
|
||||||
ErrorPage: typeof import('./src/components/ErrorPage.vue')['default']
|
ErrorPage: typeof import('./src/components/ErrorPage.vue')['default']
|
||||||
|
EventArea: typeof import('./src/components/Activities/EventArea.vue')['default']
|
||||||
|
EventIcon: typeof import('./src/components/Icons/EventIcon.vue')['default']
|
||||||
|
EventModal: typeof import('./src/components/Modals/EventModal.vue')['default']
|
||||||
ExotelCallUI: typeof import('./src/components/Telephony/ExotelCallUI.vue')['default']
|
ExotelCallUI: typeof import('./src/components/Telephony/ExotelCallUI.vue')['default']
|
||||||
ExportIcon: typeof import('./src/components/Icons/ExportIcon.vue')['default']
|
ExportIcon: typeof import('./src/components/Icons/ExportIcon.vue')['default']
|
||||||
ExternalLinkIcon: typeof import('./src/components/Icons/ExternalLinkIcon.vue')['default']
|
ExternalLinkIcon: typeof import('./src/components/Icons/ExternalLinkIcon.vue')['default']
|
||||||
|
|||||||
@ -24,6 +24,7 @@
|
|||||||
<div
|
<div
|
||||||
v-else-if="
|
v-else-if="
|
||||||
activities?.length ||
|
activities?.length ||
|
||||||
|
(events.data?.length && title == 'Events') ||
|
||||||
(whatsappMessages.data?.length && title == 'WhatsApp')
|
(whatsappMessages.data?.length && title == 'WhatsApp')
|
||||||
"
|
"
|
||||||
class="activities"
|
class="activities"
|
||||||
@ -36,6 +37,33 @@
|
|||||||
:messages="whatsappMessages.data"
|
:messages="whatsappMessages.data"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="title == 'Events'" class="activity">
|
||||||
|
<div v-for="(event, i) in events.data" :key="event.name">
|
||||||
|
<div
|
||||||
|
class="activity grid grid-cols-[30px_minmax(auto,_1fr)] gap-4 px-3 sm:px-10"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="z-0 relative flex justify-center before:absolute before:left-[50%] before:-z-[1] before:top-0 before:border-l before:border-outline-gray-modals"
|
||||||
|
:class="
|
||||||
|
i != events.data.length - 1 ? 'before:h-full' : 'before:h-4'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex h-8 w-7 items-center justify-center bg-surface-white text-ink-gray-8"
|
||||||
|
>
|
||||||
|
<EventIcon class="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<EventArea
|
||||||
|
class="mb-4"
|
||||||
|
v-model="events"
|
||||||
|
:event="event"
|
||||||
|
:doctype="doctype"
|
||||||
|
:docname="doc?.name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="title == 'Notes'"
|
v-else-if="title == 'Notes'"
|
||||||
class="grid grid-cols-1 gap-4 px-3 pb-3 sm:px-10 sm:pb-5 lg:grid-cols-2 xl:grid-cols-3"
|
class="grid grid-cols-1 gap-4 px-3 pb-3 sm:px-10 sm:pb-5 lg:grid-cols-2 xl:grid-cols-3"
|
||||||
@ -395,6 +423,11 @@
|
|||||||
:label="__('New Comment')"
|
:label="__('New Comment')"
|
||||||
@click="emailBox.showComment = true"
|
@click="emailBox.showComment = true"
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
v-else-if="title == 'Events'"
|
||||||
|
:label="__('Schedule an Event')"
|
||||||
|
@click="modalRef.showEvent()"
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
v-else-if="title == 'Tasks'"
|
v-else-if="title == 'Tasks'"
|
||||||
:label="__('Create Task')"
|
:label="__('Create Task')"
|
||||||
@ -435,6 +468,7 @@
|
|||||||
<AllModals
|
<AllModals
|
||||||
ref="modalRef"
|
ref="modalRef"
|
||||||
v-model="all_activities"
|
v-model="all_activities"
|
||||||
|
v-model:events="events"
|
||||||
:doctype="doctype"
|
:doctype="doctype"
|
||||||
:doc="doc"
|
:doc="doc"
|
||||||
/>
|
/>
|
||||||
@ -463,11 +497,14 @@ import UserAvatar from '@/components/UserAvatar.vue'
|
|||||||
import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
|
import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
|
||||||
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
||||||
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
|
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
|
||||||
|
import CalendarIcon from '@/components/Icons/CalendarIcon.vue'
|
||||||
|
import EventIcon from '@/components/Icons/EventIcon.vue'
|
||||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||||
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
||||||
import TaskIcon from '@/components/Icons/TaskIcon.vue'
|
import TaskIcon from '@/components/Icons/TaskIcon.vue'
|
||||||
import AttachmentIcon from '@/components/Icons/AttachmentIcon.vue'
|
import AttachmentIcon from '@/components/Icons/AttachmentIcon.vue'
|
||||||
import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
|
import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
|
||||||
|
import EventArea from '@/components/Activities/EventArea.vue'
|
||||||
import WhatsAppArea from '@/components/Activities/WhatsAppArea.vue'
|
import WhatsAppArea from '@/components/Activities/WhatsAppArea.vue'
|
||||||
import WhatsAppBox from '@/components/Activities/WhatsAppBox.vue'
|
import WhatsAppBox from '@/components/Activities/WhatsAppBox.vue'
|
||||||
import LoadingIndicator from '@/components/Icons/LoadingIndicator.vue'
|
import LoadingIndicator from '@/components/Icons/LoadingIndicator.vue'
|
||||||
@ -492,7 +529,7 @@ import { usersStore } from '@/stores/users'
|
|||||||
import { whatsappEnabled, callEnabled } from '@/composables/settings'
|
import { whatsappEnabled, callEnabled } from '@/composables/settings'
|
||||||
import { useDocument } from '@/data/document'
|
import { useDocument } from '@/data/document'
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
import { Button, Tooltip, createResource } from 'frappe-ui'
|
import { Button, Tooltip, createResource, createListResource } from 'frappe-ui'
|
||||||
import { useElementVisibility } from '@vueuse/core'
|
import { useElementVisibility } from '@vueuse/core'
|
||||||
import {
|
import {
|
||||||
ref,
|
ref,
|
||||||
@ -573,6 +610,47 @@ const whatsappMessages = createResource({
|
|||||||
onSuccess: () => nextTick(() => scroll()),
|
onSuccess: () => nextTick(() => scroll()),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const events = 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: {
|
||||||
|
status: 'Open',
|
||||||
|
reference_doctype: props.doctype,
|
||||||
|
reference_docname: props.docname,
|
||||||
|
},
|
||||||
|
orderBy: 'creation desc',
|
||||||
|
auto: title.value == 'Events',
|
||||||
|
transform: (data) => {
|
||||||
|
return data.map((event) => {
|
||||||
|
if (typeof event.owner !== 'object') {
|
||||||
|
event.owner = {
|
||||||
|
label: getUser(event.owner).full_name,
|
||||||
|
image: getUser(event.owner).image,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSuccess: (d) => {
|
||||||
|
console.log(d)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
$socket.off('whatsapp_message')
|
$socket.off('whatsapp_message')
|
||||||
})
|
})
|
||||||
@ -706,6 +784,8 @@ const emptyText = computed(() => {
|
|||||||
text = 'No Comments'
|
text = 'No Comments'
|
||||||
} else if (title.value == 'Data') {
|
} else if (title.value == 'Data') {
|
||||||
text = 'No Data'
|
text = 'No Data'
|
||||||
|
} else if (title.value == 'Events') {
|
||||||
|
text = 'No Events'
|
||||||
} else if (title.value == 'Calls') {
|
} else if (title.value == 'Calls') {
|
||||||
text = 'No Call Logs'
|
text = 'No Call Logs'
|
||||||
} else if (title.value == 'Notes') {
|
} else if (title.value == 'Notes') {
|
||||||
@ -728,6 +808,8 @@ const emptyTextIcon = computed(() => {
|
|||||||
icon = CommentIcon
|
icon = CommentIcon
|
||||||
} else if (title.value == 'Data') {
|
} else if (title.value == 'Data') {
|
||||||
icon = DetailsIcon
|
icon = DetailsIcon
|
||||||
|
} else if (title.value == 'Events') {
|
||||||
|
icon = EventIcon
|
||||||
} else if (title.value == 'Calls') {
|
} else if (title.value == 'Calls') {
|
||||||
icon = PhoneIcon
|
icon = PhoneIcon
|
||||||
} else if (title.value == 'Notes') {
|
} else if (title.value == 'Notes') {
|
||||||
@ -754,6 +836,9 @@ function timelineIcon(activity_type, is_lead) {
|
|||||||
case 'comment':
|
case 'comment':
|
||||||
icon = CommentIcon
|
icon = CommentIcon
|
||||||
break
|
break
|
||||||
|
case 'event':
|
||||||
|
icon = CalendarIcon
|
||||||
|
break
|
||||||
case 'incoming_call':
|
case 'incoming_call':
|
||||||
icon = InboundCallIcon
|
icon = InboundCallIcon
|
||||||
break
|
break
|
||||||
@ -783,7 +868,7 @@ watch([reload, reload_email], ([reload_value, reload_email_value]) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function scroll(hash) {
|
function scroll(hash) {
|
||||||
if (['tasks', 'notes'].includes(route.hash?.slice(1))) return
|
if (['tasks', 'notes', 'events'].includes(route.hash?.slice(1))) return
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
let el
|
let el
|
||||||
if (!hash) {
|
if (!hash) {
|
||||||
|
|||||||
@ -25,6 +25,16 @@
|
|||||||
variant="solid"
|
variant="solid"
|
||||||
:options="callActions"
|
:options="callActions"
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
v-else-if="title == 'Events'"
|
||||||
|
variant="solid"
|
||||||
|
@click="modalRef.showEvent()"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<EventIcon class="h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
<span>{{ __('Schedule an event') }}</span>
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
v-else-if="title == 'Notes'"
|
v-else-if="title == 'Notes'"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
@ -75,6 +85,7 @@
|
|||||||
import MultiActionButton from '@/components/MultiActionButton.vue'
|
import MultiActionButton from '@/components/MultiActionButton.vue'
|
||||||
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
||||||
import CommentIcon from '@/components/Icons/CommentIcon.vue'
|
import CommentIcon from '@/components/Icons/CommentIcon.vue'
|
||||||
|
import EventIcon from '@/components/Icons/EventIcon.vue'
|
||||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||||
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
||||||
import TaskIcon from '@/components/Icons/TaskIcon.vue'
|
import TaskIcon from '@/components/Icons/TaskIcon.vue'
|
||||||
@ -112,6 +123,11 @@ const defaultActions = computed(() => {
|
|||||||
label: __('New Comment'),
|
label: __('New Comment'),
|
||||||
onClick: () => (props.emailBox.showComment = true),
|
onClick: () => (props.emailBox.showComment = true),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: h(EventIcon, { class: 'h-4 w-4' }),
|
||||||
|
label: __('Schedule an event'),
|
||||||
|
onClick: () => props.modalRef.showEvent(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: h(PhoneIcon, { class: 'h-4 w-4' }),
|
icon: h(PhoneIcon, { class: 'h-4 w-4' }),
|
||||||
label: __('Log a Call'),
|
label: __('Log a Call'),
|
||||||
|
|||||||
@ -22,21 +22,47 @@
|
|||||||
:referenceDoc="referenceDoc"
|
:referenceDoc="referenceDoc"
|
||||||
:options="{ afterInsert: () => activities.reload() }"
|
:options="{ afterInsert: () => activities.reload() }"
|
||||||
/>
|
/>
|
||||||
|
<EventModal
|
||||||
|
v-if="showEventModal"
|
||||||
|
v-model="showEventModal"
|
||||||
|
v-model:events="events"
|
||||||
|
:event="event"
|
||||||
|
:doctype="doctype"
|
||||||
|
:docname="doc?.name"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import TaskModal from '@/components/Modals/TaskModal.vue'
|
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 { 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'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
doctype: String,
|
doctype: String,
|
||||||
|
doc: Object,
|
||||||
})
|
})
|
||||||
|
|
||||||
const activities = defineModel()
|
const activities = defineModel()
|
||||||
const doc = defineModel('doc')
|
const events = defineModel('events')
|
||||||
|
|
||||||
|
const showEventModal = ref(false)
|
||||||
|
const event = ref({})
|
||||||
|
|
||||||
|
function showEvent(e) {
|
||||||
|
event.value = e || {
|
||||||
|
subject: '',
|
||||||
|
description: '',
|
||||||
|
starts_on: '',
|
||||||
|
ends_on: '',
|
||||||
|
all_day: false,
|
||||||
|
event_type: 'Public',
|
||||||
|
color: 'green',
|
||||||
|
}
|
||||||
|
showEventModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// Tasks
|
// Tasks
|
||||||
const showTaskModal = ref(false)
|
const showTaskModal = ref(false)
|
||||||
@ -115,6 +141,7 @@ function redirect(tabName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
showEvent,
|
||||||
showTask,
|
showTask,
|
||||||
deleteTask,
|
deleteTask,
|
||||||
updateTaskStatus,
|
updateTaskStatus,
|
||||||
|
|||||||
113
frontend/src/components/Activities/EventArea.vue
Normal file
113
frontend/src/components/Activities/EventArea.vue
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-5">
|
||||||
|
<div class="mb-1 flex items-center justify-stretch gap-2 py-1 text-base">
|
||||||
|
<div class="inline-flex items-center flex-wrap gap-1 text-ink-gray-5">
|
||||||
|
<Avatar
|
||||||
|
:image="event.owner.image"
|
||||||
|
:label="event.owner.label"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
<span class="font-medium text-ink-gray-8 ml-1">
|
||||||
|
{{ event.owner.label }}
|
||||||
|
</span>
|
||||||
|
<span>{{ 'has created an event' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto whitespace-nowrap">
|
||||||
|
<Tooltip :text="formatDate(event.creation)">
|
||||||
|
<div class="text-sm text-ink-gray-5">
|
||||||
|
{{ __(timeAgo(event.creation)) }}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex gap-2 border cursor-pointer border-outline-gray-modals rounded-lg bg-surface-cards px-2.5 py-2.5 text-ink-gray-9"
|
||||||
|
@click="showEvent(event)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex w-[2px] rounded-lg"
|
||||||
|
:style="{ backgroundColor: event.color || '#30A66D' }"
|
||||||
|
/>
|
||||||
|
<div class="flex-1 flex flex-col gap-1 text-base">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between gap-2 font-medium text-ink-gray-7"
|
||||||
|
>
|
||||||
|
<div>{{ event.subject }}</div>
|
||||||
|
<MultipleAvatar
|
||||||
|
:avatars="[
|
||||||
|
{
|
||||||
|
image: event.owner.image,
|
||||||
|
label: event.owner.label,
|
||||||
|
name: event.owner.label,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: event.owner.image,
|
||||||
|
label: event.owner.label,
|
||||||
|
name: event.owner.label,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-2 items-center text-ink-gray-6">
|
||||||
|
<div>{{ formattedDateTime }}</div>
|
||||||
|
<div>{{ formattedDate }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<EventModal
|
||||||
|
v-if="showEventModal"
|
||||||
|
v-model="showEventModal"
|
||||||
|
v-model:events="events"
|
||||||
|
:event="event"
|
||||||
|
:doctype="doctype"
|
||||||
|
:docname="docname"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import EventModal from '@/components/Modals/EventModal.vue'
|
||||||
|
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||||
|
import { Tooltip, Avatar, dayjs } from 'frappe-ui'
|
||||||
|
import { formatDate, timeAgo } from '@/utils'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
event: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
doctype: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
docname: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const events = defineModel()
|
||||||
|
|
||||||
|
const formattedDateTime = computed(() => {
|
||||||
|
const start = dayjs(props.event.starts_on)
|
||||||
|
const end = dayjs(props.event.ends_on)
|
||||||
|
|
||||||
|
if (props.event.all_day) {
|
||||||
|
return __('All day')
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${start.format('h:mm a')} - ${end.format('h:mm a')}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const formattedDate = computed(() => {
|
||||||
|
const start = dayjs(props.event.starts_on)
|
||||||
|
return start.format('ddd, D MMM YYYY')
|
||||||
|
})
|
||||||
|
|
||||||
|
const showEventModal = ref(false)
|
||||||
|
|
||||||
|
function showEvent() {
|
||||||
|
showEventModal.value = true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -56,6 +56,30 @@
|
|||||||
<div class="text-ink-gray-6 text-p-base">{{ formattedDateTime }}</div>
|
<div class="text-ink-gray-6 text-p-base">{{ formattedDateTime }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="event.referenceDocname"
|
||||||
|
class="mx-4.5 my-2.5 border-t border-outline-gray-1"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="event.referenceDocname"
|
||||||
|
class="flex items-center px-4.5 py-1 text-ink-gray-7"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="event.referenceDoctype == 'CRM Lead' ? LeadsIcon : DealsIcon"
|
||||||
|
class="size-4"
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
class="[&_button]:bg-surface-white [&_button]:select-text [&_button]:text-ink-gray-7 [&_button]:cursor-text"
|
||||||
|
v-model="event.referenceDocname"
|
||||||
|
:doctype="event.referenceDoctype"
|
||||||
|
:disabled="true"
|
||||||
|
/>
|
||||||
|
<Button variant="ghost" @click="redirect">
|
||||||
|
<template #icon>
|
||||||
|
<ArrowUpRightIcon class="size-4 text-ink-gray-7" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="event.description && event.description !== '<p></p>'"
|
v-if="event.description && event.description !== '<p></p>'"
|
||||||
class="mx-4.5 my-2.5 border-t border-outline-gray-1"
|
class="mx-4.5 my-2.5 border-t border-outline-gray-1"
|
||||||
@ -100,16 +124,16 @@
|
|||||||
{{ __('All day') }}
|
{{ __('All day') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5 text-ink-gray-5">
|
<!-- <div class="flex items-center gap-1.5 text-ink-gray-5">
|
||||||
<LucideEarth class="size-4" />
|
<LucideEarth class="size-4" />
|
||||||
{{ __('GMT+5:30') }}
|
{{ __('GMT+5:30') }}
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between px-4.5 py-[7px] text-ink-gray-7"
|
class="flex items-center justify-between px-4.5 py-[7px] text-ink-gray-7"
|
||||||
>
|
>
|
||||||
<div class="">{{ __('Date') }}</div>
|
<div class="">{{ __('Date') }}</div>
|
||||||
<div class="flex items-center gap-x-2">
|
<div class="flex items-center gap-x-1.5">
|
||||||
<DatePicker
|
<DatePicker
|
||||||
:class="['[&_input]:w-[216px]']"
|
:class="['[&_input]:w-[216px]']"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -133,10 +157,10 @@
|
|||||||
class="flex items-center justify-between px-4.5 py-[7px] text-ink-gray-7"
|
class="flex items-center justify-between px-4.5 py-[7px] text-ink-gray-7"
|
||||||
>
|
>
|
||||||
<div class="w-20">{{ __('Time') }}</div>
|
<div class="w-20">{{ __('Time') }}</div>
|
||||||
<div class="flex items-center gap-x-3">
|
<div class="flex items-center gap-x-1.5">
|
||||||
<TimePicker
|
<TimePicker
|
||||||
v-if="!event.isFullDay"
|
v-if="!event.isFullDay"
|
||||||
class="max-w-[102px]"
|
class="max-w-[105px]"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:value="event.fromTime"
|
:value="event.fromTime"
|
||||||
:placeholder="__('Start Time')"
|
:placeholder="__('Start Time')"
|
||||||
@ -151,7 +175,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</TimePicker>
|
</TimePicker>
|
||||||
<TimePicker
|
<TimePicker
|
||||||
class="max-w-[102px]"
|
class="max-w-[105px]"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:value="event.toTime"
|
:value="event.toTime"
|
||||||
:placeholder="__('End Time')"
|
:placeholder="__('End Time')"
|
||||||
@ -168,6 +192,52 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mx-4.5 my-2.5 border-t border-outline-gray-1" />
|
<div class="mx-4.5 my-2.5 border-t border-outline-gray-1" />
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between px-4.5 py-[7px] text-ink-gray-7"
|
||||||
|
>
|
||||||
|
<div class="">{{ __('Link') }}</div>
|
||||||
|
<div class="flex items-center gap-x-1.5">
|
||||||
|
<FormControl
|
||||||
|
class="w-[216px]"
|
||||||
|
type="select"
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Lead'),
|
||||||
|
value: 'CRM Lead',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Deal'),
|
||||||
|
value: 'CRM Deal',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
v-model="event.referenceDoctype"
|
||||||
|
variant="outline"
|
||||||
|
:placeholder="__('Add Lead or Deal')"
|
||||||
|
@change="() => (event.referenceDocname = '')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="event.referenceDoctype"
|
||||||
|
class="flex items-center justify-between px-4.5 py-[7px] text-ink-gray-7"
|
||||||
|
>
|
||||||
|
<div class="">
|
||||||
|
{{ event.referenceDoctype == 'CRM Lead' ? __('Lead') : __('Deal') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-x-1.5">
|
||||||
|
<Link
|
||||||
|
class="w-[220px]"
|
||||||
|
v-model="event.referenceDocname"
|
||||||
|
:doctype="event.referenceDoctype"
|
||||||
|
variant="outline"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mx-4.5 my-2.5 border-t border-outline-gray-1" />
|
||||||
<div class="px-4.5 py-3">
|
<div class="px-4.5 py-3">
|
||||||
<div class="flex items-center gap-x-2 border rounded py-1">
|
<div class="flex items-center gap-x-2 border rounded py-1">
|
||||||
<TextEditor
|
<TextEditor
|
||||||
@ -197,6 +267,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import ArrowUpRightIcon from '@/components/Icons/ArrowUpRightIcon.vue'
|
||||||
|
import LeadsIcon from '@/components/Icons/LeadsIcon.vue'
|
||||||
|
import DealsIcon from '@/components/Icons/DealsIcon.vue'
|
||||||
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
import DescriptionIcon from '@/components/Icons/DescriptionIcon.vue'
|
import DescriptionIcon from '@/components/Icons/DescriptionIcon.vue'
|
||||||
import TimePicker from './TimePicker.vue'
|
import TimePicker from './TimePicker.vue'
|
||||||
@ -214,6 +288,9 @@ import {
|
|||||||
CalendarActiveEvent as activeEvent,
|
CalendarActiveEvent as activeEvent,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { ref, computed, watch, h } from 'vue'
|
import { ref, computed, watch, h } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
event: {
|
event: {
|
||||||
@ -290,7 +367,7 @@ function updateTime(t, fromTime = false) {
|
|||||||
'minute',
|
'minute',
|
||||||
)
|
)
|
||||||
|
|
||||||
if (diff < 0) {
|
if (diff <= 0) {
|
||||||
props.event.toTime = oldTo
|
props.event.toTime = oldTo
|
||||||
error.value = __('End time should be after start time')
|
error.value = __('End time should be after start time')
|
||||||
return
|
return
|
||||||
@ -406,4 +483,17 @@ const colors = Object.keys(colorMap).map((color) => ({
|
|||||||
props.event.color = colorMap[color].color
|
props.event.color = colorMap[color].color
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
function redirect() {
|
||||||
|
if (props.event.referenceDocname) {
|
||||||
|
let name = props.event.referenceDoctype === 'CRM Lead' ? 'Lead' : 'Deal'
|
||||||
|
|
||||||
|
let params =
|
||||||
|
props.event.referenceDoctype == 'CRM Lead'
|
||||||
|
? { leadId: props.event.referenceDocname }
|
||||||
|
: { dealId: props.event.referenceDocname }
|
||||||
|
|
||||||
|
router.push({ name, params })
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
16
frontend/src/components/Icons/EventIcon.vue
Normal file
16
frontend/src/components/Icons/EventIcon.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M5.5 1C5.5 0.723858 5.27614 0.5 5 0.5C4.72386 0.5 4.5 0.723858 4.5 1V2.00057C3.42774 2.00446 2.83574 2.03488 2.36942 2.27248C1.89901 2.51217 1.51656 2.89462 1.27688 3.36502C1.00439 3.8998 1.00439 4.59987 1.00439 6V9.0642C1 9.33719 1 9.64625 1 10V11C1 12.4001 1 13.1002 1.27248 13.635C1.51217 14.1054 1.89462 14.4878 2.36502 14.7275C2.8998 15 3.59987 15 5 15L5.00439 15L11 15L11.0044 15L11.2588 14.9999C12.4914 14.9989 13.138 14.983 13.6394 14.7275C14.1098 14.4878 14.4922 14.1054 14.7319 13.635C15.0044 13.1002 15.0044 12.4001 15.0044 11V6C15.0044 4.59987 15.0044 3.8998 14.7319 3.36502C14.4922 2.89462 14.1098 2.51217 13.6394 2.27248C13.1718 2.03423 12.5778 2.0043 11.5 2.00054V1C11.5 0.723858 11.2761 0.5 11 0.5C10.7239 0.5 10.5 0.723858 10.5 1V2H5.5V1ZM10.5 4V3H5.5V4C5.5 4.27614 5.27614 4.5 5 4.5C4.72386 4.5 4.5 4.27614 4.5 4V3.00063C4.05122 3.0023 3.71688 3.00843 3.44383 3.03074C3.08879 3.05975 2.92633 3.11105 2.82341 3.16349C2.54117 3.3073 2.31169 3.53677 2.16788 3.81901C2.11544 3.92194 2.06414 4.0844 2.03513 4.43944C2.00517 4.80615 2.00439 5.28343 2.00439 6V6.49671C2.11748 6.41228 2.23805 6.33718 2.36502 6.27248C2.8998 6 3.59987 6 5 6H11C12.4001 6 13.1002 6 13.635 6.27248C13.7653 6.33886 13.8888 6.41619 14.0044 6.5033V6C14.0044 5.28343 14.0036 4.80615 13.9737 4.43944C13.9446 4.0844 13.8933 3.92194 13.8409 3.81901C13.6971 3.53677 13.4676 3.3073 13.1854 3.16349C13.0825 3.11105 12.92 3.05975 12.565 3.03074C12.2901 3.00829 11.9532 3.00222 11.5 3.00059V4C11.5 4.27614 11.2761 4.5 11 4.5C10.7239 4.5 10.5 4.27614 10.5 4ZM3.44383 13.9693C3.75328 13.9945 4.14147 13.999 4.68573 13.9998L4.87281 14L5.00439 14L11 14L11.0044 14L11.2621 13.9999C11.8362 13.9993 12.2405 13.9954 12.5606 13.9693C12.9156 13.9403 13.0781 13.889 13.181 13.8365C13.4632 13.6927 13.6927 13.4632 13.8365 13.181C13.889 13.0781 13.9403 12.9156 13.9693 12.5606C13.9992 12.1938 14 11.7166 14 11V10C14 9.28343 13.9992 8.80615 13.9693 8.43944C13.9403 8.0844 13.889 7.92194 13.8365 7.81901C13.6927 7.53677 13.4632 7.3073 13.181 7.16349C13.0781 7.11105 12.9156 7.05975 12.5606 7.03074C12.1939 7.00078 11.7166 7 11 7H5C4.28343 7 3.80615 7.00078 3.43944 7.03074C3.0844 7.05975 2.92194 7.11105 2.81901 7.16349C2.53677 7.3073 2.3073 7.53677 2.16349 7.81901C2.11105 7.92194 2.05975 8.0844 2.03074 8.43944C2.01608 8.61883 2.00841 8.82469 2.00439 9.07208V11C2.00439 11.7166 2.00517 12.1938 2.03513 12.5606C2.06414 12.9156 2.11544 13.0781 2.16788 13.181C2.31169 13.4632 2.54117 13.6927 2.82341 13.8365C2.92633 13.889 3.08879 13.9403 3.44383 13.9693ZM6.8125 10.4375C6.8125 9.78166 7.34416 9.25 8 9.25C8.65584 9.25 9.1875 9.78166 9.1875 10.4375C9.1875 11.0933 8.65584 11.625 8 11.625C7.34416 11.625 6.8125 11.0933 6.8125 10.4375ZM8 8.25C6.79188 8.25 5.8125 9.22938 5.8125 10.4375C5.8125 11.6456 6.79188 12.625 8 12.625C9.20812 12.625 10.1875 11.6456 10.1875 10.4375C10.1875 9.22938 9.20812 8.25 8 8.25Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
311
frontend/src/components/Modals/EventModal.vue
Normal file
311
frontend/src/components/Modals/EventModal.vue
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog v-model="show" :options="{ size: 'xl' }">
|
||||||
|
<template #body-title>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<h3 class="text-2xl font-semibold leading-6 text-ink-gray-9">
|
||||||
|
{{ editMode ? __('Edit an event') : __('Create an event') }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body-content>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="text-base text-ink-gray-7 w-3/12">
|
||||||
|
{{ __('Title') }}
|
||||||
|
</div>
|
||||||
|
<TextInput
|
||||||
|
ref="title"
|
||||||
|
class="w-9/12"
|
||||||
|
size="md"
|
||||||
|
v-model="_event.title"
|
||||||
|
:placeholder="__('Call with John Doe')"
|
||||||
|
variant="outline"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="text-base text-ink-gray-7 w-3/12">
|
||||||
|
{{ __('All day') }}
|
||||||
|
</div>
|
||||||
|
<Switch v-model="_event.isFullDay" />
|
||||||
|
</div>
|
||||||
|
<div class="border-t border-outline-gray-1" />
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="text-base text-ink-gray-7 w-3/12">
|
||||||
|
{{ __('Date & Time') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 w-9/12">
|
||||||
|
<DatePicker
|
||||||
|
:class="[_event.isFullDay ? 'w-full' : 'w-[160px]']"
|
||||||
|
variant="outline"
|
||||||
|
:value="_event.fromDate"
|
||||||
|
:formatter="(date) => getFormat(date, 'MMM D, YYYY')"
|
||||||
|
:placeholder="__('May 1, 2025')"
|
||||||
|
@update:modelValue="(date) => updateDate(date, true)"
|
||||||
|
>
|
||||||
|
<template #suffix="{ togglePopover }">
|
||||||
|
<FeatherIcon
|
||||||
|
name="chevron-down"
|
||||||
|
class="h-4 w-4 cursor-pointer"
|
||||||
|
@click="togglePopover"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</DatePicker>
|
||||||
|
<TimePicker
|
||||||
|
v-if="!_event.isFullDay"
|
||||||
|
variant="outline"
|
||||||
|
:value="_event.fromTime"
|
||||||
|
:placeholder="__('Start Time')"
|
||||||
|
@update:modelValue="(time) => updateTime(time, true)"
|
||||||
|
>
|
||||||
|
<template #suffix="{ togglePopover }">
|
||||||
|
<FeatherIcon
|
||||||
|
name="chevron-down"
|
||||||
|
class="h-4 w-4 cursor-pointer"
|
||||||
|
@click="togglePopover"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</TimePicker>
|
||||||
|
<TimePicker
|
||||||
|
v-if="!_event.isFullDay"
|
||||||
|
variant="outline"
|
||||||
|
:value="_event.toTime"
|
||||||
|
:placeholder="__('End Time')"
|
||||||
|
@update:modelValue="(time) => updateTime(time)"
|
||||||
|
>
|
||||||
|
<template #suffix="{ togglePopover }">
|
||||||
|
<FeatherIcon
|
||||||
|
name="chevron-down"
|
||||||
|
class="h-4 w-4 cursor-pointer"
|
||||||
|
@click="togglePopover"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</TimePicker>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<div class="mt-2 text-base text-ink-gray-7 w-3/12">
|
||||||
|
{{ __('Description') }}
|
||||||
|
</div>
|
||||||
|
<div class="w-9/12">
|
||||||
|
<TextEditor
|
||||||
|
editor-class="!prose-sm overflow-auto min-h-[80px] max-h-80 py-1.5 px-2 rounded border border-outline-gray-2 placeholder-ink-gray-4 hover:border-outline-gray-3 hover:border-outline-gray-modals hover:shadow-sm focus:bg-surface-white focus:border-outline-gray-4 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3 text-ink-gray-8 transition-colors"
|
||||||
|
:bubbleMenu="true"
|
||||||
|
:content="_event.description"
|
||||||
|
@change="(val) => (_event.description = val)"
|
||||||
|
:placeholder="__('Add description.')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #actions>
|
||||||
|
<div class="flex gap-2 justify-end">
|
||||||
|
<Button :label="__('Cancel')" @click="show = false" />
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
:label="editMode ? __('Update') : __('Create')"
|
||||||
|
:loading="editMode ? events.setValue.loading : events.insert.loading"
|
||||||
|
@click="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
Switch,
|
||||||
|
TextEditor,
|
||||||
|
ErrorMessage,
|
||||||
|
Dialog,
|
||||||
|
DatePicker,
|
||||||
|
dayjs,
|
||||||
|
} from 'frappe-ui'
|
||||||
|
import { getFormat } from '@/utils'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
event: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
doctype: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
docname: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const show = defineModel()
|
||||||
|
const events = defineModel('events')
|
||||||
|
|
||||||
|
const title = ref(null)
|
||||||
|
const error = ref(null)
|
||||||
|
const editMode = ref(false)
|
||||||
|
|
||||||
|
const _event = ref({
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
fromDate: '',
|
||||||
|
toDate: '',
|
||||||
|
fromTime: '',
|
||||||
|
toTime: '',
|
||||||
|
isFullDay: false,
|
||||||
|
eventType: 'Public',
|
||||||
|
color: 'green',
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.event) {
|
||||||
|
let start = dayjs(props.event.starts_on)
|
||||||
|
let end = dayjs(props.event.ends_on)
|
||||||
|
|
||||||
|
if (props.event.name) {
|
||||||
|
editMode.value = true
|
||||||
|
} else {
|
||||||
|
start = dayjs()
|
||||||
|
end = dayjs().add(1, 'hour')
|
||||||
|
}
|
||||||
|
|
||||||
|
_event.value = {
|
||||||
|
id: props.event.name || '',
|
||||||
|
title: props.event.subject,
|
||||||
|
description: props.event.description,
|
||||||
|
fromDate: start.format('YYYY-MM-DD'),
|
||||||
|
toDate: end.format('YYYY-MM-DD'),
|
||||||
|
fromTime: start.format('HH:mm'),
|
||||||
|
toTime: end.format('HH:mm'),
|
||||||
|
isFullDay: props.event.all_day,
|
||||||
|
eventType: props.event.event_type,
|
||||||
|
color: props.event.color,
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => title.value?.el?.focus(), 100)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function updateDate(d) {
|
||||||
|
_event.value.fromDate = d
|
||||||
|
_event.value.toDate = d
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTime(t, fromTime = false) {
|
||||||
|
error.value = null
|
||||||
|
let oldTo = _event.value.toTime || _event.value.fromTime
|
||||||
|
|
||||||
|
if (fromTime) {
|
||||||
|
_event.value.fromTime = t
|
||||||
|
if (!_event.value.toTime) {
|
||||||
|
const hour = parseInt(t.split(':')[0])
|
||||||
|
const minute = parseInt(t.split(':')[1])
|
||||||
|
_event.value.toTime = `${hour + 1}:${minute}`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_event.value.toTime = t
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_event.value.toTime && _event.value.fromTime) {
|
||||||
|
const diff = dayjs(_event.value.toDate + ' ' + _event.value.toTime).diff(
|
||||||
|
dayjs(_event.value.fromDate + ' ' + _event.value.fromTime),
|
||||||
|
'minute',
|
||||||
|
)
|
||||||
|
|
||||||
|
if (diff <= 0) {
|
||||||
|
_event.value.toTime = oldTo
|
||||||
|
error.value = __('End time should be after start time')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
error.value = null
|
||||||
|
if (!_event.value.title) {
|
||||||
|
error.value = __('Title is required')
|
||||||
|
title.value.el.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_event.value.id ? updateEvent() : createEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEvent() {
|
||||||
|
events.value.insert.submit(
|
||||||
|
{
|
||||||
|
subject: _event.value.title,
|
||||||
|
description: _event.value.description,
|
||||||
|
starts_on: _event.value.fromDate + ' ' + _event.value.fromTime,
|
||||||
|
ends_on: _event.value.toDate + ' ' + _event.value.toTime,
|
||||||
|
all_day: _event.value.isFullDay || false,
|
||||||
|
event_type: _event.value.eventType,
|
||||||
|
color: _event.value.color,
|
||||||
|
reference_doctype: props.doctype,
|
||||||
|
reference_docname: props.docname,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: async () => {
|
||||||
|
await events.value.reload()
|
||||||
|
show.value = false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateEvent() {
|
||||||
|
if (!_event.value.id) {
|
||||||
|
error.value = __('Event ID is required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
events.value.setValue.submit(
|
||||||
|
{
|
||||||
|
name: _event.value.id,
|
||||||
|
subject: _event.value.title,
|
||||||
|
description: _event.value.description,
|
||||||
|
starts_on: _event.value.fromDate + ' ' + _event.value.fromTime,
|
||||||
|
ends_on: _event.value.toDate + ' ' + _event.value.toTime,
|
||||||
|
all_day: _event.value.isFullDay,
|
||||||
|
event_type: _event.value.eventType,
|
||||||
|
color: _event.value.color,
|
||||||
|
reference_doctype: props.doctype,
|
||||||
|
reference_docname: props.docname,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: async () => {
|
||||||
|
await events.value.reload()
|
||||||
|
show.value = false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// function deleteEvent(eventID) {
|
||||||
|
// if (!eventID) return
|
||||||
|
|
||||||
|
// $dialog({
|
||||||
|
// title: __('Delete'),
|
||||||
|
// message: __('Are you sure you want to delete this event?'),
|
||||||
|
// actions: [
|
||||||
|
// {
|
||||||
|
// label: __('Delete'),
|
||||||
|
// variant: 'solid',
|
||||||
|
// theme: 'red',
|
||||||
|
// onClick: (close) => {
|
||||||
|
// events.delete.submit(eventID, {
|
||||||
|
// onSuccess: () => events.reload(),
|
||||||
|
// })
|
||||||
|
// showEventPanel.value = false
|
||||||
|
// event.value = {}
|
||||||
|
// activeEvent.value = ''
|
||||||
|
// mode.value = ''
|
||||||
|
// close()
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
</script>
|
||||||
@ -21,9 +21,11 @@
|
|||||||
<template #body-content>
|
<template #body-content>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div>
|
<div>
|
||||||
<FormControl
|
<div class="mb-1.5 text-xs text-ink-gray-5">
|
||||||
|
{{ __('Title') }}
|
||||||
|
</div>
|
||||||
|
<TextInput
|
||||||
ref="title"
|
ref="title"
|
||||||
:label="__('Title')"
|
|
||||||
v-model="_task.title"
|
v-model="_task.title"
|
||||||
:placeholder="__('Call with John Doe')"
|
:placeholder="__('Call with John Doe')"
|
||||||
required
|
required
|
||||||
@ -225,8 +227,8 @@ async function updateTask() {
|
|||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
editMode.value = false
|
editMode.value = false
|
||||||
|
setTimeout(() => title.value?.el?.focus?.(), 100)
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
title.value?.el?.focus?.()
|
|
||||||
_task.value = { ...props.task }
|
_task.value = { ...props.task }
|
||||||
if (_task.value.title) {
|
if (_task.value.title) {
|
||||||
editMode.value = true
|
editMode.value = true
|
||||||
|
|||||||
@ -129,6 +129,8 @@ const events = createListResource({
|
|||||||
'all_day',
|
'all_day',
|
||||||
'event_type',
|
'event_type',
|
||||||
'color',
|
'color',
|
||||||
|
'reference_doctype',
|
||||||
|
'reference_docname',
|
||||||
],
|
],
|
||||||
filters: { status: 'Open', owner: user },
|
filters: { status: 'Open', owner: user },
|
||||||
auto: true,
|
auto: true,
|
||||||
@ -151,22 +153,11 @@ const events = createListResource({
|
|||||||
isFullDay: event.all_day,
|
isFullDay: event.all_day,
|
||||||
eventType: event.event_type,
|
eventType: event.event_type,
|
||||||
color: event.color,
|
color: event.color,
|
||||||
|
referenceDoctype: event.reference_doctype,
|
||||||
|
referenceDocname: event.reference_docname,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
insert: {
|
|
||||||
onSuccess: async (e) => {
|
|
||||||
await events.reload()
|
|
||||||
showDetails({ id: e.name })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
delete: { onSuccess: () => events.reload() },
|
|
||||||
setValue: {
|
|
||||||
onSuccess: async (e) => {
|
|
||||||
await events.reload()
|
|
||||||
showEventPanel.value && showDetails({ id: e.name })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function saveEvent(_event) {
|
function saveEvent(_event) {
|
||||||
@ -184,31 +175,51 @@ function saveEvent(_event) {
|
|||||||
function createEvent(_event) {
|
function createEvent(_event) {
|
||||||
if (!_event.title) return
|
if (!_event.title) return
|
||||||
|
|
||||||
events.insert.submit({
|
events.insert.submit(
|
||||||
subject: _event.title,
|
{
|
||||||
description: _event.description,
|
subject: _event.title,
|
||||||
starts_on: _event.fromDate + ' ' + _event.fromTime,
|
description: _event.description,
|
||||||
ends_on: _event.toDate + ' ' + _event.toTime,
|
starts_on: _event.fromDate + ' ' + _event.fromTime,
|
||||||
all_day: _event.isFullDay || false,
|
ends_on: _event.toDate + ' ' + _event.toTime,
|
||||||
event_type: _event.eventType,
|
all_day: _event.isFullDay || false,
|
||||||
color: _event.color,
|
event_type: _event.eventType,
|
||||||
})
|
color: _event.color,
|
||||||
|
reference_doctype: _event.referenceDoctype,
|
||||||
|
reference_docname: _event.referenceDocname,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: async (e) => {
|
||||||
|
await events.reload()
|
||||||
|
showDetails({ id: e.name })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateEvent(_event) {
|
function updateEvent(_event) {
|
||||||
if (!_event.id) return
|
if (!_event.id) return
|
||||||
|
|
||||||
if (!mode.value || mode.value === 'edit' || mode.value === 'details') {
|
if (!mode.value || mode.value === 'edit' || mode.value === 'details') {
|
||||||
events.setValue.submit({
|
events.setValue.submit(
|
||||||
name: _event.id,
|
{
|
||||||
subject: _event.title,
|
name: _event.id,
|
||||||
description: _event.description,
|
subject: _event.title,
|
||||||
starts_on: _event.fromDate + ' ' + _event.fromTime,
|
description: _event.description,
|
||||||
ends_on: _event.toDate + ' ' + _event.toTime,
|
starts_on: _event.fromDate + ' ' + _event.fromTime,
|
||||||
all_day: _event.isFullDay,
|
ends_on: _event.toDate + ' ' + _event.toTime,
|
||||||
event_type: _event.eventType,
|
all_day: _event.isFullDay,
|
||||||
color: _event.color,
|
event_type: _event.eventType,
|
||||||
})
|
color: _event.color,
|
||||||
|
reference_doctype: _event.referenceDoctype,
|
||||||
|
reference_docname: _event.referenceDocname,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: async (e) => {
|
||||||
|
await events.reload()
|
||||||
|
showEventPanel.value && showDetails({ id: e.name })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
event.value = _event
|
event.value = _event
|
||||||
@ -226,7 +237,9 @@ function deleteEvent(eventID) {
|
|||||||
variant: 'solid',
|
variant: 'solid',
|
||||||
theme: 'red',
|
theme: 'red',
|
||||||
onClick: (close) => {
|
onClick: (close) => {
|
||||||
events.delete.submit(eventID)
|
events.delete.submit(eventID, {
|
||||||
|
onSuccess: () => events.reload(),
|
||||||
|
})
|
||||||
showEventPanel.value = false
|
showEventPanel.value = false
|
||||||
event.value = {}
|
event.value = {}
|
||||||
activeEvent.value = ''
|
activeEvent.value = ''
|
||||||
|
|||||||
@ -324,6 +324,7 @@ import EmailIcon from '@/components/Icons/EmailIcon.vue'
|
|||||||
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
||||||
import CommentIcon from '@/components/Icons/CommentIcon.vue'
|
import CommentIcon from '@/components/Icons/CommentIcon.vue'
|
||||||
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
|
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
|
||||||
|
import EventIcon from '@/components/Icons/EventIcon.vue'
|
||||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||||
import TaskIcon from '@/components/Icons/TaskIcon.vue'
|
import TaskIcon from '@/components/Icons/TaskIcon.vue'
|
||||||
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
||||||
@ -541,6 +542,11 @@ const tabs = computed(() => {
|
|||||||
label: __('Data'),
|
label: __('Data'),
|
||||||
icon: DetailsIcon,
|
icon: DetailsIcon,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Events',
|
||||||
|
label: __('Events'),
|
||||||
|
icon: EventIcon,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Calls',
|
name: 'Calls',
|
||||||
label: __('Calls'),
|
label: __('Calls'),
|
||||||
|
|||||||
@ -224,6 +224,7 @@ import EmailIcon from '@/components/Icons/EmailIcon.vue'
|
|||||||
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
import Email2Icon from '@/components/Icons/Email2Icon.vue'
|
||||||
import CommentIcon from '@/components/Icons/CommentIcon.vue'
|
import CommentIcon from '@/components/Icons/CommentIcon.vue'
|
||||||
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
|
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
|
||||||
|
import EventIcon from '@/components/Icons/EventIcon.vue'
|
||||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||||
import TaskIcon from '@/components/Icons/TaskIcon.vue'
|
import TaskIcon from '@/components/Icons/TaskIcon.vue'
|
||||||
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
import NoteIcon from '@/components/Icons/NoteIcon.vue'
|
||||||
@ -398,6 +399,11 @@ const tabs = computed(() => {
|
|||||||
label: __('Data'),
|
label: __('Data'),
|
||||||
icon: DetailsIcon,
|
icon: DetailsIcon,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Events',
|
||||||
|
label: __('Events'),
|
||||||
|
icon: EventIcon,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Calls',
|
name: 'Calls',
|
||||||
label: __('Calls'),
|
label: __('Calls'),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user