Merge pull request #28 from shariquerik/contact-linking
fix: Contact/Organization layout change & Detail view dialog
This commit is contained in:
commit
9deb9afcac
@ -31,6 +31,9 @@ def get_contacts():
|
||||
"first_name",
|
||||
"last_name",
|
||||
"full_name",
|
||||
"gender",
|
||||
"address",
|
||||
"designation",
|
||||
"image",
|
||||
"email_id",
|
||||
"mobile_no",
|
||||
|
||||
16
frontend/src/components/Icons/AddressIcon.vue
Normal file
16
frontend/src/components/Icons/AddressIcon.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M4.15969 11.0123L4.17279 11.0253C4.17276 11.0253 4.17484 11.0272 4.17897 11.031C4.21695 11.0655 4.42864 11.2579 4.77688 11.5727C5.14213 11.9029 5.62149 12.3355 6.10132 12.7685C6.16929 12.8299 6.23758 12.8915 6.30581 12.9531C7.02952 13.6061 7.74656 14.2532 8.00622 14.4912C8.15155 14.3604 8.43754 14.1068 8.78577 13.7981C9.13708 13.4865 9.55174 13.1189 9.94918 12.7645C10.4342 12.3321 10.9128 11.9027 11.2691 11.5768C11.3745 11.4803 11.4673 11.3947 11.5452 11.3218V11.2964L11.8648 10.9997C12.95 9.99193 13.5109 8.58334 13.5109 7.05226V7.03944L13.5112 7.02663C13.5901 3.95314 11.1555 1.5 8.05226 1.5C4.954 1.5 2.5 3.954 2.5 7.05226C2.5 8.58334 3.06088 9.99193 4.14613 10.9997L4.15969 11.0123ZM7.90678 14.5823C7.91104 14.578 7.91527 14.574 7.91946 14.5701C7.91386 14.5754 7.90961 14.5795 7.90678 14.5823ZM7.39704 15.2894C7.35023 15.2426 6.39086 14.3768 5.43146 13.511C4.47199 12.6451 3.51248 11.7793 3.46568 11.7324C2.15523 10.5156 1.5 8.83073 1.5 7.05226C1.5 3.40172 4.40172 0.5 8.05226 0.5C11.7028 0.5 14.6045 3.40172 14.5109 7.05226C14.5109 8.83073 13.8557 10.5156 12.5452 11.7324C12.5452 11.8024 10.399 13.7046 9.27744 14.6986C8.89921 15.0338 8.63749 15.2658 8.61388 15.2894C8.33307 15.5702 7.77145 15.5702 7.39704 15.2894ZM8.05226 9.92434C6.47034 9.92434 5.18019 8.63419 5.18019 7.05226C5.18019 5.47034 6.47034 4.18019 8.05226 4.18019C9.63419 4.18019 10.9243 5.47034 10.9243 7.05226C10.9243 8.63419 9.63419 9.92434 8.05226 9.92434ZM6.18019 7.05226C6.18019 8.0819 7.02262 8.92434 8.05226 8.92434C9.0819 8.92434 9.92434 8.0819 9.92434 7.05226C9.92434 6.02262 9.0819 5.18019 8.05226 5.18019C7.02262 5.18019 6.18019 6.02262 6.18019 7.05226Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
16
frontend/src/components/Icons/CertificateIcon.vue
Normal file
16
frontend/src/components/Icons/CertificateIcon.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M3 2C2.17157 2 1.5 2.67157 1.5 3.5V10.8333C1.5 11.6618 2.17157 12.3333 3 12.3333H8C8.27614 12.3333 8.5 12.5572 8.5 12.8333C8.5 13.1095 8.27614 13.3333 8 13.3333H3C1.61929 13.3333 0.5 12.214 0.5 10.8333V3.5C0.5 2.11929 1.61929 1 3 1H11.6C12.9807 1 14.1 2.11929 14.1 3.5V3.96127C14.1 4.23741 13.8761 4.46127 13.6 4.46127C13.3239 4.46127 13.1 4.23741 13.1 3.96127V3.5C13.1 2.67157 12.4284 2 11.6 2H3ZM11.3003 10.5825V12.891L12.4434 12.3194L12.6693 12.2065L12.8943 12.3213L14.0336 12.9029V10.5824C13.6283 10.806 13.1624 10.9333 12.6668 10.9333C12.1713 10.9333 11.7055 10.8061 11.3003 10.5825ZM15.0336 9.65809V13.7196V14.5362L14.3063 14.1649L12.6646 13.3268L11.0239 14.1472L10.3003 14.509V13.7V9.65848C10.0052 9.21137 9.8335 8.67571 9.8335 8.09994C9.8335 6.53513 11.102 5.2666 12.6668 5.2666C14.2316 5.2666 15.5002 6.53513 15.5002 8.09994C15.5002 8.67554 15.3285 9.21105 15.0336 9.65809ZM10.8335 8.09994C10.8335 7.08741 11.6543 6.2666 12.6668 6.2666C13.6794 6.2666 14.5002 7.08741 14.5002 8.09994C14.5002 9.11246 13.6794 9.93327 12.6668 9.93327C11.6543 9.93327 10.8335 9.11246 10.8335 8.09994ZM3.2998 5.7666C3.2998 5.49046 3.52366 5.2666 3.7998 5.2666H8.46647C8.74261 5.2666 8.96647 5.49046 8.96647 5.7666C8.96647 6.04274 8.74261 6.2666 8.46647 6.2666H3.7998C3.52366 6.2666 3.2998 6.04274 3.2998 5.7666ZM3.7998 8.06665C3.52366 8.06665 3.2998 8.29051 3.2998 8.56665C3.2998 8.84279 3.52366 9.06665 3.7998 9.06665H6.5998C6.87595 9.06665 7.0998 8.84279 7.0998 8.56665C7.0998 8.29051 6.87595 8.06665 6.5998 8.06665H3.7998Z"
|
||||
fill="#383838"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
18
frontend/src/components/Icons/ContactIcon.vue
Normal file
18
frontend/src/components/Icons/ContactIcon.vue
Normal file
@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M9.62476 4.49023C9.62476 5.3877 8.89722 6.11523 7.99976 6.11523C7.10229 6.11523 6.37476 5.3877 6.37476 4.49023C6.37476 3.59277 7.10229 2.86523 7.99976 2.86523C8.89722 2.86523 9.62476 3.59277 9.62476 4.49023ZM10.6248 4.49023C10.6248 5.93998 9.4495 7.11523 7.99976 7.11523C6.55001 7.11523 5.37476 5.93998 5.37476 4.49023C5.37476 3.04049 6.55001 1.86523 7.99976 1.86523C9.4495 1.86523 10.6248 3.04049 10.6248 4.49023ZM3.33049 11.4862C3.67081 9.95482 5.0291 8.86523 6.59788 8.86523H9.40237C10.9711 8.86523 12.3294 9.95482 12.6698 11.4862L12.7625 11.9035C12.96 12.7925 12.2836 13.6359 11.3728 13.6359H4.62741C3.7167 13.6359 3.0402 12.7925 3.23776 11.9035L3.33049 11.4862ZM6.59788 7.86523C4.5604 7.86523 2.7963 9.28035 2.3543 11.2693L2.26158 11.6866C1.92523 13.2001 3.07695 14.6359 4.62741 14.6359H11.3728C12.9233 14.6359 14.075 13.2001 13.7387 11.6866L13.6459 11.2693C13.204 9.28035 11.4398 7.86523 9.40237 7.86523H6.59788Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
16
frontend/src/components/Icons/GenderIcon.vue
Normal file
16
frontend/src/components/Icons/GenderIcon.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12.9289 0.557617H10.4294C10.1533 0.557617 9.92944 0.781475 9.92944 1.05762C9.92944 1.33376 10.1533 1.55762 10.4294 1.55762H11.7218L9.4363 3.84316C8.78027 3.37245 7.97597 3.09532 7.10693 3.09532C4.89779 3.09532 3.10693 4.88618 3.10693 7.09532C3.10693 9.13513 4.63378 10.8183 6.60693 11.0644V12.6762H5.35693C5.08079 12.6762 4.85693 12.9001 4.85693 13.1762C4.85693 13.4523 5.08079 13.6762 5.35693 13.6762H6.60693V14.9427C6.60693 15.2188 6.83079 15.4427 7.10693 15.4427C7.38308 15.4427 7.60693 15.2188 7.60693 14.9427V13.6762H8.85693C9.13308 13.6762 9.35693 13.4523 9.35693 13.1762C9.35693 12.9001 9.13308 12.6762 8.85693 12.6762H7.60693V11.0644C9.58009 10.8183 11.1069 9.13513 11.1069 7.09532C11.1069 6.11586 10.7549 5.21862 10.1704 4.52323L12.4294 2.26424V3.55762C12.4294 3.83376 12.6533 4.05762 12.9294 4.05762C13.2056 4.05762 13.4294 3.83376 13.4294 3.55762V1.05762C13.4294 0.781475 13.2056 0.557617 12.9294 0.557617H12.9289ZM7.10693 4.09532C5.45008 4.09532 4.10693 5.43846 4.10693 7.09532C4.10693 8.75217 5.45008 10.0953 7.10693 10.0953C8.76379 10.0953 10.1069 8.75217 10.1069 7.09532C10.1069 5.43846 8.76379 4.09532 7.10693 4.09532Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
71
frontend/src/components/ListViews/OrganizationsListView.vue
Normal file
71
frontend/src/components/ListViews/OrganizationsListView.vue
Normal file
@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<ListView
|
||||
:columns="columns"
|
||||
:rows="rows"
|
||||
:options="{
|
||||
getRowRoute: (row) => ({
|
||||
name: 'Organization',
|
||||
params: { organizationId: row.name },
|
||||
}),
|
||||
selectable: options.selectable,
|
||||
}"
|
||||
row-key="name"
|
||||
>
|
||||
<ListHeader class="mx-5" />
|
||||
<ListRows>
|
||||
<ListRow
|
||||
class="mx-5"
|
||||
v-for="row in rows"
|
||||
:key="row.name"
|
||||
v-slot="{ column, item }"
|
||||
:row="row"
|
||||
>
|
||||
<ListRowItem :item="item">
|
||||
<template #prefix>
|
||||
<div v-if="column.key === 'organization'">
|
||||
<Avatar
|
||||
v-if="item.label"
|
||||
class="flex items-center"
|
||||
:image="item.logo"
|
||||
:label="item.label"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="column.key === 'modified'" class="truncate text-base">
|
||||
{{ item.timeAgo }}
|
||||
</div>
|
||||
</ListRowItem>
|
||||
</ListRow>
|
||||
</ListRows>
|
||||
<ListSelectBanner />
|
||||
</ListView>
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
Avatar,
|
||||
ListView,
|
||||
ListHeader,
|
||||
ListRows,
|
||||
ListRow,
|
||||
ListSelectBanner,
|
||||
ListRowItem,
|
||||
} from 'frappe-ui'
|
||||
|
||||
const props = defineProps({
|
||||
rows: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
selectable: true,
|
||||
}),
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@ -1,73 +1,144 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
title: editMode ? 'Edit contact' : 'New contact',
|
||||
size: 'xl',
|
||||
actions: [
|
||||
{
|
||||
label: editMode ? 'Update' : 'Create',
|
||||
variant: 'solid',
|
||||
disabled: !dirty,
|
||||
onClick: ({ close }) =>
|
||||
editMode ? updateContact(close) : callInsertDoc(close),
|
||||
},
|
||||
],
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<div class="flex flex-col gap-4">
|
||||
<Link
|
||||
variant="outline"
|
||||
size="md"
|
||||
label="Salutation"
|
||||
v-model="_contact.salutation"
|
||||
doctype="Salutation"
|
||||
placeholder="Mr./Mrs./Ms..."
|
||||
/>
|
||||
<div class="flex gap-4">
|
||||
<FormControl
|
||||
class="flex-1"
|
||||
variant="outline"
|
||||
size="md"
|
||||
type="text"
|
||||
label="First Name"
|
||||
v-model="_contact.first_name"
|
||||
/>
|
||||
<FormControl
|
||||
class="flex-1"
|
||||
variant="outline"
|
||||
size="md"
|
||||
type="text"
|
||||
label="Last Name"
|
||||
v-model="_contact.last_name"
|
||||
/>
|
||||
<Dialog v-model="show" :options="dialogOptions">
|
||||
<template #body>
|
||||
<div class="bg-white px-4 pb-6 pt-5 sm:px-6">
|
||||
<div class="mb-5 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-2xl font-semibold leading-6 text-gray-900">
|
||||
{{ dialogOptions.title || 'Untitled' }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
v-if="detailMode"
|
||||
variant="ghost"
|
||||
class="w-7"
|
||||
@click="detailMode = false"
|
||||
>
|
||||
<EditIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" class="w-7" @click="show = false">
|
||||
<FeatherIcon name="x" class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
variant="outline"
|
||||
size="md"
|
||||
label="Organization"
|
||||
v-model="_contact.company_name"
|
||||
doctype="CRM Organization"
|
||||
placeholder="Select organization"
|
||||
/>
|
||||
<div class="flex gap-4">
|
||||
<FormControl
|
||||
class="flex-1"
|
||||
variant="outline"
|
||||
size="md"
|
||||
type="text"
|
||||
label="Mobile no"
|
||||
v-model="_contact.mobile_no"
|
||||
/>
|
||||
<FormControl
|
||||
class="flex-1"
|
||||
variant="outline"
|
||||
size="md"
|
||||
type="email"
|
||||
label="Email"
|
||||
v-model="_contact.email_id"
|
||||
/>
|
||||
<div>
|
||||
<div v-if="detailMode" class="flex flex-col gap-3.5">
|
||||
<div
|
||||
v-for="field in detailFields"
|
||||
:key="field.name"
|
||||
class="flex h-7 items-center gap-2 text-base text-gray-800"
|
||||
>
|
||||
<div class="grid w-7 place-content-center">
|
||||
<component :is="field.icon" />
|
||||
</div>
|
||||
<div v-if="field.type == 'dropdown'">
|
||||
<Dropdown
|
||||
:options="field.options"
|
||||
class="form-control -ml-2 mr-2 w-full flex-1"
|
||||
>
|
||||
<template #default="{ open }">
|
||||
<Button
|
||||
variant="ghost"
|
||||
:label="contact[field.name]"
|
||||
class="dropdown-button w-full justify-between truncate hover:bg-white"
|
||||
>
|
||||
<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>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<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>
|
||||
<Dropdown
|
||||
:options="field.options"
|
||||
class="form-control w-full flex-1"
|
||||
>
|
||||
<template #default="{ 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 #footer>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-full !justify-start"
|
||||
label="Create one"
|
||||
@click="field.create()"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="plus" class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</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>
|
||||
<Dialog v-model="_show" :options="_dialogOptions">
|
||||
<template #body-content>
|
||||
<FormControl
|
||||
:type="new_field.type"
|
||||
variant="outline"
|
||||
v-model="new_field.value"
|
||||
:placeholder="new_field.placeholder"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!detailMode" class="px-4 pb-7 pt-4 sm:px-6">
|
||||
<div class="space-y-2">
|
||||
<Button
|
||||
class="w-full"
|
||||
v-for="action in dialogOptions.actions"
|
||||
:key="action.label"
|
||||
v-bind="action"
|
||||
>
|
||||
{{ action.label }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -75,9 +146,21 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import DropdownItem from '@/components/DropdownItem.vue'
|
||||
import ContactIcon from '@/components/Icons/ContactIcon.vue'
|
||||
import GenderIcon from '@/components/Icons/GenderIcon.vue'
|
||||
import EmailIcon from '@/components/Icons/EmailIcon.vue'
|
||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||
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 { FormControl, Dialog, call } from 'frappe-ui'
|
||||
import { ref, defineModel, nextTick, watch, computed } from 'vue'
|
||||
import Dropdown from '@/components/frappe-ui/Dropdown.vue'
|
||||
import { contactsStore } from '@/stores/contacts'
|
||||
import { FormControl, Dialog, call, FeatherIcon } from 'frappe-ui'
|
||||
import { ref, defineModel, nextTick, watch, computed, h } from 'vue'
|
||||
import { createToast } from '@/utils'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const props = defineProps({
|
||||
@ -89,6 +172,7 @@ const props = defineProps({
|
||||
type: Object,
|
||||
default: {
|
||||
redirect: true,
|
||||
detailMode: false,
|
||||
afterInsert: () => {},
|
||||
},
|
||||
},
|
||||
@ -96,32 +180,35 @@ const props = defineProps({
|
||||
|
||||
const router = useRouter()
|
||||
const show = defineModel()
|
||||
const contacts = defineModel('reloadContacts')
|
||||
const { contacts } = contactsStore()
|
||||
|
||||
const detailMode = ref(false)
|
||||
const editMode = ref(false)
|
||||
let _contact = ref({})
|
||||
|
||||
async function updateContact(close) {
|
||||
async function updateContact() {
|
||||
if (!dirty.value) {
|
||||
close()
|
||||
show.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const values = { ..._contact.value }
|
||||
|
||||
let name = await callSetValue(values)
|
||||
|
||||
handleContactUpdate({ name }, close)
|
||||
handleContactUpdate({ name })
|
||||
}
|
||||
|
||||
async function callSetValue(values) {
|
||||
const d = await call('frappe.client.set_value', {
|
||||
doctype: 'Contact',
|
||||
name: _contact.value.name,
|
||||
name: props.contact.name,
|
||||
fieldname: values,
|
||||
})
|
||||
return d.name
|
||||
}
|
||||
|
||||
async function callInsertDoc(close) {
|
||||
async function callInsertDoc() {
|
||||
if (_contact.value.email_id) {
|
||||
_contact.value.email_ids = [{ email_id: _contact.value.email_id }]
|
||||
delete _contact.value.email_id
|
||||
@ -138,21 +225,281 @@ async function callInsertDoc(close) {
|
||||
..._contact.value,
|
||||
},
|
||||
})
|
||||
doc.name && handleContactUpdate(doc, close)
|
||||
doc.name && handleContactUpdate(doc)
|
||||
}
|
||||
|
||||
function handleContactUpdate(doc, close) {
|
||||
contacts.value?.reload()
|
||||
function handleContactUpdate(doc) {
|
||||
contacts.reload()
|
||||
if (doc.name && props.options.redirect) {
|
||||
router.push({
|
||||
name: 'Contact',
|
||||
params: { contactId: doc.name },
|
||||
})
|
||||
}
|
||||
close && close()
|
||||
show.value = false
|
||||
props.options.afterInsert && props.options.afterInsert(doc)
|
||||
}
|
||||
|
||||
const dialogOptions = computed(() => {
|
||||
let title = !editMode.value ? 'New contact' : _contact.value.full_name
|
||||
|
||||
let size = detailMode.value ? '' : 'xl'
|
||||
let actions = detailMode.value
|
||||
? []
|
||||
: [
|
||||
{
|
||||
label: editMode.value ? 'Save' : 'Create',
|
||||
variant: 'solid',
|
||||
disabled: !dirty.value,
|
||||
onClick: () => (editMode.value ? updateContact() : callInsertDoc()),
|
||||
},
|
||||
]
|
||||
|
||||
return { title, size, actions }
|
||||
})
|
||||
|
||||
const detailFields = computed(() => {
|
||||
let details = [
|
||||
{
|
||||
icon: ContactIcon,
|
||||
name: 'full_name',
|
||||
value:
|
||||
(_contact.value.salutation ? _contact.value.salutation + '. ' : '') +
|
||||
_contact.value.full_name,
|
||||
},
|
||||
{
|
||||
icon: GenderIcon,
|
||||
name: 'gender',
|
||||
value: _contact.value.gender,
|
||||
},
|
||||
{
|
||||
icon: EmailIcon,
|
||||
name: 'email_id',
|
||||
value: _contact.value.email_id,
|
||||
...sections.value[2].fields[0],
|
||||
},
|
||||
{
|
||||
icon: PhoneIcon,
|
||||
name: 'mobile_no',
|
||||
value: _contact.value.mobile_no,
|
||||
...sections.value[3].fields[0],
|
||||
},
|
||||
{
|
||||
icon: OrganizationsIcon,
|
||||
name: 'company_name',
|
||||
value: _contact.value.company_name,
|
||||
},
|
||||
{
|
||||
icon: CertificateIcon,
|
||||
name: 'designation',
|
||||
value: _contact.value.designation,
|
||||
},
|
||||
{
|
||||
icon: AddressIcon,
|
||||
name: 'address',
|
||||
value: _contact.value.address,
|
||||
},
|
||||
]
|
||||
|
||||
return details.filter((detail) => detail.value)
|
||||
})
|
||||
|
||||
const sections = computed(() => {
|
||||
return [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
label: 'Salutation',
|
||||
type: 'link',
|
||||
name: 'salutation',
|
||||
placeholder: 'Mr./Mrs./Ms...',
|
||||
doctype: 'Salutation',
|
||||
change: (value) => {
|
||||
_contact.value.salutation = value
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
label: 'First name',
|
||||
type: 'data',
|
||||
name: 'first_name',
|
||||
},
|
||||
{
|
||||
label: 'Last name',
|
||||
type: 'data',
|
||||
name: 'last_name',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
label: 'Email',
|
||||
type: 'dropdown',
|
||||
name: 'email_id',
|
||||
options: props.contact?.email_ids?.map((email) => {
|
||||
return {
|
||||
component: h(DropdownItem, {
|
||||
value: email.email_id,
|
||||
selected: email.email_id === props.contact.email_id,
|
||||
onClick: () => setAsPrimary('email', email.email_id),
|
||||
}),
|
||||
}
|
||||
}),
|
||||
create: (value) => {
|
||||
new_field.value = {
|
||||
type: 'email',
|
||||
value,
|
||||
placeholder: 'Add email address',
|
||||
}
|
||||
_dialogOptions.value = {
|
||||
title: 'Add email',
|
||||
actions: [
|
||||
{
|
||||
label: 'Add',
|
||||
variant: 'solid',
|
||||
onClick: () => createNew('email'),
|
||||
},
|
||||
],
|
||||
}
|
||||
_show.value = true
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
label: 'Mobile no.',
|
||||
type: 'dropdown',
|
||||
name: 'mobile_no',
|
||||
options: props.contact?.phone_nos?.map((phone) => {
|
||||
return {
|
||||
component: h(DropdownItem, {
|
||||
value: phone.phone,
|
||||
selected: phone.phone === props.contact.mobile_no,
|
||||
onClick: () => setAsPrimary('mobile_no', phone.phone),
|
||||
}),
|
||||
}
|
||||
}),
|
||||
create: (value) => {
|
||||
new_field.value = {
|
||||
type: 'tel',
|
||||
value,
|
||||
placeholder: 'Add mobile no.',
|
||||
}
|
||||
_dialogOptions.value = {
|
||||
title: 'Add mobile no.',
|
||||
actions: [
|
||||
{
|
||||
label: 'Add',
|
||||
variant: 'solid',
|
||||
onClick: () => createNew('phone'),
|
||||
},
|
||||
],
|
||||
}
|
||||
_show.value = true
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Gender',
|
||||
type: 'link',
|
||||
name: 'gender',
|
||||
placeholder: 'Select gender',
|
||||
doctype: 'Gender',
|
||||
change: (value) => {
|
||||
_contact.value.gender = value
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
label: 'Organization',
|
||||
type: 'link',
|
||||
name: 'company_name',
|
||||
placeholder: 'Select organization',
|
||||
doctype: 'CRM Organization',
|
||||
change: (value) => {
|
||||
_contact.value.company_name = value
|
||||
},
|
||||
link: (data) => {
|
||||
router.push({
|
||||
name: 'Organization',
|
||||
params: { organizationId: data },
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const _show = ref(false)
|
||||
const new_field = ref({})
|
||||
|
||||
const _dialogOptions = ref({})
|
||||
|
||||
async function setAsPrimary(field, value) {
|
||||
let d = await call('crm.api.contact.set_as_primary', {
|
||||
contact: props.contact.name,
|
||||
field,
|
||||
value,
|
||||
})
|
||||
if (d) {
|
||||
contacts.reload()
|
||||
createToast({
|
||||
title: 'Contact updated',
|
||||
icon: 'check',
|
||||
iconClasses: 'text-green-600',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function createNew(field) {
|
||||
let d = await call('crm.api.contact.create_new', {
|
||||
contact: props.contact.name,
|
||||
field,
|
||||
value: new_field.value?.value,
|
||||
})
|
||||
if (d) {
|
||||
contacts.reload()
|
||||
createToast({
|
||||
title: 'Contact updated',
|
||||
icon: 'check',
|
||||
iconClasses: 'text-green-600',
|
||||
})
|
||||
}
|
||||
_show.value = false
|
||||
}
|
||||
|
||||
const dirty = computed(() => {
|
||||
return JSON.stringify(props.contact) !== JSON.stringify(_contact.value)
|
||||
})
|
||||
@ -161,6 +508,7 @@ watch(
|
||||
() => show.value,
|
||||
(value) => {
|
||||
if (!value) return
|
||||
detailMode.value = props.options.detailMode
|
||||
editMode.value = false
|
||||
nextTick(() => {
|
||||
_contact.value = { ...props.contact }
|
||||
@ -171,3 +519,9 @@ watch(
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(:has(> .dropdown-button)) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,78 +1,114 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
title: editMode ? 'Edit Organization' : 'Create Organization',
|
||||
size: 'xl',
|
||||
actions: [
|
||||
{
|
||||
label: editMode ? 'Update' : 'Create',
|
||||
variant: 'solid',
|
||||
onClick: ({ close }) =>
|
||||
editMode ? updateOrganization(close) : callInsertDoc(close),
|
||||
},
|
||||
],
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<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="Add organization name"
|
||||
/>
|
||||
<div class="flex gap-4">
|
||||
<FormControl
|
||||
class="flex-1"
|
||||
type="text"
|
||||
size="md"
|
||||
label="Website"
|
||||
variant="outline"
|
||||
v-model="_organization.website"
|
||||
placeholder="Add website"
|
||||
/>
|
||||
<FormControl
|
||||
class="flex-1"
|
||||
type="text"
|
||||
size="md"
|
||||
label="Annual revenue"
|
||||
variant="outline"
|
||||
v-model="_organization.annual_revenue"
|
||||
placeholder="Add annual revenue"
|
||||
/>
|
||||
<Dialog v-model="show" :options="dialogOptions">
|
||||
<template #body>
|
||||
<div class="bg-white px-4 pb-6 pt-5 sm:px-6">
|
||||
<div class="mb-5 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-2xl font-semibold leading-6 text-gray-900">
|
||||
{{ dialogOptions.title || 'Untitled' }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
v-if="detailMode"
|
||||
variant="ghost"
|
||||
class="w-7"
|
||||
@click="detailMode = false"
|
||||
>
|
||||
<EditIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" class="w-7" @click="show = false">
|
||||
<FeatherIcon name="x" class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<FormControl
|
||||
class="flex-1"
|
||||
type="select"
|
||||
:options="[
|
||||
'1-10',
|
||||
'11-50',
|
||||
'51-200',
|
||||
'201-500',
|
||||
'501-1000',
|
||||
'1001-5000',
|
||||
'5001-10000',
|
||||
'10001+',
|
||||
]"
|
||||
size="md"
|
||||
label="No. of employees"
|
||||
variant="outline"
|
||||
v-model="_organization.no_of_employees"
|
||||
/>
|
||||
<Link
|
||||
class="flex-1"
|
||||
size="md"
|
||||
label="Industry"
|
||||
variant="outline"
|
||||
v-model="_organization.industry"
|
||||
doctype="CRM Industry"
|
||||
placeholder="Add industry"
|
||||
/>
|
||||
<div>
|
||||
<div v-if="detailMode" class="flex flex-col gap-3.5">
|
||||
<div
|
||||
class="flex h-7 items-center gap-2 text-base text-gray-800"
|
||||
v-for="field in fields"
|
||||
:key="field.name"
|
||||
>
|
||||
<div class="grid w-7 place-content-center">
|
||||
<component :is="field.icon" />
|
||||
</div>
|
||||
<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="Add organization name"
|
||||
/>
|
||||
<div class="flex gap-4">
|
||||
<FormControl
|
||||
class="flex-1"
|
||||
type="text"
|
||||
size="md"
|
||||
label="Website"
|
||||
variant="outline"
|
||||
v-model="_organization.website"
|
||||
placeholder="Add website"
|
||||
/>
|
||||
<FormControl
|
||||
class="flex-1"
|
||||
type="text"
|
||||
size="md"
|
||||
label="Annual revenue"
|
||||
variant="outline"
|
||||
v-model="_organization.annual_revenue"
|
||||
placeholder="Add annual revenue"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<FormControl
|
||||
class="flex-1"
|
||||
type="select"
|
||||
:options="[
|
||||
'1-10',
|
||||
'11-50',
|
||||
'51-200',
|
||||
'201-500',
|
||||
'501-1000',
|
||||
'1001-5000',
|
||||
'5001-10000',
|
||||
'10001+',
|
||||
]"
|
||||
size="md"
|
||||
label="No. of employees"
|
||||
variant="outline"
|
||||
v-model="_organization.no_of_employees"
|
||||
/>
|
||||
<Link
|
||||
class="flex-1"
|
||||
size="md"
|
||||
label="Industry"
|
||||
variant="outline"
|
||||
v-model="_organization.industry"
|
||||
doctype="CRM Industry"
|
||||
placeholder="Add industry"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!detailMode" class="px-4 pb-7 pt-4 sm:px-6">
|
||||
<div class="space-y-2">
|
||||
<Button
|
||||
class="w-full"
|
||||
v-for="action in dialogOptions.actions"
|
||||
:key="action.label"
|
||||
v-bind="action"
|
||||
>
|
||||
{{ action.label }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -80,9 +116,13 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import WebsiteIcon from '@/components/Icons/WebsiteIcon.vue'
|
||||
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { FormControl, Dialog, call } from 'frappe-ui'
|
||||
import { ref, defineModel, nextTick, watch } from 'vue'
|
||||
import { organizationsStore } from '@/stores/organizations'
|
||||
import { FormControl, Dialog, call, FeatherIcon } from 'frappe-ui'
|
||||
import { ref, defineModel, nextTick, watch, computed, h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const props = defineProps({
|
||||
@ -94,6 +134,7 @@ const props = defineProps({
|
||||
type: Object,
|
||||
default: {
|
||||
redirect: true,
|
||||
detailMode: false,
|
||||
afterInsert: () => {},
|
||||
},
|
||||
},
|
||||
@ -101,9 +142,10 @@ const props = defineProps({
|
||||
|
||||
const router = useRouter()
|
||||
const show = defineModel()
|
||||
const organizations = defineModel('reloadOrganizations')
|
||||
const { organizations } = organizationsStore()
|
||||
|
||||
const title = ref(null)
|
||||
const detailMode = ref(false)
|
||||
const editMode = ref(false)
|
||||
let _organization = ref({
|
||||
organization_name: '',
|
||||
@ -113,7 +155,7 @@ let _organization = ref({
|
||||
industry: '',
|
||||
})
|
||||
|
||||
async function updateOrganization(close) {
|
||||
async function updateOrganization() {
|
||||
const old = { ...props.organization }
|
||||
const newOrg = { ..._organization.value }
|
||||
|
||||
@ -125,7 +167,7 @@ async function updateOrganization(close) {
|
||||
const values = newOrg
|
||||
|
||||
if (!nameChanged && !otherFieldChanged) {
|
||||
close()
|
||||
show.value = false
|
||||
return
|
||||
}
|
||||
|
||||
@ -136,7 +178,7 @@ async function updateOrganization(close) {
|
||||
if (otherFieldChanged) {
|
||||
name = await callSetValue(values)
|
||||
}
|
||||
handleOrganizationUpdate({ name }, close)
|
||||
handleOrganizationUpdate({ name })
|
||||
}
|
||||
|
||||
async function callRenameDoc() {
|
||||
@ -157,7 +199,7 @@ async function callSetValue(values) {
|
||||
return d.name
|
||||
}
|
||||
|
||||
async function callInsertDoc(close) {
|
||||
async function callInsertDoc() {
|
||||
const doc = await call('frappe.client.insert', {
|
||||
doc: {
|
||||
doctype: 'CRM Organization',
|
||||
@ -165,26 +207,78 @@ async function callInsertDoc(close) {
|
||||
website: _organization.value.website,
|
||||
},
|
||||
})
|
||||
doc.name && handleOrganizationUpdate(doc, close)
|
||||
doc.name && handleOrganizationUpdate(doc)
|
||||
}
|
||||
|
||||
function handleOrganizationUpdate(doc, close) {
|
||||
organizations.value?.reload()
|
||||
function handleOrganizationUpdate(doc) {
|
||||
organizations.reload()
|
||||
if (doc.name && props.options.redirect) {
|
||||
router.push({
|
||||
name: 'Organization',
|
||||
params: { organizationId: doc.name },
|
||||
})
|
||||
}
|
||||
close && close()
|
||||
show.value = false
|
||||
props.options.afterInsert && props.options.afterInsert(doc)
|
||||
}
|
||||
|
||||
const dialogOptions = computed(() => {
|
||||
let title = !editMode.value
|
||||
? 'New organization'
|
||||
: _organization.value.organization_name
|
||||
let size = detailMode.value ? '' : 'xl'
|
||||
let actions = detailMode.value
|
||||
? []
|
||||
: [
|
||||
{
|
||||
label: editMode.value ? 'Save' : 'Create',
|
||||
variant: 'solid',
|
||||
onClick: () =>
|
||||
editMode.value ? updateOrganization() : callInsertDoc(),
|
||||
},
|
||||
]
|
||||
|
||||
return { title, size, actions }
|
||||
})
|
||||
|
||||
const fields = computed(() => {
|
||||
let details = [
|
||||
{
|
||||
icon: OrganizationsIcon,
|
||||
name: 'organization_name',
|
||||
value: _organization.value.organization_name,
|
||||
},
|
||||
{
|
||||
icon: WebsiteIcon,
|
||||
name: 'website',
|
||||
value: _organization.value.website,
|
||||
},
|
||||
{
|
||||
icon: h(FeatherIcon, { name: 'dollar-sign', class: 'h-4 w-4' }),
|
||||
name: 'annual_revenue',
|
||||
value: _organization.value.annual_revenue,
|
||||
},
|
||||
{
|
||||
icon: h(FeatherIcon, { name: 'hash', class: 'h-4 w-4' }),
|
||||
name: 'no_of_employees',
|
||||
value: _organization.value.no_of_employees,
|
||||
},
|
||||
{
|
||||
icon: h(FeatherIcon, { name: 'briefcase', class: 'h-4 w-4' }),
|
||||
name: 'industry',
|
||||
value: _organization.value.industry,
|
||||
},
|
||||
]
|
||||
|
||||
return details.filter((field) => field.value)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => show.value,
|
||||
(value) => {
|
||||
if (!value) return
|
||||
editMode.value = false
|
||||
detailMode.value = props.options.detailMode
|
||||
nextTick(() => {
|
||||
// TODO: Issue with FormControl
|
||||
// title.value.el.focus()
|
||||
|
||||
@ -4,157 +4,157 @@
|
||||
<Breadcrumbs :items="breadcrumbs" />
|
||||
</template>
|
||||
</LayoutHeader>
|
||||
<div class="flex h-full overflow-hidden">
|
||||
<div class="flex w-[352px] shrink-0 flex-col border-r">
|
||||
<FileUploader @success="changeContactImage" :validateFile="validateFile">
|
||||
<template #default="{ openFileSelector, error }">
|
||||
<div class="flex items-center justify-start gap-5 border-b p-5">
|
||||
<div class="group relative h-[88px] w-[88px]">
|
||||
<Avatar
|
||||
size="3xl"
|
||||
class="h-[88px] w-[88px]"
|
||||
:label="contact.full_name"
|
||||
:image="contact.image"
|
||||
/>
|
||||
<component
|
||||
:is="contact.image ? Dropdown : 'div'"
|
||||
v-bind="
|
||||
contact.image
|
||||
? {
|
||||
options: [
|
||||
{
|
||||
icon: 'upload',
|
||||
label: contact.image
|
||||
? 'Change image'
|
||||
: 'Upload image',
|
||||
onClick: openFileSelector,
|
||||
},
|
||||
{
|
||||
icon: 'trash-2',
|
||||
label: 'Remove image',
|
||||
onClick: () => changeContactImage(''),
|
||||
},
|
||||
],
|
||||
}
|
||||
: { onClick: openFileSelector }
|
||||
"
|
||||
class="!absolute bottom-0 left-0 right-0"
|
||||
>
|
||||
<div
|
||||
class="z-1 absolute bottom-0 left-0 right-0 flex h-13 cursor-pointer items-center justify-center rounded-b-full bg-black bg-opacity-40 pt-3 opacity-0 duration-300 ease-in-out group-hover:opacity-100"
|
||||
style="
|
||||
-webkit-clip-path: inset(12px 0 0 0);
|
||||
clip-path: inset(12px 0 0 0);
|
||||
"
|
||||
>
|
||||
<CameraIcon class="h-6 w-6 cursor-pointer text-white" />
|
||||
</div>
|
||||
</component>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2.5 truncate">
|
||||
<Tooltip :text="contact.full_name">
|
||||
<div class="truncate text-2xl font-medium">
|
||||
{{ contact.full_name }}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div class="flex gap-1.5">
|
||||
<Button
|
||||
label="Call"
|
||||
size="sm"
|
||||
@click="makeCall(contact.mobile_no)"
|
||||
>
|
||||
<template #prefix>
|
||||
<PhoneIcon class="h-4 w-4" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
label="Delete"
|
||||
theme="red"
|
||||
size="sm"
|
||||
@click="deleteContact"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="trash-2" class="h-4 w-4" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
<ErrorMessage :message="error" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</FileUploader>
|
||||
<div class="flex flex-col overflow-y-auto">
|
||||
<div class="px-5 py-3 text-base font-semibold leading-5">Details</div>
|
||||
<div class="flex flex-col gap-1.5 px-2">
|
||||
<div
|
||||
v-for="field in details"
|
||||
:key="field.name"
|
||||
class="flex items-center gap-2 px-3 text-base leading-5 last:mb-3"
|
||||
>
|
||||
<div class="w-[106px] shrink-0 text-gray-600">
|
||||
{{ field.label }}
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<Dropdown
|
||||
v-if="field.type === 'dropdown' && field.options.length"
|
||||
:options="field.options"
|
||||
class="form-control show-dropdown-icon w-full flex-1"
|
||||
>
|
||||
<template #default="{ open }">
|
||||
<div
|
||||
class="dropdown-button flex w-full items-center justify-between gap-2"
|
||||
>
|
||||
<Button
|
||||
:label="contact[field.name]"
|
||||
class="w-full justify-between truncate"
|
||||
>
|
||||
<div class="truncate">{{ contact[field.name] }}</div>
|
||||
</Button>
|
||||
<FeatherIcon
|
||||
:name="open ? 'chevron-up' : 'chevron-down'"
|
||||
class="h-4 text-gray-600"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-full !justify-start"
|
||||
label="Create one"
|
||||
@click="field.create()"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="plus" class="h-4" />
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
<Link
|
||||
v-else-if="field.type === 'link'"
|
||||
class="form-control"
|
||||
:value="contact[field.name]"
|
||||
:doctype="field.doctype"
|
||||
:placeholder="field.placeholder"
|
||||
@change="(e) => field.change(e)"
|
||||
/>
|
||||
<FormControl
|
||||
v-else
|
||||
type="text"
|
||||
:value="contact[field.name]"
|
||||
@change.stop="updateContact(field.name, $event.target.value)"
|
||||
class="form-control"
|
||||
:debounce="500"
|
||||
/>
|
||||
</div>
|
||||
<ExternalLinkIcon
|
||||
v-if="field.type === 'link' && field.link && contact[field.name]"
|
||||
class="h-4 w-4 shrink-0 cursor-pointer text-gray-600"
|
||||
@click="field.link(contact[field.name])"
|
||||
<div class="flex h-full flex-col overflow-hidden">
|
||||
<FileUploader @success="changeContactImage" :validateFile="validateFile">
|
||||
<template #default="{ openFileSelector, error }">
|
||||
<div class="flex items-center justify-start gap-6 p-5">
|
||||
<div class="group relative h-24 w-24">
|
||||
<Avatar
|
||||
size="3xl"
|
||||
class="h-24 w-24"
|
||||
:label="contact.full_name"
|
||||
:image="contact.image"
|
||||
/>
|
||||
<component
|
||||
:is="contact.image ? Dropdown : 'div'"
|
||||
v-bind="
|
||||
contact.image
|
||||
? {
|
||||
options: [
|
||||
{
|
||||
icon: 'upload',
|
||||
label: contact.image
|
||||
? 'Change image'
|
||||
: 'Upload image',
|
||||
onClick: openFileSelector,
|
||||
},
|
||||
{
|
||||
icon: 'trash-2',
|
||||
label: 'Remove image',
|
||||
onClick: () => changeContactImage(''),
|
||||
},
|
||||
],
|
||||
}
|
||||
: { onClick: openFileSelector }
|
||||
"
|
||||
class="!absolute bottom-0 left-0 right-0"
|
||||
>
|
||||
<div
|
||||
class="z-1 absolute bottom-0 left-0 right-0 flex h-14 cursor-pointer items-center justify-center rounded-b-full bg-black bg-opacity-40 pt-3 opacity-0 duration-300 ease-in-out group-hover:opacity-100"
|
||||
style="
|
||||
-webkit-clip-path: inset(12px 0 0 0);
|
||||
clip-path: inset(12px 0 0 0);
|
||||
"
|
||||
>
|
||||
<CameraIcon class="h-6 w-6 cursor-pointer text-white" />
|
||||
</div>
|
||||
</component>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5 truncate">
|
||||
<Tooltip :text="contact.full_name">
|
||||
<div class="truncate text-3xl font-semibold">
|
||||
<span v-if="contact.salutation">
|
||||
{{ contact.salutation + '. ' }}
|
||||
</span>
|
||||
<span>{{ contact.full_name }}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div class="flex items-center gap-2 text-base text-gray-700">
|
||||
<div v-if="contact.email_id" class="flex items-center gap-1.5">
|
||||
<EmailIcon class="h-4 w-4" />
|
||||
<span class="">{{ contact.email_id }}</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="contact.mobile_no && contact.email_id"
|
||||
class="text-3xl leading-[0] text-gray-600"
|
||||
>
|
||||
·
|
||||
</span>
|
||||
<Tooltip
|
||||
text="Make Call"
|
||||
v-if="contact.mobile_no"
|
||||
class="flex cursor-pointer items-center gap-1.5"
|
||||
@click="makeCall(contact.mobile_no)"
|
||||
>
|
||||
<PhoneIcon class="h-4 w-4" />
|
||||
<span class="">{{ contact.mobile_no }}</span>
|
||||
</Tooltip>
|
||||
<span
|
||||
v-if="
|
||||
(contact.email_id || contact.mobile_no) &&
|
||||
contact.company_name
|
||||
"
|
||||
class="text-3xl leading-[0] text-gray-600"
|
||||
>
|
||||
·
|
||||
</span>
|
||||
<div
|
||||
v-if="contact.company_name"
|
||||
class="flex items-center gap-1.5"
|
||||
>
|
||||
<Avatar
|
||||
size="xs"
|
||||
:label="contact.company_name"
|
||||
:image="
|
||||
getOrganization(contact.company_name)?.organization_logo
|
||||
"
|
||||
/>
|
||||
<span class="">{{ contact.company_name }}</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="
|
||||
contact.email_id || contact.mobile_no || contact.company_name
|
||||
"
|
||||
class="text-3xl leading-[0] text-gray-600"
|
||||
>
|
||||
·
|
||||
</span>
|
||||
<Button
|
||||
v-if="
|
||||
contact.email_id || contact.mobile_no || contact.company_name
|
||||
"
|
||||
variant="ghost"
|
||||
label="More"
|
||||
class="-ml-1 cursor-pointer hover:text-gray-900"
|
||||
@click="
|
||||
() => {
|
||||
detailMode = true
|
||||
showContactModal = true
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-2 flex gap-1.5">
|
||||
<Button
|
||||
label="Edit"
|
||||
size="sm"
|
||||
@click="
|
||||
() => {
|
||||
detailMode = false
|
||||
showContactModal = true
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #prefix>
|
||||
<EditIcon class="h-4 w-4" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
label="Delete"
|
||||
theme="red"
|
||||
size="sm"
|
||||
@click="deleteContact"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="trash-2" class="h-4 w-4" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
<ErrorMessage :message="error" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</FileUploader>
|
||||
<Tabs class="overflow-hidden" v-model="tabIndex" :tabs="tabs">
|
||||
<template #tab="{ tab, selected }">
|
||||
<button
|
||||
@ -201,25 +201,17 @@
|
||||
</template>
|
||||
</Tabs>
|
||||
</div>
|
||||
<Dialog v-model="show" :options="dialogOptions">
|
||||
<template #body-content>
|
||||
<FormControl
|
||||
:type="new_field.type"
|
||||
variant="outline"
|
||||
v-model="new_field.value"
|
||||
:placeholder="new_field.placeholder"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
<ContactModal
|
||||
v-model="showContactModal"
|
||||
:contact="contact"
|
||||
:options="{ detailMode }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import {
|
||||
FormControl,
|
||||
FeatherIcon,
|
||||
Breadcrumbs,
|
||||
Dialog,
|
||||
Avatar,
|
||||
FileUploader,
|
||||
ErrorMessage,
|
||||
@ -231,14 +223,15 @@ import {
|
||||
} from 'frappe-ui'
|
||||
import Dropdown from '@/components/frappe-ui/Dropdown.vue'
|
||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||
import EmailIcon from '@/components/Icons/EmailIcon.vue'
|
||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import CameraIcon from '@/components/Icons/CameraIcon.vue'
|
||||
import LeadsIcon from '@/components/Icons/LeadsIcon.vue'
|
||||
import DealsIcon from '@/components/Icons/DealsIcon.vue'
|
||||
import DropdownItem from '@/components/DropdownItem.vue'
|
||||
import ExternalLinkIcon from '@/components/Icons/ExternalLinkIcon.vue'
|
||||
import LeadsListView from '@/components/ListViews/LeadsListView.vue'
|
||||
import DealsListView from '@/components/ListViews/DealsListView.vue'
|
||||
import ContactModal from '@/components/Modals/ContactModal.vue'
|
||||
import {
|
||||
dateFormat,
|
||||
dateTooltipFormat,
|
||||
@ -246,7 +239,6 @@ import {
|
||||
formatNumberIntoCurrency,
|
||||
dealStatuses,
|
||||
leadStatuses,
|
||||
createToast,
|
||||
} from '@/utils'
|
||||
import { usersStore } from '@/stores/users.js'
|
||||
import { contactsStore } from '@/stores/contacts.js'
|
||||
@ -269,6 +261,9 @@ const router = useRouter()
|
||||
|
||||
const contact = computed(() => getContactByName(props.contactId))
|
||||
|
||||
const showContactModal = ref(false)
|
||||
const detailMode = ref(false)
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
let items = [{ label: 'Contacts', route: { name: 'Contacts' } }]
|
||||
items.push({
|
||||
@ -510,184 +505,6 @@ const dealColumns = [
|
||||
width: '8rem',
|
||||
},
|
||||
]
|
||||
|
||||
const details = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: 'Salutation',
|
||||
type: 'link',
|
||||
name: 'salutation',
|
||||
placeholder: 'Mr./Mrs./Ms...',
|
||||
doctype: 'Salutation',
|
||||
change: (value) => {
|
||||
contact.value.salutation = value
|
||||
updateContact('salutation', value)
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'First name',
|
||||
type: 'data',
|
||||
name: 'first_name',
|
||||
},
|
||||
{
|
||||
label: 'Last name',
|
||||
type: 'data',
|
||||
name: 'last_name',
|
||||
},
|
||||
{
|
||||
label: 'Email',
|
||||
type: 'dropdown',
|
||||
name: 'email_id',
|
||||
options: contact.value?.email_ids?.map((email) => {
|
||||
return {
|
||||
component: h(DropdownItem, {
|
||||
value: email.email_id,
|
||||
selected: email.email_id === contact.value.email_id,
|
||||
onClick: () => setAsPrimary('email', email.email_id),
|
||||
}),
|
||||
}
|
||||
}),
|
||||
create: (value) => {
|
||||
new_field.value = {
|
||||
type: 'email',
|
||||
value,
|
||||
placeholder: 'Add email address',
|
||||
}
|
||||
dialogOptions.value = {
|
||||
title: 'Add email',
|
||||
actions: [
|
||||
{
|
||||
label: 'Add',
|
||||
variant: 'solid',
|
||||
onClick: ({ close }) => createNew('email', close),
|
||||
},
|
||||
],
|
||||
}
|
||||
show.value = true
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Mobile no.',
|
||||
type: 'dropdown',
|
||||
name: 'mobile_no',
|
||||
options: contact.value?.phone_nos?.map((phone) => {
|
||||
return {
|
||||
component: h(DropdownItem, {
|
||||
value: phone.phone,
|
||||
selected: phone.phone === contact.value.mobile_no,
|
||||
onClick: () => setAsPrimary('mobile_no', phone.phone),
|
||||
}),
|
||||
}
|
||||
}),
|
||||
create: (value) => {
|
||||
new_field.value = {
|
||||
type: 'phone',
|
||||
value,
|
||||
placeholder: 'Add mobile no.',
|
||||
}
|
||||
dialogOptions.value = {
|
||||
title: 'Add mobile no.',
|
||||
actions: [
|
||||
{
|
||||
label: 'Add',
|
||||
variant: 'solid',
|
||||
onClick: ({ close }) => createNew('phone', close),
|
||||
},
|
||||
],
|
||||
}
|
||||
show.value = true
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Organization',
|
||||
type: 'link',
|
||||
name: 'company_name',
|
||||
placeholder: 'Select organization',
|
||||
doctype: 'CRM Organization',
|
||||
change: (value) => {
|
||||
contact.value.company_name = value
|
||||
updateContact('company_name', value)
|
||||
},
|
||||
link: (data) => {
|
||||
router.push({
|
||||
name: 'Organization',
|
||||
params: { organizationId: data },
|
||||
})
|
||||
},
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const show = ref(false)
|
||||
const new_field = ref({})
|
||||
|
||||
const dialogOptions = ref({})
|
||||
|
||||
function updateContact(fieldname, value) {
|
||||
if (['mobile_no', 'email_id'].includes(fieldname)) {
|
||||
details.value.find((d) => d.name === fieldname).create(value)
|
||||
return
|
||||
}
|
||||
createResource({
|
||||
url: 'frappe.client.set_value',
|
||||
params: {
|
||||
doctype: 'Contact',
|
||||
name: props.contactId,
|
||||
fieldname,
|
||||
value,
|
||||
},
|
||||
auto: true,
|
||||
onSuccess: () => {
|
||||
contacts.reload()
|
||||
createToast({
|
||||
title: 'Contact updated',
|
||||
icon: 'check',
|
||||
iconClasses: 'text-green-600',
|
||||
})
|
||||
},
|
||||
onError: (err) => {
|
||||
createToast({
|
||||
title: 'Error updating contact',
|
||||
text: err.messages?.[0],
|
||||
icon: 'x',
|
||||
iconClasses: 'text-red-600',
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function setAsPrimary(field, value) {
|
||||
let d = await call('crm.api.contact.set_as_primary', {
|
||||
contact: props.contactId,
|
||||
field,
|
||||
value,
|
||||
})
|
||||
if (d) {
|
||||
contacts.reload()
|
||||
createToast({
|
||||
title: 'Contact updated',
|
||||
icon: 'check',
|
||||
iconClasses: 'text-green-600',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function createNew(field, close) {
|
||||
let d = await call('crm.api.contact.create_new', {
|
||||
contact: props.contactId,
|
||||
field,
|
||||
value: new_field.value.value,
|
||||
})
|
||||
if (d) {
|
||||
contacts.reload()
|
||||
createToast({
|
||||
title: 'Contact updated',
|
||||
icon: 'check',
|
||||
iconClasses: 'text-green-600',
|
||||
})
|
||||
}
|
||||
close()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -34,11 +34,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<ContactsListView :rows="rows" :columns="columns" />
|
||||
<ContactModal
|
||||
v-model="showContactModal"
|
||||
v-model:reloadContacts="contacts"
|
||||
:contact="{}"
|
||||
/>
|
||||
<ContactModal v-model="showContactModal" :contact="{}" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -177,12 +173,4 @@ const columns = [
|
||||
width: '8rem',
|
||||
},
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
const el = document.querySelector('.router-link-active')
|
||||
if (el)
|
||||
setTimeout(() => {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
<template>
|
||||
<div class="flex flex-1 flex-col overflow-hidden">
|
||||
<div class="flex gap-6 p-5">
|
||||
<FileUploader
|
||||
@success="changeOrganizationImage"
|
||||
:validateFile="validateFile"
|
||||
>
|
||||
<template #default="{ openFileSelector, error }">
|
||||
<LayoutHeader v-if="organization">
|
||||
<template #left-header>
|
||||
<Breadcrumbs :items="breadcrumbs" />
|
||||
</template>
|
||||
</LayoutHeader>
|
||||
<div v-if="organization" class="flex flex-1 flex-col overflow-hidden">
|
||||
<FileUploader
|
||||
@success="changeOrganizationImage"
|
||||
:validateFile="validateFile"
|
||||
>
|
||||
<template #default="{ openFileSelector, error }">
|
||||
<div class="flex items-center justify-start gap-6 p-5">
|
||||
<div class="group relative h-24 w-24">
|
||||
<Avatar
|
||||
size="3xl"
|
||||
@ -47,63 +52,101 @@
|
||||
<CameraIcon class="h-6 w-6 cursor-pointer text-white" />
|
||||
</div>
|
||||
</component>
|
||||
<ErrorMessage class="mt-2" :message="error" />
|
||||
</div>
|
||||
</template>
|
||||
</FileUploader>
|
||||
<div class="flex flex-col justify-center gap-2">
|
||||
<div class="text-3xl font-semibold text-gray-900">
|
||||
{{ organization.name }}
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-base text-gray-700">
|
||||
<div v-if="organization.website" class="flex items-center gap-1.5">
|
||||
<WebsiteIcon class="h-4 w-4" />
|
||||
<span class="">{{ website(organization.website) }}</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="organization.industry && organization.website"
|
||||
class="text-3xl leading-[0] text-gray-600"
|
||||
>
|
||||
·
|
||||
</span>
|
||||
<div v-if="organization.industry" class="flex items-center gap-1.5">
|
||||
<FeatherIcon name="briefcase" class="h-4 w-4" />
|
||||
<span class="">{{ organization.industry }}</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="
|
||||
(organization.website || organization.industry) &&
|
||||
organization.annual_revenue
|
||||
"
|
||||
class="text-3xl leading-[0] text-gray-600"
|
||||
>
|
||||
·
|
||||
</span>
|
||||
<div
|
||||
v-if="organization.annual_revenue"
|
||||
class="flex items-center gap-1.5"
|
||||
>
|
||||
<FeatherIcon name="dollar-sign" class="h-4 w-4" />
|
||||
<span class="">{{ organization.annual_revenue }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1 flex gap-2">
|
||||
<Button label="Edit" size="sm" @click="showOrganizationModal = true">
|
||||
<template #prefix>
|
||||
<EditIcon class="h-4 w-4" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
label="Delete"
|
||||
theme="red"
|
||||
size="sm"
|
||||
@click="deleteOrganization"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="trash-2" class="h-4 w-4" />
|
||||
</template>
|
||||
</Button>
|
||||
<!-- <Button label="Add lead" size="sm">
|
||||
<div class="flex flex-col justify-center gap-0.5">
|
||||
<div class="text-3xl font-semibold text-gray-900">
|
||||
{{ organization.name }}
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-base text-gray-700">
|
||||
<div
|
||||
v-if="organization.website"
|
||||
class="flex items-center gap-1.5"
|
||||
>
|
||||
<WebsiteIcon class="h-4 w-4" />
|
||||
<span class="">{{ website(organization.website) }}</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="organization.industry && organization.website"
|
||||
class="text-3xl leading-[0] text-gray-600"
|
||||
>
|
||||
·
|
||||
</span>
|
||||
<div
|
||||
v-if="organization.industry"
|
||||
class="flex items-center gap-1.5"
|
||||
>
|
||||
<FeatherIcon name="briefcase" class="h-4 w-4" />
|
||||
<span class="">{{ organization.industry }}</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="
|
||||
(organization.website || organization.industry) &&
|
||||
organization.annual_revenue
|
||||
"
|
||||
class="text-3xl leading-[0] text-gray-600"
|
||||
>
|
||||
·
|
||||
</span>
|
||||
<div
|
||||
v-if="organization.annual_revenue"
|
||||
class="flex items-center gap-1.5"
|
||||
>
|
||||
<FeatherIcon name="dollar-sign" class="h-4 w-4" />
|
||||
<span class="">{{ organization.annual_revenue }}</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="
|
||||
organization.website ||
|
||||
organization.industry ||
|
||||
organization.annual_revenue
|
||||
"
|
||||
class="text-3xl leading-[0] text-gray-600"
|
||||
>
|
||||
·
|
||||
</span>
|
||||
<Button
|
||||
v-if="
|
||||
organization.website ||
|
||||
organization.industry ||
|
||||
organization.annual_revenue
|
||||
"
|
||||
variant="ghost"
|
||||
label="More"
|
||||
class="-ml-1 cursor-pointer hover:text-gray-900"
|
||||
@click="
|
||||
() => {
|
||||
detailMode = true
|
||||
showOrganizationModal = true
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-2 flex gap-1.5">
|
||||
<Button
|
||||
label="Edit"
|
||||
size="sm"
|
||||
@click="
|
||||
() => {
|
||||
detailMode = false
|
||||
showOrganizationModal = true
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #prefix>
|
||||
<EditIcon class="h-4 w-4" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button
|
||||
label="Delete"
|
||||
theme="red"
|
||||
size="sm"
|
||||
@click="deleteOrganization"
|
||||
>
|
||||
<template #prefix>
|
||||
<FeatherIcon name="trash-2" class="h-4 w-4" />
|
||||
</template>
|
||||
</Button>
|
||||
<!-- <Button label="Add lead" size="sm">
|
||||
<template #prefix>
|
||||
<FeatherIcon name="plus" class="h-4 w-4" />
|
||||
</template>
|
||||
@ -113,9 +156,12 @@
|
||||
<FeatherIcon name="plus" class="h-4 w-4" />
|
||||
</template>
|
||||
</Button> -->
|
||||
</div>
|
||||
<ErrorMessage class="mt-2" :message="error" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</FileUploader>
|
||||
<Tabs v-model="tabIndex" :tabs="tabs">
|
||||
<template #tab="{ tab, selected }">
|
||||
<button
|
||||
@ -170,16 +216,17 @@
|
||||
</div>
|
||||
</template>
|
||||
</Tabs>
|
||||
<OrganizationModal
|
||||
v-model="showOrganizationModal"
|
||||
v-model:reloadOrganizations="organizations"
|
||||
:organization="organization"
|
||||
/>
|
||||
</div>
|
||||
<OrganizationModal
|
||||
v-model="showOrganizationModal"
|
||||
:organization="organization"
|
||||
:options="{ detailMode }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
Breadcrumbs,
|
||||
FeatherIcon,
|
||||
Avatar,
|
||||
FileUploader,
|
||||
@ -190,14 +237,13 @@ import {
|
||||
call,
|
||||
createListResource,
|
||||
} from 'frappe-ui'
|
||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
||||
import LeadsListView from '@/components/ListViews/LeadsListView.vue'
|
||||
import DealsListView from '@/components/ListViews/DealsListView.vue'
|
||||
import ContactsListView from '@/components/ListViews/ContactsListView.vue'
|
||||
import WebsiteIcon from '@/components/Icons/WebsiteIcon.vue'
|
||||
import EmailIcon from '@/components/Icons/EmailIcon.vue'
|
||||
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||
import PhoneIcon from '@/components/Icons/PhoneIcon.vue'
|
||||
import CameraIcon from '@/components/Icons/CameraIcon.vue'
|
||||
import LeadsIcon from '@/components/Icons/LeadsIcon.vue'
|
||||
import DealsIcon from '@/components/Icons/DealsIcon.vue'
|
||||
@ -212,17 +258,32 @@ import {
|
||||
formatNumberIntoCurrency,
|
||||
} from '@/utils'
|
||||
import { usersStore } from '@/stores/users'
|
||||
import { h, computed, ref, watch, onMounted } from 'vue'
|
||||
import { h, computed, ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
organization: {
|
||||
type: Object,
|
||||
organizationId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const { organizations } = organizationsStore()
|
||||
const { organizations, getOrganization } = organizationsStore()
|
||||
const showOrganizationModal = ref(false)
|
||||
const detailMode = ref(false)
|
||||
|
||||
const organization = computed(() => getOrganization(props.organizationId))
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
let items = [{ label: 'Organizations', route: { name: 'Organizations' } }]
|
||||
items.push({
|
||||
label: props.organizationId,
|
||||
route: {
|
||||
name: 'Organization',
|
||||
params: { organizationId: props.organizationId },
|
||||
},
|
||||
})
|
||||
return items
|
||||
})
|
||||
|
||||
function validateFile(file) {
|
||||
let extn = file.name.split('.').pop().toLowerCase()
|
||||
@ -234,7 +295,7 @@ function validateFile(file) {
|
||||
async function changeOrganizationImage(file) {
|
||||
await call('frappe.client.set_value', {
|
||||
doctype: 'CRM Organization',
|
||||
name: props.organization.name,
|
||||
name: props.organizationId,
|
||||
fieldname: 'organization_logo',
|
||||
value: file?.file_url || '',
|
||||
})
|
||||
@ -253,7 +314,7 @@ async function deleteOrganization() {
|
||||
async onClick({ close }) {
|
||||
await call('frappe.client.delete', {
|
||||
doctype: 'CRM Organization',
|
||||
name: props.organization.name,
|
||||
name: props.organizationId,
|
||||
})
|
||||
organizations.reload()
|
||||
close()
|
||||
@ -291,7 +352,7 @@ const { getUser } = usersStore()
|
||||
const leads = createListResource({
|
||||
type: 'list',
|
||||
doctype: 'CRM Lead',
|
||||
cache: ['leads', props.organization.name],
|
||||
cache: ['leads', props.organizationId],
|
||||
fields: [
|
||||
'name',
|
||||
'first_name',
|
||||
@ -305,7 +366,7 @@ const leads = createListResource({
|
||||
'modified',
|
||||
],
|
||||
filters: {
|
||||
organization: props.organization.name,
|
||||
organization: props.organizationId,
|
||||
converted: 0,
|
||||
},
|
||||
orderBy: 'modified desc',
|
||||
@ -316,7 +377,7 @@ const leads = createListResource({
|
||||
const deals = createListResource({
|
||||
type: 'list',
|
||||
doctype: 'CRM Deal',
|
||||
cache: ['deals', props.organization.name],
|
||||
cache: ['deals', props.organizationId],
|
||||
fields: [
|
||||
'name',
|
||||
'organization',
|
||||
@ -328,7 +389,7 @@ const deals = createListResource({
|
||||
'modified',
|
||||
],
|
||||
filters: {
|
||||
organization: props.organization.name,
|
||||
organization: props.organizationId,
|
||||
},
|
||||
orderBy: 'modified desc',
|
||||
pageLength: 20,
|
||||
@ -338,7 +399,7 @@ const deals = createListResource({
|
||||
const contacts = createListResource({
|
||||
type: 'list',
|
||||
doctype: 'Contact',
|
||||
cache: ['contacts', props.organization.name],
|
||||
cache: ['contacts', props.organizationId],
|
||||
fields: [
|
||||
'name',
|
||||
'full_name',
|
||||
@ -349,7 +410,7 @@ const contacts = createListResource({
|
||||
'modified',
|
||||
],
|
||||
filters: {
|
||||
company_name: props.organization.name,
|
||||
company_name: props.organizationId,
|
||||
},
|
||||
orderBy: 'modified desc',
|
||||
pageLength: 20,
|
||||
@ -566,11 +627,4 @@ function reload(val) {
|
||||
deals.reload()
|
||||
contacts.reload()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.organization.name,
|
||||
(val) => val && reload(val)
|
||||
)
|
||||
|
||||
onMounted(() => reload(props.organization.name))
|
||||
</script>
|
||||
|
||||
@ -10,78 +10,62 @@
|
||||
@click="showOrganizationModal = true"
|
||||
>
|
||||
<template #prefix><FeatherIcon name="plus" class="h-4" /></template>
|
||||
Create organization
|
||||
</Button>
|
||||
</template>
|
||||
</LayoutHeader>
|
||||
<div class="flex h-full overflow-hidden">
|
||||
<div class="flex shrink-0 flex-col overflow-y-auto border-r">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'Organization',
|
||||
params: { organizationId: organization.name },
|
||||
}"
|
||||
v-for="(organization, i) in organizations.data"
|
||||
:key="i"
|
||||
:class="[
|
||||
currentOrganization?.name === organization.name
|
||||
? 'bg-gray-50 hover:bg-gray-100'
|
||||
: 'hover:bg-gray-50',
|
||||
]"
|
||||
>
|
||||
<div class="flex w-[352px] items-center gap-3 border-b px-5 py-4">
|
||||
<Avatar
|
||||
:image="organization.organization_logo"
|
||||
:label="organization.name"
|
||||
size="xl"
|
||||
/>
|
||||
<div class="flex flex-col items-start gap-1">
|
||||
<span class="text-base font-medium text-gray-900">
|
||||
{{ organization.name }}
|
||||
</span>
|
||||
<span class="text-sm text-gray-700">{{
|
||||
website(organization.website)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
<div class="flex items-center justify-between px-5 pb-4 pt-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Dropdown :options="viewsDropdownOptions">
|
||||
<template #default="{ open }">
|
||||
<Button :label="currentView.label">
|
||||
<template #prefix>
|
||||
<FeatherIcon :name="currentView.icon" class="h-4" />
|
||||
</template>
|
||||
<template #suffix>
|
||||
<FeatherIcon
|
||||
:name="open ? 'chevron-up' : 'chevron-down'"
|
||||
class="h-4 text-gray-600"
|
||||
/>
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<router-view
|
||||
v-if="currentOrganization"
|
||||
:organization="currentOrganization"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="grid h-full flex-1 place-items-center text-xl font-medium text-gray-500"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center space-y-2">
|
||||
<OrganizationsIcon class="h-10 w-10" />
|
||||
<div>No organization selected</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Filter doctype="CRM Organization" />
|
||||
<SortBy doctype="CRM Organization" />
|
||||
<Button icon="more-horizontal" />
|
||||
</div>
|
||||
</div>
|
||||
<OrganizationsListView :rows="rows" :columns="columns" />
|
||||
<OrganizationModal
|
||||
v-model="showOrganizationModal"
|
||||
v-model:reloadOrganizations="organizations"
|
||||
:organization="{}"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
|
||||
import OrganizationsIcon from '@/components/Icons/OrganizationsIcon.vue'
|
||||
import { FeatherIcon, Breadcrumbs, Avatar } from 'frappe-ui'
|
||||
import OrganizationsListView from '@/components/ListViews/OrganizationsListView.vue'
|
||||
import SortBy from '@/components/SortBy.vue'
|
||||
import Filter from '@/components/Filter.vue'
|
||||
import { FeatherIcon, Breadcrumbs, Dropdown } from 'frappe-ui'
|
||||
import { organizationsStore } from '@/stores/organizations.js'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { dateFormat, dateTooltipFormat, timeAgo, formatNumberIntoCurrency } from '@/utils'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const { organizations } = organizationsStore()
|
||||
const route = useRoute()
|
||||
|
||||
const showOrganizationModal = ref(false)
|
||||
|
||||
const currentOrganization = computed(() => {
|
||||
return organizations.data.find(
|
||||
(organization) => organization.name === route.params.organizationId
|
||||
)
|
||||
})
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
let items = [{ label: 'Organizations', route: { name: 'Organizations' } }]
|
||||
if (!currentOrganization.value) return items
|
||||
@ -94,13 +78,102 @@ const breadcrumbs = computed(() => {
|
||||
})
|
||||
return items
|
||||
})
|
||||
onMounted(() => {
|
||||
const el = document.querySelector('.router-link-active')
|
||||
if (el)
|
||||
setTimeout(() => {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
})
|
||||
|
||||
const currentView = ref({
|
||||
label: 'List',
|
||||
icon: 'list',
|
||||
})
|
||||
|
||||
const viewsDropdownOptions = [
|
||||
{
|
||||
label: 'List',
|
||||
icon: 'list',
|
||||
onClick() {
|
||||
currentView.value = {
|
||||
label: 'List',
|
||||
icon: 'list',
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Table',
|
||||
icon: 'grid',
|
||||
onClick() {
|
||||
currentView.value = {
|
||||
label: 'Table',
|
||||
icon: 'grid',
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Calender',
|
||||
icon: 'calendar',
|
||||
onClick() {
|
||||
currentView.value = {
|
||||
label: 'Calender',
|
||||
icon: 'calendar',
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Board',
|
||||
icon: 'columns',
|
||||
onClick() {
|
||||
currentView.value = {
|
||||
label: 'Board',
|
||||
icon: 'columns',
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const rows = computed(() => {
|
||||
return organizations.data.map((organization) => {
|
||||
return {
|
||||
name: organization.name,
|
||||
organization: {
|
||||
label: organization.organization_name,
|
||||
logo: organization.organization_logo,
|
||||
},
|
||||
website: website(organization.website),
|
||||
industry: organization.industry,
|
||||
annual_revenue: formatNumberIntoCurrency(organization.annual_revenue),
|
||||
modified: {
|
||||
label: dateFormat(organization.modified, dateTooltipFormat),
|
||||
timeAgo: timeAgo(organization.modified),
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{
|
||||
label: 'Organization',
|
||||
key: 'organization',
|
||||
width: '16rem',
|
||||
},
|
||||
{
|
||||
label: 'Website',
|
||||
key: 'website',
|
||||
width: '14rem',
|
||||
},
|
||||
{
|
||||
label: 'Industry',
|
||||
key: 'industry',
|
||||
width: '14rem',
|
||||
},
|
||||
{
|
||||
label: 'Annual Revenue',
|
||||
key: 'annual_revenue',
|
||||
width: '14rem',
|
||||
},
|
||||
{
|
||||
label: 'Last modified',
|
||||
key: 'modified',
|
||||
width: '8rem',
|
||||
},
|
||||
]
|
||||
|
||||
function website(url) {
|
||||
return url && url.replace(/^(?:https?:\/\/)?(?:www\.)?/i, '')
|
||||
}
|
||||
|
||||
@ -49,14 +49,12 @@ const routes = [
|
||||
path: '/organizations',
|
||||
name: 'Organizations',
|
||||
component: () => import('@/pages/Organizations.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '/organizations/:organizationId?',
|
||||
name: 'Organization',
|
||||
component: () => import('@/pages/Organization.vue'),
|
||||
props: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/organizations/:organizationId',
|
||||
name: 'Organization',
|
||||
component: () => import('@/pages/Organization.vue'),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/call-logs',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user