import { useMemo, useState, Fragment, useCallback } from 'react'; import { useReactTable, getCoreRowModel, flexRender, } from '@tanstack/react-table'; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragOverlay, } from '@dnd-kit/core'; import type { DragEndEvent, DragStartEvent } from '@dnd-kit/core'; import { SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, } from '@dnd-kit/sortable'; import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, TableSortLabel, Skeleton, Box, Typography, TablePagination, Collapse, } from '@mui/material'; import { Inbox } from '@mui/icons-material'; import { ColumnVisibility } from './ColumnVisibility'; import { useIdeasQuery, useDeleteIdea, useReorderIdeas, useUpdateIdea, } from '../../hooks/useIdeas'; import { useEstimateIdea, useGenerateSpecification, useSpecificationHistory, useDeleteSpecificationHistoryItem, useRestoreSpecificationFromHistory, } from '../../hooks/useAi'; import { useIdeasStore } from '../../store/ideas'; import { createColumns } from './columns'; import { DraggableRow } from './DraggableRow'; import { CommentsPanel } from '../CommentsPanel'; import { AiEstimateModal } from '../AiEstimateModal'; import { SpecificationModal } from '../SpecificationModal'; import { IdeaDetailModal } from '../IdeaDetailModal'; import type { EstimateResult } from '../../services/ai'; import type { Idea, UpdateIdeaDto } from '../../types/idea'; const SKELETON_COLUMNS_COUNT = 13; export function IdeasTable() { const { data, isLoading, isError } = useIdeasQuery(); const deleteIdea = useDeleteIdea(); const reorderIdeas = useReorderIdeas(); const updateIdea = useUpdateIdea(); const estimateIdea = useEstimateIdea(); const generateSpecification = useGenerateSpecification(); const deleteSpecificationHistoryItem = useDeleteSpecificationHistoryItem(); const restoreSpecificationFromHistory = useRestoreSpecificationFromHistory(); const { sorting, setSorting, pagination, setPage, setLimit } = useIdeasStore(); // ID активно перетаскиваемого элемента const [activeId, setActiveId] = useState(null); // ID идеи с раскрытыми комментариями const [expandedId, setExpandedId] = useState(null); // AI-оценка const [estimatingId, setEstimatingId] = useState(null); const [estimateModalOpen, setEstimateModalOpen] = useState(false); const [estimateResult, setEstimateResult] = useState( null, ); // ТЗ (спецификация) const [specificationModalOpen, setSpecificationModalOpen] = useState(false); const [specificationIdea, setSpecificationIdea] = useState(null); const [generatedSpecification, setGeneratedSpecification] = useState< string | null >(null); const [generatingSpecificationId, setGeneratingSpecificationId] = useState< string | null >(null); // Детальный просмотр идеи const [detailModalOpen, setDetailModalOpen] = useState(false); const [detailIdea, setDetailIdea] = useState(null); // История ТЗ const specificationHistory = useSpecificationHistory( specificationIdea?.id ?? null, ); const handleToggleComments = (id: string) => { setExpandedId((prev) => (prev === id ? null : id)); }; const handleEstimate = useCallback( (id: string) => { setEstimatingId(id); setEstimateModalOpen(true); setEstimateResult(null); estimateIdea.mutate(id, { onSuccess: (result) => { setEstimateResult(result); setEstimatingId(null); }, onError: () => { setEstimatingId(null); }, }); }, [estimateIdea], ); const handleCloseEstimateModal = () => { setEstimateModalOpen(false); setEstimateResult(null); }; const handleViewEstimate = (idea: Idea) => { if (!idea.estimatedHours || !idea.estimateDetails) return; // Показываем сохранённые результаты оценки setEstimateResult({ ideaId: idea.id, ideaTitle: idea.title, totalHours: idea.estimatedHours, complexity: idea.complexity ?? 'medium', breakdown: idea.estimateDetails.breakdown, recommendations: idea.estimateDetails.recommendations, estimatedAt: idea.estimatedAt ?? new Date().toISOString(), }); setEstimateModalOpen(true); }; const handleSpecification = useCallback( (idea: Idea) => { setSpecificationIdea(idea); setSpecificationModalOpen(true); // Если ТЗ уже есть — показываем его if (idea.specification) { setGeneratedSpecification(idea.specification); return; } // Иначе генерируем setGeneratedSpecification(null); setGeneratingSpecificationId(idea.id); generateSpecification.mutate(idea.id, { onSuccess: (result) => { setGeneratedSpecification(result.specification); setGeneratingSpecificationId(null); }, onError: () => { setGeneratingSpecificationId(null); }, }); }, [generateSpecification], ); const handleCloseSpecificationModal = () => { setSpecificationModalOpen(false); setSpecificationIdea(null); setGeneratedSpecification(null); }; const handleSaveSpecification = (specification: string) => { if (!specificationIdea) return; updateIdea.mutate( { id: specificationIdea.id, dto: { specification } }, { onSuccess: () => { setGeneratedSpecification(specification); }, }, ); }; const handleRegenerateSpecification = () => { if (!specificationIdea) return; setGeneratingSpecificationId(specificationIdea.id); generateSpecification.mutate(specificationIdea.id, { onSuccess: (result) => { setGeneratedSpecification(result.specification); setGeneratingSpecificationId(null); }, onError: () => { setGeneratingSpecificationId(null); }, }); }; const handleDeleteHistoryItem = (historyId: string) => { deleteSpecificationHistoryItem.mutate(historyId); }; const handleRestoreFromHistory = (historyId: string) => { restoreSpecificationFromHistory.mutate(historyId, { onSuccess: (result) => { setGeneratedSpecification(result.specification); }, }); }; const handleViewDetails = useCallback((idea: Idea) => { setDetailIdea(idea); setDetailModalOpen(true); }, []); const handleCloseDetailModal = () => { setDetailModalOpen(false); setDetailIdea(null); }; const handleSaveDetail = (id: string, dto: UpdateIdeaDto) => { updateIdea.mutate( { id, dto }, { onSuccess: () => { // Обновляем только те поля, которые были отправлены в dto // Это сохраняет specification и другие поля которые не редактировались setDetailIdea((prev) => { if (!prev) return prev; const updates: Partial = {}; (Object.keys(dto) as (keyof UpdateIdeaDto)[]).forEach((key) => { if (dto[key] !== undefined) { (updates as Record)[key] = dto[key]; } }); return { ...prev, ...updates }; }); }, }, ); }; const handleOpenSpecificationFromDetail = (idea: Idea) => { handleCloseDetailModal(); handleSpecification(idea); }; const handleOpenEstimateFromDetail = (idea: Idea) => { handleCloseDetailModal(); if (idea.estimatedHours) { handleViewEstimate(idea); } else { handleEstimate(idea.id); } }; const columns = useMemo( () => createColumns({ onDelete: (id) => deleteIdea.mutate(id), onToggleComments: handleToggleComments, onEstimate: handleEstimate, onViewEstimate: handleViewEstimate, onSpecification: handleSpecification, onViewDetails: handleViewDetails, expandedId, estimatingId, generatingSpecificationId, }), [ deleteIdea, expandedId, estimatingId, generatingSpecificationId, handleEstimate, handleSpecification, handleViewDetails, ], ); // eslint-disable-next-line react-hooks/incompatible-library const table = useReactTable({ data: data?.data ?? [], columns, getCoreRowModel: getCoreRowModel(), manualSorting: true, manualPagination: true, getRowId: (row) => row.id, }); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8, }, }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ); const handleDragStart = (event: DragStartEvent) => { setActiveId(event.active.id as string); }; const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; setActiveId(null); if (over && active.id !== over.id && data?.data) { const oldIndex = data.data.findIndex((item) => item.id === active.id); const newIndex = data.data.findIndex((item) => item.id === over.id); if (oldIndex !== -1 && newIndex !== -1) { // Создаём новый порядок const items = [...data.data]; const [movedItem] = items.splice(oldIndex, 1); items.splice(newIndex, 0, movedItem); // Отправляем на сервер новый порядок const reorderItems = items.map((item, index) => ({ id: item.id, order: index, })); reorderIdeas.mutate(reorderItems); } } }; const handleSort = (columnId: string) => { setSorting(columnId); }; const handleChangePage = (_: unknown, newPage: number) => { setPage(newPage + 1); }; const handleChangeRowsPerPage = ( event: React.ChangeEvent, ) => { setLimit(parseInt(event.target.value, 10)); }; if (isError) { return ( Не удалось загрузить идеи ); } const rows = table.getRowModel().rows; const rowIds = rows.map((row) => row.original.id); const activeRow = activeId ? rows.find((row) => row.original.id === activeId) : null; return ( {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( {header.column.getCanSort() ? ( handleSort(header.id)} > {flexRender( header.column.columnDef.header, header.getContext(), )} ) : ( flexRender( header.column.columnDef.header, header.getContext(), ) )} ))} ))} {isLoading ? ( Array.from({ length: 5 }).map((_, index) => ( {Array.from({ length: SKELETON_COLUMNS_COUNT }).map( (_, colIndex) => ( ), )} )) ) : rows.length === 0 ? ( Идей пока нет Создайте первую идею, чтобы начать ) : ( {rows.map((row) => ( ))} )}
{activeRow ? ( {activeRow.getVisibleCells().map((cell) => ( {flexRender( cell.column.columnDef.cell, cell.getContext(), )} ))}
) : null}
{data && ( )}
); }