fix: added create/update task feature
This commit is contained in:
parent
ca0a4e0f26
commit
02348542c6
@ -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()
|
||||
|
||||
20
frontend/src/components/Icons/TaskPriorityIcon.vue
Normal file
20
frontend/src/components/Icons/TaskPriorityIcon.vue
Normal 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>
|
||||
54
frontend/src/components/Icons/TaskStatusIcon.vue
Normal file
54
frontend/src/components/Icons/TaskStatusIcon.vue
Normal 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>
|
||||
191
frontend/src/components/TaskModal.vue
Normal file
191
frontend/src/components/TaskModal.vue
Normal 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>
|
||||
@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<Avatar
|
||||
v-if="user"
|
||||
:label="getUser(user).full_name"
|
||||
:image="getUser(user).user_image"
|
||||
v-bind="$attrs"
|
||||
|
||||
@ -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,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user