1
0
forked from test/crm

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:
Shariq Ansari 2024-02-28 21:28:44 +05:30 committed by GitHub
commit f080f22179
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 197 additions and 110 deletions

View File

@ -53,13 +53,13 @@ def get_contacts():
contact["email_ids"] = frappe.get_all(
"Contact Email",
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",
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

View File

@ -1,39 +1,85 @@
<template>
<button
:class="[
active ? 'bg-gray-100' : 'text-gray-800',
'group flex h-7 w-full items-center justify-between gap-3 rounded px-2 text-base',
]"
@click="onClick"
>
<span class="whitespace-nowrap">
{{ value }}
</span>
<div class="flex items-center justify-between gap-7">
<div v-show="!editMode">{{ option.value }}</div>
<TextInput
ref="inputRef"
v-show="editMode"
v-model="option.value"
class="w-full"
:placeholder="option.placeholder"
@keydown.enter="saveOption"
/>
<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
v-if="selected"
v-if="option.selected"
name="check"
class="text-primary-500 h-4 w-4"
class="text-primary-500 h-4 w-6"
size="sm"
/>
</button>
</div>
</template>
<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({
value: {
type: String,
default: '',
},
onClick: {
type: Function,
option: {
type: Object,
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>

View File

@ -73,11 +73,8 @@
<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 }">
<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"
@ -91,19 +88,40 @@
</template>
</Button>
</template>
<template #footer>
<Button
variant="ghost"
class="w-full !justify-start"
label="Create New"
@click="field.create()"
<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"
>
<template #prefix>
<FeatherIcon name="plus" class="h-4" />
</template>
</Button>
<div class="">
<div
v-if="field.options?.length"
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>
</Dropdown>
</NestedPopover>
</div>
<FormControl
v-else-if="field.type === 'data'"
@ -116,16 +134,6 @@
/>
</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>
@ -146,6 +154,7 @@
</template>
<script setup>
import NestedPopover from '@/components/NestedPopover.vue'
import DropdownItem from '@/components/DropdownItem.vue'
import ContactIcon from '@/components/Icons/ContactIcon.vue'
import GenderIcon from '@/components/Icons/GenderIcon.vue'
@ -276,13 +285,11 @@ const detailFields = computed(() => {
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],
value: _contact.value.actual_mobile_no,
},
{
icon: OrganizationsIcon,
@ -340,35 +347,39 @@ const sections = computed(() => {
label: 'Email',
type: props.contact.name ? 'dropdown' : 'data',
name: 'email_id',
options: props.contact?.email_ids?.map((email) => {
return {
component: h(DropdownItem, {
options:
props.contact?.email_ids?.map((email) => {
return {
name: email.name,
value: email.email_id,
selected: email.email_id === props.contact.email_id,
placeholder: 'john@doe.com',
onClick: () => {
_contact.value.email_id = email.email_id
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'),
onSave: (option, isNew) => {
if (isNew) {
createNew('email', option.value)
} else {
editOption('Contact Email', option.name, option.value)
}
},
],
}
_show.value = true
onDelete: (option, isNew) => {
props.contact.email_ids = props.contact.email_ids.filter(
(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.',
type: props.contact.name ? 'dropdown' : 'data',
name: 'actual_mobile_no',
options: props.contact?.phone_nos?.map((phone) => {
return {
component: h(DropdownItem, {
options:
props.contact?.phone_nos?.map((phone) => {
return {
name: phone.name,
value: phone.phone,
selected: phone.phone === props.contact.actual_mobile_no,
placeholder: '+91 1234567890',
onClick: () => {
_contact.value.actual_mobile_no = phone.phone
_contact.value.mobile_no = phone.phone
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'),
onSave: (option, isNew) => {
if (isNew) {
createNew('phone', option.value)
} else {
editOption('Contact Phone', option.name, option.value)
}
},
],
}
_show.value = true
onDelete: (option, isNew) => {
props.contact.phone_nos = props.contact.phone_nos.filter(
(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) {
let d = await call('crm.api.contact.set_as_primary', {
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', {
contact: props.contact.name,
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) {
contacts.reload()
@ -504,7 +546,6 @@ async function createNew(field) {
iconClasses: 'text-green-600',
})
}
_show.value = false
}
const dirty = computed(() => {