@@ -12,7 +12,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
1212import { PageHeader } from ' @/components/shared'
1313import LanguageSwitcher from ' @/components/LanguageSwitcher.vue'
1414import { toast } from ' vue-sonner'
15- import { Settings , Bell , Loader2 , Globe , Phone } from ' lucide-vue-next'
15+ import { Settings , Bell , Loader2 , Globe , Phone , Upload , Play , Music } from ' lucide-vue-next'
1616import { usersService , organizationService } from ' @/services/api'
1717
1818const { t } = useI18n ()
@@ -39,9 +39,20 @@ const notificationSettings = ref({
3939const callingSettings = ref ({
4040 calling_enabled: false ,
4141 max_call_duration: 300 ,
42- transfer_timeout_secs: 120
42+ transfer_timeout_secs: 120 ,
43+ hold_music_file: ' ' ,
44+ ringback_file: ' '
4345})
4446
47+ const isUploadingHoldMusic = ref (false )
48+ const isUploadingRingback = ref (false )
49+ const holdMusicInput = ref <HTMLInputElement | null >(null )
50+ const ringbackInput = ref <HTMLInputElement | null >(null )
51+ const holdMusicAudio = ref <HTMLAudioElement | null >(null )
52+ const ringbackAudio = ref <HTMLAudioElement | null >(null )
53+ const playingHoldMusic = ref (false )
54+ const playingRingback = ref (false )
55+
4556onMounted (async () => {
4657 try {
4758 const [orgResponse, userResponse] = await Promise .all ([
@@ -61,7 +72,9 @@ onMounted(async () => {
6172 callingSettings .value = {
6273 calling_enabled: orgData .settings ?.calling_enabled || false ,
6374 max_call_duration: orgData .settings ?.max_call_duration || 300 ,
64- transfer_timeout_secs: orgData .settings ?.transfer_timeout_secs || 120
75+ transfer_timeout_secs: orgData .settings ?.transfer_timeout_secs || 120 ,
76+ hold_music_file: orgData .settings ?.hold_music_file || ' ' ,
77+ ringback_file: orgData .settings ?.ringback_file || ' '
6578 }
6679 }
6780
@@ -129,6 +142,52 @@ async function saveCallingSettings() {
129142 isSubmitting .value = false
130143 }
131144}
145+
146+ async function uploadAudio(type : ' hold_music' | ' ringback' , event : Event ) {
147+ const input = event .target as HTMLInputElement
148+ const file = input ?.files ?.[0 ]
149+ if (! file ) return
150+
151+ const isHold = type === ' hold_music'
152+ if (isHold ) isUploadingHoldMusic .value = true
153+ else isUploadingRingback .value = true
154+
155+ try {
156+ const response = await organizationService .uploadOrgAudio (file , type )
157+ const data = response .data .data || response .data
158+ if (isHold ) callingSettings .value .hold_music_file = data .filename
159+ else callingSettings .value .ringback_file = data .filename
160+ toast .success (t (' settings.audioUploaded' ))
161+ } catch (error ) {
162+ toast .error (t (' settings.audioUploadFailed' ))
163+ } finally {
164+ if (isHold ) isUploadingHoldMusic .value = false
165+ else isUploadingRingback .value = false
166+ input .value = ' '
167+ }
168+ }
169+
170+ function togglePlayAudio(type : ' hold_music' | ' ringback' ) {
171+ const isHold = type === ' hold_music'
172+ const filename = isHold ? callingSettings .value .hold_music_file : callingSettings .value .ringback_file
173+ if (! filename ) return
174+
175+ const audioRef = isHold ? holdMusicAudio : ringbackAudio
176+ const playingRef = isHold ? playingHoldMusic : playingRingback
177+
178+ if (playingRef .value && audioRef .value ) {
179+ audioRef .value .pause ()
180+ audioRef .value .currentTime = 0
181+ playingRef .value = false
182+ return
183+ }
184+
185+ const audio = new Audio (` /api/ivr-flows/audio/${filename } ` )
186+ audioRef .value = audio
187+ playingRef .value = true
188+ audio .play ()
189+ audio .onended = () => { playingRef .value = false }
190+ }
132191 </script >
133192
134193<template >
@@ -320,6 +379,73 @@ async function saveCallingSettings() {
320379 <p class =" text-xs text-white/40 light:text-gray-500" >{{ $t('settings.transferTimeoutDesc') }}</p >
321380 </div >
322381 </div >
382+ <Separator class =" bg-white/[0.08] light:bg-gray-200" />
383+ <!-- Hold Music Upload -->
384+ <div class =" space-y-3" :class =" { 'opacity-50 pointer-events-none': !callingSettings.calling_enabled }" >
385+ <div >
386+ <Label class =" text-white/70 light:text-gray-700 flex items-center gap-2" >
387+ <Music class =" h-4 w-4" />
388+ {{ $t('settings.holdMusic') }}
389+ </Label >
390+ <p class =" text-xs text-white/40 light:text-gray-500 mt-1" >{{ $t('settings.holdMusicDesc') }}</p >
391+ </div >
392+ <div class =" flex items-center gap-3" >
393+ <span class =" text-sm text-white/50 light:text-gray-500" >
394+ {{ callingSettings.hold_music_file ? `${$t('settings.currentFile')}: ${callingSettings.hold_music_file}` : $t('settings.noFileUploaded') }}
395+ </span >
396+ <Button
397+ v-if =" callingSettings.hold_music_file"
398+ variant =" ghost"
399+ size =" sm"
400+ class =" h-8 w-8 p-0 text-white/50 hover:text-white light:text-gray-500 light:hover:text-gray-900"
401+ @click =" togglePlayAudio('hold_music')"
402+ >
403+ <Play class =" h-4 w-4" />
404+ </Button >
405+ </div >
406+ <div class =" flex items-center gap-2" >
407+ <input ref =" holdMusicInput" type =" file" accept =" .ogg,.opus,.mp3,.wav" class =" hidden" @change =" uploadAudio('hold_music', $event)" />
408+ <Button variant =" outline" size =" sm" class =" bg-white/[0.04] border-white/[0.1] text-white/70 hover:bg-white/[0.08] hover:text-white light:bg-white light:border-gray-200 light:text-gray-700 light:hover:bg-gray-50" @click =" holdMusicInput?.click()" :disabled =" isUploadingHoldMusic" >
409+ <Loader2 v-if =" isUploadingHoldMusic" class =" mr-2 h-4 w-4 animate-spin" />
410+ <Upload v-else class =" mr-2 h-4 w-4" />
411+ {{ $t('settings.uploadAudio') }}
412+ </Button >
413+ <span class =" text-xs text-white/30 light:text-gray-400" >.ogg, .opus, .mp3, .wav (max 5MB)</span >
414+ </div >
415+ </div >
416+ <!-- Ringback Tone Upload -->
417+ <div class =" space-y-3" :class =" { 'opacity-50 pointer-events-none': !callingSettings.calling_enabled }" >
418+ <div >
419+ <Label class =" text-white/70 light:text-gray-700 flex items-center gap-2" >
420+ <Phone class =" h-4 w-4" />
421+ {{ $t('settings.ringbackTone') }}
422+ </Label >
423+ <p class =" text-xs text-white/40 light:text-gray-500 mt-1" >{{ $t('settings.ringbackToneDesc') }}</p >
424+ </div >
425+ <div class =" flex items-center gap-3" >
426+ <span class =" text-sm text-white/50 light:text-gray-500" >
427+ {{ callingSettings.ringback_file ? `${$t('settings.currentFile')}: ${callingSettings.ringback_file}` : $t('settings.noFileUploaded') }}
428+ </span >
429+ <Button
430+ v-if =" callingSettings.ringback_file"
431+ variant =" ghost"
432+ size =" sm"
433+ class =" h-8 w-8 p-0 text-white/50 hover:text-white light:text-gray-500 light:hover:text-gray-900"
434+ @click =" togglePlayAudio('ringback')"
435+ >
436+ <Play class =" h-4 w-4" />
437+ </Button >
438+ </div >
439+ <div class =" flex items-center gap-2" >
440+ <input ref =" ringbackInput" type =" file" accept =" .ogg,.opus,.mp3,.wav" class =" hidden" @change =" uploadAudio('ringback', $event)" />
441+ <Button variant =" outline" size =" sm" class =" bg-white/[0.04] border-white/[0.1] text-white/70 hover:bg-white/[0.08] hover:text-white light:bg-white light:border-gray-200 light:text-gray-700 light:hover:bg-gray-50" @click =" ringbackInput?.click()" :disabled =" isUploadingRingback" >
442+ <Loader2 v-if =" isUploadingRingback" class =" mr-2 h-4 w-4 animate-spin" />
443+ <Upload v-else class =" mr-2 h-4 w-4" />
444+ {{ $t('settings.uploadAudio') }}
445+ </Button >
446+ <span class =" text-xs text-white/30 light:text-gray-400" >.ogg, .opus, .mp3, .wav (max 5MB)</span >
447+ </div >
448+ </div >
323449 <div class =" flex justify-end pt-4" >
324450 <Button variant =" outline" size =" sm" class =" bg-white/[0.04] border-white/[0.1] text-white/70 hover:bg-white/[0.08] hover:text-white light:bg-white light:border-gray-200 light:text-gray-700 light:hover:bg-gray-50" @click =" saveCallingSettings" :disabled =" isSubmitting" >
325451 <Loader2 v-if =" isSubmitting" class =" mr-2 h-4 w-4 animate-spin" />
0 commit comments