const { useState, useEffect } = React;
/* ─────────────────────────────────────────────────────────────
AnnouncementLibrary — WAV upload/playback/delete panel
shown at the top of the IVR tab
───────────────────────────────────────────────────────────── */
function AnnouncementLibrary({ announcements, onUploaded, onDeleted }) {
const [uploading, setUploading] = React.useState(false);
const [expanded, setExpanded] = React.useState(true);
const fileInputRef = React.useRef(null);
const [uploadName, setUploadName] = React.useState('');
const [uploadDesc, setUploadDesc] = React.useState('');
const [selectedFile, setSelectedFile] = React.useState(null);
const [playingId, setPlayingId] = React.useState(null);
const audioRef = React.useRef(null);
function handleFileSelect(e) {
const f = e.target.files[0];
if (!f) return;
const allowed = ['.wav', '.mp3'];
const ext = f.name.toLowerCase().slice(f.name.lastIndexOf('.'));
if (!allowed.includes(ext)) {
alert('Sadece .wav veya .mp3 dosyaları kabul edilir.');
return;
}
setSelectedFile(f);
if (!uploadName) setUploadName(f.name.replace(/\.(wav|mp3)$/i, ''));
}
async function handleUpload() {
if (!selectedFile || !uploadName.trim()) {
alert('Dosya seçin ve anons adı girin.');
return;
}
setUploading(true);
try {
const fd = new FormData();
fd.append('name', uploadName.trim());
fd.append('description', uploadDesc.trim());
fd.append('file', selectedFile);
const r = await authFetch('/api/announcements/upload', { method: 'POST', body: fd });
if (r.ok) {
setSelectedFile(null); setUploadName(''); setUploadDesc('');
fileInputRef.current.value = '';
onUploaded();
} else {
const err = await r.json();
alert('Yükleme hatası: ' + (err.detail || JSON.stringify(err)));
}
} finally {
setUploading(false);
}
}
function handlePlay(ann) {
if (playingId === ann.id) {
audioRef.current?.pause();
setPlayingId(null);
return;
}
if (audioRef.current) {
audioRef.current.src = `/api/announcements/${ann.id}/stream`;
audioRef.current.play().catch(() => {});
setPlayingId(ann.id);
audioRef.current.onended = () => setPlayingId(null);
}
}
async function handleDelete(ann) {
if (!confirm(`"${ann.name}" silinsin mi?`)) return;
const r = await authFetch(`/api/announcements/${ann.id}`, { method: 'DELETE' });
if (r.ok) onDeleted();
}
function fmtSize(bytes) {
if (!bytes) return '';
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / 1024 / 1024).toFixed(1) + ' MB';
}
return (
{/* Header */}
setExpanded(e => !e)} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 20px', cursor: 'pointer', borderBottom: expanded ? '1px solid var(--border)' : 'none' }}>
📢 Anons Kütüphanesi {announcements.length} anons
{expanded ? '▲' : '▼'}
{expanded && (
{/* Upload form */}
{/* List */}
{announcements.length === 0 ? (
Henüz anons yüklenmemiş. WAV dosyası yükleyerek IVR menülerinde kullanabilirsiniz.
) : (
{['Ad', 'Açıklama', 'Boyut', 'Tarih', ''].map((h, i) => (
| {h} |
))}
{announcements.map(ann => (
| {ann.name} |
{ann.description || '—'} |
{fmtSize(ann.file_size)} |
{fmtDateOnly(ann.created_at)} |
|
))}
)}
)}
);
}
/* ─────────────────────────────────────────────────────────────
IVRModal — create / edit a single IVR menu
───────────────────────────────────────────────────────────── */
function IVRModal({ menu, onClose, onSave }) {
const emptyForm = {
name: '', description: '',
greet_text: 'Merhaba, şirketimize hoş geldiniz. Satış için 1\'i, destek için 2\'yi tuşlayınız.',
invalid_text: 'Hatalı tuşlama yaptınız. Lütfen tekrar deneyin.',
exit_text: 'İşleminizi gerçekleştiremiyoruz. İyi günler dileriz.',
timeout_ms: 3000, max_failures: 3, is_active: true,
greet_announcement_id: null,
invalid_announcement_id: null,
exit_announcement_id: null,
};
const [form, setForm] = useState(menu ? { ...menu } : emptyForm);
const [options, setOptions] = useState(menu?.options ? [...menu.options] : []);
const [wsCalls, setWsCalls] = useState(menu?.ws_pre_calls ? [...menu.ws_pre_calls] : []);
const [saving, setSaving] = useState(false);
const [queues, setQueues] = useState([]);
const [agents, setAgents] = useState([]);
const [services, setServices] = useState([]);
const [announcements, setAnnouncements] = useState([]);
const [greetSource, setGreetSource] = useState(menu?.greet_announcement_id ? 'ann' : 'tts');
const [invalidSource, setInvalidSource] = useState(menu?.invalid_announcement_id ? 'ann' : 'tts');
const [exitSource, setExitSource] = useState(menu?.exit_announcement_id ? 'ann' : 'tts');
const [playingPreview, setPlayingPreview] = useState(null);
const previewAudioRef = React.useRef(null);
const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
useEffect(() => {
authFetch('/api/queues?active_only=true')
.then(r => r.ok ? r.json() : []).then(setQueues).catch(() => {});
authFetch('/api/agents')
.then(r => r.ok ? r.json() : []).then(setAgents).catch(() => {});
authFetch('/api/ws/services')
.then(r => r.ok ? r.json() : []).then(setServices).catch(() => {});
authFetch('/api/announcements')
.then(r => r.ok ? r.json() : []).then(setAnnouncements).catch(() => {});
}, []);
function playPreview(annId) {
if (playingPreview === annId) {
previewAudioRef.current?.pause();
setPlayingPreview(null);
return;
}
if (previewAudioRef.current) {
previewAudioRef.current.src = `/api/announcements/${annId}/stream`;
previewAudioRef.current.play().catch(() => {});
setPlayingPreview(annId);
previewAudioRef.current.onended = () => setPlayingPreview(null);
}
}
/* TTS / Announcement toggle section — defined inside IVRModal
so it closes over form, set, playPreview, playingPreview */
function AudioSourceSection({ label, sourceState, setSourceState, textKey, annIdKey, annotations }) {
return (
{[['tts', '🔊 TTS'], ['ann', '📢 Anons']].map(([val, lbl]) => (
))}
{sourceState === 'tts' ? (
);
}
const addOption = () => {
setOptions([...options, { digit: '', action_type: 'transfer_queue', target: '' }]);
};
const updateOption = (i, field, val) => {
setOptions(opts => opts.map((opt, idx) => {
if (idx !== i) return opt;
const updated = { ...opt, [field]: val };
// Eylem türü değişince hedefi sıfırla
if (field === 'action_type') updated.target = '';
return updated;
}));
};
const removeOption = (i) => {
setOptions(opts => opts.filter((_, idx) => idx !== i));
};
async function handleSave() {
if (!form.name || !form.greet_text) {
alert('Lütfen menü adını ve karşılama metnini doldurun.');
return;
}
setSaving(true);
try {
const payload = {
...form,
greet_announcement_id: greetSource === 'ann' ? form.greet_announcement_id : null,
invalid_announcement_id: invalidSource === 'ann' ? form.invalid_announcement_id : null,
exit_announcement_id: exitSource === 'ann' ? form.exit_announcement_id : null,
options,
ws_pre_calls: wsCalls.length > 0 ? wsCalls : null,
};
const url = menu ? `/api/ivr/${menu.id}` : '/api/ivr';
const method = menu ? 'PUT' : 'POST';
const r = await authFetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (r.ok) {
onSave();
} else {
const err = await r.json();
alert('Kaydetme hatası: ' + JSON.stringify(err));
}
} finally {
setSaving(false);
}
}
/* Hedef input — eylem türüne göre farklı kontrol göster */
function TargetInput({ opt, index }) {
switch (opt.action_type) {
/* Kuyruğa Aktar — dropdown */
case 'transfer_queue':
return (
{queues.length === 0 && (
Aktif kuyruk bulunamadı. Önce Kuyruklar sekmesinden ekleyin.
)}
);
/* Başka IVR — ID girin */
case 'transfer_ivr':
return (
updateOption(index, 'target', e.target.value)}
placeholder="Örn: 2"
/>
);
/* AI Asistana Aktar — dropdown */
case 'transfer_ai':
return (
{agents.length === 0 && (
AI Asistan bulunamadı. Önce AI Asistanlar sekmesinden ekleyin.
)}
);
/* Temsilciye Aktar — dahili numara */
default:
return (
updateOption(index, 'target', e.target.value)}
placeholder="Örn: 9001"
/>
);
}
}
/* Hedef etiket (kart görünümünde) */
function targetLabel(opt) {
if (opt.action_type === 'transfer_queue') {
const q = queues.find(q => q.extension === opt.target);
return q ? `${q.name} (${q.extension})` : opt.target;
}
if (opt.action_type === 'transfer_ivr') return `Menü #${opt.target}`;
if (opt.action_type === 'transfer_human') return `Temsilci: ${opt.target}`;
if (opt.action_type === 'transfer_ai') {
const a = agents.find(a => String(a.id) === String(opt.target));
return a ? `AI: ${a.name}` : `AI: ${opt.target}`;
}
return opt.target;
}
return (
e.stopPropagation()}
style={{ maxWidth: 820, margin: 'auto' }}>
{menu ? 'IVR Menüsünü Düzenle' : 'Yeni IVR Menüsü'}
{/* Temel Bilgiler */}
{/* Sesli Mesajlar */}
{/* Web Servis Çağrıları */}
Web Servis Çağrıları (Menüye girildiğinde otomatik çalışır)
{services.length === 0 && (
Önce "Web Servisler" sekmesinden servis tanımlamalısınız.
)}
{wsCalls.length === 0 ? (
Web servis bağlantısı yok. Müşteri tanımlama, bakiye sorgulama vb. için ekleyin.
) : (
{wsCalls.map((wc, i) => (
setWsCalls(list =>
list.map((c, idx) => idx===i ? {...c, [field]: val} : c))}
onDelete={() => setWsCalls(list => list.filter((_,idx) => idx!==i))}
/>
))}
)}
{/* Tuşlama Seçenekleri */}
Tuşlama Seçenekleri
{options.length === 0 ? (
Henüz bir tuşlama seçeneği eklenmemiş.
) : (
{options.map((opt, i) => (
{/* Tuş */}
updateOption(i, 'digit', e.target.value)}
placeholder="1"
style={{ textAlign: 'center', fontWeight: 'bold',
fontSize: 18 }}
/>
{/* Eylem Türü */}
{/* Hedef — türe göre değişen */}
{/* Sil */}
))}
)}
{/* Aktif / Pasif */}
set('is_active', e.target.checked)}
style={{ width: 20, height: 20, accentColor: 'var(--green)', cursor: 'pointer' }} />
);
}
/* WsCallRow — tek bir web servis çağrı satırı */
function WsCallRow({ call, index, services, onChange, onDelete }) {
const [showVars, setShowVars] = useState(false);
const vars = call.variables || {};
const addVar = () => {
onChange('variables', { ...vars, '': '' });
};
const updateVar = (oldKey, newKey, newVal) => {
const next = {};
Object.entries(vars).forEach(([k, v]) => {
const k2 = k === oldKey ? newKey : k;
const v2 = k === oldKey ? newVal : v;
next[k2] = v2;
});
onChange('variables', next);
};
const delVar = (key) => {
const next = { ...vars };
delete next[key];
onChange('variables', next);
};
const selectedSvc = services.find(s => s.id === parseInt(call.service_id));
return (
{/* Servis Seçimi */}
{/* Değişkenler toggle */}
{/* Servis açıklaması */}
{selectedSvc && (
{selectedSvc.method} {selectedSvc.url}
)}
{/* Değişken eşleme */}
{showVars && (
Gönderilecek Değişkenler
FreeSWITCH değişkeni: {'{caller_id_number}'}
Sabit değer: 12345
{Object.entries(vars).map(([k, v], i) => (
updateVar(k, e.target.value, v)} />
updateVar(k, k, e.target.value)} />
))}
{Object.keys(vars).length === 0 && (
Değişken eklenmemiş — servis parametresiz çağrılır.
)}
)}
);
}
/* IVR Tab */
function IVRTab({ toast }) {
const { data: menus, reload } = window.useApi('/api/ivr', []);
const [modal, setModal] = useState(null);
// Kuyruk isimlerini kartlarda göstermek için yükle
const [queues, setQueues] = useState([]);
const [announcements, setAnnouncements] = useState([]);
const [services, setServices] = useState([]);
function loadAnnouncements() {
authFetch('/api/announcements').then(r => r.ok ? r.json() : []).then(setAnnouncements).catch(() => {});
}
useEffect(() => {
authFetch('/api/queues')
.then(r => r.ok ? r.json() : [])
.then(setQueues)
.catch(() => {});
authFetch('/api/ws/services')
.then(r => r.ok ? r.json() : [])
.then(setServices)
.catch(() => {});
loadAnnouncements();
}, []);
function queueLabel(extension) {
const q = queues.find(q => q.extension === extension);
return q ? `${q.name} (${extension})` : extension;
}
function optionLabel(opt) {
if (opt.action_type === 'transfer_queue') return ` ${queueLabel(opt.target)}`;
if (opt.action_type === 'transfer_ivr') return ` Menü #${opt.target}`;
if (opt.action_type === 'transfer_human') return ` ${opt.target}`;
return ` AI: ${opt.target}`;
}
async function handleDelete(id) {
if (!confirm('Bu IVR menüsünü silmek istediğinize emin misiniz?')) return;
await authFetch(`/api/ivr/${id}`, { method: 'DELETE' });
toast('IVR Menüsü başarıyla silindi', 'success');
reload();
}
return (
Sesli Yanıt Sistemi (IVR)
Gelen çağrıları karşılayacak otomatik menüler oluşturun ve yönlendirmeleri ayarlayın.
{/* Anons Kütüphanesi */}
{(!menus || menus.length === 0) ? (
Henüz IVR Menüsü yok
Arayanları kuyruklara veya temsilcilere yönlendirmek için ilk IVR'ınızı oluşturun.
) : (
{menus.map(m => (
{m.name}
ID:{' '}
ivr_menu_{m.id}
{m.is_active ? ' Aktif' : ' Pasif'}
{m.description && (
{m.description}
)}
"{m.greet_text}"
{m.ws_pre_calls?.length > 0 && (
{m.ws_pre_calls.map((wc, i) => (
{services.find(s => s.id === wc.service_id)?.name || `Servis #${wc.service_id}`}
))}
)}
{m.options.map(opt => (
{opt.digit}
→
{optionLabel(opt)}
))}
{m.options.length === 0 && (
Tuşlama seçeneği eklenmemiş
)}
))}
)}
{modal && (
setModal(null)}
onSave={() => {
setModal(null);
reload();
toast('IVR Menüsü kaydedildi ve FreeSWITCH güncellendi.', 'success');
}}
/>
)}
);
}
window.IVRTab = IVRTab;