refactor: moved convert to deal modal into separate component
(cherry picked from commit 6320e580ae556df683d50b5145e39039968ad9ad)
This commit is contained in:
parent
336ac2ad34
commit
2cd08bebb9
1
frontend/components.d.ts
vendored
1
frontend/components.d.ts
vendored
@ -56,6 +56,7 @@ declare module 'vue' {
|
||||
ContactsIcon: typeof import('./src/components/Icons/ContactsIcon.vue')['default']
|
||||
ContactsListView: typeof import('./src/components/ListViews/ContactsListView.vue')['default']
|
||||
ConvertIcon: typeof import('./src/components/Icons/ConvertIcon.vue')['default']
|
||||
ConvertToDealModal: typeof import('./src/components/Modals/ConvertToDealModal.vue')['default']
|
||||
CountUpTimer: typeof import('./src/components/CountUpTimer.vue')['default']
|
||||
CreateDocumentModal: typeof import('./src/components/Modals/CreateDocumentModal.vue')['default']
|
||||
CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default']
|
||||
|
||||
227
frontend/src/components/Modals/ConvertToDealModal.vue
Normal file
227
frontend/src/components/Modals/ConvertToDealModal.vue
Normal file
@ -0,0 +1,227 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
size: 'xl',
|
||||
actions: [
|
||||
{
|
||||
label: __('Convert'),
|
||||
variant: 'solid',
|
||||
onClick: convertToDeal,
|
||||
},
|
||||
],
|
||||
}"
|
||||
>
|
||||
<template #body-header>
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-2xl font-semibold leading-6 text-ink-gray-9">
|
||||
{{ __('Convert to Deal') }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
v-if="isManager() && !isMobileView"
|
||||
variant="ghost"
|
||||
@click="openQuickEntryModal"
|
||||
>
|
||||
<template #icon>
|
||||
<EditIcon class="h-4 w-4" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button icon="x" variant="ghost" @click="show = false" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #body-content>
|
||||
<div class="mb-4 flex items-center gap-2 text-ink-gray-5">
|
||||
<OrganizationsIcon class="h-4 w-4" />
|
||||
<label class="block text-base">{{ __('Organization') }}</label>
|
||||
</div>
|
||||
<div class="ml-6 text-ink-gray-9">
|
||||
<div class="flex items-center justify-between text-base">
|
||||
<div>{{ __('Choose Existing') }}</div>
|
||||
<Switch v-model="existingOrganizationChecked" />
|
||||
</div>
|
||||
<Link
|
||||
v-if="existingOrganizationChecked"
|
||||
class="form-control mt-2.5"
|
||||
size="md"
|
||||
:value="existingOrganization"
|
||||
doctype="CRM Organization"
|
||||
@change="(data) => (existingOrganization = data)"
|
||||
/>
|
||||
<div v-else class="mt-2.5 text-base">
|
||||
{{
|
||||
__(
|
||||
'New organization will be created based on the data in details section',
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 mt-6 flex items-center gap-2 text-ink-gray-5">
|
||||
<ContactsIcon class="h-4 w-4" />
|
||||
<label class="block text-base">{{ __('Contact') }}</label>
|
||||
</div>
|
||||
<div class="ml-6 text-ink-gray-9">
|
||||
<div class="flex items-center justify-between text-base">
|
||||
<div>{{ __('Choose Existing') }}</div>
|
||||
<Switch v-model="existingContactChecked" />
|
||||
</div>
|
||||
<Link
|
||||
v-if="existingContactChecked"
|
||||
class="form-control mt-2.5"
|
||||
size="md"
|
||||
:value="existingContact"
|
||||
doctype="Contact"
|
||||
@change="(data) => (existingContact = data)"
|
||||
/>
|
||||
<div v-else class="mt-2.5 text-base">
|
||||
{{ __("New contact will be created based on the person's details") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="dealTabs.data?.length" class="h-px w-full border-t my-6" />
|
||||
|
||||
<FieldLayout
|
||||
v-if="dealTabs.data?.length"
|
||||
:tabs="dealTabs.data"
|
||||
:data="deal.doc"
|
||||
doctype="CRM Deal"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
||||
import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import FieldLayout from '@/components/FieldLayout/FieldLayout.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { useDocument } from '@/data/document'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { statusesStore } from '@/stores/statuses'
|
||||
import { showQuickEntryModal, quickEntryProps } from '@/composables/modals'
|
||||
import { isMobileView } from '@/composables/settings'
|
||||
import { capture } from '@/telemetry'
|
||||
import { useOnboarding } from 'frappe-ui/frappe'
|
||||
import { Switch, Dialog, toast, createResource, call } from 'frappe-ui'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const props = defineProps({
|
||||
lead: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const show = defineModel()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const { statusOptions, getDealStatus } = statusesStore()
|
||||
const { isManager } = usersStore()
|
||||
const { user } = sessionStore()
|
||||
const { updateOnboardingStep } = useOnboarding('frappecrm')
|
||||
|
||||
const existingContactChecked = ref(false)
|
||||
const existingOrganizationChecked = ref(false)
|
||||
|
||||
const existingContact = ref('')
|
||||
const existingOrganization = ref('')
|
||||
|
||||
const { triggerConvertToDeal } = useDocument('CRM Lead', props.lead.name)
|
||||
const { document: deal } = useDocument('CRM Deal')
|
||||
|
||||
async function convertToDeal() {
|
||||
if (existingContactChecked.value && !existingContact.value) {
|
||||
toast.error(__('Please select an existing contact'))
|
||||
return
|
||||
}
|
||||
|
||||
if (existingOrganizationChecked.value && !existingOrganization.value) {
|
||||
toast.error(__('Please select an existing organization'))
|
||||
return
|
||||
}
|
||||
|
||||
if (!existingContactChecked.value && existingContact.value) {
|
||||
existingContact.value = ''
|
||||
}
|
||||
|
||||
if (!existingOrganizationChecked.value && existingOrganization.value) {
|
||||
existingOrganization.value = ''
|
||||
}
|
||||
|
||||
await triggerConvertToDeal?.(props.lead, deal.doc, () => (show.value = false))
|
||||
|
||||
let _deal = await call('crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal', {
|
||||
lead: props.lead.name,
|
||||
deal: deal.doc,
|
||||
existing_contact: existingContact.value,
|
||||
existing_organization: existingOrganization.value,
|
||||
}).catch((err) => {
|
||||
toast.error(__('Error converting to deal: {0}', [err.messages?.[0]]))
|
||||
})
|
||||
if (_deal) {
|
||||
show.value = false
|
||||
existingContactChecked.value = false
|
||||
existingOrganizationChecked.value = false
|
||||
existingContact.value = ''
|
||||
existingOrganization.value = ''
|
||||
updateOnboardingStep('convert_lead_to_deal', true, false, () => {
|
||||
localStorage.setItem('firstDeal' + user, _deal)
|
||||
})
|
||||
capture('convert_lead_to_deal')
|
||||
router.push({ name: 'Deal', params: { dealId: _deal } })
|
||||
}
|
||||
}
|
||||
|
||||
const dealStatuses = computed(() => {
|
||||
let statuses = statusOptions('deal')
|
||||
if (!deal.doc?.status) {
|
||||
deal.doc.status = statuses[0].value
|
||||
}
|
||||
return statuses
|
||||
})
|
||||
|
||||
const dealTabs = createResource({
|
||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||
cache: ['RequiredFields', 'CRM Deal'],
|
||||
params: { doctype: 'CRM Deal', type: 'Required Fields' },
|
||||
auto: true,
|
||||
transform: (_tabs) => {
|
||||
let hasFields = false
|
||||
let parsedTabs = _tabs?.forEach((tab) => {
|
||||
tab.sections?.forEach((section) => {
|
||||
section.columns?.forEach((column) => {
|
||||
column.fields?.forEach((field) => {
|
||||
hasFields = true
|
||||
if (field.fieldname == 'status') {
|
||||
field.fieldtype = 'Select'
|
||||
field.options = dealStatuses.value
|
||||
field.prefix = getDealStatus(deal.doc.status).color
|
||||
}
|
||||
|
||||
if (field.fieldtype === 'Table') {
|
||||
deal.doc[field.fieldname] = []
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
return hasFields ? parsedTabs : []
|
||||
},
|
||||
})
|
||||
|
||||
function openQuickEntryModal() {
|
||||
showQuickEntryModal.value = true
|
||||
quickEntryProps.value = {
|
||||
doctype: 'CRM Deal',
|
||||
onlyRequired: true,
|
||||
}
|
||||
show.value = false
|
||||
}
|
||||
</script>
|
||||
@ -193,8 +193,7 @@ export function useDocument(doctype, docname) {
|
||||
async function triggerConvertToDeal() {
|
||||
const args = Array.from(arguments)
|
||||
const handler = async function () {
|
||||
await (this.convertToDeal?.(...args) ||
|
||||
this.on_convert_to_deal?.(...args))
|
||||
await (this.convertToDeal?.(...args) || this.convert_to_deal?.(...args))
|
||||
}
|
||||
await trigger(handler)
|
||||
}
|
||||
|
||||
@ -216,108 +216,11 @@
|
||||
:errorTitle="errorTitle"
|
||||
:errorMessage="errorMessage"
|
||||
/>
|
||||
<Dialog
|
||||
<ConvertToDealModal
|
||||
v-if="showConvertToDealModal"
|
||||
v-model="showConvertToDealModal"
|
||||
:options="{
|
||||
size: 'xl',
|
||||
actions: [
|
||||
{
|
||||
label: __('Convert'),
|
||||
variant: 'solid',
|
||||
onClick: convertToDeal,
|
||||
},
|
||||
],
|
||||
}"
|
||||
>
|
||||
<template #body-header>
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-2xl font-semibold leading-6 text-ink-gray-9">
|
||||
{{ __('Convert to Deal') }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
v-if="isManager() && !isMobileView"
|
||||
variant="ghost"
|
||||
class="w-7"
|
||||
@click="openQuickEntryModal"
|
||||
>
|
||||
<template #icon>
|
||||
<EditIcon class="h-4 w-4" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-7"
|
||||
@click="showConvertToDealModal = false"
|
||||
>
|
||||
<template #icon>
|
||||
<FeatherIcon name="x" class="h-4 w-4" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #body-content>
|
||||
<div class="mb-4 flex items-center gap-2 text-ink-gray-5">
|
||||
<OrganizationsIcon class="h-4 w-4" />
|
||||
<label class="block text-base">{{ __('Organization') }}</label>
|
||||
</div>
|
||||
<div class="ml-6 text-ink-gray-9">
|
||||
<div class="flex items-center justify-between text-base">
|
||||
<div>{{ __('Choose Existing') }}</div>
|
||||
<Switch v-model="existingOrganizationChecked" />
|
||||
</div>
|
||||
<Link
|
||||
v-if="existingOrganizationChecked"
|
||||
class="form-control mt-2.5"
|
||||
size="md"
|
||||
:value="existingOrganization"
|
||||
doctype="CRM Organization"
|
||||
@change="(data) => (existingOrganization = data)"
|
||||
/>
|
||||
<div v-else class="mt-2.5 text-base">
|
||||
{{
|
||||
__(
|
||||
'New organization will be created based on the data in details section',
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 mt-6 flex items-center gap-2 text-ink-gray-5">
|
||||
<ContactsIcon class="h-4 w-4" />
|
||||
<label class="block text-base">{{ __('Contact') }}</label>
|
||||
</div>
|
||||
<div class="ml-6 text-ink-gray-9">
|
||||
<div class="flex items-center justify-between text-base">
|
||||
<div>{{ __('Choose Existing') }}</div>
|
||||
<Switch v-model="existingContactChecked" />
|
||||
</div>
|
||||
<Link
|
||||
v-if="existingContactChecked"
|
||||
class="form-control mt-2.5"
|
||||
size="md"
|
||||
:value="existingContact"
|
||||
doctype="Contact"
|
||||
@change="(data) => (existingContact = data)"
|
||||
/>
|
||||
<div v-else class="mt-2.5 text-base">
|
||||
{{ __("New contact will be created based on the person's details") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="dealTabs.data?.length" class="h-px w-full border-t my-6" />
|
||||
|
||||
<FieldLayout
|
||||
v-if="dealTabs.data?.length"
|
||||
:tabs="dealTabs.data"
|
||||
:data="deal"
|
||||
doctype="CRM Deal"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
:lead="lead.data"
|
||||
/>
|
||||
<FilesUploader
|
||||
v-if="lead.data?.name"
|
||||
v-model="showFilesUploader"
|
||||
@ -354,40 +257,28 @@ import WhatsAppIcon from '@/components/Icons/WhatsAppIcon.vue'
|
||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||
import CameraIcon from '@/components/Icons/CameraIcon.vue'
|
||||
import LinkIcon from '@/components/Icons/LinkIcon.vue'
|
||||
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
||||
import ContactsIcon from '@/components/Icons/ContactsIcon.vue'
|
||||
import AttachmentIcon from '@/components/Icons/AttachmentIcon.vue'
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||
import Activities from '@/components/Activities/Activities.vue'
|
||||
import AssignTo from '@/components/AssignTo.vue'
|
||||
import FilesUploader from '@/components/FilesUploader/FilesUploader.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import SidePanelLayout from '@/components/SidePanelLayout.vue'
|
||||
import FieldLayout from '@/components/FieldLayout/FieldLayout.vue'
|
||||
import SLASection from '@/components/SLASection.vue'
|
||||
import CustomActions from '@/components/CustomActions.vue'
|
||||
import ConvertToDealModal from '@/components/Modals/ConvertToDealModal.vue'
|
||||
import {
|
||||
openWebsite,
|
||||
setupCustomizations,
|
||||
copyToClipboard,
|
||||
validateIsImageFile,
|
||||
} from '@/utils'
|
||||
import { showQuickEntryModal, quickEntryProps } from '@/composables/modals'
|
||||
import { getView } from '@/utils/view'
|
||||
import { getSettings } from '@/stores/settings'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { globalStore } from '@/stores/global'
|
||||
import { statusesStore } from '@/stores/statuses'
|
||||
import { getMeta } from '@/stores/meta'
|
||||
import { useDocument } from '@/data/document'
|
||||
import {
|
||||
whatsappEnabled,
|
||||
callEnabled,
|
||||
isMobileView,
|
||||
} from '@/composables/settings'
|
||||
import { capture } from '@/telemetry'
|
||||
import { whatsappEnabled, callEnabled } from '@/composables/settings'
|
||||
import {
|
||||
createResource,
|
||||
FileUploader,
|
||||
@ -395,26 +286,20 @@ import {
|
||||
Tooltip,
|
||||
Avatar,
|
||||
Tabs,
|
||||
Switch,
|
||||
Breadcrumbs,
|
||||
call,
|
||||
usePageMeta,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { useOnboarding } from 'frappe-ui/frappe'
|
||||
import { ref, reactive, computed, onMounted, watch, nextTick } from 'vue'
|
||||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useActiveTabManager } from '@/composables/useActiveTabManager'
|
||||
|
||||
const { brand } = getSettings()
|
||||
const { user } = sessionStore()
|
||||
const { isManager } = usersStore()
|
||||
const { $dialog, $socket, makeCall } = globalStore()
|
||||
const { statusOptions, getLeadStatus, getDealStatus } = statusesStore()
|
||||
const { statusOptions, getLeadStatus } = statusesStore()
|
||||
const { doctypeMeta } = getMeta('CRM Lead')
|
||||
|
||||
const { updateOnboardingStep } = useOnboarding('frappecrm')
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
@ -428,6 +313,12 @@ const props = defineProps({
|
||||
const errorTitle = ref('')
|
||||
const errorMessage = ref('')
|
||||
const showDeleteLinkedDocModal = ref(false)
|
||||
const showConvertToDealModal = ref(false)
|
||||
|
||||
const { triggerOnChange, assignees, document } = useDocument(
|
||||
'CRM Lead',
|
||||
props.leadId,
|
||||
)
|
||||
|
||||
const lead = createResource({
|
||||
url: 'crm.fcrm.doctype.crm_lead.api.get_lead',
|
||||
@ -630,64 +521,6 @@ async function deleteLeadWithModal(name) {
|
||||
showDeleteLinkedDocModal.value = true
|
||||
}
|
||||
|
||||
// Convert to Deal
|
||||
const showConvertToDealModal = ref(false)
|
||||
const existingContactChecked = ref(false)
|
||||
const existingOrganizationChecked = ref(false)
|
||||
|
||||
const existingContact = ref('')
|
||||
const existingOrganization = ref('')
|
||||
|
||||
const { triggerConvertToDeal, triggerOnChange, assignees, document } =
|
||||
useDocument('CRM Lead', props.leadId)
|
||||
|
||||
async function convertToDeal() {
|
||||
if (existingContactChecked.value && !existingContact.value) {
|
||||
toast.error(__('Please select an existing contact'))
|
||||
return
|
||||
}
|
||||
|
||||
if (existingOrganizationChecked.value && !existingOrganization.value) {
|
||||
toast.error(__('Please select an existing organization'))
|
||||
return
|
||||
}
|
||||
|
||||
if (!existingContactChecked.value && existingContact.value) {
|
||||
existingContact.value = ''
|
||||
}
|
||||
|
||||
if (!existingOrganizationChecked.value && existingOrganization.value) {
|
||||
existingOrganization.value = ''
|
||||
}
|
||||
|
||||
await triggerConvertToDeal?.(
|
||||
lead.data,
|
||||
deal,
|
||||
() => (showConvertToDealModal.value = false),
|
||||
)
|
||||
|
||||
let _deal = await call('crm.fcrm.doctype.crm_lead.crm_lead.convert_to_deal', {
|
||||
lead: lead.data.name,
|
||||
deal,
|
||||
existing_contact: existingContact.value,
|
||||
existing_organization: existingOrganization.value,
|
||||
}).catch((err) => {
|
||||
toast.error(__('Error converting to deal: {0}', [err.messages?.[0]]))
|
||||
})
|
||||
if (_deal) {
|
||||
showConvertToDealModal.value = false
|
||||
existingContactChecked.value = false
|
||||
existingOrganizationChecked.value = false
|
||||
existingContact.value = ''
|
||||
existingOrganization.value = ''
|
||||
updateOnboardingStep('convert_lead_to_deal', true, false, () => {
|
||||
localStorage.setItem('firstDeal' + user, _deal)
|
||||
})
|
||||
capture('convert_lead_to_deal')
|
||||
router.push({ name: 'Deal', params: { dealId: _deal } })
|
||||
}
|
||||
}
|
||||
|
||||
const activities = ref(null)
|
||||
|
||||
function openEmailBox() {
|
||||
@ -698,54 +531,6 @@ function openEmailBox() {
|
||||
nextTick(() => (activities.value.emailBox.show = true))
|
||||
}
|
||||
|
||||
const deal = reactive({})
|
||||
|
||||
const dealStatuses = computed(() => {
|
||||
let statuses = statusOptions('deal')
|
||||
if (!deal.status) {
|
||||
deal.status = statuses[0].value
|
||||
}
|
||||
return statuses
|
||||
})
|
||||
|
||||
const dealTabs = createResource({
|
||||
url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout',
|
||||
cache: ['RequiredFields', 'CRM Deal'],
|
||||
params: { doctype: 'CRM Deal', type: 'Required Fields' },
|
||||
auto: true,
|
||||
transform: (_tabs) => {
|
||||
let hasFields = false
|
||||
let parsedTabs = _tabs?.forEach((tab) => {
|
||||
tab.sections?.forEach((section) => {
|
||||
section.columns?.forEach((column) => {
|
||||
column.fields?.forEach((field) => {
|
||||
hasFields = true
|
||||
if (field.fieldname == 'status') {
|
||||
field.fieldtype = 'Select'
|
||||
field.options = dealStatuses.value
|
||||
field.prefix = getDealStatus(deal.status).color
|
||||
}
|
||||
|
||||
if (field.fieldtype === 'Table') {
|
||||
deal[field.fieldname] = []
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
return hasFields ? parsedTabs : []
|
||||
},
|
||||
})
|
||||
|
||||
function openQuickEntryModal() {
|
||||
showQuickEntryModal.value = true
|
||||
quickEntryProps.value = {
|
||||
doctype: 'CRM Deal',
|
||||
onlyRequired: true,
|
||||
}
|
||||
showConvertToDealModal.value = false
|
||||
}
|
||||
|
||||
function reloadAssignees(data) {
|
||||
if (data?.hasOwnProperty('lead_owner')) {
|
||||
assignees.reload()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user