add view any columns and view mode for ideas
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
22
CONTEXT.md
22
CONTEXT.md
@ -6,9 +6,9 @@
|
|||||||
|
|
||||||
## Текущий статус
|
## Текущий статус
|
||||||
|
|
||||||
**Этап:** Фаза 3.1 завершена ✅ | Новые требования (Фазы 4-8) запланированы 📋
|
**Этап:** Фаза 3.1 завершена ✅ | Фаза 3.2 запланирована 📋
|
||||||
**Фаза MVP:** Базовый функционал + авторизация + расширенный функционал + AI-оценка + мини-ТЗ + история ТЗ готовы
|
**Фаза MVP:** Базовый функционал + авторизация + расширенный функционал + AI-оценка + мини-ТЗ + история ТЗ готовы
|
||||||
**Следующий этап:** Фаза 4 — Права доступа
|
**Следующий этап:** Фаза 3.2 — Полный просмотр идеи (все поля)
|
||||||
**Последнее обновление:** 2026-01-15
|
**Последнее обновление:** 2026-01-15
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -81,6 +81,7 @@
|
|||||||
| 2026-01-15 | **Фаза 3.1:** AI-промпты (ТЗ и оценка) теперь учитывают комментарии к идее |
|
| 2026-01-15 | **Фаза 3.1:** AI-промпты (ТЗ и оценка) теперь учитывают комментарии к идее |
|
||||||
| 2026-01-15 | **Планирование:** Добавлены новые требования — права доступа, аудит, WebSocket, темная тема, экспорт |
|
| 2026-01-15 | **Планирование:** Добавлены новые требования — права доступа, аудит, WebSocket, темная тема, экспорт |
|
||||||
| 2026-01-15 | **Документация:** Обновлены REQUIREMENTS.md, ARCHITECTURE.md, ROADMAP.md — добавлены Фазы 4-8 |
|
| 2026-01-15 | **Документация:** Обновлены REQUIREMENTS.md, ARCHITECTURE.md, ROADMAP.md — добавлены Фазы 4-8 |
|
||||||
|
| 2026-01-15 | **Планирование:** Добавлена Фаза 3.2 — Полный просмотр идеи (все поля доступны для просмотра и редактирования) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -89,7 +90,22 @@
|
|||||||
> Смотри [ROADMAP.md](ROADMAP.md) для полного плана разработки
|
> Смотри [ROADMAP.md](ROADMAP.md) для полного плана разработки
|
||||||
|
|
||||||
**Готово:** Фазы 0-3.1 завершены ✅
|
**Готово:** Фазы 0-3.1 завершены ✅
|
||||||
**Следующий шаг:** Фаза 4 — Права доступа 📋
|
**Следующий шаг:** Фаза 3.2 — Полный просмотр идеи 📋
|
||||||
|
|
||||||
|
### Фаза 3.2: Полный просмотр идеи
|
||||||
|
|
||||||
|
**Колонки в таблице:**
|
||||||
|
- [ ] Колонки pain, aiRole, verificationMethod
|
||||||
|
- [ ] Column visibility (скрытие/показ колонок, localStorage)
|
||||||
|
|
||||||
|
**Модалка IdeaDetailModal:**
|
||||||
|
- [ ] Режим просмотра (readonly по умолчанию)
|
||||||
|
- [ ] Режим редактирования (кнопка "Редактировать")
|
||||||
|
- [ ] Кнопки "Сохранить" / "Отмена"
|
||||||
|
- [ ] Быстрый доступ к ТЗ и AI-оценке
|
||||||
|
|
||||||
|
**E2E тесты:**
|
||||||
|
- [ ] Column visibility, модалка, редактирование, сохранение
|
||||||
|
|
||||||
### Новые требования (Фазы 4-8):
|
### Новые требования (Фазы 4-8):
|
||||||
|
|
||||||
|
|||||||
@ -33,9 +33,17 @@
|
|||||||
- Отображение автора в таблице и детальном просмотре
|
- Отображение автора в таблице и детальном просмотре
|
||||||
|
|
||||||
#### 1.3 Редактирование идей
|
#### 1.3 Редактирование идей
|
||||||
|
- **Полный просмотр**: пользователь может просмотреть ВСЕ поля идеи (включая pain, aiRole, verificationMethod)
|
||||||
|
- **Полное редактирование**: пользователь может отредактировать ВСЕ редактируемые поля идеи
|
||||||
- **Inline-редактирование**: возможность редактировать любое поле прямо в таблице двойным кликом
|
- **Inline-редактирование**: возможность редактировать любое поле прямо в таблице двойным кликом
|
||||||
|
- **Детальный просмотр**: модалка с полной информацией об идее
|
||||||
|
- Открывается в **режиме просмотра** (readonly)
|
||||||
|
- Кнопка "Редактировать" переводит в **режим редактирования**
|
||||||
|
- Кнопка "Сохранить" сохраняет изменения
|
||||||
|
- Кнопка "Отмена" отменяет изменения
|
||||||
|
- **Column visibility**: возможность скрыть/показать колонки таблицы
|
||||||
- **Быстрое изменение статуса и приоритета** через dropdown
|
- **Быстрое изменение статуса и приоритета** через dropdown
|
||||||
- **Автосохранение** изменений
|
- **Автосохранение** изменений (для inline-редактирования)
|
||||||
|
|
||||||
#### 1.4 Drag & Drop
|
#### 1.4 Drag & Drop
|
||||||
- Перемещение идей в списке для ручной сортировки
|
- Перемещение идей в списке для ручной сортировки
|
||||||
|
|||||||
45
ROADMAP.md
45
ROADMAP.md
@ -15,6 +15,7 @@
|
|||||||
| 2 | Расширенный функционал | ✅ Завершена | Drag&Drop, цвета, комментарии, команда |
|
| 2 | Расширенный функционал | ✅ Завершена | Drag&Drop, цвета, комментарии, команда |
|
||||||
| 3 | AI-интеграция | ✅ Завершена | Оценка времени, рекомендации |
|
| 3 | AI-интеграция | ✅ Завершена | Оценка времени, рекомендации |
|
||||||
| 3.1 | Генерация мини-ТЗ | ✅ Завершена | Генерация, редактирование, история ТЗ |
|
| 3.1 | Генерация мини-ТЗ | ✅ Завершена | Генерация, редактирование, история ТЗ |
|
||||||
|
| 3.2 | Полный просмотр идеи | 📋 Планируется | Просмотр и редактирование всех полей |
|
||||||
| 4 | Права доступа | 📋 Планируется | Гранулярные права, панель админа |
|
| 4 | Права доступа | 📋 Планируется | Гранулярные права, панель админа |
|
||||||
| 5 | Аудит и история | 📋 Планируется | Логирование действий, восстановление |
|
| 5 | Аудит и история | 📋 Планируется | Логирование действий, восстановление |
|
||||||
| 6 | Real-time и WebSocket | 📋 Планируется | Многопользовательская работа |
|
| 6 | Real-time и WebSocket | 📋 Планируется | Многопользовательская работа |
|
||||||
@ -247,6 +248,50 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Фаза 3.2: Полный просмотр идеи 📋
|
||||||
|
|
||||||
|
> **Просмотр и редактирование ВСЕХ полей идеи**
|
||||||
|
|
||||||
|
### Проблема
|
||||||
|
Сейчас в таблице отображаются не все поля идеи. Поля `pain`, `aiRole`, `verificationMethod` невозможно ни посмотреть, ни отредактировать.
|
||||||
|
|
||||||
|
### Frontend — Дополнительные колонки в таблице
|
||||||
|
- [ ] Добавить колонку "Боль" (pain) с inline-редактированием
|
||||||
|
- [ ] Добавить колонку "Роль AI" (aiRole) с inline-редактированием
|
||||||
|
- [ ] Добавить колонку "Способ проверки" (verificationMethod) с inline-редактированием
|
||||||
|
- [ ] Column visibility — возможность скрыть/показать колонки
|
||||||
|
- [ ] Кнопка настройки колонок (⚙️) в header таблицы
|
||||||
|
- [ ] Dropdown с чекбоксами для каждой колонки
|
||||||
|
- [ ] Сохранение настроек в localStorage
|
||||||
|
- [ ] data-testid для новых колонок
|
||||||
|
|
||||||
|
### Frontend — Модалка детального просмотра
|
||||||
|
- [ ] IdeaDetailModal компонент
|
||||||
|
- [ ] Открытие по кнопке "Подробнее" (👁️ Visibility icon)
|
||||||
|
- [ ] **Режим просмотра** (по умолчанию):
|
||||||
|
- [ ] Все поля отображаются как readonly текст
|
||||||
|
- [ ] Кнопка "Редактировать" для перехода в режим редактирования
|
||||||
|
- [ ] **Режим редактирования**:
|
||||||
|
- [ ] Все редактируемые поля становятся input/textarea/select
|
||||||
|
- [ ] Кнопка "Сохранить" — сохраняет изменения и возвращает в режим просмотра
|
||||||
|
- [ ] Кнопка "Отмена" — отменяет изменения и возвращает в режим просмотра
|
||||||
|
- [ ] Поля для редактирования: title, description, status, priority, module, targetAudience, pain, aiRole, verificationMethod
|
||||||
|
- [ ] Readonly поля (только просмотр): estimatedHours, complexity, createdAt, updatedAt
|
||||||
|
- [ ] Быстрый доступ: кнопки "Открыть ТЗ" и "AI-оценка"
|
||||||
|
- [ ] Кнопка "Подробнее" в колонке actions
|
||||||
|
- [ ] data-testid для всех элементов модалки
|
||||||
|
|
||||||
|
### E2E тестирование
|
||||||
|
- [ ] Column visibility — скрытие/показ колонок
|
||||||
|
- [ ] Открытие модалки детального просмотра
|
||||||
|
- [ ] Просмотр всех полей в режиме readonly
|
||||||
|
- [ ] Переход в режим редактирования
|
||||||
|
- [ ] Редактирование полей pain, aiRole, verificationMethod
|
||||||
|
- [ ] Сохранение изменений
|
||||||
|
- [ ] Отмена редактирования
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Фаза 4: Права доступа 📋
|
## Фаза 4: Права доступа 📋
|
||||||
|
|
||||||
> **Гранулярная система прав доступа и панель администратора**
|
> **Гранулярная система прав доступа и панель администратора**
|
||||||
|
|||||||
491
frontend/src/components/IdeaDetailModal/IdeaDetailModal.tsx
Normal file
491
frontend/src/components/IdeaDetailModal/IdeaDetailModal.tsx
Normal file
@ -0,0 +1,491 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Button,
|
||||||
|
TextField,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
FormControl,
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Chip,
|
||||||
|
Divider,
|
||||||
|
Grid,
|
||||||
|
IconButton,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { Close, Edit, Description, AutoAwesome } from '@mui/icons-material';
|
||||||
|
import type {
|
||||||
|
Idea,
|
||||||
|
IdeaStatus,
|
||||||
|
IdeaPriority,
|
||||||
|
UpdateIdeaDto,
|
||||||
|
} from '../../types/idea';
|
||||||
|
import { statusOptions, priorityOptions } from '../IdeasTable/constants';
|
||||||
|
|
||||||
|
interface IdeaDetailModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
idea: Idea | null;
|
||||||
|
onSave: (id: string, dto: UpdateIdeaDto) => void;
|
||||||
|
isSaving: boolean;
|
||||||
|
onOpenSpecification: (idea: Idea) => void;
|
||||||
|
onOpenEstimate: (idea: Idea) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusColors: Record<
|
||||||
|
IdeaStatus,
|
||||||
|
'default' | 'primary' | 'secondary' | 'success' | 'error'
|
||||||
|
> = {
|
||||||
|
backlog: 'default',
|
||||||
|
todo: 'primary',
|
||||||
|
in_progress: 'secondary',
|
||||||
|
done: 'success',
|
||||||
|
cancelled: 'error',
|
||||||
|
};
|
||||||
|
|
||||||
|
const priorityColors: Record<
|
||||||
|
IdeaPriority,
|
||||||
|
'default' | 'info' | 'warning' | 'error'
|
||||||
|
> = {
|
||||||
|
low: 'default',
|
||||||
|
medium: 'info',
|
||||||
|
high: 'warning',
|
||||||
|
critical: 'error',
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatDate(dateString: string | null): string {
|
||||||
|
if (!dateString) return '—';
|
||||||
|
return new Date(dateString).toLocaleString('ru-RU');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatHours(hours: number | null): string {
|
||||||
|
if (!hours) return '—';
|
||||||
|
if (hours < 8) return `${String(hours)} ч`;
|
||||||
|
const days = Math.floor(hours / 8);
|
||||||
|
const remainingHours = hours % 8;
|
||||||
|
if (remainingHours === 0) return `${String(days)} д`;
|
||||||
|
return `${String(days)} д ${String(remainingHours)} ч`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IdeaDetailModal({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
idea,
|
||||||
|
onSave,
|
||||||
|
isSaving,
|
||||||
|
onOpenSpecification,
|
||||||
|
onOpenEstimate,
|
||||||
|
}: IdeaDetailModalProps) {
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [formData, setFormData] = useState<UpdateIdeaDto>({});
|
||||||
|
|
||||||
|
// Сброс при открытии/закрытии или смене идеи
|
||||||
|
useEffect(() => {
|
||||||
|
if (idea) {
|
||||||
|
setFormData({
|
||||||
|
title: idea.title,
|
||||||
|
description: idea.description ?? '',
|
||||||
|
status: idea.status,
|
||||||
|
priority: idea.priority,
|
||||||
|
module: idea.module ?? '',
|
||||||
|
targetAudience: idea.targetAudience ?? '',
|
||||||
|
pain: idea.pain ?? '',
|
||||||
|
aiRole: idea.aiRole ?? '',
|
||||||
|
verificationMethod: idea.verificationMethod ?? '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setIsEditing(false);
|
||||||
|
}, [idea, open]);
|
||||||
|
|
||||||
|
if (!idea) return null;
|
||||||
|
|
||||||
|
const handleChange = (field: keyof UpdateIdeaDto, value: string) => {
|
||||||
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
onSave(idea.id, formData);
|
||||||
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setFormData({
|
||||||
|
title: idea.title,
|
||||||
|
description: idea.description ?? '',
|
||||||
|
status: idea.status,
|
||||||
|
priority: idea.priority,
|
||||||
|
module: idea.module ?? '',
|
||||||
|
targetAudience: idea.targetAudience ?? '',
|
||||||
|
pain: idea.pain ?? '',
|
||||||
|
aiRole: idea.aiRole ?? '',
|
||||||
|
verificationMethod: idea.verificationMethod ?? '',
|
||||||
|
});
|
||||||
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setIsEditing(false);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusLabel =
|
||||||
|
statusOptions.find((s) => s.value === idea.status)?.label ?? idea.status;
|
||||||
|
const priorityLabel =
|
||||||
|
priorityOptions.find((p) => p.value === idea.priority)?.label ??
|
||||||
|
idea.priority;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
maxWidth="md"
|
||||||
|
fullWidth
|
||||||
|
data-testid="idea-detail-modal"
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<Box sx={{ flex: 1 }}>
|
||||||
|
{isEditing ? (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
value={formData.title ?? ''}
|
||||||
|
onChange={(e) => handleChange('title', e.target.value)}
|
||||||
|
variant="standard"
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: { 'data-testid': 'idea-detail-title-input' },
|
||||||
|
}}
|
||||||
|
sx={{ '& input': { fontSize: '1.25rem', fontWeight: 500 } }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Typography variant="h6" data-testid="idea-detail-title">
|
||||||
|
{idea.title}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<IconButton onClick={handleClose} size="small">
|
||||||
|
<Close />
|
||||||
|
</IconButton>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
{/* Статус и приоритет */}
|
||||||
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||||
|
Статус
|
||||||
|
</Typography>
|
||||||
|
{isEditing ? (
|
||||||
|
<FormControl fullWidth size="small">
|
||||||
|
<Select
|
||||||
|
value={formData.status ?? idea.status}
|
||||||
|
onChange={(e) => handleChange('status', e.target.value)}
|
||||||
|
data-testid="idea-detail-status-select"
|
||||||
|
>
|
||||||
|
{statusOptions.map((opt) => (
|
||||||
|
<MenuItem key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
) : (
|
||||||
|
<Chip
|
||||||
|
label={statusLabel}
|
||||||
|
color={statusColors[idea.status]}
|
||||||
|
size="small"
|
||||||
|
data-testid="idea-detail-status"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||||
|
Приоритет
|
||||||
|
</Typography>
|
||||||
|
{isEditing ? (
|
||||||
|
<FormControl fullWidth size="small">
|
||||||
|
<Select
|
||||||
|
value={formData.priority ?? idea.priority}
|
||||||
|
onChange={(e) => handleChange('priority', e.target.value)}
|
||||||
|
data-testid="idea-detail-priority-select"
|
||||||
|
>
|
||||||
|
{priorityOptions.map((opt) => (
|
||||||
|
<MenuItem key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
) : (
|
||||||
|
<Chip
|
||||||
|
label={priorityLabel}
|
||||||
|
color={priorityColors[idea.priority]}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
data-testid="idea-detail-priority"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Модуль */}
|
||||||
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||||
|
Модуль
|
||||||
|
</Typography>
|
||||||
|
{isEditing ? (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
value={formData.module ?? ''}
|
||||||
|
onChange={(e) => handleChange('module', e.target.value)}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: { 'data-testid': 'idea-detail-module-input' },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Typography data-testid="idea-detail-module">
|
||||||
|
{idea.module ?? '—'}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Целевая аудитория */}
|
||||||
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||||
|
Целевая аудитория
|
||||||
|
</Typography>
|
||||||
|
{isEditing ? (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
value={formData.targetAudience ?? ''}
|
||||||
|
onChange={(e) => handleChange('targetAudience', e.target.value)}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: {
|
||||||
|
'data-testid': 'idea-detail-target-audience-input',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Typography data-testid="idea-detail-target-audience">
|
||||||
|
{idea.targetAudience ?? '—'}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Описание */}
|
||||||
|
<Grid size={12}>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||||
|
Описание
|
||||||
|
</Typography>
|
||||||
|
{isEditing ? (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={3}
|
||||||
|
value={formData.description ?? ''}
|
||||||
|
onChange={(e) => handleChange('description', e.target.value)}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: { 'data-testid': 'idea-detail-description-input' },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Typography
|
||||||
|
data-testid="idea-detail-description"
|
||||||
|
sx={{ whiteSpace: 'pre-wrap' }}
|
||||||
|
>
|
||||||
|
{idea.description ?? '—'}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Боль */}
|
||||||
|
<Grid size={12}>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||||
|
Какую боль решает
|
||||||
|
</Typography>
|
||||||
|
{isEditing ? (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={2}
|
||||||
|
value={formData.pain ?? ''}
|
||||||
|
onChange={(e) => handleChange('pain', e.target.value)}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: { 'data-testid': 'idea-detail-pain-input' },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Typography
|
||||||
|
data-testid="idea-detail-pain"
|
||||||
|
sx={{ whiteSpace: 'pre-wrap' }}
|
||||||
|
>
|
||||||
|
{idea.pain ?? '—'}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Роль AI */}
|
||||||
|
<Grid size={12}>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||||
|
Роль AI
|
||||||
|
</Typography>
|
||||||
|
{isEditing ? (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={2}
|
||||||
|
value={formData.aiRole ?? ''}
|
||||||
|
onChange={(e) => handleChange('aiRole', e.target.value)}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: { 'data-testid': 'idea-detail-ai-role-input' },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Typography
|
||||||
|
data-testid="idea-detail-ai-role"
|
||||||
|
sx={{ whiteSpace: 'pre-wrap' }}
|
||||||
|
>
|
||||||
|
{idea.aiRole ?? '—'}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Способ проверки */}
|
||||||
|
<Grid size={12}>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||||
|
Способ проверки
|
||||||
|
</Typography>
|
||||||
|
{isEditing ? (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={2}
|
||||||
|
value={formData.verificationMethod ?? ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleChange('verificationMethod', e.target.value)
|
||||||
|
}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: {
|
||||||
|
'data-testid': 'idea-detail-verification-method-input',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Typography
|
||||||
|
data-testid="idea-detail-verification-method"
|
||||||
|
sx={{ whiteSpace: 'pre-wrap' }}
|
||||||
|
>
|
||||||
|
{idea.verificationMethod ?? '—'}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={12}>
|
||||||
|
<Divider sx={{ my: 1 }} />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* AI-оценка и ТЗ */}
|
||||||
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||||
|
AI-оценка
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<Typography data-testid="idea-detail-estimate">
|
||||||
|
{formatHours(idea.estimatedHours)}
|
||||||
|
</Typography>
|
||||||
|
{idea.complexity && (
|
||||||
|
<Chip label={idea.complexity} size="small" variant="outlined" />
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
startIcon={<AutoAwesome />}
|
||||||
|
onClick={() => onOpenEstimate(idea)}
|
||||||
|
data-testid="idea-detail-open-estimate-button"
|
||||||
|
>
|
||||||
|
{idea.estimatedHours ? 'Подробнее' : 'Оценить'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||||
|
Техническое задание
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<Typography data-testid="idea-detail-specification-status">
|
||||||
|
{idea.specification ? 'Есть' : 'Нет'}
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
startIcon={<Description />}
|
||||||
|
onClick={() => onOpenSpecification(idea)}
|
||||||
|
data-testid="idea-detail-open-specification-button"
|
||||||
|
>
|
||||||
|
{idea.specification ? 'Открыть' : 'Создать'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={12}>
|
||||||
|
<Divider sx={{ my: 1 }} />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Метаданные */}
|
||||||
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||||
|
Создано
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" data-testid="idea-detail-created-at">
|
||||||
|
{formatDate(idea.createdAt)}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ xs: 12, sm: 6 }}>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||||
|
Обновлено
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" data-testid="idea-detail-updated-at">
|
||||||
|
{formatDate(idea.updatedAt)}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions sx={{ px: 3, py: 2 }}>
|
||||||
|
{isEditing ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
onClick={handleCancel}
|
||||||
|
data-testid="idea-detail-cancel-button"
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={isSaving}
|
||||||
|
data-testid="idea-detail-save-button"
|
||||||
|
>
|
||||||
|
{isSaving ? 'Сохранение...' : 'Сохранить'}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button onClick={handleClose}>Закрыть</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<Edit />}
|
||||||
|
onClick={() => setIsEditing(true)}
|
||||||
|
data-testid="idea-detail-edit-button"
|
||||||
|
>
|
||||||
|
Редактировать
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
frontend/src/components/IdeaDetailModal/index.ts
Normal file
1
frontend/src/components/IdeaDetailModal/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { IdeaDetailModal } from './IdeaDetailModal';
|
||||||
157
frontend/src/components/IdeasTable/ColumnVisibility.tsx
Normal file
157
frontend/src/components/IdeasTable/ColumnVisibility.tsx
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
IconButton,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Checkbox,
|
||||||
|
ListItemText,
|
||||||
|
Tooltip,
|
||||||
|
Divider,
|
||||||
|
Typography,
|
||||||
|
Box,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { Settings } from '@mui/icons-material';
|
||||||
|
import type { Table } from '@tanstack/react-table';
|
||||||
|
import type { Idea } from '../../types/idea';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'team-planner-column-visibility';
|
||||||
|
|
||||||
|
const columnLabels: Record<string, string> = {
|
||||||
|
drag: 'Перетаскивание',
|
||||||
|
color: 'Цвет',
|
||||||
|
title: 'Название',
|
||||||
|
status: 'Статус',
|
||||||
|
priority: 'Приоритет',
|
||||||
|
module: 'Модуль',
|
||||||
|
targetAudience: 'Целевая аудитория',
|
||||||
|
pain: 'Боль',
|
||||||
|
aiRole: 'Роль AI',
|
||||||
|
verificationMethod: 'Способ проверки',
|
||||||
|
description: 'Описание',
|
||||||
|
estimatedHours: 'Оценка',
|
||||||
|
actions: 'Действия',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Колонки, которые нельзя скрыть
|
||||||
|
const alwaysVisibleColumns = ['drag', 'title', 'actions'];
|
||||||
|
|
||||||
|
interface ColumnVisibilityProps {
|
||||||
|
table: Table<Idea>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ColumnVisibility({ table }: ColumnVisibilityProps) {
|
||||||
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
const open = Boolean(anchorEl);
|
||||||
|
|
||||||
|
// Загрузка сохранённых настроек при монтировании
|
||||||
|
useEffect(() => {
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
const visibility = JSON.parse(saved) as Record<string, boolean>;
|
||||||
|
table.setColumnVisibility(visibility);
|
||||||
|
} catch {
|
||||||
|
// Игнорируем ошибки парсинга
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [table]);
|
||||||
|
|
||||||
|
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggle = (columnId: string) => {
|
||||||
|
const column = table.getColumn(columnId);
|
||||||
|
if (!column) return;
|
||||||
|
|
||||||
|
const newVisibility = !column.getIsVisible();
|
||||||
|
column.toggleVisibility(newVisibility);
|
||||||
|
|
||||||
|
// Сохраняем в localStorage
|
||||||
|
const currentVisibility = table.getState().columnVisibility;
|
||||||
|
const updatedVisibility = {
|
||||||
|
...currentVisibility,
|
||||||
|
[columnId]: newVisibility,
|
||||||
|
};
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedVisibility));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShowAll = () => {
|
||||||
|
const allVisible: Record<string, boolean> = {};
|
||||||
|
table.getAllColumns().forEach((col) => {
|
||||||
|
allVisible[col.id] = true;
|
||||||
|
});
|
||||||
|
table.setColumnVisibility(allVisible);
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(allVisible));
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = table.getAllColumns().filter((col) => col.id in columnLabels);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip title="Настройка колонок">
|
||||||
|
<IconButton
|
||||||
|
onClick={handleClick}
|
||||||
|
size="small"
|
||||||
|
data-testid="column-visibility-button"
|
||||||
|
sx={{ ml: 1 }}
|
||||||
|
>
|
||||||
|
<Settings />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Menu
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
data-testid="column-visibility-menu"
|
||||||
|
slotProps={{
|
||||||
|
paper: {
|
||||||
|
sx: { minWidth: 220 },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ px: 2, py: 1 }}>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary">
|
||||||
|
Показать колонки
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
{columns.map((column) => {
|
||||||
|
const isAlwaysVisible = alwaysVisibleColumns.includes(column.id);
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
key={column.id}
|
||||||
|
onClick={() => !isAlwaysVisible && handleToggle(column.id)}
|
||||||
|
disabled={isAlwaysVisible}
|
||||||
|
dense
|
||||||
|
data-testid={`column-visibility-item-${column.id}`}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={column.getIsVisible()}
|
||||||
|
disabled={isAlwaysVisible}
|
||||||
|
size="small"
|
||||||
|
sx={{ p: 0, mr: 1 }}
|
||||||
|
/>
|
||||||
|
<ListItemText primary={columnLabels[column.id]} />
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Divider />
|
||||||
|
<MenuItem
|
||||||
|
onClick={handleShowAll}
|
||||||
|
dense
|
||||||
|
data-testid="column-visibility-show-all"
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary="Показать все"
|
||||||
|
slotProps={{ primary: { color: 'primary' } }}
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -36,6 +36,7 @@ import {
|
|||||||
Collapse,
|
Collapse,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Inbox } from '@mui/icons-material';
|
import { Inbox } from '@mui/icons-material';
|
||||||
|
import { ColumnVisibility } from './ColumnVisibility';
|
||||||
import {
|
import {
|
||||||
useIdeasQuery,
|
useIdeasQuery,
|
||||||
useDeleteIdea,
|
useDeleteIdea,
|
||||||
@ -55,10 +56,11 @@ import { DraggableRow } from './DraggableRow';
|
|||||||
import { CommentsPanel } from '../CommentsPanel';
|
import { CommentsPanel } from '../CommentsPanel';
|
||||||
import { AiEstimateModal } from '../AiEstimateModal';
|
import { AiEstimateModal } from '../AiEstimateModal';
|
||||||
import { SpecificationModal } from '../SpecificationModal';
|
import { SpecificationModal } from '../SpecificationModal';
|
||||||
|
import { IdeaDetailModal } from '../IdeaDetailModal';
|
||||||
import type { EstimateResult } from '../../services/ai';
|
import type { EstimateResult } from '../../services/ai';
|
||||||
import type { Idea } from '../../types/idea';
|
import type { Idea, UpdateIdeaDto } from '../../types/idea';
|
||||||
|
|
||||||
const SKELETON_COLUMNS_COUNT = 10;
|
const SKELETON_COLUMNS_COUNT = 13;
|
||||||
|
|
||||||
export function IdeasTable() {
|
export function IdeasTable() {
|
||||||
const { data, isLoading, isError } = useIdeasQuery();
|
const { data, isLoading, isError } = useIdeasQuery();
|
||||||
@ -91,6 +93,9 @@ export function IdeasTable() {
|
|||||||
const [generatingSpecificationId, setGeneratingSpecificationId] = useState<
|
const [generatingSpecificationId, setGeneratingSpecificationId] = useState<
|
||||||
string | null
|
string | null
|
||||||
>(null);
|
>(null);
|
||||||
|
// Детальный просмотр идеи
|
||||||
|
const [detailModalOpen, setDetailModalOpen] = useState(false);
|
||||||
|
const [detailIdea, setDetailIdea] = useState<Idea | null>(null);
|
||||||
|
|
||||||
// История ТЗ
|
// История ТЗ
|
||||||
const specificationHistory = useSpecificationHistory(
|
const specificationHistory = useSpecificationHistory(
|
||||||
@ -211,6 +216,41 @@ export function IdeasTable() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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: (updatedIdea) => {
|
||||||
|
setDetailIdea(updatedIdea);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenSpecificationFromDetail = (idea: Idea) => {
|
||||||
|
handleCloseDetailModal();
|
||||||
|
handleSpecification(idea);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenEstimateFromDetail = (idea: Idea) => {
|
||||||
|
handleCloseDetailModal();
|
||||||
|
if (idea.estimatedHours) {
|
||||||
|
handleViewEstimate(idea);
|
||||||
|
} else {
|
||||||
|
handleEstimate(idea.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createColumns({
|
createColumns({
|
||||||
@ -219,6 +259,7 @@ export function IdeasTable() {
|
|||||||
onEstimate: handleEstimate,
|
onEstimate: handleEstimate,
|
||||||
onViewEstimate: handleViewEstimate,
|
onViewEstimate: handleViewEstimate,
|
||||||
onSpecification: handleSpecification,
|
onSpecification: handleSpecification,
|
||||||
|
onViewDetails: handleViewDetails,
|
||||||
expandedId,
|
expandedId,
|
||||||
estimatingId,
|
estimatingId,
|
||||||
generatingSpecificationId,
|
generatingSpecificationId,
|
||||||
@ -230,6 +271,7 @@ export function IdeasTable() {
|
|||||||
generatingSpecificationId,
|
generatingSpecificationId,
|
||||||
handleEstimate,
|
handleEstimate,
|
||||||
handleSpecification,
|
handleSpecification,
|
||||||
|
handleViewDetails,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -316,6 +358,19 @@ export function IdeasTable() {
|
|||||||
sx={{ width: '100%', overflow: 'hidden' }}
|
sx={{ width: '100%', overflow: 'hidden' }}
|
||||||
data-testid="ideas-table-container"
|
data-testid="ideas-table-container"
|
||||||
>
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
alignItems: 'center',
|
||||||
|
px: 2,
|
||||||
|
py: 1,
|
||||||
|
borderBottom: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ColumnVisibility table={table} />
|
||||||
|
</Box>
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
collisionDetection={closestCenter}
|
collisionDetection={closestCenter}
|
||||||
@ -500,6 +555,15 @@ export function IdeasTable() {
|
|||||||
onRestoreFromHistory={handleRestoreFromHistory}
|
onRestoreFromHistory={handleRestoreFromHistory}
|
||||||
isRestoring={restoreSpecificationFromHistory.isPending}
|
isRestoring={restoreSpecificationFromHistory.isPending}
|
||||||
/>
|
/>
|
||||||
|
<IdeaDetailModal
|
||||||
|
open={detailModalOpen}
|
||||||
|
onClose={handleCloseDetailModal}
|
||||||
|
idea={detailIdea}
|
||||||
|
onSave={handleSaveDetail}
|
||||||
|
isSaving={updateIdea.isPending}
|
||||||
|
onOpenSpecification={handleOpenSpecificationFromDetail}
|
||||||
|
onOpenEstimate={handleOpenEstimateFromDetail}
|
||||||
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
AutoAwesome,
|
AutoAwesome,
|
||||||
AccessTime,
|
AccessTime,
|
||||||
Description,
|
Description,
|
||||||
|
Visibility,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import type {
|
import type {
|
||||||
Idea,
|
Idea,
|
||||||
@ -82,6 +83,7 @@ interface ColumnsConfig {
|
|||||||
onEstimate: (id: string) => void;
|
onEstimate: (id: string) => void;
|
||||||
onViewEstimate: (idea: Idea) => void;
|
onViewEstimate: (idea: Idea) => void;
|
||||||
onSpecification: (idea: Idea) => void;
|
onSpecification: (idea: Idea) => void;
|
||||||
|
onViewDetails: (idea: Idea) => void;
|
||||||
expandedId: string | null;
|
expandedId: string | null;
|
||||||
estimatingId: string | null;
|
estimatingId: string | null;
|
||||||
generatingSpecificationId: string | null;
|
generatingSpecificationId: string | null;
|
||||||
@ -93,6 +95,7 @@ export const createColumns = ({
|
|||||||
onEstimate,
|
onEstimate,
|
||||||
onViewEstimate,
|
onViewEstimate,
|
||||||
onSpecification,
|
onSpecification,
|
||||||
|
onViewDetails,
|
||||||
expandedId,
|
expandedId,
|
||||||
estimatingId,
|
estimatingId,
|
||||||
generatingSpecificationId,
|
generatingSpecificationId,
|
||||||
@ -195,6 +198,60 @@ export const createColumns = ({
|
|||||||
),
|
),
|
||||||
size: 150,
|
size: 150,
|
||||||
}),
|
}),
|
||||||
|
columnHelper.accessor('pain', {
|
||||||
|
header: 'Боль',
|
||||||
|
cell: (info) => {
|
||||||
|
const value = info.getValue();
|
||||||
|
return (
|
||||||
|
<EditableCell
|
||||||
|
idea={info.row.original}
|
||||||
|
field="pain"
|
||||||
|
value={value}
|
||||||
|
renderDisplay={(val) => {
|
||||||
|
if (!val) return '—';
|
||||||
|
return val.length > 60 ? `${val.slice(0, 60)}...` : val;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
size: 180,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('aiRole', {
|
||||||
|
header: 'Роль AI',
|
||||||
|
cell: (info) => {
|
||||||
|
const value = info.getValue();
|
||||||
|
return (
|
||||||
|
<EditableCell
|
||||||
|
idea={info.row.original}
|
||||||
|
field="aiRole"
|
||||||
|
value={value}
|
||||||
|
renderDisplay={(val) => {
|
||||||
|
if (!val) return '—';
|
||||||
|
return val.length > 60 ? `${val.slice(0, 60)}...` : val;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
size: 180,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('verificationMethod', {
|
||||||
|
header: 'Способ проверки',
|
||||||
|
cell: (info) => {
|
||||||
|
const value = info.getValue();
|
||||||
|
return (
|
||||||
|
<EditableCell
|
||||||
|
idea={info.row.original}
|
||||||
|
field="verificationMethod"
|
||||||
|
value={value}
|
||||||
|
renderDisplay={(val) => {
|
||||||
|
if (!val) return '—';
|
||||||
|
return val.length > 60 ? `${val.slice(0, 60)}...` : val;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
size: 180,
|
||||||
|
}),
|
||||||
columnHelper.accessor('description', {
|
columnHelper.accessor('description', {
|
||||||
header: 'Описание',
|
header: 'Описание',
|
||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
@ -274,6 +331,16 @@ export const createColumns = ({
|
|||||||
const hasSpecification = !!idea.specification;
|
const hasSpecification = !!idea.specification;
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
||||||
|
<Tooltip title="Подробнее">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => onViewDetails(idea)}
|
||||||
|
data-testid="view-details-button"
|
||||||
|
sx={{ opacity: 0.6, '&:hover': { opacity: 1 } }}
|
||||||
|
>
|
||||||
|
<Visibility fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={hasSpecification ? 'Просмотр ТЗ' : 'Сгенерировать ТЗ'}
|
title={hasSpecification ? 'Просмотр ТЗ' : 'Сгенерировать ТЗ'}
|
||||||
>
|
>
|
||||||
@ -341,6 +408,6 @@ export const createColumns = ({
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
size: 150,
|
size: 180,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user