1<template>
2 <SettingsLayoutBase
3 :description="__('Configure your Twilio settings for Opendesk.')"
4 :back-label="__('Twilio')"
5 :on-back="goBack"
6 :dirty="isDirty.twilio"
7 >
8 <template #header-actions>
9 <Button
10 :label="__('Save')"
11 theme="gray"
12 variant="solid"
13 @click="save"
14 v-if="isDirty.twilio"
15 :disabled="!isDirty.twilio"
16 :loading="twilio.save.loading"
17 />
18 </template>
19 <template #content>
20 <div v-if="twilio?.doc">
21 <div>
22 <div class="grid grid-cols-2 gap-4">
23 <Checkbox
24 :label="__('Enabled')"
25 v-model="twilio.doc.enabled"
26 @update:modelValue="twilio.doc.enabled = $event ? 1 : 0"
27 />
28 <Checkbox
29 :label="__('Record Calls')"
30 v-model="twilio.doc.record_calls"
31 v-if="twilio.doc.enabled"
32 @update:modelValue="twilio.doc.record_calls = $event ? 1 : 0"
33 />
34 </div>
35 <div class="grid grid-cols-2 gap-4 mt-4" v-if="twilio.doc.enabled">
36 <div class="flex flex-col gap-2">
37 <FormControl
38 label="Account SID"
39 required
40 v-model="twilio.doc.account_sid"
41 placeholder="Account SID"
42 />
43 <ErrorMessage :message="twilioErrors.accountSid" />
44 </div>
45 <div class="flex flex-col gap-2">
46 <Password
47 label="Auth Token"
48 required
49 v-model="twilio.doc.auth_token"
50 placeholder="Auth Token"
51 />
52 <ErrorMessage :message="twilioErrors.authToken" />
53 </div>
54 <FormControl
55 v-if="twilio.doc.api_key"
56 label="API Key"
57 v-model="twilio.doc.api_key"
58 disabled
59 />
60 <Password
61 v-if="twilio.doc.api_secret"
62 label="API Secret"
63 v-model="twilio.doc.api_secret"
64 disabled
65 />
66 <Combobox
67 v-if="twilio.originalDoc?.account_sid && twilioApps.length > 0"
68 trigger="button"
69 label="TwiML App Name"
70 :model-value="twilio.doc.app_name"
71 :options="twilioApps"
72 @update:model-value="twilio.doc.app_name = $event"
73 >
74 <template #footer="{ setOpen }">
75 <Button
76 :label="__('Refresh Apps')"
77 theme="gray"
78 variant="subtle"
79 class="w-full"
80 icon-left="lucide-refresh-cw"
81 :loading="twilioAppsResource.loading"
82 @click="refreshApps(setOpen)"
83 />
84 </template>
85 </Combobox>
86 <FormControl
87 v-if="twilio.doc.twiml_sid"
88 label="TwiML App SID"
89 v-model="twilio.doc.twiml_sid"
90 disabled
91 />
92 <div
93 class="flex flex-col gap-1.5"
94 v-if="telephonyAgent.doc && twilio.doc?.enabled"
95 >
96 <FormControl
97 label="Twilio number"
98 type="text"
99 required
100 v-model="telephonyAgent.doc.twilio_number"
101 />
102 <ErrorMessage :message="twilioErrors.number" />
103 </div>
104 </div>
105 </div>
106 </div>
107 </template>
108 </SettingsLayoutBase>
109 <ConfirmDialog
110 v-if="showConfirmDialog.show"
111 :title="showConfirmDialog.title"
112 :message="showConfirmDialog.message"
113 @confirm="showConfirmDialog.onConfirm"
114 @cancel="showConfirmDialog.onCancel"
115 />
116</template>
117<script setup>
118import Password from "@/components/Password.vue";
119import SettingsLayoutBase from "@/components/layouts/SettingsLayoutBase.vue";
120import ConfirmDialog from "@/components/ConfirmDialog.vue";
121import {
122 Button,
123 Checkbox,
124 Combobox,
125 FormControl,
126 createDocumentResource,
127 toast,
128 ErrorMessage,
129 createResource,
130 Badge,
131} from "frappe-ui";
132import { nextTick, ref, watch } from "vue";
133import { isDocDirty, validateExotel, validateTwilio } from "./utils";
134import { useAuthStore } from "@/stores/auth";
135
136import { disableSettingModalOutsideClick } from "../settingsModal";
137const twilioApps = ref([]);
138const auth = useAuthStore();
139
140const twilioErrors = ref({
141 accountSid: "",
142 authToken: "",
143 number: "",
144 default_medium: "",
145});
146const isDirty = ref({
147 twilio: false,
148 telephonyAgent: false,
149});
150
151const twilio = createDocumentResource({
152 doctype: "TP Twilio Settings",
153 name: "TP Twilio Settings",
154 cache: ["tp_twilio_settings"],
155 fields: ["*"],
156 auto: true,
157});
158
159const twilioAppsResource = createResource({
160 url: "telephony.twilio.api.fetch_applications",
161 onSuccess() {
162 twilio.reload();
163 },
164});
165
166const telephonyAgent = createDocumentResource({
167 doctype: "TP Telephony Agent",
168 name: auth.user,
169 cache: ["tp_telephony_agent"],
170 fields: ["*"],
171 auto: false,
172 onError(er) {
173 toast.error(er?.messages?.[0] || __("Failed to load telephony agent"));
174 },
175});
176
177const showConfirmDialog = ref({ show: false });
178
179const goBack = () => {
180 if (isDirty.value.twilio || isDirty.value.telephonyAgent) {
181 if (!showConfirmDialog.value.show) {
182 showConfirmDialog.value = {
183 show: true,
184 title: __("Unsaved changes"),
185 message: __(
186 "Are you sure you want to go back? Unsaved changes will be lost."
187 ),
188 onConfirm: () => {
189 showConfirmDialog.value.show = false;
190 emit("updateStep", "telephony-settings");
191 },
192 onCancel: () => {
193 showConfirmDialog.value.show = false;
194 },
195 };
196 }
197 return;
198 }
199 emit("updateStep", "telephony-settings");
200};
201
202async function save() {
203 validateTwilio(twilio.doc, telephonyAgent.doc, twilioErrors);
204 if (Object.values(twilioErrors.value).some((v) => v)) {
205 toast.error(__("Please fill all required fields for Twilio"));
206 return;
207 }
208
209 const promises = [];
210
211 // Temporary fix, as createDocumentResource's dirty state has bug
212 if (isDirty.value.twilio) {
213 promises.push(
214 twilio.save.submit().catch((er) => {
215 const error = __(`Twilio error: {0}`, er?.messages?.[0]);
216 toast.error(error || __("Failed to save Twilio settings"));
217 })
218 );
219 }
220
221 if (isDirty.value.telephonyAgent) {
222 if (telephonyAgent.doc.twilio_number) {
223 telephonyAgent.doc.twilio = true;
224 } else {
225 telephonyAgent.doc.twilio = false;
226 }
227
228 promises.push(telephonyAgent.save.submit());
229 }
230
231 const results = await Promise.all(promises);
232
233 if (!results.some((result) => result == undefined)) {
234 toast.success(__("Telephony settings updated successfully."));
235 }
236
237 // Reload twilio to prevent "doc has been modified" error, as an application is created and doc is updated on save
238 await twilio.reload();
239 telephonyStore.fetchCallIntegrationStatus();
240}
241
242function refreshApps(setOpen) {
243 twilioAppsResource.submit().then(() => {
244 // Close and reopen popover to fix bug where search does not work after refreshing list
245 setOpen(false);
246 nextTick(() => setOpen(true));
247 });
248}
249
250watch(
251 () => twilio.doc,
252 (newVal) => {
253 isDirty.value.twilio = isDocDirty(newVal, twilio.originalDoc);
254 twilioApps.value =
255 newVal.twilio_apps?.split(",").map((app) => ({
256 label: app,
257 value: app,
258 })) || [];
259 if (isDirty.value.twilio) {
260 disableSettingModalOutsideClick.value = true;
261 } else {
262 disableSettingModalOutsideClick.value = false;
263 }
264 },
265 { deep: true }
266);
267
268const emit = defineEmits(["updateStep"]);
269</script>
270