1<template>
2 <SettingsLayoutBase
3 :description="__('Configure your Exotel settings for Opendesk.')"
4 :back-label="__('Exotel')"
5 :on-back="goBack"
6 :dirty="isDirty.exotel"
7 >
8 <template #header-actions>
9 <Button
10 :label="__('Save')"
11 theme="gray"
12 variant="solid"
13 @click="save"
14 v-if="isDirty.exotel"
15 :disabled="!isDirty.exotel"
16 :loading="exotel.save.loading"
17 />
18 </template>
19 <template #content>
20 <div class="flex flex-col h-full w-full pb-8">
21 <div v-if="exotel?.doc">
22 <div>
23 <div class="grid grid-cols-2 gap-4">
24 <Checkbox
25 :label="__('Enabled')"
26 v-model="exotel.doc.enabled"
27 @update:modelValue="exotel.doc.enabled = $event ? 1 : 0"
28 />
29 <Checkbox
30 :label="__('Record Calls')"
31 v-model="exotel.doc.record_call"
32 v-if="exotel.doc.enabled"
33 @update:modelValue="exotel.doc.record_call = $event ? 1 : 0"
34 />
35 </div>
36 <div class="grid grid-cols-2 gap-4 mt-4" v-if="exotel.doc.enabled">
37 <div
38 class="flex flex-col gap-2"
39 v-if="telephonyAgent.doc && exotel.doc?.enabled"
40 >
41 <FormControl
42 label="Exotel number"
43 type="text"
44 required
45 v-model="telephonyAgent.doc.exotel_number"
46 />
47 <ErrorMessage :message="exotelErrors.number" />
48 </div>
49 <div
50 class="flex flex-col gap-2"
51 v-if="telephonyAgent.doc && exotel.doc?.enabled"
52 >
53 <FormControl
54 :label="__('Personal mobile no')"
55 type="text"
56 required
57 v-model="telephonyAgent.doc.mobile_no"
58 />
59 <ErrorMessage :message="exotelErrors.mobileNo" />
60 </div>
61 <div class="flex flex-col gap-2">
62 <FormControl
63 label="Account SID"
64 required
65 v-model="exotel.doc.account_sid"
66 placeholder="Account SID"
67 />
68 <ErrorMessage :message="exotelErrors.accountSid" />
69 </div>
70 <div class="flex flex-col gap-2">
71 <FormControl
72 label="Webhook Verify Token"
73 required
74 v-model="exotel.doc.webhook_verify_token"
75 placeholder="Webhook Verify Token"
76 />
77 <ErrorMessage :message="exotelErrors.webhookVerifyToken" />
78 </div>
79
80 <div class="flex flex-col gap-2">
81 <FormControl
82 label="API Key"
83 required
84 v-model="exotel.doc.api_key"
85 placeholder="API Key"
86 />
87 <ErrorMessage :message="exotelErrors.apiKey" />
88 </div>
89 <div class="flex flex-col gap-2">
90 <Password
91 label="API Token"
92 required
93 v-model="exotel.doc.api_token"
94 placeholder="API Token"
95 />
96 <ErrorMessage :message="exotelErrors.apiToken" />
97 </div>
98 <div class="flex flex-col gap-2">
99 <FormControl
100 label="Subdomain"
101 required
102 v-model="exotel.doc.subdomain"
103 placeholder="Subdomain"
104 />
105 <ErrorMessage :message="exotelErrors.subdomain" />
106 </div>
107 </div>
108 </div>
109 </div>
110 </div>
111 </template>
112 </SettingsLayoutBase>
113 <ConfirmDialog
114 v-if="showConfirmDialog.show"
115 :title="showConfirmDialog.title"
116 :message="showConfirmDialog.message"
117 @confirm="showConfirmDialog.onConfirm"
118 @cancel="showConfirmDialog.onCancel"
119 />
120</template>
121<script setup>
122import Password from "@/components/Password.vue";
123import {
124 Checkbox,
125 FormControl,
126 createDocumentResource,
127 toast,
128 ErrorMessage,
129 createResource,
130} from "frappe-ui";
131import { ref, watch } from "vue";
132import { isDocDirty, validateExotel, validateTwilio } from "./utils";
133import { useAuthStore } from "@/stores/auth";
134import { useTelephonyStore } from "@/stores/telephony";
135import { disableSettingModalOutsideClick } from "../settingsModal";
136import SettingsLayoutBase from "@/components/layouts/SettingsLayoutBase.vue";
137import ConfirmDialog from "@/components/ConfirmDialog.vue";
138
139import { __ } from "@/translation";
140
141const auth = useAuthStore();
142const telephonyStore = useTelephonyStore();
143const isDirty = ref({
144 twilio: false,
145 exotel: false,
146 telephonyAgent: false,
147});
148const emit = defineEmits(["updateStep"]);
149
150const twilioApps = ref([]);
151const showConfirmDialog = ref({ show: false });
152
153const twilioErrors = ref({
154 accountSid: "",
155 authToken: "",
156 number: "",
157 default_medium: "",
158});
159
160const exotelErrors = ref({
161 accountSid: "",
162 webhookVerifyToken: "",
163 subdomain: "",
164 apiKey: "",
165 apiToken: "",
166 number: "",
167 mobileNo: "",
168 default_medium: "",
169});
170
171const twilio = createDocumentResource({
172 doctype: "TP Twilio Settings",
173 name: "TP Twilio Settings",
174 cache: ["tp_twilio_settings"],
175 fields: ["*"],
176 auto: true,
177});
178
179const exotel = createDocumentResource({
180 doctype: "TP Exotel Settings",
181 name: "TP Exotel Settings",
182 cache: ["tp_exotel_settings"],
183 fields: ["*"],
184 auto: true,
185});
186
187const telephonyAgent = createDocumentResource({
188 doctype: "TP Telephony Agent",
189 name: auth.user,
190 cache: ["tp_telephony_agent"],
191 fields: ["*"],
192 auto: false,
193 onError(er) {
194 toast.error(er?.messages?.[0] || __("Failed to load telephony agent"));
195 },
196});
197
198const goBack = () => {
199 if (
200 isDirty.value.twilio ||
201 isDirty.value.exotel ||
202 isDirty.value.telephonyAgent
203 ) {
204 if (!showConfirmDialog.value.show) {
205 showConfirmDialog.value = {
206 show: true,
207 title: __("Unsaved changes"),
208 message: __(
209 "Are you sure you want to go back? Unsaved changes will be lost."
210 ),
211 onConfirm: () => {
212 showConfirmDialog.value.show = false;
213 emit("updateStep", "telephony-settings");
214 },
215 onCancel: () => {
216 showConfirmDialog.value.show = false;
217 },
218 };
219 }
220 return;
221 }
222 emit("updateStep", "telephony-settings");
223};
224
225async function save() {
226 validateTwilio(twilio.doc, telephonyAgent.doc, twilioErrors);
227 validateExotel(exotel.doc, telephonyAgent.doc, exotelErrors);
228 if (Object.values(twilioErrors.value).some((v) => v)) {
229 toast.error(__("Please fill all required fields for Twilio"));
230 return;
231 }
232 if (Object.values(exotelErrors.value).some((v) => v)) {
233 toast.error(__("Please fill all required fields for Exotel"));
234 return;
235 }
236
237 const promises = [];
238
239 // Temporary fix, as createDocumentResource's dirty state has bug
240 if (isDirty.value.twilio) {
241 promises.push(
242 twilio.save.submit().catch((er) => {
243 const error = __(`Twilio error: {0}`, er?.messages?.[0]);
244 toast.error(error || __("Failed to save Twilio settings"));
245 })
246 );
247 }
248 if (isDirty.value.exotel) {
249 promises.push(
250 exotel.save.submit().catch((er) => {
251 const error = __(`Exotel error: {0}`, er?.messages?.[0]);
252 toast.error(error || __("Failed to save Exotel settings"));
253 })
254 );
255 }
256 if (isDirty.value.telephonyAgent) {
257 if (telephonyAgent.doc.twilio_number) {
258 telephonyAgent.doc.twilio = true;
259 } else {
260 telephonyAgent.doc.twilio = false;
261 }
262
263 if (telephonyAgent.doc.exotel_number) {
264 telephonyAgent.doc.exotel = true;
265 } else {
266 telephonyAgent.doc.exotel = false;
267 }
268 promises.push(telephonyAgent.save.submit());
269 }
270
271 const results = await Promise.all(promises);
272
273 if (!results.some((result) => result == undefined)) {
274 toast.success(__("Telephony settings updated successfully."));
275 }
276
277 // Reload twilio to prevent "doc has been modified" error, as an application is created and doc is updated on save
278 await twilio.reload();
279 telephonyStore.fetchCallIntegrationStatus();
280}
281
282createResource({
283 url: "telephony.api.create_telephony_agent",
284 auto: true,
285 onSuccess() {
286 telephonyAgent.get.submit();
287 },
288});
289
290watch(
291 () => telephonyAgent.doc,
292 (newVal) => {
293 isDirty.value.telephonyAgent = isDocDirty(
294 newVal,
295 telephonyAgent.originalDoc
296 );
297 if (isDirty.value.telephonyAgent) {
298 disableSettingModalOutsideClick.value = true;
299 } else {
300 disableSettingModalOutsideClick.value = false;
301 }
302 },
303 { deep: true }
304);
305
306watch(
307 () => twilio.doc,
308 (newVal) => {
309 isDirty.value.twilio = isDocDirty(newVal, twilio.originalDoc);
310 twilioApps.value =
311 newVal.twilio_apps?.split(",").map((app) => ({
312 label: app,
313 value: app,
314 })) || [];
315 if (isDirty.value.twilio) {
316 disableSettingModalOutsideClick.value = true;
317 } else {
318 disableSettingModalOutsideClick.value = false;
319 }
320 },
321 { deep: true }
322);
323
324watch(
325 () => exotel.doc,
326 (newVal) => {
327 isDirty.value.exotel = isDocDirty(newVal, exotel.originalDoc);
328 if (isDirty.value.exotel) {
329 disableSettingModalOutsideClick.value = true;
330 } else {
331 disableSettingModalOutsideClick.value = false;
332 }
333 },
334 { deep: true }
335);
336</script>
337