Merge pull request #155 from frappe/develop
chore: Merge develop to main
This commit is contained in:
commit
0e247f0347
@ -37,7 +37,11 @@
|
|||||||
- **Tasks:** Create tasks for leads and deals.
|
- **Tasks:** Create tasks for leads and deals.
|
||||||
- **Notes:** Add notes to leads and deals.
|
- **Notes:** Add notes to leads and deals.
|
||||||
- **Call Logs:** See the call logs with call details and recordings.
|
- **Call Logs:** See the call logs with call details and recordings.
|
||||||
- **Twilio Integration:** Integrate Twilio to make and receive calls from the CRM.
|
|
||||||
|
## Integrations
|
||||||
|
|
||||||
|
- **Twilio:** Integrate Twilio to make and receive calls from the CRM. You can also record calls. It is a built-in integration.
|
||||||
|
- **WhatsApp:** Integrate WhatsApp to send and receive messages from the CRM. [Frappe WhatsApp](https://github.com/shridarpatil/frappe_whatsapp) is used for this integration.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
__version__ = '1.2.0'
|
__version__ = "2.0.0-dev"
|
||||||
|
__title__ = "Frappe CRM"
|
||||||
|
|
||||||
|
|||||||
147
frontend/src/components/Fields.vue
Normal file
147
frontend/src/components/Fields.vue
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div
|
||||||
|
v-for="section in sections"
|
||||||
|
:key="section.section"
|
||||||
|
class="first:border-t-0 first:pt-0"
|
||||||
|
:class="section.hideBorder ? '' : 'border-t pt-4'"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="grid gap-4"
|
||||||
|
:class="
|
||||||
|
section.columns ? 'grid-cols-' + section.columns : 'grid-cols-3'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div v-for="field in section.fields" :key="field.name">
|
||||||
|
<div class="mb-2 text-sm text-gray-600">
|
||||||
|
{{ __(field.label) }}
|
||||||
|
<span class="text-red-500" v-if="field.mandatory">*</span>
|
||||||
|
</div>
|
||||||
|
<FormControl
|
||||||
|
v-if="field.type === 'select'"
|
||||||
|
type="select"
|
||||||
|
class="form-control"
|
||||||
|
:class="field.prefix ? 'prefix' : ''"
|
||||||
|
:options="field.options"
|
||||||
|
v-model="data[field.name]"
|
||||||
|
:placeholder="__(field.placeholder)"
|
||||||
|
>
|
||||||
|
<template v-if="field.prefix" #prefix>
|
||||||
|
<IndicatorIcon :class="field.prefix" />
|
||||||
|
</template>
|
||||||
|
</FormControl>
|
||||||
|
<Link
|
||||||
|
v-else-if="field.type === 'link'"
|
||||||
|
class="form-control"
|
||||||
|
:value="data[field.name]"
|
||||||
|
:doctype="field.doctype"
|
||||||
|
@change="(v) => (data[field.name] = v)"
|
||||||
|
:placeholder="__(field.placeholder)"
|
||||||
|
:onCreate="field.create"
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
v-else-if="field.type === 'user'"
|
||||||
|
class="form-control"
|
||||||
|
:value="getUser(data[field.name]).full_name"
|
||||||
|
:doctype="field.doctype"
|
||||||
|
@change="(v) => (data[field.name] = v)"
|
||||||
|
:placeholder="__(field.placeholder)"
|
||||||
|
:hideMe="true"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<UserAvatar class="mr-2" :user="data[field.name]" 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">
|
||||||
|
<div class="cursor-pointer">
|
||||||
|
{{ getUser(option.value).full_name }}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</template>
|
||||||
|
</Link>
|
||||||
|
<div v-else-if="field.type === 'dropdown'">
|
||||||
|
<NestedPopover>
|
||||||
|
<template #target="{ open }">
|
||||||
|
<Button
|
||||||
|
:label="data[field.name]"
|
||||||
|
class="dropdown-button flex w-full items-center justify-between rounded border border-gray-100 bg-gray-100 px-2 py-1.5 text-base text-gray-800 placeholder-gray-500 transition-colors hover:border-gray-200 hover:bg-gray-200 focus:border-gray-500 focus:bg-white focus:shadow-sm focus:outline-none focus:ring-0 focus-visible:ring-2 focus-visible:ring-gray-400"
|
||||||
|
>
|
||||||
|
<div class="truncate">{{ data[field.name] }}</div>
|
||||||
|
<template #suffix>
|
||||||
|
<FeatherIcon
|
||||||
|
:name="open ? 'chevron-up' : 'chevron-down'"
|
||||||
|
class="h-4 text-gray-600"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div
|
||||||
|
class="my-2 space-y-1.5 divide-y rounded-lg border border-gray-100 bg-white p-1.5 shadow-xl"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<DropdownItem
|
||||||
|
v-if="field.options?.length"
|
||||||
|
v-for="option in field.options"
|
||||||
|
:key="option.name"
|
||||||
|
:option="option"
|
||||||
|
/>
|
||||||
|
<div v-else>
|
||||||
|
<div class="p-1.5 px-7 text-base text-gray-500">
|
||||||
|
{{ __('No {0} Available', [field.label]) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pt-1.5">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="w-full !justify-start"
|
||||||
|
:label="__('Create New')"
|
||||||
|
@click="field.create()"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="plus" class="h-4" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</NestedPopover>
|
||||||
|
</div>
|
||||||
|
<FormControl
|
||||||
|
v-else
|
||||||
|
type="text"
|
||||||
|
:placeholder="__(field.placeholder)"
|
||||||
|
v-model="data[field.name]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import NestedPopover from '@/components/NestedPopover.vue'
|
||||||
|
import DropdownItem from '@/components/DropdownItem.vue'
|
||||||
|
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||||
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
import { usersStore } from '@/stores/users'
|
||||||
|
import { Tooltip } from 'frappe-ui'
|
||||||
|
|
||||||
|
const { getUser } = usersStore()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
sections: Array,
|
||||||
|
data: Object,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.form-control.prefix select) {
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -57,82 +57,7 @@
|
|||||||
<div v-else>{{ field.value }}</div>
|
<div v-else>{{ field.value }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-4" v-else>
|
<Fields v-else :sections="sections" :data="_contact" />
|
||||||
<div class="flex gap-4" v-for="(section, i) in sections" :key="i">
|
|
||||||
<div v-for="(field, j) in section.fields" :key="j" class="flex-1">
|
|
||||||
<Link
|
|
||||||
v-if="field.type === 'link'"
|
|
||||||
variant="outline"
|
|
||||||
size="md"
|
|
||||||
:label="__(field.label)"
|
|
||||||
v-model="_contact[field.name]"
|
|
||||||
:doctype="field.doctype"
|
|
||||||
:placeholder="field.placeholder"
|
|
||||||
/>
|
|
||||||
<div class="space-y-1.5" v-if="field.type === 'dropdown'">
|
|
||||||
<label class="block text-base text-gray-600">
|
|
||||||
{{ __(field.label) }}
|
|
||||||
</label>
|
|
||||||
<NestedPopover>
|
|
||||||
<template #target="{ open }">
|
|
||||||
<Button
|
|
||||||
:label="_contact[field.name]"
|
|
||||||
class="dropdown-button h-8 w-full justify-between truncate rounded border border-gray-300 bg-white px-2.5 py-1.5 text-base placeholder-gray-500 hover:border-gray-400 hover:bg-white hover:shadow-sm focus:border-gray-500 focus:bg-white focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-gray-400"
|
|
||||||
>
|
|
||||||
<div class="truncate">{{ _contact[field.name] }}</div>
|
|
||||||
<template #suffix>
|
|
||||||
<FeatherIcon
|
|
||||||
:name="open ? 'chevron-up' : 'chevron-down'"
|
|
||||||
class="h-4 text-gray-600"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
<template #body>
|
|
||||||
<div
|
|
||||||
class="my-2 space-y-1.5 divide-y rounded-lg border border-gray-100 bg-white p-1.5 shadow-xl"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<DropdownItem
|
|
||||||
v-if="field.options?.length"
|
|
||||||
v-for="option in field.options"
|
|
||||||
:key="option.name"
|
|
||||||
:option="option"
|
|
||||||
/>
|
|
||||||
<div v-else>
|
|
||||||
<div class="p-1.5 px-7 text-base text-gray-500">
|
|
||||||
{{ __('No {0} Available', [field.label]) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pt-1.5">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
class="w-full !justify-start"
|
|
||||||
:label="__('Create New')"
|
|
||||||
@click="field.create()"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<FeatherIcon name="plus" class="h-4" />
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</NestedPopover>
|
|
||||||
</div>
|
|
||||||
<FormControl
|
|
||||||
v-else-if="field.type === 'data'"
|
|
||||||
variant="outline"
|
|
||||||
size="md"
|
|
||||||
type="text"
|
|
||||||
:label="__(field.label)"
|
|
||||||
:placeholder="field.placeholder"
|
|
||||||
v-model="_contact[field.name]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!detailMode" class="px-4 pb-7 pt-4 sm:px-6">
|
<div v-if="!detailMode" class="px-4 pb-7 pt-4 sm:px-6">
|
||||||
@ -152,8 +77,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import NestedPopover from '@/components/NestedPopover.vue'
|
import Fields from '@/components/Fields.vue'
|
||||||
import DropdownItem from '@/components/DropdownItem.vue'
|
|
||||||
import ContactIcon from '@/components/Icons/ContactIcon.vue'
|
import ContactIcon from '@/components/Icons/ContactIcon.vue'
|
||||||
import GenderIcon from '@/components/Icons/GenderIcon.vue'
|
import GenderIcon from '@/components/Icons/GenderIcon.vue'
|
||||||
import EmailIcon from '@/components/Icons/EmailIcon.vue'
|
import EmailIcon from '@/components/Icons/EmailIcon.vue'
|
||||||
@ -162,9 +86,8 @@ import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
|||||||
import AddressIcon from '@/components/Icons/AddressIcon.vue'
|
import AddressIcon from '@/components/Icons/AddressIcon.vue'
|
||||||
import CertificateIcon from '@/components/Icons/CertificateIcon.vue'
|
import CertificateIcon from '@/components/Icons/CertificateIcon.vue'
|
||||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
|
||||||
import Dropdown from '@/components/frappe-ui/Dropdown.vue'
|
import Dropdown from '@/components/frappe-ui/Dropdown.vue'
|
||||||
import { Tooltip, call } from 'frappe-ui'
|
import { call } from 'frappe-ui'
|
||||||
import { ref, nextTick, watch, computed, h } from 'vue'
|
import { ref, nextTick, watch, computed, h } from 'vue'
|
||||||
import { createToast } from '@/utils'
|
import { createToast } from '@/utils'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
@ -219,9 +142,9 @@ async function callInsertDoc() {
|
|||||||
delete _contact.value.email_id
|
delete _contact.value.email_id
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_contact.value.mobile_no) {
|
if (_contact.value.actual_mobile_no) {
|
||||||
_contact.value.phone_nos = [{ phone: _contact.value.mobile_no }]
|
_contact.value.phone_nos = [{ phone: _contact.value.actual_mobile_no }]
|
||||||
delete _contact.value.mobile_no
|
delete _contact.value.actual_mobile_no
|
||||||
}
|
}
|
||||||
|
|
||||||
const doc = await call('frappe.client.insert', {
|
const doc = await call('frappe.client.insert', {
|
||||||
@ -310,39 +233,48 @@ const detailFields = computed(() => {
|
|||||||
const sections = computed(() => {
|
const sections = computed(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
section: 'Salutation',
|
||||||
|
columns: 1,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Salutation',
|
label: 'Salutation',
|
||||||
type: 'link',
|
|
||||||
name: 'salutation',
|
name: 'salutation',
|
||||||
placeholder: 'Mr./Mrs./Ms...',
|
type: 'link',
|
||||||
|
placeholder: 'Mr',
|
||||||
doctype: 'Salutation',
|
doctype: 'Salutation',
|
||||||
change: (value) => {
|
|
||||||
_contact.value.salutation = value
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
section: 'Full Name',
|
||||||
|
columns: 2,
|
||||||
|
hideBorder: true,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'First Name',
|
label: 'First Name',
|
||||||
type: 'data',
|
|
||||||
name: 'first_name',
|
name: 'first_name',
|
||||||
|
type: 'data',
|
||||||
|
mandatory: true,
|
||||||
|
placeholder: 'John',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Last Name',
|
label: 'Last Name',
|
||||||
type: 'data',
|
|
||||||
name: 'last_name',
|
name: 'last_name',
|
||||||
|
type: 'data',
|
||||||
|
placeholder: 'Doe',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
section: 'Email',
|
||||||
|
columns: 1,
|
||||||
|
hideBorder: true,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Email',
|
label: 'Email',
|
||||||
type: props.contact?.data?.name ? 'dropdown' : 'data',
|
|
||||||
name: 'email_id',
|
name: 'email_id',
|
||||||
|
type: props.contact?.data?.name ? 'dropdown' : 'data',
|
||||||
|
placeholder: 'john@doe.com',
|
||||||
options:
|
options:
|
||||||
props.contact.data?.email_ids?.map((email) => {
|
props.contact.data?.email_ids?.map((email) => {
|
||||||
return {
|
return {
|
||||||
@ -395,11 +327,15 @@ const sections = computed(() => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
section: 'Mobile No. & Gender',
|
||||||
|
columns: 2,
|
||||||
|
hideBorder: true,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Mobile No.',
|
label: 'Mobile No.',
|
||||||
type: props.contact?.data?.name ? 'dropdown' : 'data',
|
|
||||||
name: 'actual_mobile_no',
|
name: 'actual_mobile_no',
|
||||||
|
type: props.contact?.data?.name ? 'dropdown' : 'data',
|
||||||
|
placeholder: '+91 9876543210',
|
||||||
options:
|
options:
|
||||||
props.contact.data?.phone_nos?.map((phone) => {
|
props.contact.data?.phone_nos?.map((phone) => {
|
||||||
return {
|
return {
|
||||||
@ -452,56 +388,37 @@ const sections = computed(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Gender',
|
label: 'Gender',
|
||||||
type: 'link',
|
|
||||||
name: 'gender',
|
name: 'gender',
|
||||||
placeholder: 'Select Gender',
|
type: 'link',
|
||||||
doctype: 'Gender',
|
doctype: 'Gender',
|
||||||
change: (value) => {
|
placeholder: 'Male',
|
||||||
_contact.value.gender = value
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
section: 'Organization',
|
||||||
|
columns: 1,
|
||||||
|
hideBorder: true,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Organization',
|
label: 'Organization',
|
||||||
type: 'link',
|
|
||||||
name: 'company_name',
|
name: 'company_name',
|
||||||
placeholder: 'Select Organization',
|
type: 'link',
|
||||||
doctype: 'CRM Organization',
|
doctype: 'CRM Organization',
|
||||||
change: (value) => {
|
placeholder: 'Frappé Technologies',
|
||||||
_contact.value.company_name = value
|
|
||||||
},
|
|
||||||
link: (data) => {
|
|
||||||
router.push({
|
|
||||||
name: 'Organization',
|
|
||||||
params: { organizationId: data },
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
section: 'Designation',
|
||||||
|
columns: 1,
|
||||||
|
hideBorder: true,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Designation',
|
label: 'Designation',
|
||||||
type: 'data',
|
|
||||||
name: 'designation',
|
name: 'designation',
|
||||||
},
|
type: 'data',
|
||||||
],
|
placeholder: 'CEO',
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
label: 'Address',
|
|
||||||
type: 'link',
|
|
||||||
name: 'address',
|
|
||||||
placeholder: 'Select Address',
|
|
||||||
doctype: 'Address',
|
|
||||||
change: (value) => {
|
|
||||||
_contact.value.address = value
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -17,71 +17,7 @@
|
|||||||
<Switch v-model="chooseExistingContact" />
|
<Switch v-model="chooseExistingContact" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-4">
|
<Fields class="border-t" :sections="sections" :data="deal" />
|
||||||
<div
|
|
||||||
v-for="section in allFields"
|
|
||||||
:key="section.section"
|
|
||||||
class="border-t pt-4"
|
|
||||||
>
|
|
||||||
<div class="grid grid-cols-3 gap-4">
|
|
||||||
<div v-for="field in section.fields" :key="field.name">
|
|
||||||
<div class="mb-2 text-sm text-gray-600">
|
|
||||||
{{ __(field.label) }}
|
|
||||||
</div>
|
|
||||||
<FormControl
|
|
||||||
v-if="field.type === 'select'"
|
|
||||||
type="select"
|
|
||||||
class="form-control"
|
|
||||||
:options="field.options"
|
|
||||||
v-model="deal[field.name]"
|
|
||||||
:placeholder="__(field.placeholder)"
|
|
||||||
>
|
|
||||||
<template v-if="field.prefix" #prefix>
|
|
||||||
<IndicatorIcon :class="field.prefix" />
|
|
||||||
</template>
|
|
||||||
</FormControl>
|
|
||||||
<Link
|
|
||||||
v-else-if="field.type === 'link'"
|
|
||||||
class="form-control"
|
|
||||||
:value="deal[field.name]"
|
|
||||||
:doctype="field.doctype"
|
|
||||||
@change="(v) => (deal[field.name] = v)"
|
|
||||||
:placeholder="__(field.placeholder)"
|
|
||||||
:onCreate="field.create"
|
|
||||||
/>
|
|
||||||
<Link
|
|
||||||
v-else-if="field.type === 'user'"
|
|
||||||
class="form-control"
|
|
||||||
:value="getUser(deal[field.name]).full_name"
|
|
||||||
:doctype="field.doctype"
|
|
||||||
@change="(v) => (deal[field.name] = v)"
|
|
||||||
:placeholder="__(field.placeholder)"
|
|
||||||
:hideMe="true"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<UserAvatar class="mr-2" :user="deal[field.name]" 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">
|
|
||||||
<div class="cursor-pointer">
|
|
||||||
{{ getUser(option.value).full_name }}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</template>
|
|
||||||
</Link>
|
|
||||||
<FormControl
|
|
||||||
v-else
|
|
||||||
type="text"
|
|
||||||
:placeholder="__(field.placeholder)"
|
|
||||||
v-model="deal[field.name]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
|
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
|
||||||
</template>
|
</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
@ -98,12 +34,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
import Fields from '@/components/Fields.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
|
||||||
import Link from '@/components/Controls/Link.vue'
|
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { statusesStore } from '@/stores/statuses'
|
import { statusesStore } from '@/stores/statuses'
|
||||||
import { Tooltip, Switch, createResource } from 'frappe-ui'
|
import { Switch, createResource } from 'frappe-ui'
|
||||||
import { computed, ref, reactive, onMounted } from 'vue'
|
import { computed, ref, reactive, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
@ -137,7 +71,7 @@ const isDealCreating = ref(false)
|
|||||||
const chooseExistingContact = ref(false)
|
const chooseExistingContact = ref(false)
|
||||||
const chooseExistingOrganization = ref(false)
|
const chooseExistingOrganization = ref(false)
|
||||||
|
|
||||||
const allFields = computed(() => {
|
const sections = computed(() => {
|
||||||
let fields = []
|
let fields = []
|
||||||
if (chooseExistingOrganization.value) {
|
if (chooseExistingOrganization.value) {
|
||||||
fields.push({
|
fields.push({
|
||||||
@ -267,12 +201,13 @@ const allFields = computed(() => {
|
|||||||
}
|
}
|
||||||
fields.push({
|
fields.push({
|
||||||
section: 'Deal Details',
|
section: 'Deal Details',
|
||||||
|
columns: 2,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
name: 'status',
|
name: 'status',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: statusOptions('deal', (field, value) => (deal[field] = value)),
|
options: statusOptions('deal'),
|
||||||
prefix: getDealStatus(deal.status).iconColorClass,
|
prefix: getDealStatus(deal.status).iconColorClass,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -323,17 +258,8 @@ function createDeal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!deal.status) {
|
|
||||||
deal.status = computed(() => getDealStatus().name)
|
|
||||||
}
|
|
||||||
if (!deal.deal_owner) {
|
if (!deal.deal_owner) {
|
||||||
deal.deal_owner = getUser().email
|
deal.deal_owner = getUser().email
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
:deep(.form-control select) {
|
|
||||||
padding-left: 2rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -7,72 +7,7 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template #body-content>
|
<template #body-content>
|
||||||
<div class="flex flex-col gap-4">
|
<Fields :sections="sections" :data="lead" />
|
||||||
<div
|
|
||||||
v-for="section in allFields"
|
|
||||||
:key="section.section"
|
|
||||||
class="border-t pt-4 first:border-t-0"
|
|
||||||
>
|
|
||||||
<div class="grid grid-cols-3 gap-4">
|
|
||||||
<div v-for="field in section.fields" :key="field.name">
|
|
||||||
<div class="mb-2 text-sm text-gray-600">
|
|
||||||
{{ __(field.label) }}
|
|
||||||
<span class="text-red-500" v-if="field.mandatory">*</span>
|
|
||||||
</div>
|
|
||||||
<FormControl
|
|
||||||
v-if="field.type === 'select'"
|
|
||||||
type="select"
|
|
||||||
class="form-control"
|
|
||||||
:options="field.options"
|
|
||||||
v-model="lead[field.name]"
|
|
||||||
:placeholder="__(field.placeholder)"
|
|
||||||
>
|
|
||||||
<template v-if="field.prefix" #prefix>
|
|
||||||
<IndicatorIcon :class="field.prefix" />
|
|
||||||
</template>
|
|
||||||
</FormControl>
|
|
||||||
<Link
|
|
||||||
v-else-if="field.type === 'link'"
|
|
||||||
class="form-control"
|
|
||||||
:value="lead[field.name]"
|
|
||||||
:doctype="field.doctype"
|
|
||||||
@change="(v) => (lead[field.name] = v)"
|
|
||||||
:placeholder="__(field.placeholder)"
|
|
||||||
:onCreate="field.create"
|
|
||||||
/>
|
|
||||||
<Link
|
|
||||||
v-else-if="field.type === 'user'"
|
|
||||||
class="form-control"
|
|
||||||
:value="getUser(lead[field.name]).full_name"
|
|
||||||
:doctype="field.doctype"
|
|
||||||
@change="(v) => (lead[field.name] = v)"
|
|
||||||
:placeholder="__(field.placeholder)"
|
|
||||||
:hideMe="true"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<UserAvatar class="mr-2" :user="lead[field.name]" 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">
|
|
||||||
<div class="cursor-pointer">
|
|
||||||
{{ getUser(option.value).full_name }}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</template>
|
|
||||||
</Link>
|
|
||||||
<FormControl
|
|
||||||
v-else
|
|
||||||
type="text"
|
|
||||||
:placeholder="__(field.placeholder)"
|
|
||||||
v-model="lead[field.name]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
|
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
|
||||||
</template>
|
</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
@ -84,12 +19,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
import Fields from '@/components/Fields.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
|
||||||
import Link from '@/components/Controls/Link.vue'
|
|
||||||
import { usersStore } from '@/stores/users'
|
import { usersStore } from '@/stores/users'
|
||||||
import { statusesStore } from '@/stores/statuses'
|
import { statusesStore } from '@/stores/statuses'
|
||||||
import { Tooltip, createResource } from 'frappe-ui'
|
import { createResource } from 'frappe-ui'
|
||||||
import { computed, onMounted, ref, reactive } from 'vue'
|
import { computed, onMounted, ref, reactive } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
@ -118,7 +51,7 @@ const lead = reactive({
|
|||||||
lead_owner: '',
|
lead_owner: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const allFields = computed(() => {
|
const sections = computed(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
section: 'Contact Details',
|
section: 'Contact Details',
|
||||||
@ -219,15 +152,13 @@ const allFields = computed(() => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
section: 'Other Details',
|
section: 'Other Details',
|
||||||
|
columns: 2,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
name: 'status',
|
name: 'status',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: statusOptions(
|
options: statusOptions('lead'),
|
||||||
'lead',
|
|
||||||
(field, value) => (lead[field] = value)
|
|
||||||
),
|
|
||||||
prefix: getLeadStatus(lead.status).iconColorClass,
|
prefix: getLeadStatus(lead.status).iconColorClass,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -291,17 +222,8 @@ function createNewLead() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!lead.status) {
|
|
||||||
lead.status = computed(() => getLeadStatus().name)
|
|
||||||
}
|
|
||||||
if (!lead.lead_owner) {
|
if (!lead.lead_owner) {
|
||||||
lead.lead_owner = getUser().email
|
lead.lead_owner = getUser().email
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
:deep(.form-control select) {
|
|
||||||
padding-left: 2rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -35,78 +35,7 @@
|
|||||||
<div>{{ field.value }}</div>
|
<div>{{ field.value }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<Fields v-else :sections="sections" :data="_organization" />
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<FormControl
|
|
||||||
type="text"
|
|
||||||
ref="title"
|
|
||||||
size="md"
|
|
||||||
:label="__('Organization Name')"
|
|
||||||
variant="outline"
|
|
||||||
v-model="_organization.organization_name"
|
|
||||||
placeholder="Frappé Technologies"
|
|
||||||
/>
|
|
||||||
<div class="flex gap-4">
|
|
||||||
<FormControl
|
|
||||||
class="flex-1"
|
|
||||||
type="text"
|
|
||||||
size="md"
|
|
||||||
:label="__('Website')"
|
|
||||||
variant="outline"
|
|
||||||
v-model="_organization.website"
|
|
||||||
placeholder="https://example.com"
|
|
||||||
/>
|
|
||||||
<FormControl
|
|
||||||
class="flex-1"
|
|
||||||
type="text"
|
|
||||||
size="md"
|
|
||||||
:label="__('Annual Revenue')"
|
|
||||||
variant="outline"
|
|
||||||
v-model="_organization.annual_revenue"
|
|
||||||
:placeholder="__('9,999,999')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Link
|
|
||||||
class="flex-1"
|
|
||||||
size="md"
|
|
||||||
:label="__('Territory')"
|
|
||||||
variant="outline"
|
|
||||||
v-model="_organization.territory"
|
|
||||||
doctype="CRM Territory"
|
|
||||||
placeholder="India"
|
|
||||||
/>
|
|
||||||
<div class="flex gap-4">
|
|
||||||
<FormControl
|
|
||||||
class="flex-1"
|
|
||||||
type="select"
|
|
||||||
:options="[
|
|
||||||
{ label: __('1-10'), value: '1-10' },
|
|
||||||
{ label: __('11-50'), value: '11-50' },
|
|
||||||
{ label: __('51-200'), value: '51-200' },
|
|
||||||
{ label: __('201-500'), value: '201-500' },
|
|
||||||
{ label: __('501-1000'), value: '501-1000' },
|
|
||||||
{ label: __('1001-5000'), value: '1001-5000' },
|
|
||||||
{ label: __('5001-10000'), value: '5001-10000' },
|
|
||||||
{ label: __('10001+'), value: '10001+' },
|
|
||||||
]"
|
|
||||||
size="md"
|
|
||||||
:label="__('No of Employees')"
|
|
||||||
variant="outline"
|
|
||||||
:placeholder="__('1-10')"
|
|
||||||
v-model="_organization.no_of_employees"
|
|
||||||
/>
|
|
||||||
<Link
|
|
||||||
class="flex-1"
|
|
||||||
size="md"
|
|
||||||
:label="__('Industry')"
|
|
||||||
variant="outline"
|
|
||||||
v-model="_organization.industry"
|
|
||||||
doctype="CRM Industry"
|
|
||||||
:placeholder="__('Technology')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!detailMode" class="px-4 pb-7 pt-4 sm:px-6">
|
<div v-if="!detailMode" class="px-4 pb-7 pt-4 sm:px-6">
|
||||||
@ -126,11 +55,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import Fields from '@/components/Fields.vue'
|
||||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
import WebsiteIcon from '@/components/Icons/WebsiteIcon.vue'
|
import WebsiteIcon from '@/components/Icons/WebsiteIcon.vue'
|
||||||
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
||||||
import TerritoryIcon from '@/components/Icons/TerritoryIcon.vue'
|
import TerritoryIcon from '@/components/Icons/TerritoryIcon.vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
|
||||||
import { call, FeatherIcon } from 'frappe-ui'
|
import { call, FeatherIcon } from 'frappe-ui'
|
||||||
import { ref, nextTick, watch, computed, h } from 'vue'
|
import { ref, nextTick, watch, computed, h } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
@ -291,6 +220,85 @@ const fields = computed(() => {
|
|||||||
return details.filter((field) => field.value)
|
return details.filter((field) => field.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const sections = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
section: 'Organization Name',
|
||||||
|
columns: 1,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Organization Name',
|
||||||
|
name: 'organization_name',
|
||||||
|
type: 'data',
|
||||||
|
placeholder: 'Frappé Technologies',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
section: 'Website & Revenue',
|
||||||
|
columns: 2,
|
||||||
|
hideBorder: true,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Website',
|
||||||
|
name: 'website',
|
||||||
|
type: 'data',
|
||||||
|
placeholder: 'https://example.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Annual Revenue',
|
||||||
|
name: 'annual_revenue',
|
||||||
|
type: 'data',
|
||||||
|
placeholder: '9,999,999',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
section: 'Territory',
|
||||||
|
columns: 1,
|
||||||
|
hideBorder: true,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Territory',
|
||||||
|
name: 'territory',
|
||||||
|
type: 'link',
|
||||||
|
doctype: 'CRM Territory',
|
||||||
|
placeholder: 'India',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
section: 'No of Employees & Industry',
|
||||||
|
columns: 2,
|
||||||
|
hideBorder: true,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'No of Employees',
|
||||||
|
name: 'no_of_employees',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: __('1-10'), value: '1-10' },
|
||||||
|
{ label: __('11-50'), value: '11-50' },
|
||||||
|
{ label: __('51-200'), value: '51-200' },
|
||||||
|
{ label: __('201-500'), value: '201-500' },
|
||||||
|
{ label: __('501-1000'), value: '501-1000' },
|
||||||
|
{ label: __('1001-5000'), value: '1001-5000' },
|
||||||
|
{ label: __('5001-10000'), value: '5001-10000' },
|
||||||
|
{ label: __('10001+'), value: '10001+' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Industry',
|
||||||
|
name: 'industry',
|
||||||
|
type: 'link',
|
||||||
|
doctype: 'CRM Industry',
|
||||||
|
placeholder: 'Technology',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => show.value,
|
() => show.value,
|
||||||
(value) => {
|
(value) => {
|
||||||
|
|||||||
@ -17,6 +17,9 @@ export const usersStore = defineStore('crm-users', () => {
|
|||||||
auto: true,
|
auto: true,
|
||||||
transform(users) {
|
transform(users) {
|
||||||
for (let user of users) {
|
for (let user of users) {
|
||||||
|
if (user.name === 'Administrator') {
|
||||||
|
user.email = 'Administrator'
|
||||||
|
}
|
||||||
usersByName[user.name] = user
|
usersByName[user.name] = user
|
||||||
}
|
}
|
||||||
return users
|
return users
|
||||||
|
|||||||
@ -6,7 +6,10 @@ module.exports = {
|
|||||||
'./node_modules/frappe-ui/src/components/**/*.{vue,js,ts,jsx,tsx}',
|
'./node_modules/frappe-ui/src/components/**/*.{vue,js,ts,jsx,tsx}',
|
||||||
'../node_modules/frappe-ui/src/components/**/*.{vue,js,ts,jsx,tsx}',
|
'../node_modules/frappe-ui/src/components/**/*.{vue,js,ts,jsx,tsx}',
|
||||||
],
|
],
|
||||||
safelist: [{ pattern: /!(text|bg)-/, variants: ['hover', 'active'] }],
|
safelist: [
|
||||||
|
{ pattern: /!(text|bg)-/, variants: ['hover', 'active'] },
|
||||||
|
{ pattern: /^grid-cols-/ },
|
||||||
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user