1<template>
2 <SettingsLayoutBase :description="__('Configure your telephony settings.')">
3 <template #title>
4 <div class="flex items-center gap-2">
5 <h1 class="text-md-semibold text-ink-gray-8">
6 {{ __("Telephony") }}
7 </h1>
8 <UnsavedBadge
9 :show="isDirty.twilio || isDirty.exotel || isDirty.telephonyAgent"
10 />
11 </div>
12 </template>
13 <template #header-actions>
14 <Transition name="fade">
15 <div v-if="isDirty.twilio || isDirty.exotel || isDirty.telephonyAgent">
16 <Button
17 :label="__('Save')"
18 theme="gray"
19 variant="solid"
20 @click="save"
21 :disabled="
22 !isDirty.twilio && !isDirty.exotel && !isDirty.telephonyAgent
23 "
24 :loading="
25 twilio.save.loading ||
26 exotel.save.loading ||
27 telephonyAgent.save.loading
28 "
29 />
30 </div>
31 </Transition>
32 </template>
33 <template #content>
34 <div class="-ms-2 grow">
35 <div class="flex-1 flex flex-col">
36 <!-- General -->
37 <div
38 class="flex items-center justify-between gap-8 py-3 hover:bg-surface-sidebar rounded-4 px-2"
39 >
40 <div class="flex flex-col">
41 <div class="text-p-base-medium text-ink-gray-7 truncate">
42 {{ __("Default medium") }}
43 </div>
44 <div class="text-p-sm text-ink-gray-5">
45 {{ __("Default calling medium for logged in user") }}
46 </div>
47 </div>
48 <div class="flex items-center gap-2">
49 <!-- <Select
50 v-if="telephonyAgent.doc"
51 :options="telephonyProviders"
52 :modelValue="telephonyAgent.doc?.default_medium"
53 @update:modelValue="telephonyAgent.doc.default_medium = $event"
54 /> -->
55 <SelectDropdown
56 :options="telephonyProviders"
57 :modelValue="telephonyAgent.doc?.default_medium"
58 @update:modelValue="telephonyAgent.doc.default_medium = $event"
59 :defaultValue="telephonyAgent.originalDoc?.default_medium"
60 placement="bottom-start"
61 />
62 </div>
63 </div>
64 <div class="h-px border-t mx-2 border-outline-elevation-2" />
65
66 <div
67 class="flex items-center justify-between py-3 cursor-pointer rounded-4 hover:bg-surface-sidebar px-2"
68 @click="emit('updateStep', 'twilio-settings')"
69 >
70 <div class="flex flex-col">
71 <div class="text-p-base-medium text-ink-gray-7 truncate">
72 {{ __("Twilio") }}
73 </div>
74 <div class="text-p-sm text-ink-gray-5 truncate">
75 {{
76 __(
77 "Configure your Twilio telephony integration settings here"
78 )
79 }}
80 </div>
81 </div>
82 <LucideChevronRight class="size-4 text-ink-gray-5 rtl:rotate-180" />
83 </div>
84
85 <div class="h-px border-t mx-2 border-outline-elevation-2" />
86
87 <div
88 class="flex items-center justify-between py-3 cursor-pointer rounded-4 hover:bg-surface-sidebar px-2"
89 @click="emit('updateStep', 'exotel-settings')"
90 >
91 <div class="flex flex-col">
92 <div class="text-p-base-medium text-ink-gray-7 truncate">
93 {{ __("Exotel") }}
94 </div>
95 <div class="text-p-sm text-ink-gray-5 truncate">
96 {{
97 __(
98 "Configure your Exotel telephony integration settings here"
99 )
100 }}
101 </div>
102 </div>
103 <LucideChevronRight class="size-4 text-ink-gray-5 rtl:rotate-180" />
104 </div>
105 </div>
106 </div>
107 </template>
108 </SettingsLayoutBase>
109</template>
110
111<script setup lang="ts">
112import LucideChevronRight from "~icons/lucide/chevron-right";
113import Password from "@/components/Password.vue";
114import SettingsLayoutHeader from "../SettingsLayoutHeader.vue";
115import {
116 Button,
117 FormLabel,
118 Checkbox,
119 FormControl,
120 createDocumentResource,
121 toast,
122 createResource,
123 Badge,
124} from "frappe-ui";
125import SelectDropdown from "@/components/SelectDropdown.vue";
126import { ref, watch } from "vue";
127import { isDocDirty, validateExotel, validateTwilio } from "./utils";
128import { useAuthStore } from "@/stores/auth";
129import { useTelephonyStore } from "@/stores/telephony";
130import { disableSettingModalOutsideClick } from "../settingsModal";
131import { __ } from "@/translation";
132import SettingsLayoutBase from "@/components/layouts/SettingsLayoutBase.vue";
133import UnsavedBadge from "@/components/UnsavedBadge.vue";
134
135const auth = useAuthStore();
136const telephonyStore = useTelephonyStore();
137const isDirty = ref({
138 twilio: false,
139 exotel: false,
140 telephonyAgent: false,
141});
142const emit = defineEmits(["updateStep"]);
143
144const twilioApps = ref([]);
145
146const twilioErrors = ref({
147 accountSid: "",
148 authToken: "",
149 number: "",
150 default_medium: "",
151});
152
153const exotelErrors = ref({
154 accountSid: "",
155 webhookVerifyToken: "",
156 subdomain: "",
157 apiKey: "",
158 apiToken: "",
159 number: "",
160 mobileNo: "",
161 default_medium: "",
162});
163
164const twilio = createDocumentResource({
165 doctype: "TP Twilio Settings",
166 name: "TP Twilio Settings",
167 cache: ["tp_twilio_settings"],
168 fields: ["*"],
169 auto: true,
170});
171
172const exotel = createDocumentResource({
173 doctype: "TP Exotel Settings",
174 name: "TP Exotel Settings",
175 cache: ["tp_exotel_settings"],
176 fields: ["*"],
177 auto: true,
178});
179
180const telephonyAgent = createDocumentResource({
181 doctype: "TP Telephony Agent",
182 name: auth.user,
183 cache: ["tp_telephony_agent"],
184 fields: ["*"],
185 auto: false,
186 onError(er) {
187 toast.error(er?.messages?.[0] || __("Failed to load telephony agent"));
188 },
189});
190
191const telephonyProviders = [
192 { label: "Twilio", value: "Twilio" },
193 { label: "Exotel", value: "Exotel" },
194];
195
196async function save() {
197 validateTwilio(twilio.doc, telephonyAgent.doc, twilioErrors);
198 validateExotel(exotel.doc, telephonyAgent.doc, exotelErrors);
199 if (Object.values(twilioErrors.value).some((v) => v)) {
200 toast.error(__("Please configure your Twilio settings correctly"));
201 return;
202 }
203 if (Object.values(exotelErrors.value).some((v) => v)) {
204 toast.error(__("Please configure your Exotel settings correctly"));
205 return;
206 }
207
208 const promises = [];
209
210 // Temporary fix, as createDocumentResource's dirty state has bug
211 if (isDirty.value.twilio) {
212 promises.push(
213 twilio.save.submit().catch((er) => {
214 const error = __(`Twilio error: {0}`, er?.messages?.[0]);
215 toast.error(error || __("Failed to save Twilio settings"));
216 })
217 );
218 }
219 if (isDirty.value.exotel) {
220 promises.push(
221 exotel.save.submit().catch((er) => {
222 const error = __(`Exotel error: {0}`, er?.messages?.[0]);
223 toast.error(error || __("Failed to save Exotel settings"));
224 })
225 );
226 }
227 if (isDirty.value.telephonyAgent) {
228 if (telephonyAgent.doc.twilio_number) {
229 telephonyAgent.doc.twilio = true;
230 } else {
231 telephonyAgent.doc.twilio = false;
232 }
233
234 if (telephonyAgent.doc.exotel_number) {
235 telephonyAgent.doc.exotel = true;
236 } else {
237 telephonyAgent.doc.exotel = false;
238 }
239 promises.push(telephonyAgent.save.submit());
240 }
241
242 const results = await Promise.all(promises);
243
244 if (!results.some((result) => result == undefined)) {
245 toast.success(__("Telephony settings updated successfully."));
246 }
247
248 // Reload twilio to prevent "doc has been modified" error, as an application is created and doc is updated on save
249 await twilio.reload();
250 telephonyStore.fetchCallIntegrationStatus();
251}
252
253createResource({
254 url: "telephony.api.create_telephony_agent",
255 auto: true,
256 onSuccess() {
257 telephonyAgent.get.submit();
258 },
259});
260
261watch(
262 () => telephonyAgent.doc,
263 (newVal) => {
264 isDirty.value.telephonyAgent = isDocDirty(
265 newVal,
266 telephonyAgent.originalDoc
267 );
268 if (isDirty.value.telephonyAgent) {
269 disableSettingModalOutsideClick.value = true;
270 } else {
271 disableSettingModalOutsideClick.value = false;
272 }
273 },
274 { deep: true }
275);
276
277watch(
278 () => twilio.doc,
279 (newVal) => {
280 isDirty.value.twilio = isDocDirty(newVal, twilio.originalDoc);
281 twilioApps.value =
282 newVal.twilio_apps?.split(",").map((app) => ({
283 label: app,
284 value: app,
285 })) || [];
286 if (isDirty.value.twilio) {
287 disableSettingModalOutsideClick.value = true;
288 } else {
289 disableSettingModalOutsideClick.value = false;
290 }
291 },
292 { deep: true }
293);
294
295watch(
296 () => exotel.doc,
297 (newVal) => {
298 isDirty.value.exotel = isDocDirty(newVal, exotel.originalDoc);
299 if (isDirty.value.exotel) {
300 disableSettingModalOutsideClick.value = true;
301 } else {
302 disableSettingModalOutsideClick.value = false;
303 }
304 },
305 { deep: true }
306);
307</script>
308