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.
|
||||
- **Notes:** Add notes to leads and deals.
|
||||
- **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
|
||||
|
||||
|
||||
@ -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>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4" v-else>
|
||||
<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>
|
||||
<Fields v-else :sections="sections" :data="_contact" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!detailMode" class="px-4 pb-7 pt-4 sm:px-6">
|
||||
@ -152,8 +77,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import NestedPopover from '@/components/NestedPopover.vue'
|
||||
import DropdownItem from '@/components/DropdownItem.vue'
|
||||
import Fields from '@/components/Fields.vue'
|
||||
import ContactIcon from '@/components/Icons/ContactIcon.vue'
|
||||
import GenderIcon from '@/components/Icons/GenderIcon.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 CertificateIcon from '@/components/Icons/CertificateIcon.vue'
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import Link from '@/components/Controls/Link.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 { createToast } from '@/utils'
|
||||
import { useRouter } from 'vue-router'
|
||||
@ -219,9 +142,9 @@ async function callInsertDoc() {
|
||||
delete _contact.value.email_id
|
||||
}
|
||||
|
||||
if (_contact.value.mobile_no) {
|
||||
_contact.value.phone_nos = [{ phone: _contact.value.mobile_no }]
|
||||
delete _contact.value.mobile_no
|
||||
if (_contact.value.actual_mobile_no) {
|
||||
_contact.value.phone_nos = [{ phone: _contact.value.actual_mobile_no }]
|
||||
delete _contact.value.actual_mobile_no
|
||||
}
|
||||
|
||||
const doc = await call('frappe.client.insert', {
|
||||
@ -310,39 +233,48 @@ const detailFields = computed(() => {
|
||||
const sections = computed(() => {
|
||||
return [
|
||||
{
|
||||
section: 'Salutation',
|
||||
columns: 1,
|
||||
fields: [
|
||||
{
|
||||
label: 'Salutation',
|
||||
type: 'link',
|
||||
name: 'salutation',
|
||||
placeholder: 'Mr./Mrs./Ms...',
|
||||
type: 'link',
|
||||
placeholder: 'Mr',
|
||||
doctype: 'Salutation',
|
||||
change: (value) => {
|
||||
_contact.value.salutation = value
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
section: 'Full Name',
|
||||
columns: 2,
|
||||
hideBorder: true,
|
||||
fields: [
|
||||
{
|
||||
label: 'First Name',
|
||||
type: 'data',
|
||||
name: 'first_name',
|
||||
type: 'data',
|
||||
mandatory: true,
|
||||
placeholder: 'John',
|
||||
},
|
||||
{
|
||||
label: 'Last Name',
|
||||
type: 'data',
|
||||
name: 'last_name',
|
||||
type: 'data',
|
||||
placeholder: 'Doe',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
section: 'Email',
|
||||
columns: 1,
|
||||
hideBorder: true,
|
||||
fields: [
|
||||
{
|
||||
label: 'Email',
|
||||
type: props.contact?.data?.name ? 'dropdown' : 'data',
|
||||
name: 'email_id',
|
||||
type: props.contact?.data?.name ? 'dropdown' : 'data',
|
||||
placeholder: 'john@doe.com',
|
||||
options:
|
||||
props.contact.data?.email_ids?.map((email) => {
|
||||
return {
|
||||
@ -395,11 +327,15 @@ const sections = computed(() => {
|
||||
],
|
||||
},
|
||||
{
|
||||
section: 'Mobile No. & Gender',
|
||||
columns: 2,
|
||||
hideBorder: true,
|
||||
fields: [
|
||||
{
|
||||
label: 'Mobile No.',
|
||||
type: props.contact?.data?.name ? 'dropdown' : 'data',
|
||||
name: 'actual_mobile_no',
|
||||
type: props.contact?.data?.name ? 'dropdown' : 'data',
|
||||
placeholder: '+91 9876543210',
|
||||
options:
|
||||
props.contact.data?.phone_nos?.map((phone) => {
|
||||
return {
|
||||
@ -452,56 +388,37 @@ const sections = computed(() => {
|
||||
},
|
||||
{
|
||||
label: 'Gender',
|
||||
type: 'link',
|
||||
name: 'gender',
|
||||
placeholder: 'Select Gender',
|
||||
type: 'link',
|
||||
doctype: 'Gender',
|
||||
change: (value) => {
|
||||
_contact.value.gender = value
|
||||
},
|
||||
placeholder: 'Male',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
section: 'Organization',
|
||||
columns: 1,
|
||||
hideBorder: true,
|
||||
fields: [
|
||||
{
|
||||
label: 'Organization',
|
||||
type: 'link',
|
||||
name: 'company_name',
|
||||
placeholder: 'Select Organization',
|
||||
type: 'link',
|
||||
doctype: 'CRM Organization',
|
||||
change: (value) => {
|
||||
_contact.value.company_name = value
|
||||
},
|
||||
link: (data) => {
|
||||
router.push({
|
||||
name: 'Organization',
|
||||
params: { organizationId: data },
|
||||
})
|
||||
},
|
||||
placeholder: 'Frappé Technologies',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
section: 'Designation',
|
||||
columns: 1,
|
||||
hideBorder: true,
|
||||
fields: [
|
||||
{
|
||||
label: 'Designation',
|
||||
type: 'data',
|
||||
name: 'designation',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
label: 'Address',
|
||||
type: 'link',
|
||||
name: 'address',
|
||||
placeholder: 'Select Address',
|
||||
doctype: 'Address',
|
||||
change: (value) => {
|
||||
_contact.value.address = value
|
||||
},
|
||||
type: 'data',
|
||||
placeholder: 'CEO',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -17,71 +17,7 @@
|
||||
<Switch v-model="chooseExistingContact" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<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>
|
||||
<Fields class="border-t" :sections="sections" :data="deal" />
|
||||
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
|
||||
</template>
|
||||
<template #actions>
|
||||
@ -98,12 +34,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import Fields from '@/components/Fields.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
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 { useRouter } from 'vue-router'
|
||||
|
||||
@ -137,7 +71,7 @@ const isDealCreating = ref(false)
|
||||
const chooseExistingContact = ref(false)
|
||||
const chooseExistingOrganization = ref(false)
|
||||
|
||||
const allFields = computed(() => {
|
||||
const sections = computed(() => {
|
||||
let fields = []
|
||||
if (chooseExistingOrganization.value) {
|
||||
fields.push({
|
||||
@ -267,12 +201,13 @@ const allFields = computed(() => {
|
||||
}
|
||||
fields.push({
|
||||
section: 'Deal Details',
|
||||
columns: 2,
|
||||
fields: [
|
||||
{
|
||||
label: 'Status',
|
||||
name: 'status',
|
||||
type: 'select',
|
||||
options: statusOptions('deal', (field, value) => (deal[field] = value)),
|
||||
options: statusOptions('deal'),
|
||||
prefix: getDealStatus(deal.status).iconColorClass,
|
||||
},
|
||||
{
|
||||
@ -323,17 +258,8 @@ function createDeal() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!deal.status) {
|
||||
deal.status = computed(() => getDealStatus().name)
|
||||
}
|
||||
if (!deal.deal_owner) {
|
||||
deal.deal_owner = getUser().email
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.form-control select) {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -7,72 +7,7 @@
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<div class="flex flex-col gap-4">
|
||||
<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>
|
||||
<Fields :sections="sections" :data="lead" />
|
||||
<ErrorMessage class="mt-4" v-if="error" :message="__(error)" />
|
||||
</template>
|
||||
<template #actions>
|
||||
@ -84,12 +19,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import Fields from '@/components/Fields.vue'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { statusesStore } from '@/stores/statuses'
|
||||
import { Tooltip, createResource } from 'frappe-ui'
|
||||
import { createResource } from 'frappe-ui'
|
||||
import { computed, onMounted, ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
@ -118,7 +51,7 @@ const lead = reactive({
|
||||
lead_owner: '',
|
||||
})
|
||||
|
||||
const allFields = computed(() => {
|
||||
const sections = computed(() => {
|
||||
return [
|
||||
{
|
||||
section: 'Contact Details',
|
||||
@ -219,15 +152,13 @@ const allFields = computed(() => {
|
||||
},
|
||||
{
|
||||
section: 'Other Details',
|
||||
columns: 2,
|
||||
fields: [
|
||||
{
|
||||
label: 'Status',
|
||||
name: 'status',
|
||||
type: 'select',
|
||||
options: statusOptions(
|
||||
'lead',
|
||||
(field, value) => (lead[field] = value)
|
||||
),
|
||||
options: statusOptions('lead'),
|
||||
prefix: getLeadStatus(lead.status).iconColorClass,
|
||||
},
|
||||
{
|
||||
@ -291,17 +222,8 @@ function createNewLead() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!lead.status) {
|
||||
lead.status = computed(() => getLeadStatus().name)
|
||||
}
|
||||
if (!lead.lead_owner) {
|
||||
lead.lead_owner = getUser().email
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.form-control select) {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -35,78 +35,7 @@
|
||||
<div>{{ field.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<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>
|
||||
<Fields v-else :sections="sections" :data="_organization" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!detailMode" class="px-4 pb-7 pt-4 sm:px-6">
|
||||
@ -126,11 +55,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Fields from '@/components/Fields.vue'
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import WebsiteIcon from '@/components/Icons/WebsiteIcon.vue'
|
||||
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
||||
import TerritoryIcon from '@/components/Icons/TerritoryIcon.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { call, FeatherIcon } from 'frappe-ui'
|
||||
import { ref, nextTick, watch, computed, h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
@ -291,6 +220,85 @@ const fields = computed(() => {
|
||||
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(
|
||||
() => show.value,
|
||||
(value) => {
|
||||
|
||||
@ -17,6 +17,9 @@ export const usersStore = defineStore('crm-users', () => {
|
||||
auto: true,
|
||||
transform(users) {
|
||||
for (let user of users) {
|
||||
if (user.name === 'Administrator') {
|
||||
user.email = 'Administrator'
|
||||
}
|
||||
usersByName[user.name] = user
|
||||
}
|
||||
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}',
|
||||
],
|
||||
safelist: [{ pattern: /!(text|bg)-/, variants: ['hover', 'active'] }],
|
||||
safelist: [
|
||||
{ pattern: /!(text|bg)-/, variants: ['hover', 'active'] },
|
||||
{ pattern: /^grid-cols-/ },
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user