import React, { useState, useMemo } from 'react'; import { useLocation } from 'react-router-dom'; import { useCrm } from '../../services/hooks/useCrm'; import { useOrders } from '../../services/hooks/useOrders'; import { useMetadata } from '../../services/hooks/useMetadata'; import { Contact } from '../../entities/crm/contact'; import { Company } from '../../entities/crm/company'; import { Deal, DealStage } from '../../entities/crm/deal'; import { Lead } from '../../entities/crm/lead'; import { TimelineItem, Activity } from '../../services/contracts/crm'; import { Button } from '../../shared/ui/Button'; import { Badge } from '../../shared/ui/Badge'; import { MaturityBadge } from '../../shared/ui/MaturityBadge'; import { Modal } from '../../shared/ui/Modal'; import { Drawer } from '../../shared/ui/Drawer'; import { useToast } from '../../shared/ui/useToast'; import { formatCurrencyMinor } from '../../shared/lib/currency'; import { formatDateTime } from '../../shared/lib/date'; import { DynamicFormRenderer } from '../metadata/DynamicFormRenderer'; import { CsvImportWizard } from './CsvImportWizard'; import { Search, Plus, ArrowRight, ShoppingBag, Building, Check, UploadCloud, ChevronRight, Sparkles, } from 'lucide-react'; const DEAL_STAGES: { key: DealStage; label: string; color: string }[] = [ { key: 'prospecting', label: 'Первичный контакт', color: 'border-sky-500/40' }, { key: 'discovery', label: 'Квалификация', color: 'border-indigo-500/40' }, { key: 'proposal', label: 'Предложение', color: 'border-amber-500/40' }, { key: 'negotiation', label: 'Переговоры', color: 'border-orange-500/40' }, { key: 'contract', label: 'Согласование', color: 'border-purple-500/40' }, { key: 'won', label: 'Выиграна', color: 'border-emerald-500/60 bg-emerald-500/5' }, { key: 'lost', label: 'Проиграна', color: 'border-rose-500/40 bg-rose-500/5' }, ]; export const CrmModule: React.FC = () => { const location = useLocation(); const { showToast } = useToast(); const { contacts, companies, leads, deals, createContact, createCompany, createDeal, updateDeal, convertLead, getTimeline, createActivity, } = useCrm(); const { orders, createOrder } = useOrders(); // Active Entity metadata for dynamic forms const contactMeta = useMetadata('contact'); const companyMeta = useMetadata('company'); const dealMeta = useMetadata('deal'); const [activeTab, setActiveTab] = useState<'deals' | 'contacts' | 'companies' | 'leads' | 'import'>('deals'); const [search, setSearch] = useState(''); // Modals state const [isAddContactModalOpen, setIsAddContactModalOpen] = useState(false); const [isAddCompanyModalOpen, setIsAddCompanyModalOpen] = useState(false); const [isAddDealModalOpen, setIsAddDealModalOpen] = useState(false); const [isConvertLeadModalOpen, setIsConvertLeadModalOpen] = useState(false); const [selectedLeadToConvert, setSelectedLeadToConvert] = useState(null); const [convertDealTitle, setConvertDealTitle] = useState(''); // Deal Won -> Order flow const [isDealWonModalOpen, setIsDealWonModalOpen] = useState(false); const [wonDeal, setWonDeal] = useState(null); const [orderTitle, setOrderTitle] = useState(''); const [orderWarehouse, setOrderWarehouse] = useState('wh_main'); // Customer 360 Detail View const [selectedContact, setSelectedContact] = useState(null); const [selectedCompany, setSelectedCompany] = useState(null); const [customer360Tab, setCustomer360Tab] = useState<'overview' | 'deals' | 'orders' | 'activities' | 'timeline'>('overview'); const [timelineItems, setTimelineItems] = useState([]); const [newActivityTitle, setNewActivityTitle] = useState(''); const [newActivityType, setNewActivityType] = useState('call'); // Drag and Drop state const [draggedDealId, setDraggedDealId] = useState(null); // Sync tab with URL React.useEffect(() => { const path = location.pathname; if (path.includes('/crm/deals')) setActiveTab('deals'); else if (path.includes('/crm/leads')) setActiveTab('leads'); else if (path.includes('/crm/companies')) setActiveTab('companies'); else if (path.includes('/crm/import')) setActiveTab('import'); else if (path.includes('/crm/contacts') || path === '/crm') setActiveTab('contacts'); }, [location.pathname]); // Load timeline when selecting contact/company React.useEffect(() => { if (selectedContact) { getTimeline('contact', selectedContact.id).then(setTimelineItems).catch(() => {}); } else if (selectedCompany) { getTimeline('company', selectedCompany.id).then(setTimelineItems).catch(() => {}); } else { setTimelineItems([]); } }, [selectedContact, selectedCompany, getTimeline]); // Deal stage transition handler const handleStageChange = async (dealId: string, nextStage: DealStage) => { const deal = deals.find((d) => d.id === dealId); if (!deal) return; await updateDeal(dealId, { stage: nextStage }); showToast(`Сделка «${deal.title}» переведена на этап: ${DEAL_STAGES.find((s) => s.key === nextStage)?.label}`, 'info'); // Trigger Deal Won Flow if (nextStage === 'won') { setWonDeal(deal); setOrderTitle(`Заказ по сделке: ${deal.title}`); setIsDealWonModalOpen(true); } }; // Drag & Drop Handlers const handleDragStart = (e: React.DragEvent, dealId: string) => { e.dataTransfer.setData('text/plain', dealId); setDraggedDealId(dealId); }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); }; const handleDrop = async (e: React.DragEvent, targetStage: DealStage) => { e.preventDefault(); const dealId = e.dataTransfer.getData('text/plain') || draggedDealId; if (dealId) { await handleStageChange(dealId, targetStage); } setDraggedDealId(null); }; // Confirm Deal Won -> Order creation const handleConfirmOrderCreation = async () => { if (!wonDeal) return; try { const customer = contacts.find((c) => c.id === wonDeal.contactId); const company = companies.find((c) => c.id === wonDeal.companyId); const customerName = customer ? `${customer.firstName} ${customer.lastName}` : (company?.name || 'Клиент'); const customerId = wonDeal.contactId || wonDeal.companyId || 'cnt_default'; const orderNumber = `ORD-${Date.now().toString().slice(-6)}`; await createOrder({ orderNumber, customerType: wonDeal.contactId ? 'contact' : 'company', customerId, customerName, status: 'draft', paymentStatus: 'pending', fulfillmentStatus: 'unfulfilled', items: [ { id: `item_${Date.now()}`, productId: 'prd_coffee_beans', sku: 'COF-100', name: wonDeal.title, quantity: 1, unitPrice: wonDeal.amount, discountMinor: 0, totalPrice: wonDeal.amount, }, ], subtotal: wonDeal.amount, discountMinor: 0, taxMinor: Math.round(wonDeal.amount.minor * 0.2), shippingMinor: 0, total: wonDeal.amount, channel: 'b2b', }); showToast(`Связанный Заказ ${orderNumber} успешно создан в Коммерции!`, 'success'); setIsDealWonModalOpen(false); setWonDeal(null); } catch (err) { showToast(`Ошибка создания заказа: ${err instanceof Error ? err.message : String(err)}`, 'error'); } }; // Convert Lead const handleConvertLead = async () => { if (!selectedLeadToConvert) return; try { const res = await convertLead(selectedLeadToConvert.id, convertDealTitle); showToast(`Лид конвертирован! Создана сделка «${res.deal.title}»`, 'success'); setIsConvertLeadModalOpen(false); setSelectedLeadToConvert(null); } catch (err) { showToast(`Ошибка конвертации: ${err instanceof Error ? err.message : String(err)}`, 'error'); } }; // Add Activity in Customer 360 const handleAddActivity = async (e: React.FormEvent) => { e.preventDefault(); if (!newActivityTitle) return; const targetType = selectedContact ? 'contact' : 'company'; const targetId = selectedContact?.id || selectedCompany?.id || ''; await createActivity({ targetEntityType: targetType, targetEntityId: targetId, type: newActivityType, title: newActivityTitle, completed: false, dueDate: new Date(Date.now() + 86400000).toISOString(), }); setNewActivityTitle(''); showToast('Активность запланирована', 'success'); getTimeline(targetType, targetId).then(setTimelineItems).catch(() => {}); }; // Linked items for Customer 360 const currentCustomerId = selectedContact?.id || selectedCompany?.id || ''; const linkedDeals = useMemo( () => deals.filter((d) => d.contactId === currentCustomerId || d.companyId === currentCustomerId), [deals, currentCustomerId] ); const linkedOrders = useMemo( () => orders.filter((o) => o.customerId === currentCustomerId), [orders, currentCustomerId] ); return (
{/* Module Header */}

CRM: Клиенты, Воронка продаж & Сделки

Сквозной процесс: Лид → Контакт/Компания → Сделка (Kanban) → Выиграна → Заказ в Коммерции

{/* Navigation Tabs */}
{/* ---------------------------------------------------- */} {/* 1. DEALS KANBAN BOARD */} {/* ---------------------------------------------------- */} {activeTab === 'deals' && (
setSearch(e.target.value)} placeholder="Поиск по сделкам..." className="w-full pl-8 pr-3 py-1.5 text-xs bg-krona-surface border border-krona-border rounded-lg text-krona-primary focus:outline-none focus:border-krona-green" />
{DEAL_STAGES.map((col) => { const colDeals = deals.filter( (d) => d.stage === col.key && (d.title.toLowerCase().includes(search.toLowerCase()) || !search) ); const colTotalMinor = colDeals.reduce((sum, d) => sum + d.amount.minor, 0); return (
handleDrop(e, col.key)} className={`bg-krona-surface/70 border rounded-xl p-2.5 flex flex-col min-w-[200px] max-h-[750px] shadow-krona-soft transition-all ${col.color}`} >
{col.label}
{formatCurrencyMinor(colTotalMinor)}
{colDeals.length}
{colDeals.map((deal) => { const contact = contacts.find((c) => c.id === deal.contactId); const company = companies.find((c) => c.id === deal.companyId); const nextStageIdx = DEAL_STAGES.findIndex((s) => s.key === deal.stage) + 1; const nextStage = DEAL_STAGES[nextStageIdx]?.key; return (
handleDragStart(e, deal.id)} className="bg-krona-workspace border border-krona-border hover:border-krona-green/60 p-2.5 rounded-lg text-xs space-y-2 shadow-xs cursor-grab active:cursor-grabbing transition-all" >
{deal.title}
{formatCurrencyMinor(deal.amount.minor)} {deal.probability}% вер.
{(contact || company) && (
{company?.name || `${contact?.firstName} ${contact?.lastName}`}
)} {/* Quick stage transition button */} {nextStage && deal.stage !== 'won' && deal.stage !== 'lost' && ( )}
); })} {colDeals.length === 0 && (
Пусто
)}
); })}
)} {/* ---------------------------------------------------- */} {/* 2. CONTACTS VIEW */} {/* ---------------------------------------------------- */} {activeTab === 'contacts' && (
setSearch(e.target.value)} placeholder="Поиск по имени, телефону, email..." className="w-full pl-8 pr-3 py-1.5 text-xs bg-krona-surface border border-krona-border rounded-lg text-krona-primary focus:outline-none focus:border-krona-green" />
{contacts .filter( (c) => `${c.firstName} ${c.lastName}`.toLowerCase().includes(search.toLowerCase()) || c.phone?.includes(search) || c.email?.toLowerCase().includes(search.toLowerCase()) ) .map((contact) => { const comp = companies.find((co) => co.id === contact.companyId); return ( { setSelectedContact(contact); setSelectedCompany(null); }} className="hover:bg-krona-workspace/50 cursor-pointer transition-colors" > ); })}
ФИО / Контакт Компания Телефон Email Роли Действие
{contact.firstName[0]} {contact.lastName?.[0]}
{contact.firstName} {contact.lastName}
{contact.jobTitle && (
{contact.jobTitle}
)}
{comp?.name || '—'} {contact.phone || '—'} {contact.email || '—'}
{contact.roles?.map((r) => ( {r} ))}
)} {/* ---------------------------------------------------- */} {/* 3. COMPANIES VIEW */} {/* ---------------------------------------------------- */} {activeTab === 'companies' && (
setSearch(e.target.value)} placeholder="Поиск по названию, ИНН..." className="w-full pl-8 pr-3 py-1.5 text-xs bg-krona-surface border border-krona-border rounded-lg text-krona-primary focus:outline-none focus:border-krona-green" />
{companies .filter( (c) => c.name.toLowerCase().includes(search.toLowerCase()) || c.inn?.includes(search) || c.address?.toLowerCase().includes(search.toLowerCase()) ) .map((company) => ( { setSelectedCompany(company); setSelectedContact(null); }} className="hover:bg-krona-workspace/50 cursor-pointer transition-colors" > ))}
Название компании ИНН Город Телефон Роли Действие
{company.name}
{company.legalName && (
{company.legalName}
)}
{company.inn || '—'} {company.address || '—'} {company.phone || '—'}
{company.roles?.map((r) => ( {r} ))}
)} {/* ---------------------------------------------------- */} {/* 4. LEADS VIEW */} {/* ---------------------------------------------------- */} {activeTab === 'leads' && (
setSearch(e.target.value)} placeholder="Поиск лидов..." className="w-full pl-8 pr-3 py-1.5 text-xs bg-krona-surface border border-krona-border rounded-lg text-krona-primary focus:outline-none focus:border-krona-green" />
{leads.map((lead) => ( ))}
Название лида Контактное лицо Контакты Оценка бюджета Статус Конвертация
{lead.title} {lead.contactName}
{lead.phone}
{lead.email}
{lead.estimatedValue ? formatCurrencyMinor(lead.estimatedValue.minor) : '—'} {lead.status === 'converted' ? 'Конвертирован' : lead.status === 'new' ? 'Новый' : lead.status} {lead.status !== 'converted' ? ( ) : ( Сделка создана )}
)} {/* ---------------------------------------------------- */} {/* 5. CSV IMPORT WIZARD */} {/* ---------------------------------------------------- */} {activeTab === 'import' && } {/* ---------------------------------------------------- */} {/* CUSTOMER 360 DETAIL DRAWER */} {/* ---------------------------------------------------- */} { setSelectedContact(null); setSelectedCompany(null); }} title={ selectedContact ? `${selectedContact.firstName} ${selectedContact.lastName} (Customer 360)` : `${selectedCompany?.name} (Company 360)` } size="lg" >
{/* Sub-tabs */}
{(['overview', 'deals', 'orders', 'activities', 'timeline'] as const).map((tab) => ( ))}
{/* TAB 1: Overview */} {customer360Tab === 'overview' && (
{selectedContact && ( <>
ФИО:{' '} {selectedContact.firstName} {selectedContact.lastName}
Телефон:{' '} {selectedContact.phone || '—'}
Email:{' '} {selectedContact.email || '—'}
Должность:{' '} {selectedContact.jobTitle || '—'}
)} {selectedCompany && ( <>
Название:{' '} {selectedCompany.name}
ИНН:{' '} {selectedCompany.inn || '—'}
Адрес:{' '} {selectedCompany.address || '—'}
Телефон:{' '} {selectedCompany.phone || '—'}
)}
{/* Dynamic Custom Fields viewer if any exist */}
Кастомные атрибуты (Схема Krona Studio):
{((selectedContact?.customFields && Object.keys(selectedContact.customFields).length > 0) || (selectedCompany?.customFields && Object.keys(selectedCompany.customFields).length > 0)) ? (
{Object.entries( (selectedContact?.customFields || selectedCompany?.customFields) as Record ).map(([k, v]) => (
{k}:{' '} {String(v)}
))}
) : (

Дополнительные поля из Конструктора полей отображаются здесь в реальном времени.

)}
)} {/* TAB 2: Linked Deals */} {customer360Tab === 'deals' && (
{linkedDeals.map((d) => (
{d.title}
Этап:{' '} {DEAL_STAGES.find((s) => s.key === d.stage)?.label}
{formatCurrencyMinor(d.amount.minor)}
{d.probability}% вер.
))} {linkedDeals.length === 0 && (
Нет связанных сделок
)}
)} {/* TAB 3: Linked Orders */} {customer360Tab === 'orders' && (
{linkedOrders.map((ord) => (
{ord.orderNumber}
{ord.items.length} поз. • Статус: {ord.status}
{formatCurrencyMinor(ord.total.minor)}
{ord.paymentStatus}
))} {linkedOrders.length === 0 && (
Нет связанных заказов
)}
)} {/* TAB 4: Activities */} {customer360Tab === 'activities' && (
Запланировать активность:
setNewActivityTitle(e.target.value)} placeholder="Например: Обсудить коммерческое предложение..." className="flex-1 px-3 py-1 text-xs bg-krona-surface border border-krona-border rounded-md text-krona-primary" />
)} {/* TAB 5: Unified Timeline */} {customer360Tab === 'timeline' && (
{timelineItems.map((item) => (
{item.title} {formatDateTime(item.timestamp)}
{item.description && (

{item.description}

)}
Автор: {item.actor}
))} {timelineItems.length === 0 && (
Таймлайн пуст
)}
)}
{/* ---------------------------------------------------- */} {/* DEAL WON -> CREATE ORDER MODAL */} {/* ---------------------------------------------------- */} setIsDealWonModalOpen(false)} title="Сделка выиграна! Создать заказ в Коммерции?" size="md" >
Поздравляем с успехом! Сделка «{wonDeal?.title}» переведена в статус «Выиграна». Система предлагает автоматически сформировать коммерческий Заказ.
setOrderTitle(e.target.value)} className="w-full px-3 py-1.5 bg-krona-workspace border border-krona-border rounded-lg text-krona-primary" />
{wonDeal ? formatCurrencyMinor(wonDeal.amount.minor) : '0 ₽'}
{/* ---------------------------------------------------- */} {/* CONVERT LEAD MODAL */} {/* ---------------------------------------------------- */} setIsConvertLeadModalOpen(false)} title="Конвертация лида в сделку и контакт" size="md" >

При конвертации будет автоматически создан Контакт клиента и новая Сделка на этапе «Предложение».

setConvertDealTitle(e.target.value)} className="w-full px-3 py-1.5 bg-krona-workspace border border-krona-border rounded-lg text-krona-primary" />
{/* ---------------------------------------------------- */} {/* NEW CONTACT MODAL WITH DYNAMIC METADATA FORM */} {/* ---------------------------------------------------- */} setIsAddContactModalOpen(false)} title="Создание нового контакта" size="lg" > {contactMeta.currentEntity ? ( { await createContact({ firstName: String(values.firstName || 'Новый'), lastName: String(values.lastName || 'Контакт'), phone: String(values.phone || ''), email: String(values.email || ''), jobTitle: String(values.jobTitle || ''), roles: ['client'], status: 'active', customFields: values, }); setIsAddContactModalOpen(false); showToast('Контакт успешно создан!', 'success'); }} onCancel={() => setIsAddContactModalOpen(false)} /> ) : (
Загрузка схемы...
)}
{/* ---------------------------------------------------- */} {/* NEW COMPANY MODAL WITH DYNAMIC METADATA FORM */} {/* ---------------------------------------------------- */} setIsAddCompanyModalOpen(false)} title="Создание новой компании" size="lg" > {companyMeta.currentEntity ? ( { await createCompany({ name: String(values.name || 'Новая компания'), inn: String(values.inn || ''), address: String(values.city || values.address || 'г. Москва'), phone: String(values.phone || ''), roles: ['client'], status: 'active', customFields: values, }); setIsAddCompanyModalOpen(false); showToast('Компания успешно создана!', 'success'); }} onCancel={() => setIsAddCompanyModalOpen(false)} /> ) : (
Загрузка схемы...
)}
{/* ---------------------------------------------------- */} {/* NEW DEAL MODAL */} {/* ---------------------------------------------------- */} setIsAddDealModalOpen(false)} title="Создание новой сделки" size="lg" > {dealMeta.currentEntity ? ( { const amountMinor = typeof values.amount === 'number' ? values.amount : 5000000; await createDeal({ title: String(values.title || 'Новая сделка'), pipelineId: 'pipe_default', stage: 'prospecting', amount: { minor: amountMinor, currency: 'RUB' }, probability: 20, contactId: contacts[0]?.id, companyId: companies[0]?.id, customFields: values, }); setIsAddDealModalOpen(false); showToast('Сделка успешно создана в воронке!', 'success'); }} onCancel={() => setIsAddDealModalOpen(false)} /> ) : (
Загрузка схемы...
)}
); };