// CRM — Müşteri İlişkileri Yönetimi (satış hattı, kişiler, etkileşimler)
const CRMIcons = window.Icons;
const CRM_STAGE_LIST = window.PMStore.CRM_STAGES;
const CRM_ACT_LIST = window.PMStore.CRM_ACTIVITY_TYPES;
const STAGE_BY_ID = Object.fromEntries(CRM_STAGE_LIST.map(s => [s.id, s]));
const ACT_BY_ID = Object.fromEntries(CRM_ACT_LIST.map(a => [a.id, a]));
const PIPELINE_STAGES = CRM_STAGE_LIST.filter(s => s.id !== 'won' && s.id !== 'lost');
const crmMoney = (n) => new Intl.NumberFormat('tr-TR', { maximumFractionDigits: 0 }).format(n || 0) + ' ₺';
const crmMoneyShort = (n) => Math.abs(n) >= 1000
? new Intl.NumberFormat('tr-TR', { maximumFractionDigits: 1 }).format(n / 1000) + 'B ₺'
: crmMoney(n);
const crmInitials = (name) => (name || '?').split(/\s+/).filter(Boolean).map(w => w[0]).join('').slice(0, 2).toUpperCase();
const crmDateLabel = (d) => d ? new Date(d + 'T00:00:00').toLocaleDateString('tr-TR', { day: 'numeric', month: 'short' }) : '';
const onlyDigits = (s) => (s || '').replace(/[^\d+]/g, '');
// ── Fırsat kartı (satış hattı) ───────────────────────────────
function DealCard({ deal, firm, contact, onClick, onStageChange, onDragStart }) {
const overdue = deal.expectedClose && deal.status === 'open' &&
new Date(deal.expectedClose + 'T00:00:00') < new Date(new Date().toDateString());
return (
{deal.title}
{deal.value > 0 && {crmMoneyShort(deal.value)}}
{firm
? {firm.name}
: deal.company && {deal.company}}
{(contact || deal.expectedClose) && (
{contact && {contact.name}}
{deal.expectedClose && (
{crmDateLabel(deal.expectedClose)}
)}
)}
);
}
// ── Kişi kartı ───────────────────────────────────────────────
function ContactCard({ contact, firm, onClick, dealCount }) {
const color = firm?.color || 'var(--accent)';
return (
);
}
// ── Yeni / Düzenle: Fırsat modalı ────────────────────────────
function DealModal({ state, editing, onSave, onDelete, onClose, onConvert }) {
const isEdit = !!(editing && editing.id);
const projects = (state.projects || []).filter(p => !p.archived);
const [title, setTitle] = React.useState(editing?.title || '');
const [projectId, setProjectId] = React.useState(editing?.projectId || '');
const [company, setCompany] = React.useState(editing?.company || '');
const [contactId, setContactId] = React.useState(editing?.contactId || '');
const [value, setValue] = React.useState(editing?.value ? String(editing.value) : '');
const [stage, setStage] = React.useState(editing?.stage || 'lead');
const [expClose, setExpClose] = React.useState(editing?.expectedClose || '');
const [notes, setNotes] = React.useState(editing?.notes || '');
const firmContacts = (state.crmContacts || []).filter(c => projectId ? c.projectId === projectId : true);
React.useEffect(() => {
const fn = e => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', fn);
return () => window.removeEventListener('keydown', fn);
}, [onClose]);
const save = () => {
if (!title.trim()) return;
onSave({
title: title.trim(),
projectId: projectId || null,
company: projectId ? '' : company.trim(),
contactId: contactId || null,
value: parseFloat(String(value).replace(',', '.')) || 0,
stage,
expectedClose: expClose || null,
notes: notes.trim(),
});
};
return (
e.stopPropagation()}>
{isEdit ? 'Fırsatı Düzenle' : 'Yeni Fırsat'}
{isEdit && }
);
}
// ── Yeni / Düzenle: Kişi modalı ──────────────────────────────
function ContactModal({ state, editing, onSave, onDelete, onClose }) {
const isEdit = !!(editing && editing.id);
const projects = (state.projects || []).filter(p => !p.archived);
const [name, setName] = React.useState(editing?.name || '');
const [role, setRole] = React.useState(editing?.role || '');
const [projectId, setProjectId] = React.useState(editing?.projectId || '');
const [company, setCompany] = React.useState(editing?.company || '');
const [email, setEmail] = React.useState(editing?.email || '');
const [phone, setPhone] = React.useState(editing?.phone || '');
const [notes, setNotes] = React.useState(editing?.notes || '');
const [isPrimary, setIsPrimary] = React.useState(!!editing?.isPrimary);
React.useEffect(() => {
const fn = e => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', fn);
return () => window.removeEventListener('keydown', fn);
}, [onClose]);
const save = () => {
if (!name.trim()) return;
onSave({
name: name.trim(), role: role.trim(),
projectId: projectId || null,
company: projectId ? '' : company.trim(),
email: email.trim(), phone: phone.trim(),
notes: notes.trim(), isPrimary,
});
};
return (
e.stopPropagation()}>
{isEdit ? 'Kişiyi Düzenle' : 'Yeni Kişi'}
{isEdit && }
);
}
// ── Hızlı etkileşim ekleme ───────────────────────────────────
function ActivityComposer({ state, onAdd }) {
const [type, setType] = React.useState('call');
const [summary, setSummary] = React.useState('');
const [projectId, setProjectId] = React.useState('');
const projects = (state.projects || []).filter(p => !p.archived);
const submit = () => {
if (!summary.trim()) return;
onAdd({ type, summary: summary.trim(), projectId: projectId || null, date: new Date().toISOString().slice(0, 10) });
setSummary('');
};
return (
setSummary(e.target.value)} onKeyDown={e => e.key === 'Enter' && submit()} />
);
}
// ── Ana CRM Görünümü ─────────────────────────────────────────
function CRMView({ state, onAddContact, onUpdateContact, onDeleteContact, onAddDeal, onUpdateDeal, onDeleteDeal, onAddActivity, onDeleteActivity, onConvertDeal }) {
const [tab, setTab] = React.useState('pipeline');
const [dealModal, setDealModal] = React.useState(null);
const [contactModal, setContactModal] = React.useState(null);
const [search, setSearch] = React.useState('');
const [actFilter, setActFilter] = React.useState('all');
const [dragStage, setDragStage] = React.useState(null);
const dragDealId = React.useRef(null);
const projects = (state.projects || []).filter(p => !p.archived);
const projById = Object.fromEntries(projects.map(p => [p.id, p]));
const contacts = state.crmContacts || [];
const deals = state.crmDeals || [];
const activities = state.crmActivities || [];
const contactById = Object.fromEntries(contacts.map(c => [c.id, c]));
// KPI'lar
const openDeals = deals.filter(d => d.status === 'open');
const pipelineValue = openDeals.reduce((s, d) => s + (d.value || 0), 0);
const monthKey = new Date().toISOString().slice(0, 7);
const wonThisMonth = deals.filter(d => d.status === 'won' && (d.closedAt || '').slice(0, 7) === monthKey);
const wonValue = wonThisMonth.reduce((s, d) => s + (d.value || 0), 0);
const wonCount = deals.filter(d => d.status === 'won').length;
const lostCount = deals.filter(d => d.status === 'lost').length;
const convRate = (wonCount + lostCount) > 0 ? Math.round(wonCount / (wonCount + lostCount) * 100) : null;
const setDealStage = (id, stage) => onUpdateDeal(id, { stage });
// Satış hattı kolonları (won/lost ayrı özet)
const dealsByStage = (stageId) => deals
.filter(d => d.stage === stageId)
.sort((a, b) => (b.value || 0) - (a.value || 0));
const onDrop = (stageId) => {
if (dragDealId.current) setDealStage(dragDealId.current, stageId);
dragDealId.current = null;
setDragStage(null);
};
// Kişi araması
const filteredContacts = contacts.filter(c => {
if (!search) return true;
const q = search.toLowerCase();
const firmName = projById[c.projectId]?.name || c.company || '';
return [c.name, c.role, c.email, c.phone, firmName].filter(Boolean).join(' ').toLowerCase().includes(q);
}).sort((a, b) => (b.isPrimary - a.isPrimary) || a.name.localeCompare(b.name, 'tr'));
const dealCountByContact = (cid) => deals.filter(d => d.contactId === cid).length;
// Etkileşim listesi (tarihe göre)
const filteredActivities = activities
.filter(a => actFilter === 'all' || a.type === actFilter)
.sort((a, b) => (b.date || '').localeCompare(a.date || '') || (b.createdAt || '').localeCompare(a.createdAt || ''));
const TABS = [
{ id: 'pipeline', label: 'Satış Hattı', icon: 'Handshake' },
{ id: 'contacts', label: 'Kişiler', icon: 'Users' },
{ id: 'activities', label: 'Etkileşimler', icon: 'Activity' },
];
return (
{/* Header */}
CRM
{tab === 'contacts' && (
setSearch(e.target.value)} />
{search && }
)}
{/* KPI şeridi */}
{openDeals.length}
Açık Fırsat
{crmMoneyShort(pipelineValue)}
Pipeline Değeri
{crmMoneyShort(wonValue)}
Bu Ay Kazanılan
{convRate !== null ? `%${convRate}` : '—'}
Dönüşüm Oranı
{/* Sekmeler */}
{TABS.map(t => (
))}
{/* ── Satış Hattı ── */}
{tab === 'pipeline' && (
deals.length === 0 ? (
Henüz fırsat yok
) : (
{PIPELINE_STAGES.map(stage => {
const list = dealsByStage(stage.id);
const sum = list.reduce((s, d) => s + (d.value || 0), 0);
return (
{ e.preventDefault(); setDragStage(stage.id); }}
onDragLeave={() => setDragStage(s => s === stage.id ? null : s)}
onDrop={() => onDrop(stage.id)}>
{stage.label}
{list.length}
{sum > 0 && {crmMoneyShort(sum)}}
{list.map(deal => (
setDealModal(deal)}
onStageChange={setDealStage}
onDragStart={() => { dragDealId.current = deal.id; }} />
))}
{list.length === 0 && —
}
);
})}
{/* Kazanılan / Kaybedilen özet sütunu */}
{['won', 'lost'].map(sid => {
const st = STAGE_BY_ID[sid];
const list = dealsByStage(sid);
return (
{ e.preventDefault(); setDragStage(sid); }}
onDragLeave={() => setDragStage(s => s === sid ? null : s)}
onDrop={() => onDrop(sid)}>
{st.label}
{list.length}
{list.slice(0, 6).map(deal => (
setDealModal(deal)}
onStageChange={setDealStage}
onDragStart={() => { dragDealId.current = deal.id; }} />
))}
);
})}
)
)}
{/* ── Kişiler ── */}
{tab === 'contacts' && (
contacts.length === 0 ? (
Henüz kişi eklenmemiş
) : (
{filteredContacts.map(c => (
setContactModal(c)} />
))}
{filteredContacts.length === 0 && Aramayla eşleşen kişi yok
}
)
)}
{/* ── Etkileşimler ── */}
{tab === 'activities' && (
{CRM_ACT_LIST.map(t => (
))}
{filteredActivities.length === 0 ? (
Henüz etkileşim kaydı yok
) : (
{filteredActivities.map(a => {
const at = ACT_BY_ID[a.type] || ACT_BY_ID.note;
const Icon = CRMIcons[at.icon] || CRMIcons.FileText;
const firm = projById[a.projectId];
const contact = contactById[a.contactId];
return (
{a.summary}
{at.label}
{contact && <>·{contact.name}>}
{firm && <>·{firm.name}>}
·{crmDateLabel(a.date)}
);
})}
)}
)}
{/* Modallar */}
{dealModal && (
setDealModal(null)}
onSave={(data) => { if (dealModal.id) onUpdateDeal(dealModal.id, data); else onAddDeal(data); setDealModal(null); }}
onDelete={(id) => { onDeleteDeal(id); setDealModal(null); }}
onConvert={(deal) => { onConvertDeal(deal); setDealModal(null); }} />
)}
{contactModal && (
setContactModal(null)}
onSave={(data) => { if (contactModal.id) onUpdateContact(contactModal.id, data); else onAddContact(data); setContactModal(null); }}
onDelete={(id) => { onDeleteContact(id); setContactModal(null); }} />
)}
);
}
window.CRMView = CRMView;