fix: added create/update task feature

This commit is contained in:
Shariq Ansari 2023-09-28 18:39:43 +05:30
parent ca0a4e0f26
commit 02348542c6
6 changed files with 375 additions and 8 deletions

View File

@ -8,10 +8,22 @@
variant="solid" variant="solid"
@click="makeCall(lead.data.mobile_no)" @click="makeCall(lead.data.mobile_no)"
> >
<PhoneIcon class="h-4 w-4" /> <template #prefix>
<PhoneIcon class="h-4 w-4" />
</template>
<span>Make a call</span>
</Button> </Button>
<Button v-else-if="title == 'Notes'" variant="solid" @click="showNote"> <Button v-else-if="title == 'Notes'" variant="solid" @click="showNote()">
<FeatherIcon name="plus" class="h-4 w-4" /> <template #prefix>
<FeatherIcon name="plus" class="h-4 w-4" />
</template>
<span>New note</span>
</Button>
<Button v-else-if="title == 'Tasks'" variant="solid" @click="showTask()">
<template #prefix>
<FeatherIcon name="plus" class="h-4 w-4" />
</template>
<span>New task</span>
</Button> </Button>
</div> </div>
<div v-if="activities?.length" class="flex-1 overflow-y-auto"> <div v-if="activities?.length" class="flex-1 overflow-y-auto">
@ -65,6 +77,28 @@
</div> </div>
</div> </div>
</div> </div>
<div v-else-if="title == 'Tasks'">
<div v-for="(task, i) in activities">
<div class="grid grid-cols-[30px_minmax(auto,_1fr)] gap-4 px-10">
<div
class="relative flex justify-center after:absolute after:left-[50%] after:top-0 after:-z-10 after:border-l after:border-gray-200"
:class="i != activities.length - 1 ? 'after:h-full' : 'after:h-4'"
>
<div
class="z-10 mt-[15px] flex h-7 w-7 items-center justify-center rounded-full bg-gray-100"
>
<TaskIcon class="text-gray-800" />
</div>
</div>
<div
class="mb-3 flex max-w-[70%] flex-col gap-3 rounded-md bg-gray-50 p-4"
@click="showTask(task)"
>
{{ task.title }}
</div>
</div>
</div>
</div>
<div v-else-if="title == 'Calls'"> <div v-else-if="title == 'Calls'">
<div v-for="(call, i) in activities"> <div v-for="(call, i) in activities">
<div class="grid grid-cols-[30px_minmax(auto,_1fr)] gap-4 px-10"> <div class="grid grid-cols-[30px_minmax(auto,_1fr)] gap-4 px-10">
@ -468,7 +502,7 @@
v-else-if="title == 'Notes'" v-else-if="title == 'Notes'"
variant="solid" variant="solid"
label="Create note" label="Create note"
@click="showNote" @click="showNote()"
/> />
<Button <Button
v-else-if="title == 'Emails'" v-else-if="title == 'Emails'"
@ -476,7 +510,12 @@
label="Send email" label="Send email"
@click="$refs.emailBox.show = true" @click="$refs.emailBox.show = true"
/> />
<Button v-else-if="title == 'Tasks'" variant="solid" label="Create task" /> <Button
v-else-if="title == 'Tasks'"
variant="solid"
label="Create task"
@click="showTask()"
/>
</div> </div>
<CommunicationArea <CommunicationArea
ref="emailBox" ref="emailBox"
@ -485,6 +524,12 @@
v-model:reload="reload_email" v-model:reload="reload_email"
/> />
<NoteModal v-model="showNoteModal" :note="note" @updateNote="updateNote" /> <NoteModal v-model="showNoteModal" :note="note" @updateNote="updateNote" />
<TaskModal
v-model="showTaskModal"
v-model:reloadTasks="tasks"
:task="task"
:lead="lead.data?.name"
/>
</template> </template>
<script setup> <script setup>
import UserAvatar from '@/components/UserAvatar.vue' import UserAvatar from '@/components/UserAvatar.vue'
@ -502,6 +547,7 @@ import InboundCallIcon from '@/components/Icons/InboundCallIcon.vue'
import OutboundCallIcon from '@/components/Icons/OutboundCallIcon.vue' import OutboundCallIcon from '@/components/Icons/OutboundCallIcon.vue'
import CommunicationArea from '@/components/CommunicationArea.vue' import CommunicationArea from '@/components/CommunicationArea.vue'
import NoteModal from '@/components/NoteModal.vue' import NoteModal from '@/components/NoteModal.vue'
import TaskModal from '@/components/TaskModal.vue'
import { import {
timeAgo, timeAgo,
dateFormat, dateFormat,
@ -610,6 +656,27 @@ const notes = createListResource({
auto: true, auto: true,
}) })
const tasks = createListResource({
type: 'list',
doctype: 'CRM Task',
cache: ['Tasks', lead.value.data.name],
fields: [
'name',
'title',
'description',
'assigned_to',
'assigned_to',
'due_date',
'priority',
'status',
'modified',
],
filters: { lead: lead.value.data.name },
orderBy: 'modified desc',
pageLength: 999,
auto: true,
})
function all_activities() { function all_activities() {
if (!versions.data) return [] if (!versions.data) return []
if (!calls.data) return versions.data if (!calls.data) return versions.data
@ -628,6 +695,8 @@ const activities = computed(() => {
) )
} else if (props.title == 'Calls') { } else if (props.title == 'Calls') {
return calls.data return calls.data
} else if (props.title == 'Tasks') {
return tasks.data
} else if (props.title == 'Notes') { } else if (props.title == 'Notes') {
return notes.data return notes.data
} }
@ -727,6 +796,7 @@ function timelineIcon(activity_type, is_lead) {
return markRaw(icon) return markRaw(icon)
} }
// Notes
const showNoteModal = ref(false) const showNoteModal = ref(false)
const note = ref({ const note = ref({
title: '', title: '',
@ -765,7 +835,7 @@ async function updateNote(note) {
doctype: 'CRM Note', doctype: 'CRM Note',
title: note.title, title: note.title,
content: note.content, content: note.content,
lead: props.leadId, lead: lead.value.data.name,
}, },
}) })
if (d.name) { if (d.name) {
@ -774,6 +844,22 @@ async function updateNote(note) {
} }
} }
// Tasks
const showTaskModal = ref(false)
const task = ref({})
function showTask(t) {
task.value = t || {
title: '',
description: '',
assigned_to: '',
due_date: '',
priority: 'Low',
status: 'Backlog',
}
showTaskModal.value = true
}
watch([reload, reload_email], ([reload_value, reload_email_value]) => { watch([reload, reload_email], ([reload_value, reload_email_value]) => {
if (reload_value || reload_email_value) { if (reload_value || reload_email_value) {
versions.reload() versions.reload()

View File

@ -0,0 +1,20 @@
<template>
<div class="grid place-items-center">
<div
class="h-3 w-3 rounded-full"
:class="{
'bg-red-500': priority === 'High',
'bg-yellow-500': priority === 'Medium',
'bg-gray-300': priority === 'Low',
}"
></div>
</div>
</template>
<script setup>
const props = defineProps({
priority: {
type: String,
required: true,
},
})
</script>

View File

@ -0,0 +1,54 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="none"
class="text-gray-700"
:aria-label="status"
>
<path
v-if="status == 'Backlog'"
fill="currentColor"
fill-rule="evenodd"
d="M4 5 2.576 3.574A6.968 6.968 0 0 0 1.043 7.22H3.06c.13-.824.46-1.582.94-2.22Zm-.948 3.72H1.037a6.968 6.968 0 0 0 1.54 3.707l1.425-1.424a4.975 4.975 0 0 1-.95-2.283Zm2.02 3.333-1.427 1.428a6.966 6.966 0 0 0 3.6 1.479v-2.017a4.972 4.972 0 0 1-2.173-.89Zm3.673.892v2.016a6.968 6.968 0 0 0 3.68-1.537L11.001 12a4.974 4.974 0 0 1-2.256.945Zm3.307-2.015 1.427 1.427a6.967 6.967 0 0 0 1.484-3.637H12.95a4.973 4.973 0 0 1-.897 2.21Zm.888-3.71h2.017a6.967 6.967 0 0 0-1.476-3.575l-1.428 1.427c.452.623.762 1.355.887 2.148Zm-1.937-3.218 1.424-1.425A6.968 6.968 0 0 0 8.745 1.04v2.016c.839.125 1.61.459 2.258.947Zm-3.758-.945V1.04a6.966 6.966 0 0 0-3.602 1.48L5.07 3.949a4.973 4.973 0 0 1 2.175-.891Z"
clip-rule="evenodd"
/>
<path
v-else-if="status == 'Todo'"
fill="currentColor"
fill-rule="evenodd"
d="M8 13A5 5 0 1 0 8 3a5 5 0 0 0 0 10Zm0 2A7 7 0 1 0 8 1a7 7 0 0 0 0 14Z"
clip-rule="evenodd"
/>
<path
v-else-if="status == 'In Progress'"
fill="currentColor"
fill-rule="evenodd"
d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14Zm0-1.75a5.25 5.25 0 1 1 0-10.5 5.25 5.25 0 0 1 0 10.5Zm2.828-2.422A4 4 0 0 1 8 12V4a4 4 0 0 1 2.828 6.828Z"
clip-rule="evenodd"
/>
<path
v-else-if="status == 'Done'"
fill="currentColor"
fill-rule="evenodd"
d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14Zm3.173-9.015a.5.5 0 0 0-.772-.636l-3.45 4.183L5.612 7.8a.5.5 0 1 0-.792.612l1.725 2.228a.5.5 0 0 0 .781.013l3.848-4.667Z"
clip-rule="evenodd"
/>
<path
v-else-if="status == 'Canceled'"
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
d="M8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15ZM5.14645 5.14645C4.95118 5.34171 4.95118 5.65829 5.14645 5.85355L7.29289 8L5.14645 10.1464C4.95118 10.3417 4.95118 10.6583 5.14645 10.8536C5.34171 11.0488 5.65829 11.0488 5.85355 10.8536L8 8.70711L10.1464 10.8536C10.3417 11.0488 10.6583 11.0488 10.8536 10.8536C11.0488 10.6583 11.0488 10.3417 10.8536 10.1464L8.70711 8L10.8536 5.85355C11.0488 5.65829 11.0488 5.34171 10.8536 5.14645C10.6583 4.95118 10.3417 4.95118 10.1464 5.14645L8 7.29289L5.85355 5.14645C5.65829 4.95118 5.34171 4.95118 5.14645 5.14645Z"
/>
</svg>
</template>
<script setup>
const props = defineProps({
status: {
type: String,
required: true,
},
})
</script>

View File

@ -0,0 +1,191 @@
<template>
<Dialog
v-model="show"
:options="{
title: editMode ? 'Edit Task' : 'Create Task',
size: 'xl',
actions: [
{
label: editMode ? 'Update' : 'Create',
variant: 'solid',
onClick: ({ close }) => updateTask(close),
},
],
}"
>
<template #body-content>
<div class="flex flex-col gap-4">
<div>
<div class="mb-1.5 text-sm text-gray-600">Title</div>
<TextInput
ref="title"
variant="outline"
v-model="_task.title"
placeholder="Add title"
/>
</div>
<div>
<div class="mb-1.5 text-sm text-gray-600">Description</div>
<TextEditor
variant="outline"
ref="description"
editor-class="!prose-sm overflow-auto min-h-[80px] max-h-80 py-1.5 px-2 rounded border border-gray-300 bg-white hover:border-gray-400 hover:shadow-sm focus:bg-white focus:border-gray-500 focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-gray-400 text-gray-800 transition-colors"
:bubbleMenu="true"
:content="_task.description"
@change="(val) => (_task.description = val)"
placeholder="Type a description"
/>
</div>
<div class="flex items-center gap-2">
<Dropdown :options="statusOptions()">
<Button :label="_task.status" class="w-full justify-between">
<template #prefix>
<TaskStatusIcon :status="_task.status" />
</template>
</Button>
</Dropdown>
<Autocomplete
:options="activeAgents"
:value="getUser(_task.assigned_to).full_name"
@change="(option) => (_task.assigned_to = option.email)"
placeholder="Assignee"
>
<template #prefix>
<UserAvatar class="mr-2 !h-4 !w-4" :user="_task.assigned_to" />
</template>
<template #item-prefix="{ option }">
<UserAvatar class="mr-2" :user="option.email" size="sm" />
</template>
</Autocomplete>
<DatePicker
class="datepicker w-36"
v-model="_task.due_date"
placeholder="Due date"
input-class="border-none"
:formatValue="(val) => val.split('-').reverse().join('-')"
/>
<Dropdown :options="priorityOptions()">
<Button :label="_task.priority" class="w-full justify-between">
<template #prefix>
<TaskPriorityIcon :priority="_task.priority" />
</template>
</Button>
</Dropdown>
</div>
</div>
</template>
</Dialog>
</template>
<script setup>
import TaskStatusIcon from '@/components/Icons/TaskStatusIcon.vue'
import TaskPriorityIcon from '@/components/Icons/TaskPriorityIcon.vue'
import UserAvatar from '@/components/UserAvatar.vue'
import { activeAgents } from '@/utils'
import { usersStore } from '@/stores/users'
import {
TextInput,
TextEditor,
Dialog,
Dropdown,
Autocomplete,
DatePicker,
call,
} from 'frappe-ui'
import { ref, defineModel, h, watch, nextTick } from 'vue'
const props = defineProps({
task: {
type: Object,
default: {},
},
lead: {
type: String,
default: '',
},
})
const show = defineModel()
const tasks = defineModel('reloadTasks')
const emit = defineEmits(['updateTask'])
const title = ref(null)
const editMode = ref(false)
const _task = ref({})
const { getUser } = usersStore()
function statusOptions() {
return ['Backlog', 'Todo', 'In Progress', 'Done', 'Canceled'].map(
(status) => {
return {
icon: () => h(TaskStatusIcon, { status }),
label: status,
onClick: () => {
_task.value.status = status
},
}
}
)
}
function priorityOptions() {
return ['Low', 'Medium', 'High'].map((priority) => {
return {
label: priority,
icon: () => h(TaskPriorityIcon, { priority }),
onClick: () => {
_task.value.priority = priority
},
}
})
}
async function updateTask(close) {
if (_task.value.name) {
let d = await call('frappe.client.set_value', {
doctype: 'CRM Task',
name: _task.value.name,
fieldname: _task.value,
})
if (d.name) {
tasks.value.reload()
}
} else {
let d = await call('frappe.client.insert', {
doc: {
doctype: 'CRM Task',
lead: props.lead || null,
..._task.value,
},
})
if (d.name) {
tasks.value.reload()
}
}
close()
}
watch(
() => show.value,
(value) => {
if (!value) return
editMode.value = false
nextTick(() => {
title.value.el.focus()
_task.value = { ...props.task }
if (_task.value.title) {
editMode.value = true
}
})
}
)
</script>
<style scoped>
:deep(.datepicker svg) {
width: 0.875rem;
height: 0.875rem;
}
</style>

View File

@ -1,6 +1,5 @@
<template> <template>
<Avatar <Avatar
v-if="user"
:label="getUser(user).full_name" :label="getUser(user).full_name"
:image="getUser(user).user_image" :image="getUser(user).user_image"
v-bind="$attrs" v-bind="$attrs"

View File

@ -1,7 +1,8 @@
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue' import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import { usersStore } from '@/stores/users'
import { useDateFormat, useTimeAgo } from '@vueuse/core' import { useDateFormat, useTimeAgo } from '@vueuse/core'
import { toast } from 'frappe-ui' import { toast } from 'frappe-ui'
import { h } from 'vue' import { h, computed } from 'vue'
export function createToast(options) { export function createToast(options) {
toast({ toast({
@ -128,3 +129,19 @@ export function formatNumberIntoCurrency(value) {
export function startCase(str) { export function startCase(str) {
return str.charAt(0).toUpperCase() + str.slice(1) return str.charAt(0).toUpperCase() + str.slice(1)
} }
const { users } = usersStore()
export const activeAgents = computed(() => {
const nonAgents = ['Administrator', 'Guest']
return users.data
.filter((user) => !nonAgents.includes(user.name))
.sort((a, b) => a.full_name - b.full_name)
.map((user) => {
return {
label: user.full_name,
value: user.email,
...user,
}
})
})