1<template>
2 <Dialog v-model:open="show" bare>
3 <template #default>
4 <div class="bg-surface-elevation-2 px-4 pb-6 pt-5 sm:px-6">
5 <div class="mb-5 flex items-center justify-between">
6 <div>
7 <h3 class="text-2xl-semibold leading-6 text-ink-gray-9">
8 {{ __("Call Details") }}
9 </h3>
10 </div>
11 <div class="flex items-center gap-1">
12 <Button
13 v-if="isManager"
14 variant="ghost"
15 class="w-7"
16 @click="openCallLogModal"
17 icon="lucide-edit"
18 />
19 <Button
20 variant="ghost"
21 class="w-7"
22 @click="show = false"
23 icon="lucide-x"
24 />
25 </div>
26 </div>
27 <div class="flex flex-col gap-3.5">
28 <div
29 v-for="field in detailFields"
30 :key="field.name"
31 class="flex gap-2 text-base text-ink-gray-8"
32 >
33 <div class="grid size-7 place-content-center">
34 <component :is="field.icon" class="size-4 text-ink-gray-8" />
35 </div>
36 <div class="flex min-h-7 w-full items-center gap-2">
37 <div
38 v-if="field.name == 'receiver'"
39 class="flex items-center gap-1"
40 >
41 <Avatar
42 :image="field.value.caller.image"
43 :label="field.value.caller.label"
44 size="sm"
45 />
46 <div class="ms-1 flex flex-col gap-1">
47 {{ field.value.caller.label }}
48 </div>
49 <LucideArrowRight
50 class="mx-1 h-4 w-4 text-ink-gray-5 rtl:rotate-180"
51 />
52 <Avatar
53 :image="field.value.receiver.image"
54 :label="field.value.receiver.label"
55 size="sm"
56 />
57 <div class="ms-1 flex flex-col gap-1">
58 {{ field.value.receiver.label }}
59 </div>
60 </div>
61 <Tooltip v-else-if="field.tooltip" :text="field.tooltip">
62 {{ field.value }}
63 </Tooltip>
64 <div class="w-full" v-else-if="field.name == 'recording_url'">
65 <audio
66 class="audio-control w-full"
67 controls
68 :src="field.value"
69 ></audio>
70 </div>
71 <div class="w-full" v-else-if="field.name == 'ticket'">
72 <div
73 class="flex items-center gap-1.5 cursor-pointer"
74 @click="() => field.link()"
75 >
76 #{{ field.value }}
77 <ArrowUpRightIcon
78 class="h-4 w-4 shrink-0 cursor-pointer text-ink-gray-5 hover:text-ink-gray-8"
79 />
80 </div>
81 </div>
82 <div v-else :class="statusTextColorMap[field.color] ?? ''">
83 {{ field.value }}
84 </div>
85 </div>
86 </div>
87 </div>
88 </div>
89 </template>
90 </Dialog>
91</template>
92
93<script setup lang="ts">
94import Icon from "@opendesk/shared/Icon.vue";
95import LucideArrowRight from "~icons/lucide/arrow-right";
96import ArrowUpRightIcon from "@/components/icons/ArrowUpRightIcon.vue";
97import DurationIcon from "@/components/icons/DurationIcon.vue";
98import ContactsIcon from "@/components/icons/ContactsIcon.vue";
99import CalendarIcon from "@/components/icons/CalendarIcon.vue";
100import CheckCircleIcon from "@/components/icons/CheckCircleIcon.vue";
101import TicketIcon from "@/components/icons/TicketIcon.vue";
102import { Avatar, Tooltip, createResource, dayjs } from "frappe-ui";
103import { computed, h, nextTick, ref, watch } from "vue";
104import { formatDate } from "@vueuse/core";
105import { timeAgo } from "@/utils";
106import { statusColorMap, statusLabelMap, statusTextColorMap } from "./utils";
107import { useAuthStore } from "@/stores/auth";
108
109const show = defineModel();
110const callLog = ref(null);
111const { isManager } = useAuthStore();
112
113const props = defineProps({
114 callLogId: {
115 type: String,
116 default: "",
117 },
118 hideTicket: {
119 type: Boolean,
120 default: false,
121 },
122});
123
124const callLogData = createResource({
125 url: "telephony.api.get_call_log",
126 onSuccess(data) {
127 callLog.value = data;
128 },
129 auto: false,
130});
131
132const detailFields = computed(() => {
133 if (!callLog.value) return [];
134
135 let data = JSON.parse(JSON.stringify(callLog.value));
136
137 for (const key in data) {
138 data[key] = getCallLogDetail(key, data);
139 }
140
141 let details = [
142 {
143 icon: h(Icon, {
144 icon: data.type.icon,
145 class: "h-3.5 w-3.5",
146 }),
147 name: "type",
148 value: data.type.label + " Call",
149 },
150 {
151 icon: ContactsIcon,
152 name: "receiver",
153 value: {
154 receiver: data.receiver,
155 caller: data.caller,
156 },
157 },
158 {
159 icon: CalendarIcon,
160 name: "creation",
161 value: data.creation.label,
162 tooltip: data.creation.label,
163 },
164 {
165 icon: DurationIcon,
166 name: "duration",
167 value: data.duration.label,
168 },
169 {
170 icon: CheckCircleIcon,
171 name: "status",
172 value: data.status.label,
173 color: data.status.color,
174 },
175 {
176 icon: h(Icon, {
177 icon: "lucide-circle-play",
178 class: "h-4 w-4 mt-2",
179 }),
180 name: "recording_url",
181 value: data.recording_url,
182 },
183 ];
184
185 const ticketLink = data.links?.find(
186 (link) => link.link_doctype == "OD Ticket"
187 );
188
189 if (ticketLink && !props.hideTicket) {
190 details.push({
191 icon: TicketIcon,
192 name: "ticket",
193 value: ticketLink.link_name,
194 link: () => {
195 window.open(`/opendesk/tickets/${ticketLink.link_name}`);
196 },
197 });
198 }
199
200 return details.filter((detail) => detail.value);
201});
202
203const showCallLogModal = defineModel("callLogModal");
204
205function openCallLogModal() {
206 showCallLogModal.value = true;
207 nextTick(() => {
208 show.value = false;
209 });
210}
211
212function getCallLogDetail(row, log, columns = []) {
213 let incoming = log.type === "Incoming";
214
215 if (row === "duration") {
216 return {
217 label: log._duration,
218 icon: "lucide-clock",
219 };
220 } else if (row === "caller") {
221 return {
222 label: log._caller?.label,
223 image: log._caller?.image,
224 };
225 } else if (row === "receiver") {
226 return {
227 label: log._receiver?.label,
228 image: log._receiver?.image,
229 };
230 } else if (row === "type") {
231 return {
232 label: log.type,
233 icon: incoming ? "lucide-phone-incoming" : "lucide-phone-outgoing",
234 };
235 } else if (row === "status") {
236 return {
237 label: statusLabelMap[log.status],
238 color: statusColorMap[log.status],
239 };
240 } else if (["modified", "creation"].includes(row)) {
241 return {
242 label: dayjs(log[row]).format("ddd, MMM D, YYYY h:mm a"),
243 timeAgo: timeAgo(log[row]),
244 };
245 }
246
247 let fieldType = columns?.find((col) => (col.key || col.value) == row)?.type;
248
249 if (fieldType && ["Date", "Datetime"].includes(fieldType)) {
250 return formatDate(log[row], "");
251 }
252
253 return log[row];
254}
255
256watch(show, (newValue) => {
257 if (newValue && props.callLogId) {
258 callLogData.submit({
259 name: props.callLogId,
260 });
261 }
262});
263</script>
264
265<style scoped>
266.audio-control {
267 height: 36px;
268 outline: none;
269 border-radius: 10px;
270 cursor: pointer;
271 background-color: rgb(237, 237, 237);
272}
273
274audio::-webkit-media-controls-panel {
275 background-color: rgb(237, 237, 237) !important;
276}
277
278.audio-control::-webkit-media-controls-panel {
279 background-color: white;
280}
281</style>
282