Merge pull request #44 from shariquerik/assignment
feat: Multiple assignment in Lead/Deal
This commit is contained in:
commit
2a51773225
@ -107,6 +107,7 @@ def get_list_data(doctype: str, filters: dict, order_by: str):
|
|||||||
"value": "modified_by",
|
"value": "modified_by",
|
||||||
"options": "User",
|
"options": "User",
|
||||||
},
|
},
|
||||||
|
{"label": "Assigned To", "type": "Text", "value": "_assign"},
|
||||||
{"label": "Owner", "type": "Link", "value": "owner", "options": "User"},
|
{"label": "Owner", "type": "Link", "value": "owner", "options": "User"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -27,4 +27,5 @@ def get_deal(name):
|
|||||||
)
|
)
|
||||||
|
|
||||||
deal["doctype_fields"] = get_doctype_fields("CRM Deal")
|
deal["doctype_fields"] = get_doctype_fields("CRM Deal")
|
||||||
|
deal["doctype"] = "CRM Deal"
|
||||||
return deal
|
return deal
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.desk.form.assign_to import add as assign
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla
|
from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla
|
||||||
@ -15,6 +17,12 @@ class CRMDeal(Document):
|
|||||||
def validate(self):
|
def validate(self):
|
||||||
self.set_primary_contact()
|
self.set_primary_contact()
|
||||||
self.set_primary_email_mobile_no()
|
self.set_primary_email_mobile_no()
|
||||||
|
if self.deal_owner and not self.is_new():
|
||||||
|
self.assign_agent(self.deal_owner)
|
||||||
|
|
||||||
|
def after_insert(self):
|
||||||
|
if self.deal_owner:
|
||||||
|
self.assign_agent(self.deal_owner)
|
||||||
|
|
||||||
def before_save(self):
|
def before_save(self):
|
||||||
self.apply_sla()
|
self.apply_sla()
|
||||||
@ -53,6 +61,19 @@ class CRMDeal(Document):
|
|||||||
self.email = ""
|
self.email = ""
|
||||||
self.mobile_no = ""
|
self.mobile_no = ""
|
||||||
|
|
||||||
|
def assign_agent(self, agent):
|
||||||
|
if not agent:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._assign:
|
||||||
|
assignees = json.loads(self._assign)
|
||||||
|
for assignee in assignees:
|
||||||
|
if agent == assignee:
|
||||||
|
# the agent is already set as an assignee
|
||||||
|
return
|
||||||
|
|
||||||
|
assign({"assign_to": [agent], "doctype": "CRM Deal", "name": self.name})
|
||||||
|
|
||||||
def set_sla(self):
|
def set_sla(self):
|
||||||
"""
|
"""
|
||||||
Find an SLA to apply to the deal.
|
Find an SLA to apply to the deal.
|
||||||
@ -119,10 +140,9 @@ class CRMDeal(Document):
|
|||||||
'width': '11rem',
|
'width': '11rem',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': 'Deal Owner',
|
'label': 'Assigned To',
|
||||||
'type': 'Link',
|
'type': 'Text',
|
||||||
'key': 'deal_owner',
|
'key': '_assign',
|
||||||
'options': 'User',
|
|
||||||
'width': '10rem',
|
'width': '10rem',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -145,6 +165,7 @@ class CRMDeal(Document):
|
|||||||
"first_response_time",
|
"first_response_time",
|
||||||
"first_responded_on",
|
"first_responded_on",
|
||||||
"modified",
|
"modified",
|
||||||
|
"_assign",
|
||||||
]
|
]
|
||||||
return {'columns': columns, 'rows': rows}
|
return {'columns': columns, 'rows': rows}
|
||||||
|
|
||||||
|
|||||||
@ -15,4 +15,5 @@ def get_lead(name):
|
|||||||
lead = lead.pop()
|
lead = lead.pop()
|
||||||
|
|
||||||
lead["doctype_fields"] = get_doctype_fields("CRM Lead")
|
lead["doctype_fields"] = get_doctype_fields("CRM Lead")
|
||||||
|
lead["doctype"] = "CRM Lead"
|
||||||
return lead
|
return lead
|
||||||
@ -1,8 +1,10 @@
|
|||||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.desk.form.assign_to import add as assign
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
from frappe.utils import has_gravatar, validate_email_address
|
from frappe.utils import has_gravatar, validate_email_address
|
||||||
@ -18,6 +20,12 @@ class CRMLead(Document):
|
|||||||
self.set_lead_name()
|
self.set_lead_name()
|
||||||
self.set_title()
|
self.set_title()
|
||||||
self.validate_email()
|
self.validate_email()
|
||||||
|
if self.lead_owner and not self.is_new():
|
||||||
|
self.assign_agent(self.lead_owner)
|
||||||
|
|
||||||
|
def after_insert(self):
|
||||||
|
if self.lead_owner:
|
||||||
|
self.assign_agent(self.lead_owner)
|
||||||
|
|
||||||
def before_save(self):
|
def before_save(self):
|
||||||
self.apply_sla()
|
self.apply_sla()
|
||||||
@ -54,6 +62,19 @@ class CRMLead(Document):
|
|||||||
if self.is_new() or not self.image:
|
if self.is_new() or not self.image:
|
||||||
self.image = has_gravatar(self.email)
|
self.image = has_gravatar(self.email)
|
||||||
|
|
||||||
|
def assign_agent(self, agent):
|
||||||
|
if not agent:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.get("_assign"):
|
||||||
|
assignees = json.loads(self._assign)
|
||||||
|
for assignee in assignees:
|
||||||
|
if agent == assignee:
|
||||||
|
# the agent is already set as an assignee
|
||||||
|
return
|
||||||
|
|
||||||
|
assign({"assign_to": [agent], "doctype": "CRM Lead", "name": self.name})
|
||||||
|
|
||||||
def create_contact(self, throw=True):
|
def create_contact(self, throw=True):
|
||||||
if not self.lead_name:
|
if not self.lead_name:
|
||||||
self.set_full_name()
|
self.set_full_name()
|
||||||
@ -210,10 +231,9 @@ class CRMLead(Document):
|
|||||||
'width': '11rem',
|
'width': '11rem',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': 'Lead Owner',
|
'label': 'Assigned To',
|
||||||
'type': 'Link',
|
'type': 'Text',
|
||||||
'key': 'lead_owner',
|
'key': '_assign',
|
||||||
'options': 'User',
|
|
||||||
'width': '10rem',
|
'width': '10rem',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -237,6 +257,7 @@ class CRMLead(Document):
|
|||||||
"first_response_time",
|
"first_response_time",
|
||||||
"first_responded_on",
|
"first_responded_on",
|
||||||
"modified",
|
"modified",
|
||||||
|
"_assign",
|
||||||
"image",
|
"image",
|
||||||
]
|
]
|
||||||
return {'columns': columns, 'rows': rows}
|
return {'columns': columns, 'rows': rows}
|
||||||
|
|||||||
@ -17,7 +17,10 @@
|
|||||||
v-slot="{ column, item }"
|
v-slot="{ column, item }"
|
||||||
:row="row"
|
:row="row"
|
||||||
>
|
>
|
||||||
<ListRowItem :item="item">
|
<div v-if="column.key === '_assign'" class="flex items-center">
|
||||||
|
<MultipleAvatar :avatars="item" />
|
||||||
|
</div>
|
||||||
|
<ListRowItem v-else :item="item">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<div v-if="column.key === 'status'">
|
<div v-if="column.key === 'status'">
|
||||||
<IndicatorIcon :class="item.color" />
|
<IndicatorIcon :class="item.color" />
|
||||||
@ -86,6 +89,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||||
import {
|
import {
|
||||||
|
|||||||
@ -17,7 +17,10 @@
|
|||||||
v-slot="{ column, item }"
|
v-slot="{ column, item }"
|
||||||
:row="row"
|
:row="row"
|
||||||
>
|
>
|
||||||
<ListRowItem :item="item">
|
<div v-if="column.key === '_assign'" class="flex items-center">
|
||||||
|
<MultipleAvatar :avatars="item" />
|
||||||
|
</div>
|
||||||
|
<ListRowItem v-else :item="item">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<div v-if="column.key === 'status'">
|
<div v-if="column.key === 'status'">
|
||||||
<IndicatorIcon :class="item.color" />
|
<IndicatorIcon :class="item.color" />
|
||||||
@ -97,6 +100,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||||
|
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
ListView,
|
ListView,
|
||||||
|
|||||||
150
frontend/src/components/Modals/AssignmentModal.vue
Normal file
150
frontend/src/components/Modals/AssignmentModal.vue
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
v-model="show"
|
||||||
|
:options="{
|
||||||
|
title: 'Assign To',
|
||||||
|
size: 'xl',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: 'Cancel',
|
||||||
|
variant: 'subtle',
|
||||||
|
onClick: () => {
|
||||||
|
assignees = oldAssignees
|
||||||
|
show = false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Update',
|
||||||
|
variant: 'solid',
|
||||||
|
onClick: () => updateAssignees(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #body-content>
|
||||||
|
<Link
|
||||||
|
class="form-control"
|
||||||
|
value=""
|
||||||
|
doctype="User"
|
||||||
|
@change="(option) => addValue(option) && ($refs.input.value = '')"
|
||||||
|
>
|
||||||
|
<template #item-prefix="{ option }">
|
||||||
|
<UserAvatar class="mr-2" :user="option.value" size="sm" />
|
||||||
|
</template>
|
||||||
|
<template #item-label="{ option }">
|
||||||
|
<Tooltip :text="option.value">
|
||||||
|
{{ getUser(option.value).full_name }}
|
||||||
|
</Tooltip>
|
||||||
|
</template>
|
||||||
|
</Link>
|
||||||
|
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||||||
|
<Tooltip
|
||||||
|
:text="assignee.name"
|
||||||
|
v-for="assignee in assignees"
|
||||||
|
:key="assignee.name"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
:label="getUser(assignee.name).full_name"
|
||||||
|
theme="gray"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<UserAvatar :user="assignee.name" size="sm" />
|
||||||
|
</template>
|
||||||
|
<template #suffix>
|
||||||
|
<FeatherIcon
|
||||||
|
class="h-3.5"
|
||||||
|
name="x"
|
||||||
|
@click.stop="removeValue(assignee.name)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<ErrorMessage class="mt-2" v-if="error" :message="error" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
import { usersStore } from '@/stores/users'
|
||||||
|
import { Dialog, Tooltip, FeatherIcon, call, ErrorMessage } from 'frappe-ui'
|
||||||
|
import { defineModel, ref } from 'vue'
|
||||||
|
import { watchOnce } from '@vueuse/core'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
doc: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const show = defineModel()
|
||||||
|
const assignees = defineModel('assignees')
|
||||||
|
const oldAssignees = ref([])
|
||||||
|
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
const { getUser } = usersStore()
|
||||||
|
|
||||||
|
const removeValue = (value) => {
|
||||||
|
assignees.value = assignees.value.filter(
|
||||||
|
(assignee) => assignee.name !== value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addValue = (value) => {
|
||||||
|
error.value = ''
|
||||||
|
let obj = {
|
||||||
|
name: value,
|
||||||
|
image: getUser(value).user_image,
|
||||||
|
label: getUser(value).full_name,
|
||||||
|
}
|
||||||
|
if (!assignees.value.find((assignee) => assignee.name === value)) {
|
||||||
|
assignees.value.push(obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAssignees() {
|
||||||
|
if (assignees.value.length === 0) {
|
||||||
|
error.value = 'Please select at least one assignee'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const removedAssignees = oldAssignees.value
|
||||||
|
.filter(
|
||||||
|
(assignee) => !assignees.value.find((a) => a.name === assignee.name)
|
||||||
|
)
|
||||||
|
.map((assignee) => assignee.name)
|
||||||
|
|
||||||
|
const addedAssignees = assignees.value
|
||||||
|
.filter(
|
||||||
|
(assignee) => !oldAssignees.value.find((a) => a.name === assignee.name)
|
||||||
|
)
|
||||||
|
.map((assignee) => assignee.name)
|
||||||
|
|
||||||
|
if (removedAssignees.length) {
|
||||||
|
for (let a of removedAssignees) {
|
||||||
|
call('frappe.desk.form.assign_to.remove', {
|
||||||
|
doctype: props.doc.doctype,
|
||||||
|
name: props.doc.name,
|
||||||
|
assign_to: a,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addedAssignees.length) {
|
||||||
|
call('frappe.desk.form.assign_to.add', {
|
||||||
|
doctype: props.doc.doctype,
|
||||||
|
name: props.doc.name,
|
||||||
|
assign_to: addedAssignees,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
show.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
watchOnce(assignees, (value) => {
|
||||||
|
oldAssignees.value = [...value]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
32
frontend/src/components/MultipleAvatar.vue
Normal file
32
frontend/src/components/MultipleAvatar.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="avatars?.length"
|
||||||
|
class="mr-1.5 flex cursor-pointer flex-row-reverse items-center"
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
:text="avatar.name"
|
||||||
|
v-for="avatar in reverseAvatars"
|
||||||
|
:key="avatar.name"
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
class="-mr-1.5 transform border-2 border-white transition hover:z-10 hover:scale-110"
|
||||||
|
shape="circle"
|
||||||
|
:image="avatar.image"
|
||||||
|
:label="avatar.label"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { Avatar, Tooltip } from 'frappe-ui'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
avatars: {
|
||||||
|
type: Array,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const reverseAvatars = computed(() => props.avatars.reverse())
|
||||||
|
</script>
|
||||||
@ -31,25 +31,10 @@
|
|||||||
>
|
>
|
||||||
<Button icon="more-horizontal" />
|
<Button icon="more-horizontal" />
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Link
|
<MultipleAvatar
|
||||||
class="form-control"
|
:avatars="deal.data._assignedTo"
|
||||||
:value="getUser(deal.data.deal_owner).full_name"
|
@click="showAssignmentModal = true"
|
||||||
doctype="User"
|
/>
|
||||||
@change="(option) => updateField('deal_owner', option)"
|
|
||||||
placeholder="Deal Owner"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<UserAvatar class="mr-2" :user="deal.data.deal_owner" size="sm" />
|
|
||||||
</template>
|
|
||||||
<template #item-prefix="{ option }">
|
|
||||||
<UserAvatar class="mr-2" :user="option.value" size="sm" />
|
|
||||||
</template>
|
|
||||||
<template #item-label="{ option }">
|
|
||||||
<Tooltip :text="option.value">
|
|
||||||
{{ getUser(option.value).full_name }}
|
|
||||||
</Tooltip>
|
|
||||||
</template>
|
|
||||||
</Link>
|
|
||||||
<Dropdown :options="statusOptions('deal', updateField)">
|
<Dropdown :options="statusOptions('deal', updateField)">
|
||||||
<template #default="{ open }">
|
<template #default="{ open }">
|
||||||
<Button
|
<Button
|
||||||
@ -296,6 +281,12 @@
|
|||||||
afterInsert: (doc) => addContact(doc.name),
|
afterInsert: (doc) => addContact(doc.name),
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
|
<AssignmentModal
|
||||||
|
v-if="deal.data"
|
||||||
|
:doc="deal.data"
|
||||||
|
v-model="showAssignmentModal"
|
||||||
|
v-model:assignees="deal.data._assignedTo"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
|
import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
|
||||||
@ -309,8 +300,9 @@ import ArrowUpRightIcon from '@/components/Icons/ArrowUpRightIcon.vue'
|
|||||||
import SuccessIcon from '@/components/Icons/SuccessIcon.vue'
|
import SuccessIcon from '@/components/Icons/SuccessIcon.vue'
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import Activities from '@/components/Activities.vue'
|
import Activities from '@/components/Activities.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
|
||||||
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
||||||
|
import AssignmentModal from '@/components/Modals/AssignmentModal.vue'
|
||||||
|
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||||
import ContactModal from '@/components/Modals/ContactModal.vue'
|
import ContactModal from '@/components/Modals/ContactModal.vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import Section from '@/components/Section.vue'
|
import Section from '@/components/Section.vue'
|
||||||
@ -353,10 +345,19 @@ const deal = createResource({
|
|||||||
params: { name: props.dealId },
|
params: { name: props.dealId },
|
||||||
cache: ['deal', props.dealId],
|
cache: ['deal', props.dealId],
|
||||||
auto: true,
|
auto: true,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
let assignees = JSON.parse(data._assign) || []
|
||||||
|
data._assignedTo = assignees.map((user) => ({
|
||||||
|
name: user,
|
||||||
|
image: getUser(user).user_image,
|
||||||
|
label: getUser(user).full_name,
|
||||||
|
}))
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const reload = ref(false)
|
const reload = ref(false)
|
||||||
const showOrganizationModal = ref(false)
|
const showOrganizationModal = ref(false)
|
||||||
|
const showAssignmentModal = ref(false)
|
||||||
const _organization = ref({})
|
const _organization = ref({})
|
||||||
|
|
||||||
const organization = computed(() => {
|
const organization = computed(() => {
|
||||||
|
|||||||
@ -33,7 +33,11 @@
|
|||||||
<ViewSettings doctype="CRM Deal" v-model="deals" />
|
<ViewSettings doctype="CRM Deal" v-model="deals" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DealsListView v-if="deals.data && rows.length" :rows="rows" :columns="deals.data.columns" />
|
<DealsListView
|
||||||
|
v-if="deals.data && rows.length"
|
||||||
|
:rows="rows"
|
||||||
|
:columns="deals.data.columns"
|
||||||
|
/>
|
||||||
<div v-else-if="deals.data" class="flex h-full items-center justify-center">
|
<div v-else-if="deals.data" class="flex h-full items-center justify-center">
|
||||||
<div
|
<div
|
||||||
class="flex flex-col items-center gap-3 text-xl font-medium text-gray-500"
|
class="flex flex-col items-center gap-3 text-xl font-medium text-gray-500"
|
||||||
@ -193,6 +197,13 @@ const rows = computed(() => {
|
|||||||
label: deal.deal_owner && getUser(deal.deal_owner).full_name,
|
label: deal.deal_owner && getUser(deal.deal_owner).full_name,
|
||||||
...(deal.deal_owner && getUser(deal.deal_owner)),
|
...(deal.deal_owner && getUser(deal.deal_owner)),
|
||||||
}
|
}
|
||||||
|
} else if (row == '_assign') {
|
||||||
|
let assignees = JSON.parse(deal._assign) || []
|
||||||
|
_rows[row] = assignees.map((user) => ({
|
||||||
|
name: user,
|
||||||
|
image: getUser(user).user_image,
|
||||||
|
label: getUser(user).full_name,
|
||||||
|
}))
|
||||||
} else if (['modified', 'creation'].includes(row)) {
|
} else if (['modified', 'creation'].includes(row)) {
|
||||||
_rows[row] = {
|
_rows[row] = {
|
||||||
label: dateFormat(deal[row], dateTooltipFormat),
|
label: dateFormat(deal[row], dateTooltipFormat),
|
||||||
|
|||||||
@ -31,25 +31,10 @@
|
|||||||
>
|
>
|
||||||
<Button icon="more-horizontal" />
|
<Button icon="more-horizontal" />
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Link
|
<MultipleAvatar
|
||||||
class="form-control"
|
:avatars="lead.data._assignedTo"
|
||||||
:value="getUser(lead.data.lead_owner).full_name"
|
@click="showAssignmentModal = true"
|
||||||
doctype="User"
|
/>
|
||||||
@change="(option) => updateField('lead_owner', option)"
|
|
||||||
placeholder="Lead Owner"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<UserAvatar class="mr-2" :user="lead.data.lead_owner" size="sm" />
|
|
||||||
</template>
|
|
||||||
<template #item-prefix="{ option }">
|
|
||||||
<UserAvatar class="mr-2" :user="option.value" size="sm" />
|
|
||||||
</template>
|
|
||||||
<template #item-label="{ option }">
|
|
||||||
<Tooltip :text="option.value">
|
|
||||||
{{ getUser(option.value).full_name }}
|
|
||||||
</Tooltip>
|
|
||||||
</template>
|
|
||||||
</Link>
|
|
||||||
<Dropdown :options="statusOptions('lead', updateField)">
|
<Dropdown :options="statusOptions('lead', updateField)">
|
||||||
<template #default="{ open }">
|
<template #default="{ open }">
|
||||||
<Button
|
<Button
|
||||||
@ -205,6 +190,12 @@
|
|||||||
}),
|
}),
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
|
<AssignmentModal
|
||||||
|
v-if="lead.data"
|
||||||
|
:doc="lead.data"
|
||||||
|
v-model="showAssignmentModal"
|
||||||
|
v-model:assignees="lead.data._assignedTo"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
|
import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
|
||||||
@ -217,12 +208,12 @@ import CameraIcon from '@/components/Icons/CameraIcon.vue'
|
|||||||
import LinkIcon from '@/components/Icons/LinkIcon.vue'
|
import LinkIcon from '@/components/Icons/LinkIcon.vue'
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import Activities from '@/components/Activities.vue'
|
import Activities from '@/components/Activities.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
|
||||||
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
||||||
|
import AssignmentModal from '@/components/Modals/AssignmentModal.vue'
|
||||||
|
import MultipleAvatar from '@/components/MultipleAvatar.vue'
|
||||||
import Section from '@/components/Section.vue'
|
import Section from '@/components/Section.vue'
|
||||||
import SectionFields from '@/components/SectionFields.vue'
|
import SectionFields from '@/components/SectionFields.vue'
|
||||||
import SLASection from '@/components/SLASection.vue'
|
import SLASection from '@/components/SLASection.vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
|
||||||
import { openWebsite, createToast } from '@/utils'
|
import { openWebsite, createToast } from '@/utils'
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { contactsStore } from '@/stores/contacts'
|
import { contactsStore } from '@/stores/contacts'
|
||||||
@ -261,10 +252,19 @@ const lead = createResource({
|
|||||||
params: { name: props.leadId },
|
params: { name: props.leadId },
|
||||||
cache: ['lead', props.leadId],
|
cache: ['lead', props.leadId],
|
||||||
auto: true,
|
auto: true,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
let assignees = JSON.parse(data._assign) || []
|
||||||
|
data._assignedTo = assignees.map((user) => ({
|
||||||
|
name: user,
|
||||||
|
image: getUser(user).user_image,
|
||||||
|
label: getUser(user).full_name,
|
||||||
|
}))
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const reload = ref(false)
|
const reload = ref(false)
|
||||||
const showOrganizationModal = ref(false)
|
const showOrganizationModal = ref(false)
|
||||||
|
const showAssignmentModal = ref(false)
|
||||||
const _organization = ref({})
|
const _organization = ref({})
|
||||||
|
|
||||||
const organization = computed(() => {
|
const organization = computed(() => {
|
||||||
|
|||||||
@ -196,6 +196,13 @@ const rows = computed(() => {
|
|||||||
label: lead.lead_owner && getUser(lead.lead_owner).full_name,
|
label: lead.lead_owner && getUser(lead.lead_owner).full_name,
|
||||||
...(lead.lead_owner && getUser(lead.lead_owner)),
|
...(lead.lead_owner && getUser(lead.lead_owner)),
|
||||||
}
|
}
|
||||||
|
} else if (row == '_assign') {
|
||||||
|
let assignees = JSON.parse(lead._assign) || []
|
||||||
|
_rows[row] = assignees.map((user) => ({
|
||||||
|
name: user,
|
||||||
|
image: getUser(user).user_image,
|
||||||
|
label: getUser(user).full_name,
|
||||||
|
}))
|
||||||
} else if (['modified', 'creation'].includes(row)) {
|
} else if (['modified', 'creation'].includes(row)) {
|
||||||
_rows[row] = {
|
_rows[row] = {
|
||||||
label: dateFormat(lead[row], dateTooltipFormat),
|
label: dateFormat(lead[row], dateTooltipFormat),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user