Skip to content

Commit 074b50f

Browse files
Merge pull request #2274 from adarshkh2397/feature/new-contact-optional-customer
feat: contact dialogs refactor
2 parents 69e9ac9 + e6a5d8f commit 074b50f

File tree

2 files changed

+140
-26
lines changed

2 files changed

+140
-26
lines changed

desk/src/components/desk/global/NewContactDialog.vue

+27-22
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
>
1111
<span class="mb-2 block text-sm leading-4 text-gray-700">
1212
{{ field.label }}
13+
<span
14+
v-if="field.required"
15+
class="place-self-center text-red-500"
16+
>
17+
*
18+
</span>
1319
</span>
1420
<Input
1521
v-if="field.type === 'input'"
@@ -21,7 +27,6 @@
2127
v-else
2228
v-model="state[field.value]"
2329
:options="customerResource.data"
24-
:value="state[field.value]"
2530
@update:model-value="handleCustomerChange"
2631
/>
2732
<ErrorMessage :message="error[field.error]" />
@@ -76,7 +81,7 @@ const state = ref({
7681
firstName: "",
7782
lastName: "",
7883
phone: "",
79-
selectedCustomer: "",
84+
selectedCustomer: null,
8085
});
8186
8287
const error = ref({
@@ -92,6 +97,7 @@ interface FormField {
9297
value: string;
9398
error: string;
9499
type: string;
100+
required: boolean;
95101
action?: () => void;
96102
}
97103
@@ -101,34 +107,38 @@ const formFields: FormField[] = [
101107
value: "emailID",
102108
error: "emailValidationError",
103109
type: "input",
110+
required: true,
104111
action: () => validateEmailInput(state.value.emailID),
105112
},
106113
{
107114
label: "First Name",
108115
value: "firstName",
109116
error: "firstNameValidationError",
110117
type: "input",
118+
required: true,
111119
action: () => validateFirstName(state.value.firstName),
112120
},
113121
{
114122
label: "Last Name",
115123
value: "lastName",
116124
error: "lastNameValidationError",
117125
type: "input",
126+
required: false,
118127
},
119128
{
120129
label: "Phone",
121130
value: "phone",
122131
error: "phoneValidationError",
123132
type: "input",
133+
required: false,
124134
action: () => validatePhone(state.value.phone),
125135
},
126136
{
127137
label: "Customer",
128138
value: "selectedCustomer",
129139
error: "customerValidationError",
130140
type: "autocomplete",
131-
action: () => validateCustomer(state.value.selectedCustomer),
141+
required: false,
132142
},
133143
];
134144
@@ -165,7 +175,7 @@ const contactResource = createResource({
165175
firstName: "",
166176
lastName: "",
167177
phone: "",
168-
selectedCustomer: "",
178+
selectedCustomer: null,
169179
};
170180
createToast({
171181
title: "Contact Created Successfully ",
@@ -184,31 +194,34 @@ function createContact() {
184194
first_name: state.value.firstName,
185195
last_name: state.value.lastName,
186196
email_ids: [{ email_id: state.value.emailID, is_primary: true }],
187-
links: [
188-
{
189-
link_doctype: "HD Customer",
190-
link_name: state.value.selectedCustomer,
191-
},
192-
],
197+
links: [],
193198
phone_nos: [],
194199
};
195200
if (state.value.phone) {
196201
doc.phone_nos = [{ phone: state.value.phone }];
197202
}
203+
if (state.value.selectedCustomer) {
204+
doc.links.push({
205+
link_doctype: "HD Customer",
206+
link_name: state.value.selectedCustomer,
207+
});
208+
}
198209
199210
contactResource.submit({ doc });
200211
}
201212
202-
function handleCustomerChange(item: AutoCompleteItem) {
203-
if (!item) return;
204-
state.value.selectedCustomer = item.value;
213+
function handleCustomerChange(item: AutoCompleteItem | null) {
214+
if (!item || item.label === "No label") {
215+
state.value.selectedCustomer = null;
216+
} else {
217+
state.value.selectedCustomer = item.value;
218+
}
205219
}
206220
207221
function validateInputs() {
208222
let error = validateEmailInput(state.value.emailID);
209223
error += validateFirstName(state.value.firstName);
210224
error += validatePhone(state.value.phone);
211-
error += validateCustomer(state.value.selectedCustomer);
212225
return error;
213226
}
214227
@@ -243,14 +256,6 @@ function validatePhone(value: string) {
243256
return error.value.phoneValidationError;
244257
}
245258
246-
function validateCustomer(value: string) {
247-
error.value.customerValidationError = "";
248-
if (!value || value.trim() === "") {
249-
error.value.customerValidationError = "Customer should not be empty";
250-
}
251-
return error.value.customerValidationError;
252-
}
253-
254259
function existingContactEmails(contacts) {
255260
return contacts.map((contact) => contact.email_id);
256261
}

desk/src/pages/desk/contact/ContactDialog.vue

+113-4
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
<template #body-main>
44
<div class="flex flex-col items-center gap-4 p-6">
55
<div class="text-xl font-medium text-gray-900">
6-
{{ contact.doc?.name }}
6+
{{ contact.doc?.full_name }}
77
</div>
88
<Avatar
99
size="2xl"
10-
:label="contact.doc?.name"
10+
:label="contact.doc?.full_name"
1111
:image="contact.doc?.image"
1212
class="cursor-pointer hover:opacity-80"
1313
/>
@@ -29,6 +29,12 @@
2929
label="Remove photo"
3030
@click="updateImage(null)"
3131
/>
32+
<Button
33+
v-if="!contact.doc?.user"
34+
label="Invite as user"
35+
@click="inviteContact"
36+
:loading="isLoading"
37+
/>
3238
</div>
3339
<div class="w-full space-y-2 text-sm text-gray-700">
3440
<div class="space-y-1">
@@ -47,6 +53,14 @@
4753
:validate="validatePhone"
4854
/>
4955
</div>
56+
<div class="space-y-1">
57+
<div class="text-xs">Customer</div>
58+
<Autocomplete
59+
v-model="selectedCustomer"
60+
:options="customerResource.data"
61+
@update:model-value="handleCustomerChange"
62+
/>
63+
</div>
5064
</div>
5165
</div>
5266
</template>
@@ -61,10 +75,13 @@ import {
6175
Avatar,
6276
Dialog,
6377
FileUploader,
78+
Autocomplete,
79+
createListResource,
80+
call,
6481
} from "frappe-ui";
6582
import zod from "zod";
6683
import { createToast } from "@/utils";
67-
import { useError } from "@/composables/error";
84+
6885
import MultiSelect from "@/components/MultiSelect.vue";
6986
import { File, AutoCompleteItem } from "@/types";
7087
@@ -138,10 +155,65 @@ const phones = computed({
138155
},
139156
});
140157
158+
const selectedCustomer = computed({
159+
get() {
160+
const customerLink = contact.doc?.links?.find(
161+
(link) => link.link_doctype === "HD Customer"
162+
);
163+
return customerLink?.link_name || null;
164+
},
165+
set(value) {
166+
const currentCustomer = contact.doc?.links?.find(
167+
(link) => link.link_doctype === "HD Customer"
168+
)?.link_name;
169+
170+
if (value !== currentCustomer) {
171+
if (value) {
172+
const existingCustomerLinkIndex = contact.doc.links?.findIndex(
173+
(link) => link.link_doctype === "HD Customer"
174+
);
175+
if (existingCustomerLinkIndex !== -1) {
176+
contact.doc.links[existingCustomerLinkIndex].link_name = value;
177+
} else {
178+
contact.doc.links.push({
179+
link_doctype: "HD Customer",
180+
link_name: value,
181+
});
182+
}
183+
} else {
184+
contact.doc.links = contact.doc.links?.filter(
185+
(link) => link.link_doctype !== "HD Customer"
186+
);
187+
}
188+
isDirty.value = true;
189+
}
190+
},
191+
});
192+
193+
const customerResource = createListResource({
194+
doctype: "HD Customer",
195+
fields: ["name"],
196+
cache: "customers",
197+
transform: (data) => {
198+
return data.map((option) => ({
199+
label: option.name,
200+
value: option.name,
201+
}));
202+
},
203+
auto: true,
204+
});
205+
206+
function handleCustomerChange(item: AutoCompleteItem | null) {
207+
if (!item || item.label === "No label") {
208+
selectedCustomer.value = null;
209+
} else {
210+
selectedCustomer.value = item.value;
211+
}
212+
}
213+
141214
const contact = createDocumentResource({
142215
doctype: "Contact",
143216
name: props.name,
144-
cache: [`contact-${props.name}`, props.name],
145217
auto: true,
146218
setValue: {
147219
onSuccess() {
@@ -181,6 +253,7 @@ function update(): void {
181253
is_primary_phone: phoneNum.value === contact.doc.phone,
182254
is_primary_mobile: phoneNum.value === contact.doc.phone,
183255
})),
256+
links: contact.doc.links,
184257
});
185258
}
186259
@@ -217,4 +290,40 @@ function validateFile(file: File): string | void {
217290
return "Invalid file type, only PNG and JPG images are allowed";
218291
}
219292
}
293+
294+
const isLoading = ref(false);
295+
async function inviteContact(): Promise<void> {
296+
try {
297+
isLoading.value = true;
298+
const user = await call(
299+
"frappe.contacts.doctype.contact.contact.invite_user",
300+
{
301+
contact: contact.doc.name,
302+
}
303+
);
304+
createToast({
305+
title: "Contact invited successfully",
306+
icon: "user-plus",
307+
iconClasses: "text-green-600",
308+
});
309+
await contact.setValue.submit({
310+
user: user,
311+
});
312+
} catch (error) {
313+
isLoading.value = false;
314+
const parser = new DOMParser();
315+
const doc = parser.parseFromString(
316+
error.messages?.[0] || error.message,
317+
"text/html"
318+
);
319+
const errMsg = doc.body.innerText;
320+
createToast({
321+
title: errMsg,
322+
icon: "x",
323+
iconClasses: "text-red-600",
324+
});
325+
} finally {
326+
isLoading.value = false;
327+
}
328+
}
220329
</script>

0 commit comments

Comments
 (0)