import React, { useState, useEffect, useCallback } from 'react'; import { DevPipelineStatus, DevPipelineItem } from '../../types'; import { Building2, Search, Filter, Plus, GripVertical } from 'lucide-react'; import { backendApi } from '../../services/apiClient'; import { readCache, saveCache } from '../../hooks/useCachedFetch'; import { REFRESH_EVENTS } from '../../constants/refreshEvents'; import { AddPipelineObjectModal } from './AddPipelineObjectModal'; import { EditPipelineObjectModal } from './EditPipelineObjectModal'; const STAGES: { id: DevPipelineStatus; label: string; color: string }[] = [ { id: 'incoming', label: 'Входящие', color: 'bg-slate-500' }, { id: 'analysis', label: 'Анализ', color: 'bg-sky-500' }, { id: 'agenda_approval', label: 'Согласование повестки', color: 'bg-blue-500' }, { id: 'in_person', label: 'Очная часть', color: 'bg-indigo-500' }, { id: 'absentee', label: 'Заочная часть', color: 'bg-violet-500' }, { id: 'protocol_formation', label: 'Формирование протокола', color: 'bg-amber-500' }, { id: 'protocol_to_gzhi', label: 'Отправка протокола в ГЖИ', color: 'bg-orange-500' }, { id: 'gzhi_order', label: 'Приказ ГЖИ', color: 'bg-rose-500' }, { id: 'success', label: 'Успех', color: 'bg-emerald-500' }, { id: 'failure', label: 'Провал', color: 'bg-red-500' }, ]; const DRAG_TYPE = 'application/x-pipeline-item'; const CACHE_KEY = 'mkd_dev_pipeline_cache'; /** Событие: перейти на воронку с поиском по адресу (из аудита) */ const PIPELINE_SEARCH_EVENT = 'mkd-pipeline-search-request'; export const PipelineRegistry: React.FC = () => { const cached = readCache(CACHE_KEY, []); const [pipeline, setPipeline] = useState(cached); const [loading, setLoading] = useState(cached.length === 0); const [search, setSearch] = useState(''); const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [editingItem, setEditingItem] = useState(null); const [draggingId, setDraggingId] = useState(null); const [dragOverStageId, setDragOverStageId] = useState(null); const [updatingStatusId, setUpdatingStatusId] = useState(null); const fetchPipeline = useCallback(async (showSpinner = true) => { try { if (showSpinner && cached.length === 0 && !search) setLoading(true); const data = await backendApi.getDevelopmentPipeline({ search: search || undefined }); setPipeline(data); if (!search) saveCache(CACHE_KEY, data); } catch (error) { console.error('Error fetching pipeline:', error); setPipeline([]); } finally { setLoading(false); } }, [search]); useEffect(() => { const delay = search ? 300 : 0; const t = setTimeout(() => fetchPipeline(), delay); return () => clearTimeout(t); }, [search, fetchPipeline]); useEffect(() => { const onRefresh = () => fetchPipeline(false); window.addEventListener(REFRESH_EVENTS.pipeline, onRefresh); return () => window.removeEventListener(REFRESH_EVENTS.pipeline, onRefresh); }, [fetchPipeline]); useEffect(() => { const onSearchRequest = (e: Event) => { const customEvent = e as CustomEvent<{ search?: string }>; const q = customEvent.detail?.search; if (typeof q === 'string' && q.trim()) setSearch(q.trim()); }; window.addEventListener(PIPELINE_SEARCH_EVENT, onSearchRequest); return () => window.removeEventListener(PIPELINE_SEARCH_EVENT, onSearchRequest); }, []); useEffect(() => { const interval = setInterval(() => fetchPipeline(false), 10 * 1000); return () => clearInterval(interval); }, [fetchPipeline]); const updateItemStatus = useCallback(async (itemId: string, newStatus: DevPipelineStatus) => { try { setUpdatingStatusId(itemId); await backendApi.updateDevelopmentPipeline(itemId, { status: newStatus }); window.dispatchEvent(new CustomEvent('mkd-pipeline-changed')); await fetchPipeline(); } catch (err) { console.error('Error updating status:', err); } finally { setUpdatingStatusId(null); } }, [fetchPipeline]); const handleDragStart = (e: React.DragEvent, item: DevPipelineItem) => { setDraggingId(item.id); e.dataTransfer.setData(DRAG_TYPE, JSON.stringify({ id: item.id, status: item.status })); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setDragImage(e.currentTarget, 0, 0); }; const handleDragEnd = () => { setDraggingId(null); setDragOverStageId(null); }; const handleDragOver = (e: React.DragEvent, stageId: DevPipelineStatus) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setDragOverStageId(stageId); }; const handleDragLeave = () => { setDragOverStageId(null); }; const handleDrop = (e: React.DragEvent, targetStageId: DevPipelineStatus) => { e.preventDefault(); setDragOverStageId(null); setDraggingId(null); const raw = e.dataTransfer.getData(DRAG_TYPE); if (!raw) return; try { const { id, status } = JSON.parse(raw) as { id: string; status: DevPipelineStatus }; if (status === targetStageId) return; updateItemStatus(id, targetStageId); } catch { // ignore } }; const handleCardClick = (e: React.MouseEvent, item: DevPipelineItem) => { if ((e.target as HTMLElement).closest('select') || (e.target as HTMLElement).closest('[data-drag-handle]')) return; setEditingItem(item); }; return (
{/* Toolbar */}
setSearch(e.target.value)} className="w-full pl-10 pr-4 py-2.5 rounded-xl border border-slate-200 text-sm outline-none focus:ring-2 focus:ring-primary-500 shadow-sm" />
{/* Kanban Horizontal View */}
{STAGES.map(stage => { const items = pipeline.filter(p => p.status === stage.id); return (

{stage.label}

{items.length}
handleDragOver(e, stage.id)} onDragLeave={handleDragLeave} onDrop={(e) => handleDrop(e, stage.id)} > {items.map(item => (
handleCardClick(e, item)} onKeyDown={(e) => e.key === 'Enter' && setEditingItem(item)} className={`bg-white p-4 rounded-xl border shadow-sm hover:shadow-md hover:border-primary-300 transition-all group cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-500 ${draggingId === item.id ? 'opacity-50 ring-2 ring-primary-400' : 'border-slate-200'}`} draggable onDragStart={(e) => handleDragStart(e, item)} onDragEnd={handleDragEnd} >
{item.type === 'new' ? 'Новостройка' : 'Вторичка'} {item.probability}%

{item.address}

{item.apartments} кв • {item.area} м²

{item.manager?.[0] ?? '?'}
{item.manager}

{((item.expectedRevenue ?? 0) / 1000).toFixed(0)}k ₽

Маржа

))} {items.length === 0 && !loading && (

Пусто

)} {loading && items.length === 0 && (

Загрузка...

)}
) })}
setIsAddModalOpen(false)} onSuccess={() => { fetchPipeline(); }} /> setEditingItem(null)} onSuccess={() => { setEditingItem(null); fetchPipeline(); }} />
); };