1
0
forked from test/crm

refactor: update mobile lead/deal components

(cherry picked from commit 7e42599b494395b50305fcb806817d043d514637)
This commit is contained in:
Shariq Ansari 2025-07-30 17:31:22 +05:30 committed by Mergify
parent 9ebd7bde2c
commit 12b92b3f21
4 changed files with 213 additions and 223 deletions

View File

@ -338,6 +338,7 @@
/>
</template>
<script setup>
import DeleteLinkedDocModal from '@/components/DeleteLinkedDocModal.vue'
import ErrorPage from '@/components/ErrorPage.vue'
import Icon from '@/components/Icon.vue'
import Resizer from '@/components/Resizer.vue'
@ -420,6 +421,7 @@ const props = defineProps({
const errorTitle = ref('')
const errorMessage = ref('')
const showDeleteLinkedDocModal = ref(false)
const { triggerOnChange, assignees, document, scripts, error } = useDocument(
'CRM Deal',
@ -454,7 +456,7 @@ watch(
toast,
updateField,
createToast: toast.create,
deleteDoc: deleteDealWithModal,
deleteDoc: deleteDeal,
call,
})
document._actions = s.actions || []
@ -496,7 +498,6 @@ const reload = ref(false)
const showOrganizationModal = ref(false)
const showFilesUploader = ref(false)
const _organization = ref({})
const showDeleteLinkedDocModal = ref(false)
const breadcrumbs = computed(() => {
let items = [{ label: __('Deals'), route: { name: 'Deals' } }]
@ -742,7 +743,7 @@ function updateField(name, value) {
})
}
async function deleteDealWithModal() {
function deleteDeal() {
showDeleteLinkedDocModal.value = true
}

View File

@ -185,7 +185,7 @@
<Tooltip :text="__('Delete')">
<div>
<Button
@click="deleteLeadWithModal"
@click="deleteLead"
variant="subtle"
theme="red"
icon="trash-2"
@ -248,6 +248,7 @@
/>
</template>
<script setup>
import DeleteLinkedDocModal from '@/components/DeleteLinkedDocModal.vue'
import ErrorPage from '@/components/ErrorPage.vue'
import Icon from '@/components/Icon.vue'
import Resizer from '@/components/Resizer.vue'
@ -297,7 +298,7 @@ import {
usePageMeta,
toast,
} from 'frappe-ui'
import { ref, computed, onMounted, watch, nextTick } from 'vue'
import { ref, computed, watch, nextTick } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useActiveTabManager } from '@/composables/useActiveTabManager'
@ -354,7 +355,7 @@ watch(
toast,
updateField,
createToast: toast.create,
deleteDoc: deleteLeadWithModal,
deleteDoc: deleteLead,
call,
})
document._actions = s.actions || []
@ -504,7 +505,7 @@ function updateField(name, value) {
})
}
async function deleteLeadWithModal() {
function deleteLead() {
showDeleteLinkedDocModal.value = true
}

View File

@ -1,5 +1,5 @@
<template>
<LayoutHeader v-if="deal.data">
<LayoutHeader>
<header
class="relative flex h-10.5 items-center justify-between gap-2 py-2.5 pl-2"
>
@ -10,23 +10,21 @@
</Breadcrumbs>
<div class="absolute right-0">
<Dropdown
v-if="document.doc"
v-if="doc"
:options="
statusOptions(
'deal',
document.statuses?.length
? document.statuses
: deal.data._customStatuses,
: document._statuses,
triggerStatusChange,
)
"
>
<template #default="{ open }">
<Button :label="document.doc.status">
<Button v-if="doc.status" :label="doc.status">
<template #prefix>
<IndicatorIcon
:class="getDealStatus(document.doc.status).color"
/>
<IndicatorIcon :class="getDealStatus(doc.status).color" />
</template>
<template #suffix>
<FeatherIcon
@ -41,18 +39,14 @@
</header>
</LayoutHeader>
<div
v-if="deal.data"
v-if="doc.name"
class="flex h-12 items-center justify-between gap-2 border-b px-3 py-2.5"
>
<AssignTo
v-model="assignees.data"
:data="document.doc"
doctype="CRM Deal"
/>
<AssignTo v-model="assignees.data" :data="doc" doctype="CRM Deal" />
<div class="flex items-center gap-2">
<CustomActions
v-if="deal.data._customActions?.length"
:actions="deal.data._customActions"
v-if="document._actions?.length"
:actions="document._actions"
/>
<CustomActions
v-if="document.actions?.length"
@ -60,14 +54,14 @@
/>
</div>
</div>
<div v-if="deal.data" class="flex h-full overflow-hidden">
<div v-if="doc.name" class="flex h-full overflow-hidden">
<Tabs as="div" v-model="tabIndex" :tabs="tabs" class="overflow-auto">
<TabList class="!px-3" />
<TabPanel v-slot="{ tab }">
<div v-if="tab.name == 'Details'">
<SLASection
v-if="deal.data.sla_status"
v-model="deal.data"
v-if="doc.sla_status"
v-model="doc"
@updateField="updateField"
/>
<div
@ -77,7 +71,7 @@
<SidePanelLayout
:sections="sections.data"
doctype="CRM Deal"
:docname="deal.data.name"
:docname="dealId"
@reload="sections.reload"
@beforeFieldChange="beforeStatusChange"
@afterFieldChange="reloadAssignees"
@ -92,7 +86,7 @@
(value, close) => {
_contact = {
first_name: value,
company_name: deal.data.organization,
company_name: doc.organization,
}
showContactModal = true
close()
@ -220,23 +214,28 @@
<Activities
v-else
doctype="CRM Deal"
:docname="dealId"
:tabs="tabs"
v-model:reload="reload"
v-model:tabIndex="tabIndex"
v-model="deal"
@beforeSave="beforeStatusChange"
@afterSave="reloadAssignees"
/>
</TabPanel>
</Tabs>
</div>
<ErrorPage
v-else-if="errorTitle"
:errorTitle="errorTitle"
:errorMessage="errorMessage"
/>
<OrganizationModal
v-if="showOrganizationModal"
v-model="showOrganizationModal"
:data="_organization"
:options="{
redirect: false,
afterInsert: (doc) => updateField('organization', doc.name),
afterInsert: (_doc) => updateField('organization', _doc.name),
}"
/>
<ContactModal
@ -245,9 +244,16 @@
:contact="_contact"
:options="{
redirect: false,
afterInsert: (doc) => addContact(doc.name),
afterInsert: (_doc) => addContact(_doc.name),
}"
/>
<DeleteLinkedDocModal
v-if="showDeleteLinkedDocModal"
v-model="showDeleteLinkedDocModal"
:doctype="'CRM Deal'"
:docname="dealId"
name="Deals"
/>
<LostReasonModal
v-if="showLostReasonModal"
v-model="showLostReasonModal"
@ -255,6 +261,8 @@
/>
</template>
<script setup>
import DeleteLinkedDocModal from '@/components/DeleteLinkedDocModal.vue'
import ErrorPage from '@/components/ErrorPage.vue'
import Icon from '@/components/Icon.vue'
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
import LoadingIndicator from '@/components/Icons/LoadingIndicator.vue'
@ -306,7 +314,7 @@ import {
usePageMeta,
toast,
} from 'frappe-ui'
import { ref, computed, h, onMounted } from 'vue'
import { ref, computed, h, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const { brand } = getSettings()
@ -323,86 +331,57 @@ const props = defineProps({
},
})
const deal = createResource({
url: 'crm.fcrm.doctype.crm_deal.api.get_deal',
params: { name: props.dealId },
cache: ['deal', props.dealId],
onSuccess: (data) => {
if (data.organization) {
organization.update({
params: { doctype: 'CRM Organization', name: data.organization },
const errorTitle = ref('')
const errorMessage = ref('')
const showDeleteLinkedDocModal = ref(false)
const { triggerOnChange, assignees, document, scripts, error } = useDocument(
'CRM Deal',
props.dealId,
)
const doc = computed(() => document.doc || {})
watch(error, (err) => {
if (err) {
errorTitle.value = __(
err.exc_type == 'DoesNotExistError'
? 'Document not found'
: 'Error occurred',
)
errorMessage.value = __(err.messages?.[0] || 'An error occurred')
} else {
errorTitle.value = ''
errorMessage.value = ''
}
})
watch(
() => document.doc,
async (_doc) => {
if (scripts.data?.length) {
let s = await setupCustomizations(scripts.data, {
doc: _doc,
$dialog,
$socket,
router,
toast,
updateField,
createToast: toast.create,
deleteDoc: deleteDeal,
call,
})
organization.fetch()
document._actions = s.actions || []
document._statuses = s.statuses || []
}
setupCustomizations(deal, {
doc: data,
$dialog,
$socket,
router,
toast,
updateField,
createToast: toast.create,
deleteDoc: deleteDeal,
resource: {
deal,
dealContacts,
sections,
},
call,
})
},
})
const organization = createResource({
url: 'frappe.client.get',
onSuccess: (data) => (deal.data._organizationObj = data),
})
onMounted(() => {
if (deal.data) return
deal.fetch()
})
{ once: true },
)
const reload = ref(false)
const showOrganizationModal = ref(false)
const _organization = ref({})
function updateDeal(fieldname, value, callback) {
value = Array.isArray(fieldname) ? '' : value
if (validateRequired(fieldname, value)) return
createResource({
url: 'frappe.client.set_value',
params: {
doctype: 'CRM Deal',
name: props.dealId,
fieldname,
value,
},
auto: true,
onSuccess: () => {
deal.reload()
reload.value = true
toast.success(__('Deal updated'))
callback?.()
},
onError: (err) => {
toast.error(err.messages?.[0] || __('Error updating deal'))
},
})
}
function validateRequired(fieldname, value) {
let meta = deal.data.fields_meta || {}
if (meta[fieldname]?.reqd && !value) {
toast.error(__('{0} is a required field', [meta[fieldname].label]))
return true
}
return false
}
const breadcrumbs = computed(() => {
let items = [{ label: __('Deals'), route: { name: 'Deals' } }]
@ -423,14 +402,14 @@ const breadcrumbs = computed(() => {
items.push({
label: title.value,
route: { name: 'Deal', params: { dealId: deal.data.name } },
route: { name: 'Deal', params: { dealId: props.dealId } },
})
return items
})
const title = computed(() => {
let t = doctypeMeta['CRM Deal']?.title_field || 'name'
return deal.data?.[t] || props.dealId
return doc.value?.[t] || props.dealId
})
usePageMeta(() => {
@ -614,26 +593,33 @@ const dealContacts = createResource({
},
})
function updateField(name, value, callback) {
updateDeal(name, value, () => {
deal.data[name] = value
callback?.()
function updateField(name, value) {
value = Array.isArray(name) ? '' : value
let oldValues = Array.isArray(name) ? {} : doc.value[name]
if (Array.isArray(name)) {
name.forEach((field) => (doc.value[field] = value))
} else {
doc.value[name] = value
}
document.save.submit(null, {
onSuccess: () => (reload.value = true),
onError: (err) => {
if (Array.isArray(name)) {
name.forEach((field) => (doc.value[field] = oldValues[field]))
} else {
doc.value[name] = oldValues
}
toast.error(err.messages?.[0] || __('Error updating field'))
},
})
}
async function deleteDeal(name) {
await call('frappe.client.delete', {
doctype: 'CRM Deal',
name,
})
router.push({ name: 'Deals' })
function deleteDeal() {
showDeleteLinkedDocModal.value = true
}
const { assignees, document, triggerOnChange } = useDocument(
'CRM Deal',
props.dealId,
)
async function triggerStatusChange(value) {
await triggerOnChange('status', value)
setLostReason()
@ -643,9 +629,9 @@ const showLostReasonModal = ref(false)
function setLostReason() {
if (
getDealStatus(document.doc.status).type !== 'Lost' ||
(document.doc.lost_reason && document.doc.lost_reason !== 'Other') ||
(document.doc.lost_reason === 'Other' && document.doc.lost_notes)
getDealStatus(doc.status).type !== 'Lost' ||
(doc.lost_reason && doc.lost_reason !== 'Other') ||
(doc.lost_reason === 'Other' && doc.lost_notes)
) {
document.save.submit()
return
@ -655,7 +641,10 @@ function setLostReason() {
}
function beforeStatusChange(data) {
if (data?.hasOwnProperty('status') && getDealStatus(data.status).type == 'Lost') {
if (
data?.hasOwnProperty('status') &&
getDealStatus(data.status).type == 'Lost'
) {
setLostReason()
} else {
document.save.submit(null, {

View File

@ -1,5 +1,5 @@
<template>
<LayoutHeader v-if="lead.data">
<LayoutHeader>
<header
class="relative flex h-10.5 items-center justify-between gap-2 py-2.5 pl-2"
>
@ -10,23 +10,21 @@
</Breadcrumbs>
<div class="absolute right-0">
<Dropdown
v-if="document.doc"
v-if="doc"
:options="
statusOptions(
'lead',
document.statuses?.length
? document.statuses
: lead.data._customStatuses,
: document._statuses,
triggerStatusChange,
)
"
>
<template #default="{ open }">
<Button :label="document.doc.status">
<Button v-if="doc.status" :label="doc.status">
<template #prefix>
<IndicatorIcon
:class="getLeadStatus(document.doc.status).color"
/>
<IndicatorIcon :class="getLeadStatus(doc.status).color" />
</template>
<template #suffix>
<FeatherIcon
@ -41,18 +39,14 @@
</header>
</LayoutHeader>
<div
v-if="lead.data"
v-if="doc.name"
class="flex h-12 items-center justify-between gap-2 border-b px-3 py-2.5"
>
<AssignTo
v-model="assignees.data"
:data="document.doc"
doctype="CRM Lead"
/>
<AssignTo v-model="assignees.data" :data="doc" doctype="CRM Lead" />
<div class="flex items-center gap-2">
<CustomActions
v-if="lead.data._customActions?.length"
:actions="lead.data._customActions"
v-if="document._actions?.length"
:actions="document._actions"
/>
<CustomActions
v-if="document.actions?.length"
@ -65,14 +59,14 @@
/>
</div>
</div>
<div v-if="lead?.data" class="flex h-full overflow-hidden">
<div v-if="doc.name" class="flex h-full overflow-hidden">
<Tabs as="div" v-model="tabIndex" :tabs="tabs" class="overflow-auto">
<TabList class="!px-3" />
<TabPanel v-slot="{ tab }">
<div v-if="tab.name == 'Details'">
<SLASection
v-if="lead.data.sla_status"
v-model="lead.data"
v-if="doc.sla_status"
v-model="doc"
@updateField="updateField"
/>
<div
@ -82,7 +76,7 @@
<SidePanelLayout
:sections="sections.data"
doctype="CRM Lead"
:docname="lead.data.name"
:docname="leadId"
@reload="sections.reload"
@afterFieldChange="reloadAssignees"
/>
@ -91,16 +85,21 @@
<Activities
v-else
doctype="CRM Lead"
:docname="leadId"
:tabs="tabs"
v-model:reload="reload"
v-model:tabIndex="tabIndex"
v-model="lead"
@beforeSave="saveChanges"
@afterSave="reloadAssignees"
/>
</TabPanel>
</Tabs>
</div>
<ErrorPage
v-else-if="errorTitle"
:errorTitle="errorTitle"
:errorMessage="errorMessage"
/>
<Dialog
v-model="showConvertToDealModal"
:options="{
@ -167,8 +166,17 @@
</div>
</template>
</Dialog>
<DeleteLinkedDocModal
v-if="showDeleteLinkedDocModal"
v-model="showDeleteLinkedDocModal"
:doctype="'CRM Lead'"
:docname="leadId"
name="Leads"
/>
</template>
<script setup>
import DeleteLinkedDocModal from '@/components/DeleteLinkedDocModal.vue'
import ErrorPage from '@/components/ErrorPage.vue'
import Icon from '@/components/Icon.vue'
import DetailsIcon from '@/components/Icons/DetailsIcon.vue'
import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
@ -215,7 +223,7 @@ import {
usePageMeta,
toast,
} from 'frappe-ui'
import { ref, computed, onMounted, watch } from 'vue'
import { ref, computed, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const { brand } = getSettings()
@ -232,71 +240,55 @@ const props = defineProps({
},
})
const lead = createResource({
url: 'crm.fcrm.doctype.crm_lead.api.get_lead',
params: { name: props.leadId },
cache: ['lead', props.leadId],
onSuccess: (data) => {
setupCustomizations(lead, {
doc: data,
$dialog,
$socket,
router,
toast,
updateField,
createToast: toast.create,
deleteDoc: deleteLead,
resource: {
lead,
sections,
},
call,
})
},
const errorTitle = ref('')
const errorMessage = ref('')
const showDeleteLinkedDocModal = ref(false)
const { triggerOnChange, assignees, document, scripts, error } = useDocument(
'CRM Lead',
props.leadId,
)
const doc = computed(() => document.doc || {})
watch(error, (err) => {
if (err) {
errorTitle.value = __(
err.exc_type == 'DoesNotExistError'
? 'Document not found'
: 'Error occurred',
)
errorMessage.value = __(err.messages?.[0] || 'An error occurred')
} else {
errorTitle.value = ''
errorMessage.value = ''
}
})
onMounted(() => {
if (lead.data) return
lead.fetch()
})
watch(
() => document.doc,
async (_doc) => {
if (scripts.data?.length) {
let s = await setupCustomizations(scripts.data, {
doc: _doc,
$dialog,
$socket,
router,
toast,
updateField,
createToast: toast.create,
deleteDoc: deleteLead,
call,
})
document._actions = s.actions || []
document._statuses = s.statuses || []
}
},
{ once: true },
)
const reload = ref(false)
function updateLead(fieldname, value, callback) {
value = Array.isArray(fieldname) ? '' : value
if (!Array.isArray(fieldname) && validateRequired(fieldname, value)) return
createResource({
url: 'frappe.client.set_value',
params: {
doctype: 'CRM Lead',
name: props.leadId,
fieldname,
value,
},
auto: true,
onSuccess: () => {
lead.reload()
reload.value = true
toast.success(__('Lead updated successfully'))
callback?.()
},
onError: (err) => {
toast.error(__(err.messages?.[0] || 'Error updating lead'))
},
})
}
function validateRequired(fieldname, value) {
let meta = lead.data.fields_meta || {}
if (meta[fieldname]?.reqd && !value) {
toast.error(__('{0} is a required field', [meta[fieldname].label]))
return true
}
return false
}
const breadcrumbs = computed(() => {
let items = [{ label: __('Leads'), route: { name: 'Leads' } }]
@ -317,14 +309,14 @@ const breadcrumbs = computed(() => {
items.push({
label: title.value,
route: { name: 'Lead', params: { leadId: lead.data.name } },
route: { name: 'Lead', params: { leadId: props.leadId } },
})
return items
})
const title = computed(() => {
let t = doctypeMeta['CRM Lead']?.title_field || 'name'
return lead.data?.[t] || props.leadId
return doc.value?.[t] || props.leadId
})
usePageMeta(() => {
@ -412,19 +404,31 @@ const sections = createResource({
auto: true,
})
function updateField(name, value, callback) {
updateLead(name, value, () => {
lead.data[name] = value
callback?.()
function updateField(name, value) {
value = Array.isArray(name) ? '' : value
let oldValues = Array.isArray(name) ? {} : doc.value[name]
if (Array.isArray(name)) {
name.forEach((field) => (doc.value[field] = value))
} else {
doc.value[name] = value
}
document.save.submit(null, {
onSuccess: () => (reload.value = true),
onError: (err) => {
if (Array.isArray(name)) {
name.forEach((field) => (doc.value[field] = oldValues[field]))
} else {
doc.value[name] = oldValues
}
toast.error(err.messages?.[0] || __('Error updating field'))
},
})
}
async function deleteLead(name) {
await call('frappe.client.delete', {
doctype: 'CRM Lead',
name,
})
router.push({ name: 'Leads' })
function deleteLead() {
showDeleteLinkedDocModal.value = true
}
// Convert to Deal
@ -455,7 +459,7 @@ async function convertToDeal() {
}
let deal = await call('crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal', {
lead: lead.data.name,
lead: props.leadId,
deal: {},
existing_contact: existingContact.value,
existing_organization: existingOrganization.value,
@ -471,11 +475,6 @@ async function convertToDeal() {
}
}
const { assignees, document, triggerOnChange } = useDocument(
'CRM Lead',
props.leadId,
)
async function triggerStatusChange(value) {
await triggerOnChange('status', value)
document.save.submit()