Merge pull request #80 from shariquerik/crud-contact-email-and-phone
feat: Allow CRUD operations for contact email & phone no
This commit is contained in:
commit
f080f22179
@ -53,13 +53,13 @@ def get_contacts():
|
|||||||
contact["email_ids"] = frappe.get_all(
|
contact["email_ids"] = frappe.get_all(
|
||||||
"Contact Email",
|
"Contact Email",
|
||||||
filters={"parenttype": "Contact", "parent": contact.name},
|
filters={"parenttype": "Contact", "parent": contact.name},
|
||||||
fields=["email_id", "is_primary"],
|
fields=["name", "email_id", "is_primary"],
|
||||||
)
|
)
|
||||||
|
|
||||||
contact["phone_nos"] = frappe.get_all(
|
contact["phone_nos"] = frappe.get_all(
|
||||||
"Contact Phone",
|
"Contact Phone",
|
||||||
filters={"parenttype": "Contact", "parent": contact.name},
|
filters={"parenttype": "Contact", "parent": contact.name},
|
||||||
fields=["phone", "is_primary_phone", "is_primary_mobile_no"],
|
fields=["name", "phone", "is_primary_phone", "is_primary_mobile_no"],
|
||||||
)
|
)
|
||||||
|
|
||||||
return contacts
|
return contacts
|
||||||
|
|||||||
@ -1,39 +1,85 @@
|
|||||||
<template>
|
<template>
|
||||||
<button
|
<div class="flex items-center justify-between gap-7">
|
||||||
:class="[
|
<div v-show="!editMode">{{ option.value }}</div>
|
||||||
active ? 'bg-gray-100' : 'text-gray-800',
|
<TextInput
|
||||||
'group flex h-7 w-full items-center justify-between gap-3 rounded px-2 text-base',
|
ref="inputRef"
|
||||||
]"
|
v-show="editMode"
|
||||||
@click="onClick"
|
v-model="option.value"
|
||||||
>
|
class="w-full"
|
||||||
<span class="whitespace-nowrap">
|
:placeholder="option.placeholder"
|
||||||
{{ value }}
|
@keydown.enter="saveOption"
|
||||||
</span>
|
/>
|
||||||
|
|
||||||
|
<div class="actions flex items-center justify-center">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
v-if="!isNew && !option.selected"
|
||||||
|
class="opacity-0 hover:bg-gray-300 group-hover:opacity-100"
|
||||||
|
@click="option.onClick"
|
||||||
|
>
|
||||||
|
<SuccessIcon />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="opacity-0 hover:bg-gray-300 group-hover:opacity-100"
|
||||||
|
@click="toggleEditMode"
|
||||||
|
>
|
||||||
|
<EditIcon />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
icon="x"
|
||||||
|
size="sm"
|
||||||
|
class="opacity-0 hover:bg-gray-300 group-hover:opacity-100"
|
||||||
|
@click="() => option.onDelete(option, isNew)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
<FeatherIcon
|
<FeatherIcon
|
||||||
v-if="selected"
|
v-if="option.selected"
|
||||||
name="check"
|
name="check"
|
||||||
class="text-primary-500 h-4 w-4"
|
class="text-primary-500 h-4 w-6"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</button>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import SuccessIcon from '@/components/Icons/SuccessIcon.vue'
|
||||||
|
import EditIcon from '@/components/Icons/EditIcon.vue'
|
||||||
|
import { TextInput } from 'frappe-ui'
|
||||||
|
import { nextTick, ref, onMounted } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
value: {
|
option: {
|
||||||
type: String,
|
type: Object,
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
onClick: {
|
|
||||||
type: Function,
|
|
||||||
default: () => {},
|
default: () => {},
|
||||||
},
|
},
|
||||||
active: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
selected: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const editMode = ref(false)
|
||||||
|
const isNew = ref(false)
|
||||||
|
const inputRef = ref(null)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!props.option?.value) {
|
||||||
|
editMode.value = true
|
||||||
|
isNew.value = true
|
||||||
|
nextTick(() => inputRef.value.el.focus())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleEditMode = () => {
|
||||||
|
editMode.value = !editMode.value
|
||||||
|
editMode.value && nextTick(() => inputRef.value.el.focus())
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveOption = () => {
|
||||||
|
toggleEditMode()
|
||||||
|
props.option.onSave(props.option, isNew.value)
|
||||||
|
isNew.value = false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -73,11 +73,8 @@
|
|||||||
<label class="block text-base text-gray-600">
|
<label class="block text-base text-gray-600">
|
||||||
{{ field.label }}
|
{{ field.label }}
|
||||||
</label>
|
</label>
|
||||||
<Dropdown
|
<NestedPopover>
|
||||||
:options="field.options"
|
<template #target="{ open }">
|
||||||
class="form-control w-full flex-1"
|
|
||||||
>
|
|
||||||
<template #default="{ open }">
|
|
||||||
<Button
|
<Button
|
||||||
:label="_contact[field.name]"
|
: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"
|
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"
|
||||||
@ -91,19 +88,40 @@
|
|||||||
</template>
|
</template>
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #body>
|
||||||
<Button
|
<div
|
||||||
variant="ghost"
|
class="my-2 space-y-1.5 divide-y rounded-lg border border-gray-100 bg-white p-1.5 shadow-xl"
|
||||||
class="w-full !justify-start"
|
|
||||||
label="Create New"
|
|
||||||
@click="field.create()"
|
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<div class="">
|
||||||
<FeatherIcon name="plus" class="h-4" />
|
<div
|
||||||
</template>
|
v-if="field.options?.length"
|
||||||
</Button>
|
v-for="option in field.options"
|
||||||
|
:key="option.value"
|
||||||
|
class="group flex w-full items-center justify-between rounded bg-transparent p-1 pl-2 text-base text-gray-800 transition-colors hover:bg-gray-200 active:bg-gray-300"
|
||||||
|
>
|
||||||
|
<DropdownItem :option="option" />
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div class="p-1.5 px-7 text-base text-gray-500">
|
||||||
|
No {{ field.label }} Available
|
||||||
|
</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>
|
</template>
|
||||||
</Dropdown>
|
</NestedPopover>
|
||||||
</div>
|
</div>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-else-if="field.type === 'data'"
|
v-else-if="field.type === 'data'"
|
||||||
@ -116,16 +134,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
@ -146,6 +154,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import NestedPopover from '@/components/NestedPopover.vue'
|
||||||
import DropdownItem from '@/components/DropdownItem.vue'
|
import DropdownItem from '@/components/DropdownItem.vue'
|
||||||
import ContactIcon from '@/components/Icons/ContactIcon.vue'
|
import ContactIcon from '@/components/Icons/ContactIcon.vue'
|
||||||
import GenderIcon from '@/components/Icons/GenderIcon.vue'
|
import GenderIcon from '@/components/Icons/GenderIcon.vue'
|
||||||
@ -276,13 +285,11 @@ const detailFields = computed(() => {
|
|||||||
icon: EmailIcon,
|
icon: EmailIcon,
|
||||||
name: 'email_id',
|
name: 'email_id',
|
||||||
value: _contact.value.email_id,
|
value: _contact.value.email_id,
|
||||||
...sections.value[2].fields[0],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: PhoneIcon,
|
icon: PhoneIcon,
|
||||||
name: 'mobile_no',
|
name: 'mobile_no',
|
||||||
value: _contact.value.mobile_no,
|
value: _contact.value.actual_mobile_no,
|
||||||
...sections.value[3].fields[0],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: OrganizationsIcon,
|
icon: OrganizationsIcon,
|
||||||
@ -340,35 +347,39 @@ const sections = computed(() => {
|
|||||||
label: 'Email',
|
label: 'Email',
|
||||||
type: props.contact.name ? 'dropdown' : 'data',
|
type: props.contact.name ? 'dropdown' : 'data',
|
||||||
name: 'email_id',
|
name: 'email_id',
|
||||||
options: props.contact?.email_ids?.map((email) => {
|
options:
|
||||||
return {
|
props.contact?.email_ids?.map((email) => {
|
||||||
component: h(DropdownItem, {
|
return {
|
||||||
|
name: email.name,
|
||||||
value: email.email_id,
|
value: email.email_id,
|
||||||
selected: email.email_id === props.contact.email_id,
|
selected: email.email_id === props.contact.email_id,
|
||||||
|
placeholder: 'john@doe.com',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
_contact.value.email_id = email.email_id
|
_contact.value.email_id = email.email_id
|
||||||
setAsPrimary('email', email.email_id)
|
setAsPrimary('email', email.email_id)
|
||||||
},
|
},
|
||||||
}),
|
onSave: (option, isNew) => {
|
||||||
}
|
if (isNew) {
|
||||||
}),
|
createNew('email', option.value)
|
||||||
create: (value) => {
|
} else {
|
||||||
new_field.value = {
|
editOption('Contact Email', option.name, option.value)
|
||||||
type: 'email',
|
}
|
||||||
value,
|
|
||||||
placeholder: 'Add Email Address',
|
|
||||||
}
|
|
||||||
_dialogOptions.value = {
|
|
||||||
title: 'Add Email',
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
label: 'Add',
|
|
||||||
variant: 'solid',
|
|
||||||
onClick: () => createNew('email'),
|
|
||||||
},
|
},
|
||||||
],
|
onDelete: (option, isNew) => {
|
||||||
}
|
props.contact.email_ids = props.contact.email_ids.filter(
|
||||||
_show.value = true
|
(email) => email.name !== option.name
|
||||||
|
)
|
||||||
|
!isNew && deleteOption('Contact Email', option.name)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}) || [],
|
||||||
|
create: () => {
|
||||||
|
props.contact?.email_ids?.push({
|
||||||
|
name: 'new-1',
|
||||||
|
value: '',
|
||||||
|
selected: false,
|
||||||
|
isNew: true,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -379,36 +390,40 @@ const sections = computed(() => {
|
|||||||
label: 'Mobile No.',
|
label: 'Mobile No.',
|
||||||
type: props.contact.name ? 'dropdown' : 'data',
|
type: props.contact.name ? 'dropdown' : 'data',
|
||||||
name: 'actual_mobile_no',
|
name: 'actual_mobile_no',
|
||||||
options: props.contact?.phone_nos?.map((phone) => {
|
options:
|
||||||
return {
|
props.contact?.phone_nos?.map((phone) => {
|
||||||
component: h(DropdownItem, {
|
return {
|
||||||
|
name: phone.name,
|
||||||
value: phone.phone,
|
value: phone.phone,
|
||||||
selected: phone.phone === props.contact.actual_mobile_no,
|
selected: phone.phone === props.contact.actual_mobile_no,
|
||||||
|
placeholder: '+91 1234567890',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
_contact.value.actual_mobile_no = phone.phone
|
_contact.value.actual_mobile_no = phone.phone
|
||||||
_contact.value.mobile_no = phone.phone
|
_contact.value.mobile_no = phone.phone
|
||||||
setAsPrimary('mobile_no', phone.phone)
|
setAsPrimary('mobile_no', phone.phone)
|
||||||
},
|
},
|
||||||
}),
|
onSave: (option, isNew) => {
|
||||||
}
|
if (isNew) {
|
||||||
}),
|
createNew('phone', option.value)
|
||||||
create: (value) => {
|
} else {
|
||||||
new_field.value = {
|
editOption('Contact Phone', option.name, option.value)
|
||||||
type: 'tel',
|
}
|
||||||
value,
|
|
||||||
placeholder: 'Add Mobile No.',
|
|
||||||
}
|
|
||||||
_dialogOptions.value = {
|
|
||||||
title: 'Add Mobile No.',
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
label: 'Add',
|
|
||||||
variant: 'solid',
|
|
||||||
onClick: () => createNew('phone'),
|
|
||||||
},
|
},
|
||||||
],
|
onDelete: (option, isNew) => {
|
||||||
}
|
props.contact.phone_nos = props.contact.phone_nos.filter(
|
||||||
_show.value = true
|
(phone) => phone.name !== option.name
|
||||||
|
)
|
||||||
|
!isNew && deleteOption('Contact Phone', option.name)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}) || [],
|
||||||
|
create: () => {
|
||||||
|
props.contact?.phone_nos?.push({
|
||||||
|
name: 'new-1',
|
||||||
|
value: '',
|
||||||
|
selected: false,
|
||||||
|
isNew: true,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -469,11 +484,6 @@ const sections = computed(() => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const _show = ref(false)
|
|
||||||
const new_field = ref({})
|
|
||||||
|
|
||||||
const _dialogOptions = ref({})
|
|
||||||
|
|
||||||
async function setAsPrimary(field, value) {
|
async function setAsPrimary(field, value) {
|
||||||
let d = await call('crm.api.contact.set_as_primary', {
|
let d = await call('crm.api.contact.set_as_primary', {
|
||||||
contact: props.contact.name,
|
contact: props.contact.name,
|
||||||
@ -490,11 +500,43 @@ async function setAsPrimary(field, value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createNew(field) {
|
async function createNew(field, value) {
|
||||||
let d = await call('crm.api.contact.create_new', {
|
let d = await call('crm.api.contact.create_new', {
|
||||||
contact: props.contact.name,
|
contact: props.contact.name,
|
||||||
field,
|
field,
|
||||||
value: new_field.value?.value,
|
value,
|
||||||
|
})
|
||||||
|
if (d) {
|
||||||
|
contacts.reload()
|
||||||
|
createToast({
|
||||||
|
title: 'Contact updated',
|
||||||
|
icon: 'check',
|
||||||
|
iconClasses: 'text-green-600',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function editOption(doctype, name, value) {
|
||||||
|
let d = await call('frappe.client.set_value', {
|
||||||
|
doctype,
|
||||||
|
name,
|
||||||
|
fieldname: doctype == 'Contact Phone' ? 'phone' : 'email',
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
if (d) {
|
||||||
|
contacts.reload()
|
||||||
|
createToast({
|
||||||
|
title: 'Contact updated',
|
||||||
|
icon: 'check',
|
||||||
|
iconClasses: 'text-green-600',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteOption(doctype, name) {
|
||||||
|
let d = await call('frappe.client.delete', {
|
||||||
|
doctype,
|
||||||
|
name,
|
||||||
})
|
})
|
||||||
if (d) {
|
if (d) {
|
||||||
contacts.reload()
|
contacts.reload()
|
||||||
@ -504,7 +546,6 @@ async function createNew(field) {
|
|||||||
iconClasses: 'text-green-600',
|
iconClasses: 'text-green-600',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_show.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dirty = computed(() => {
|
const dirty = computed(() => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user