// CustomersTab.jsx — Müşteri Kartları (Customer CRM) UI
// Exported as window.CustomersTab
(function () {
const { useState, useEffect, useCallback, useRef } = React;
/* ── Format helpers ─────────────────────────────────────────────────────── */
// Backend naive UTC döndüğü için parseUTC ile yerel saate çevirilir.
function formatTR(iso) {
return fmtDateTime(iso, {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit',
});
}
function formatDuration(seconds) {
if (!seconds && seconds !== 0) return '—';
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
if (m === 0) return `${s}sn`;
return `${m}dk ${s}sn`;
}
function formatDate(iso) {
return fmtDateOnly(iso);
}
/* ── Tag badges ─────────────────────────────────────────────────────────── */
const TAG_COLORS = [
{ bg: '#1e3a5f', border: '#3b82f6', text: '#93c5fd' },
{ bg: '#064e3b', border: '#10b981', text: '#6ee7b7' },
{ bg: '#4a1942', border: '#a855f7', text: '#d8b4fe' },
{ bg: '#451a03', border: '#f59e0b', text: '#fcd34d' },
{ bg: '#3b0764', border: '#8b5cf6', text: '#c4b5fd' },
{ bg: '#0f172a', border: '#64748b', text: '#94a3b8' },
];
function tagColor(tag) {
let h = 0;
for (let i = 0; i < tag.length; i++) h = (h * 31 + tag.charCodeAt(i)) & 0xffffffff;
return TAG_COLORS[Math.abs(h) % TAG_COLORS.length];
}
function TagBadge({ tag }) {
const c = tagColor(tag);
return (
{tag}
);
}
function BlacklistBadge() {
return (
Kara Liste
);
}
function StatusBadge({ status }) {
const map = {
completed: { bg: '#064e3b20', border: '#10b981', text: '#34d399', label: 'Tamamlandı' },
missed: { bg: '#7f1d1d20', border: '#ef4444', text: '#f87171', label: 'Cevapsız' },
failed: { bg: '#7f1d1d20', border: '#ef4444', text: '#f87171', label: 'Başarısız' },
answered: { bg: '#064e3b20', border: '#10b981', text: '#34d399', label: 'Yanıtlandı' },
busy: { bg: '#451a0320', border: '#f59e0b', text: '#fcd34d', label: 'Meşgul' },
voicemail: { bg: '#1e3a5f20', border: '#3b82f6', text: '#93c5fd', label: 'Sesli Mesaj' },
};
const s = map[status] || { bg: '#1f293720', border: '#6b7280', text: '#9ca3af', label: status || '—' };
return (
{s.label}
);
}
/* ── Stat Card ──────────────────────────────────────────────────────────── */
function StatCard({ label, value, color, icon }) {
return (
);
}
/* ── Confirm Delete Modal ───────────────────────────────────────────────── */
function ConfirmDeleteModal({ customer, onConfirm, onCancel }) {
return (
e.stopPropagation()} style={{ maxWidth: 420, width: '90vw', padding: 0 }}>
Müşteriyi Sil
{customer.name || customer.phone}adlı müşteriyi kalıcı olarak silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.
);
}
/* ── New Customer Modal ─────────────────────────────────────────────────── */
function NewCustomerModal({ onClose, onSaved, toast }) {
const [form, setForm] = useState({ phone: '', name: '', company: '', email: '', tags: '' });
const [saving, setSaving] = useState(false);
const set = (k, v) =>setForm(f => ({ ...f, [k]: v }));
async function handleSave() {
if (!form.phone.trim()) {
toast('Hata', 'Telefon numarası zorunludur.', 'error');
return;
}
setSaving(true);
try {
const payload = {
phone: form.phone.trim(),
name: form.name.trim(),
company: form.company.trim(),
email: form.email.trim(),
tags: form.tags ? form.tags.split(',').map(t =>t.trim()).filter(Boolean) : [],
};
const r = await authFetch('/api/customers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (r.ok) {
toast('Başarılı', 'Müşteri oluşturuldu.', 'success');
onSaved();
} else {
const e = await r.json().catch(() => ({}));
toast('Hata', e.error || e.message || 'Müşteri oluşturulamadı.', 'error');
}
} finally {
setSaving(false);
}
}
return (
e.stopPropagation()} style={{ maxWidth: 500, width: '92vw', padding: 0 }}>
Yeni Müşteri Ekle
);
}
/* ── Customer Detail/Edit Modal ─────────────────────────────────────────── */
function CustomerDetailModal({ customerId, initialTab, onClose, onSaved, toast }) {
const [activeTab, setActiveTab] = useState(initialTab || 'info');
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [calls, setCalls] = useState([]);
const [callsLoading, setCallsLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [noteText, setNoteText] = useState('');
const [addingNote, setAddingNote] = useState(false);
// Edit form state
const [form, setForm] = useState(null);
const setField = (k, v) =>setForm(f => ({ ...f, [k]: v }));
useEffect(() => {
loadCustomer();
}, [customerId]);
useEffect(() => {
if (activeTab === 'calls' && calls.length === 0 && data) {
loadCalls();
}
}, [activeTab, data]);
async function loadCustomer() {
setLoading(true);
try {
const r = await authFetch(`/api/customers/lookup?phone=${encodeURIComponent('')}&id=${customerId}`);
// fallback: try direct GET if lookup by id not supported
const r2 = await authFetch(`/api/customers/${customerId}`);
if (r2.ok) {
const d = await r2.json();
setData(d);
setForm({
name: d.name || '',
company: d.company || '',
email: d.email || '',
tags: (d.tags || []).join(', '),
notes: d.notes || '',
is_blacklist: !!d.is_blacklist,
});
} else if (r.ok) {
const d = await r.json();
const c = d.customer || d;
setData(c);
setForm({
name: c.name || '',
company: c.company || '',
email: c.email || '',
tags: (c.tags || []).join(', '),
notes: c.notes || '',
is_blacklist: !!c.is_blacklist,
});
}
} finally {
setLoading(false);
}
}
async function loadCalls() {
setCallsLoading(true);
try {
const r = await authFetch(`/api/customers/${customerId}/calls`);
if (r.ok) {
const d = await r.json();
setCalls(Array.isArray(d) ? d : (d.items || d.calls || []));
}
} finally {
setCallsLoading(false);
}
}
async function handleSave() {
setSaving(true);
try {
const payload = {
name: form.name.trim(),
company: form.company.trim(),
email: form.email.trim(),
tags: form.tags ? form.tags.split(',').map(t =>t.trim()).filter(Boolean) : [],
notes: form.notes,
is_blacklist: form.is_blacklist,
};
const r = await authFetch(`/api/customers/${customerId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (r.ok) {
toast('Başarılı', 'Müşteri bilgileri güncellendi.', 'success');
onSaved();
loadCustomer();
} else {
const e = await r.json().catch(() => ({}));
toast('Hata', e.error || e.message || 'Güncelleme başarısız.', 'error');
}
} finally {
setSaving(false);
}
}
async function handleAddNote() {
if (!noteText.trim()) return;
setAddingNote(true);
try {
const r = await authFetch(`/api/customers/${customerId}/notes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ note: noteText.trim() }),
});
if (r.ok) {
toast('Başarılı', 'Not eklendi.', 'success');
setNoteText('');
loadCustomer();
} else {
const e = await r.json().catch(() => ({}));
toast('Hata', e.error || e.message || 'Not eklenemedi.', 'error');
}
} finally {
setAddingNote(false);
}
}
const TABS = [
{ key: 'info', label: 'Bilgiler' },
{ key: 'calls', label: 'Çağrı Geçmişi' },
{ key: 'notes', label: 'Notlar' },
];
const tabStyle = (key) => ({
padding: '9px 18px',
fontSize: 13,
fontWeight: 600,
border: 'none',
borderBottom: activeTab === key ? '2px solid var(--blue)' : '2px solid transparent',
background: 'transparent',
color: activeTab === key ? 'var(--blue)' : 'var(--text-dim)',
cursor: 'pointer',
transition: 'color .15s, border-color .15s',
});
return (
e.stopPropagation()} style={{ maxWidth: 720, width: '94vw', padding: 0, margin: 'auto' }}>
{/* Header */}
{loading || !data ? (
Yükleniyor...
) : (
{data.name || '(İsimsiz Müşteri)'}
{data.is_blacklist && }
{data.phone}
)}
{/* Tab bar */}
{TABS.map(t => (
))}
{/* Tab content */}
{loading ? (
) : (
<>
{/* ── INFO TAB ── */}
{activeTab === 'info' && form && (
)}
{/* ── CALLS TAB ── */}
{activeTab === 'calls' && (
{callsLoading ? (
Çağrı geçmişi yükleniyor...
) : calls.length === 0 ? (
) : (
{['Tarih', 'Süre', 'Durum', 'Asistan', 'Disposisyon', 'Özet'].map(h => (
| {h} |
))}
{calls.map((call, i) => (
e.currentTarget.style.background = 'var(--surface2)'}
onMouseLeave={e =>e.currentTarget.style.background = ''}
>
|
{formatTR(call.start_time)}
|
{formatDuration(call.duration)}
|
|
{call.agent_name || '—'}
|
{call.disposition_code ? (
{call.disposition_code}
) : '—'}
|
{call.summary ? (
{call.summary}
) : '—'}
|
))}
)}
)}
{/* ── NOTES TAB ── */}
{activeTab === 'notes' && (
{/* Existing notes */}
Mevcut Notlar
{data.notes ? (
{data.notes}
) : (
Henüz not eklenmemiş.
)}
{/* Add new note */}
)}
>
)}
);
}
/* ── Main CustomersTab ──────────────────────────────────────────────────── */
function CustomersTab({ toast }) {
const [customers, setCustomers] = useState([]);
const [total, setTotal] = useState(0);
const [blacklistCount, setBlacklistCount] = useState(0);
const [page, setPage] = useState(1);
const perPage = 20;
const [loading, setLoading] = useState(false);
// Filters
const [filterPhone, setFilterPhone] = useState('');
const [filterName, setFilterName] = useState('');
const [filterBlacklist, setFilterBlacklist] = useState(false);
// Modals
const [showNewModal, setShowNewModal] = useState(false);
const [detailCustomerId, setDetailCustomerId] = useState(null);
const [detailInitialTab, setDetailInitialTab] = useState('info');
const [deleteTarget, setDeleteTarget] = useState(null);
const searchTimer = useRef(null);
const loadCustomers = useCallback(async (p = page) => {
setLoading(true);
try {
const params = new URLSearchParams({
page: p,
per_page: perPage,
});
if (filterPhone.trim()) params.set('phone', filterPhone.trim());
if (filterName.trim()) params.set('name', filterName.trim());
if (filterBlacklist) params.set('is_blacklist', 'true');
const r = await authFetch(`/api/customers?${params.toString()}`);
if (r.ok) {
const d = await r.json();
setCustomers(d.items || []);
setTotal(d.total || 0);
}
} finally {
setLoading(false);
}
}, [page, filterPhone, filterName, filterBlacklist]);
const loadBlacklistCount = useCallback(async () => {
try {
const r = await authFetch('/api/customers?page=1&per_page=1&is_blacklist=true');
if (r.ok) {
const d = await r.json();
setBlacklistCount(d.total || 0);
}
} catch { /* ignore */ }
}, []);
useEffect(() => {
loadCustomers(page);
}, [page, filterBlacklist]);
useEffect(() => {
loadBlacklistCount();
}, []);
// Debounced search on text inputs
function handleSearchChange(setter, value) {
setter(value);
clearTimeout(searchTimer.current);
searchTimer.current = setTimeout(() => {
setPage(1);
// loadCustomers will fire via effect when page resets, but page may not change
// So trigger directly
loadCustomersWithValues(1, filterPhone, filterName, filterBlacklist, setter === setFilterPhone ? value : null, setter === setFilterName ? value : null);
}, 400);
}
const loadCustomersWithValues = useCallback(async (p, phone, name, bl, overridePhone, overrideName) => {
setLoading(true);
try {
const ph = overridePhone !== null && overridePhone !== undefined ? overridePhone : phone;
const nm = overrideName !== null && overrideName !== undefined ? overrideName : name;
const params = new URLSearchParams({ page: p, per_page: perPage });
if (ph.trim()) params.set('phone', ph.trim());
if (nm.trim()) params.set('name', nm.trim());
if (bl) params.set('is_blacklist', 'true');
const r = await authFetch(`/api/customers?${params.toString()}`);
if (r.ok) {
const d = await r.json();
setCustomers(d.items || []);
setTotal(d.total || 0);
}
} finally {
setLoading(false);
}
}, []);
async function handleDelete(customer) {
const r = await authFetch(`/api/customers/${customer.id}`, { method: 'DELETE' });
if (r.ok) {
toast('Silindi', `${customer.name || customer.phone} silindi.`, 'success');
setDeleteTarget(null);
loadBlacklistCount();
loadCustomers(page);
} else {
const e = await r.json().catch(() => ({}));
toast('Hata', e.error || e.message || 'Silinemedi.', 'error');
}
}
const totalPages = Math.max(1, Math.ceil(total / perPage));
const thStyle = {
padding: '10px 14px',
textAlign: 'left',
fontSize: 11,
fontWeight:600,
color: 'var(--text-dim)',
textTransform: 'uppercase',
letterSpacing: '0.06em',
borderBottom: '1px solid var(--border)',
whiteSpace: 'nowrap',
};
const tdStyle = {
padding: '11px 14px',
borderBottom: '1px solid var(--border)',
fontSize: 13,
color: 'var(--text)',
verticalAlign: 'middle',
};
return (
{/* ── Stats bar ── */}
{/* ── Search & Filter bar ── */}
{/* ── Customer table ── */}
{loading ? (
) : customers.length === 0 ? (
Müşteri bulunamadı
Yeni müşteri ekleyin veya arama kriterlerini değiştirin.
) : (
| Ad / Soyad |
Telefon |
Şirket |
Etiketler |
Kara Liste |
Son Çağrı |
İşlemler |
{customers.map((c, i) => (
e.currentTarget.style.background = 'var(--blue)10'}
onMouseLeave={e =>e.currentTarget.style.background = i % 2 === 0 ? 'transparent' : 'var(--surface2)'}
>
|
{c.name || (İsimsiz)}
|
{c.phone}
|
{c.company || '—'}
|
{(c.tags || []).length > 0
? (c.tags || []).map(t => )
: —
}
|
{c.is_blacklist ? : —}
|
{c.last_call_at ? formatDate(c.last_call_at) : '—'}
|
|
))}
)}
{/* ── Pagination ── */}
{totalPages > 1 && (
Toplam {total}müşteri
·
Sayfa {page} / {totalPages}
{/* Page numbers (show up to 7 around current page) */}
{Array.from({ length: totalPages }, (_, i) =>i + 1)
.filter(p =>p === 1 || p === totalPages || Math.abs(p - page) <= 2)
.reduce((acc, p, idx, arr) => {
if (idx > 0 && p - arr[idx - 1] > 1) acc.push('...');
acc.push(p);
return acc;
}, [])
.map((p, idx) =>
p === '...' ? (
…
) : (
)
)
}
)}
{/* ── Modals ── */}
{showNewModal && (
setShowNewModal(false)}
onSaved={() => {
setShowNewModal(false);
setPage(1);
loadCustomers(1);
loadBlacklistCount();
}}
/>
)}
{detailCustomerId && (
setDetailCustomerId(null)}
onSaved={() => {
loadCustomers(page);
loadBlacklistCount();
}}
/>
)}
{deleteTarget && (
handleDelete(deleteTarget)}
onCancel={() =>setDeleteTarget(null)}
/>
)}
);
}
window.CustomersTab = CustomersTab;
})();