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"
@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 v-else-if="title == 'Notes'" variant="solid" @click="showNote">
<FeatherIcon name="plus" class="h-4 w-4" />
<Button v-else-if="title == 'Notes'" variant="solid" @click="showNote()">
<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>
</div>
<div v-if="activities?.length" class="flex-1 overflow-y-auto">
@ -65,6 +77,28 @@
</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-for="(call, i) in activities">
<div class="grid grid-cols-[30px_minmax(auto,_1fr)] gap-4 px-10">
@ -468,7 +502,7 @@
v-else-if="title == 'Notes'"
variant="solid"
label="Create note"
@click="showNote"
@click="showNote()"
/>
<Button
v-else-if="title == 'Emails'"
@ -476,7 +510,12 @@
label="Send email"
@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>
<CommunicationArea
ref="emailBox"
@ -485,6 +524,12 @@
v-model:reload="reload_email"
/>
<NoteModal v-model="showNoteModal" :note="note" @updateNote="updateNote" />
<TaskModal
v-model="showTaskModal"
v-model:reloadTasks="tasks"
:task="task"
:lead="lead.data?.name"
/>
</template>
<script setup>
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 CommunicationArea from '@/components/CommunicationArea.vue'
import NoteModal from '@/components/NoteModal.vue'
import TaskModal from '@/components/TaskModal.vue'
import {
timeAgo,
dateFormat,
@ -610,6 +656,27 @@ const notes = createListResource({
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() {
if (!versions.data) return []
if (!calls.data) return versions.data
@ -628,6 +695,8 @@ const activities = computed(() => {
)
} else if (props.title == 'Calls') {
return calls.data
} else if (props.title == 'Tasks') {
return tasks.data
} else if (props.title == 'Notes') {
return notes.data
}
@ -727,6 +796,7 @@ function timelineIcon(activity_type, is_lead) {
return markRaw(icon)
}
// Notes
const showNoteModal = ref(false)
const note = ref({
title: '',
@ -765,7 +835,7 @@ async function updateNote(note) {
doctype: 'CRM Note',
title: note.title,
content: note.content,
lead: props.leadId,
lead: lead.value.data.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]) => {
if (reload_value || reload_email_value) {
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>
<Avatar
v-if="user"
:label="getUser(user).full_name"
:image="getUser(user).user_image"
v-bind="$attrs"

View File

@ -1,7 +1,8 @@
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import { usersStore } from '@/stores/users'
import { useDateFormat, useTimeAgo } from '@vueuse/core'
import { toast } from 'frappe-ui'
import { h } from 'vue'
import { h, computed } from 'vue'
export function createToast(options) {
toast({
@ -128,3 +129,19 @@ export function formatNumberIntoCurrency(value) {
export function startCase(str) {
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,
}
})
})