This commit is contained in:
@ -85,7 +85,15 @@ export function ColorPickerCell({ idea }: ColorPickerCellProps) {
|
||||
} as React.HTMLAttributes<HTMLDivElement>,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ p: 1, display: 'flex', gap: 0.5, flexWrap: 'wrap', maxWidth: 180 }}>
|
||||
<Box
|
||||
sx={{
|
||||
p: 1,
|
||||
display: 'flex',
|
||||
gap: 0.5,
|
||||
flexWrap: 'wrap',
|
||||
maxWidth: 180,
|
||||
}}
|
||||
>
|
||||
{COLORS.map((color) => (
|
||||
<IconButton
|
||||
key={color}
|
||||
|
||||
@ -80,7 +80,12 @@ export function DraggableRow({ row }: DraggableRowProps) {
|
||||
|
||||
return (
|
||||
<DragHandleContext.Provider value={{ attributes, listeners, isDragging }}>
|
||||
<TableRow ref={setNodeRef} hover sx={style} data-testid={`idea-row-${row.original.id}`}>
|
||||
<TableRow
|
||||
ref={setNodeRef}
|
||||
hover
|
||||
sx={style}
|
||||
data-testid={`idea-row-${row.original.id}`}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useMemo, useState, Fragment } from 'react';
|
||||
import { useMemo, useState, Fragment, useCallback } from 'react';
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
@ -79,34 +79,45 @@ export function IdeasTable() {
|
||||
// AI-оценка
|
||||
const [estimatingId, setEstimatingId] = useState<string | null>(null);
|
||||
const [estimateModalOpen, setEstimateModalOpen] = useState(false);
|
||||
const [estimateResult, setEstimateResult] = useState<EstimateResult | null>(null);
|
||||
const [estimateResult, setEstimateResult] = useState<EstimateResult | null>(
|
||||
null,
|
||||
);
|
||||
// ТЗ (спецификация)
|
||||
const [specificationModalOpen, setSpecificationModalOpen] = useState(false);
|
||||
const [specificationIdea, setSpecificationIdea] = useState<Idea | null>(null);
|
||||
const [generatedSpecification, setGeneratedSpecification] = useState<string | null>(null);
|
||||
const [generatingSpecificationId, setGeneratingSpecificationId] = useState<string | null>(null);
|
||||
const [generatedSpecification, setGeneratedSpecification] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [generatingSpecificationId, setGeneratingSpecificationId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
|
||||
// История ТЗ
|
||||
const specificationHistory = useSpecificationHistory(specificationIdea?.id ?? null);
|
||||
const specificationHistory = useSpecificationHistory(
|
||||
specificationIdea?.id ?? null,
|
||||
);
|
||||
|
||||
const handleToggleComments = (id: string) => {
|
||||
setExpandedId((prev) => (prev === id ? null : id));
|
||||
};
|
||||
|
||||
const handleEstimate = (id: string) => {
|
||||
setEstimatingId(id);
|
||||
setEstimateModalOpen(true);
|
||||
setEstimateResult(null);
|
||||
estimateIdea.mutate(id, {
|
||||
onSuccess: (result) => {
|
||||
setEstimateResult(result);
|
||||
setEstimatingId(null);
|
||||
},
|
||||
onError: () => {
|
||||
setEstimatingId(null);
|
||||
},
|
||||
});
|
||||
};
|
||||
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);
|
||||
@ -121,37 +132,40 @@ export function IdeasTable() {
|
||||
ideaId: idea.id,
|
||||
ideaTitle: idea.title,
|
||||
totalHours: idea.estimatedHours,
|
||||
complexity: idea.complexity!,
|
||||
complexity: idea.complexity ?? 'medium',
|
||||
breakdown: idea.estimateDetails.breakdown,
|
||||
recommendations: idea.estimateDetails.recommendations,
|
||||
estimatedAt: idea.estimatedAt!,
|
||||
estimatedAt: idea.estimatedAt ?? new Date().toISOString(),
|
||||
});
|
||||
setEstimateModalOpen(true);
|
||||
};
|
||||
|
||||
const handleSpecification = (idea: Idea) => {
|
||||
setSpecificationIdea(idea);
|
||||
setSpecificationModalOpen(true);
|
||||
const handleSpecification = useCallback(
|
||||
(idea: Idea) => {
|
||||
setSpecificationIdea(idea);
|
||||
setSpecificationModalOpen(true);
|
||||
|
||||
// Если ТЗ уже есть — показываем его
|
||||
if (idea.specification) {
|
||||
setGeneratedSpecification(idea.specification);
|
||||
return;
|
||||
}
|
||||
// Если ТЗ уже есть — показываем его
|
||||
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);
|
||||
},
|
||||
});
|
||||
};
|
||||
// Иначе генерируем
|
||||
setGeneratedSpecification(null);
|
||||
setGeneratingSpecificationId(idea.id);
|
||||
generateSpecification.mutate(idea.id, {
|
||||
onSuccess: (result) => {
|
||||
setGeneratedSpecification(result.specification);
|
||||
setGeneratingSpecificationId(null);
|
||||
},
|
||||
onError: () => {
|
||||
setGeneratingSpecificationId(null);
|
||||
},
|
||||
});
|
||||
},
|
||||
[generateSpecification],
|
||||
);
|
||||
|
||||
const handleCloseSpecificationModal = () => {
|
||||
setSpecificationModalOpen(false);
|
||||
@ -162,7 +176,7 @@ export function IdeasTable() {
|
||||
const handleSaveSpecification = (specification: string) => {
|
||||
if (!specificationIdea) return;
|
||||
updateIdea.mutate(
|
||||
{ id: specificationIdea.id, data: { specification } },
|
||||
{ id: specificationIdea.id, dto: { specification } },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setGeneratedSpecification(specification);
|
||||
@ -209,7 +223,14 @@ export function IdeasTable() {
|
||||
estimatingId,
|
||||
generatingSpecificationId,
|
||||
}),
|
||||
[deleteIdea, expandedId, estimatingId, generatingSpecificationId],
|
||||
[
|
||||
deleteIdea,
|
||||
expandedId,
|
||||
estimatingId,
|
||||
generatingSpecificationId,
|
||||
handleEstimate,
|
||||
handleSpecification,
|
||||
],
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-hooks/incompatible-library
|
||||
@ -291,7 +312,10 @@ export function IdeasTable() {
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Paper sx={{ width: '100%', overflow: 'hidden' }} data-testid="ideas-table-container">
|
||||
<Paper
|
||||
sx={{ width: '100%', overflow: 'hidden' }}
|
||||
data-testid="ideas-table-container"
|
||||
>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
@ -386,9 +410,17 @@ export function IdeasTable() {
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={SKELETON_COLUMNS_COUNT}
|
||||
sx={{ p: 0, borderBottom: expandedId === row.original.id ? 1 : 0, borderColor: 'divider' }}
|
||||
sx={{
|
||||
p: 0,
|
||||
borderBottom:
|
||||
expandedId === row.original.id ? 1 : 0,
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
<Collapse in={expandedId === row.original.id} unmountOnExit>
|
||||
<Collapse
|
||||
in={expandedId === row.original.id}
|
||||
unmountOnExit
|
||||
>
|
||||
<CommentsPanel ideaId={row.original.id} />
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
|
||||
@ -1,7 +1,26 @@
|
||||
import { createColumnHelper } from '@tanstack/react-table';
|
||||
import { Chip, Box, IconButton, Tooltip, CircularProgress, Typography } from '@mui/material';
|
||||
import { Delete, Comment, ExpandLess, AutoAwesome, AccessTime, Description } from '@mui/icons-material';
|
||||
import type { Idea, IdeaStatus, IdeaPriority, IdeaComplexity } from '../../types/idea';
|
||||
import {
|
||||
Chip,
|
||||
Box,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
CircularProgress,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Delete,
|
||||
Comment,
|
||||
ExpandLess,
|
||||
AutoAwesome,
|
||||
AccessTime,
|
||||
Description,
|
||||
} from '@mui/icons-material';
|
||||
import type {
|
||||
Idea,
|
||||
IdeaStatus,
|
||||
IdeaPriority,
|
||||
IdeaComplexity,
|
||||
} from '../../types/idea';
|
||||
import { EditableCell } from './EditableCell';
|
||||
import { ColorPickerCell } from './ColorPickerCell';
|
||||
import { statusOptions, priorityOptions } from './constants';
|
||||
@ -51,10 +70,10 @@ const complexityColors: Record<
|
||||
|
||||
function formatHoursShort(hours: number): string {
|
||||
if (hours < 8) {
|
||||
return `${hours}ч`;
|
||||
return `${String(hours)}ч`;
|
||||
}
|
||||
const days = Math.floor(hours / 8);
|
||||
return `${days}д`;
|
||||
return `${String(days)}д`;
|
||||
}
|
||||
|
||||
interface ColumnsConfig {
|
||||
@ -68,7 +87,16 @@ interface ColumnsConfig {
|
||||
generatingSpecificationId: string | null;
|
||||
}
|
||||
|
||||
export const createColumns = ({ onDelete, onToggleComments, onEstimate, onViewEstimate, onSpecification, expandedId, estimatingId, generatingSpecificationId }: ColumnsConfig) => [
|
||||
export const createColumns = ({
|
||||
onDelete,
|
||||
onToggleComments,
|
||||
onEstimate,
|
||||
onViewEstimate,
|
||||
onSpecification,
|
||||
expandedId,
|
||||
estimatingId,
|
||||
generatingSpecificationId,
|
||||
}: ColumnsConfig) => [
|
||||
columnHelper.display({
|
||||
id: 'drag',
|
||||
header: '',
|
||||
@ -246,7 +274,9 @@ export const createColumns = ({ onDelete, onToggleComments, onEstimate, onViewEs
|
||||
const hasSpecification = !!idea.specification;
|
||||
return (
|
||||
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
||||
<Tooltip title={hasSpecification ? 'Просмотр ТЗ' : 'Сгенерировать ТЗ'}>
|
||||
<Tooltip
|
||||
title={hasSpecification ? 'Просмотр ТЗ' : 'Сгенерировать ТЗ'}
|
||||
>
|
||||
<span>
|
||||
<IconButton
|
||||
size="small"
|
||||
@ -254,7 +284,10 @@ export const createColumns = ({ onDelete, onToggleComments, onEstimate, onViewEs
|
||||
disabled={isGeneratingSpec}
|
||||
color={hasSpecification ? 'primary' : 'default'}
|
||||
data-testid="specification-button"
|
||||
sx={{ opacity: hasSpecification ? 0.9 : 0.5, '&:hover': { opacity: 1 } }}
|
||||
sx={{
|
||||
opacity: hasSpecification ? 0.9 : 0.5,
|
||||
'&:hover': { opacity: 1 },
|
||||
}}
|
||||
>
|
||||
{isGeneratingSpec ? (
|
||||
<CircularProgress size={18} />
|
||||
@ -290,7 +323,11 @@ export const createColumns = ({ onDelete, onToggleComments, onEstimate, onViewEs
|
||||
data-testid="toggle-comments-button"
|
||||
sx={{ opacity: isExpanded ? 1 : 0.5, '&:hover': { opacity: 1 } }}
|
||||
>
|
||||
{isExpanded ? <ExpandLess fontSize="small" /> : <Comment fontSize="small" />}
|
||||
{isExpanded ? (
|
||||
<ExpandLess fontSize="small" />
|
||||
) : (
|
||||
<Comment fontSize="small" />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
|
||||
Reference in New Issue
Block a user