From 684e4165882eb440a13eb2e806d529eee3bddd2c Mon Sep 17 00:00:00 2001 From: vigdorov Date: Thu, 15 Jan 2026 11:41:01 +0300 Subject: [PATCH] add view any columns and view mode for ideas --- CONTEXT.md | 22 +- REQUIREMENTS.md | 10 +- ROADMAP.md | 45 ++ .../IdeaDetailModal/IdeaDetailModal.tsx | 491 ++++++++++++++++++ .../src/components/IdeaDetailModal/index.ts | 1 + .../IdeasTable/ColumnVisibility.tsx | 157 ++++++ .../src/components/IdeasTable/IdeasTable.tsx | 68 ++- .../src/components/IdeasTable/columns.tsx | 69 ++- 8 files changed, 856 insertions(+), 7 deletions(-) create mode 100644 frontend/src/components/IdeaDetailModal/IdeaDetailModal.tsx create mode 100644 frontend/src/components/IdeaDetailModal/index.ts create mode 100644 frontend/src/components/IdeasTable/ColumnVisibility.tsx diff --git a/CONTEXT.md b/CONTEXT.md index 65d64d7..302530d 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -6,9 +6,9 @@ ## Текущий статус -**Этап:** Фаза 3.1 завершена ✅ | Новые требования (Фазы 4-8) запланированы 📋 +**Этап:** Фаза 3.1 завершена ✅ | Фаза 3.2 запланирована 📋 **Фаза MVP:** Базовый функционал + авторизация + расширенный функционал + AI-оценка + мини-ТЗ + история ТЗ готовы -**Следующий этап:** Фаза 4 — Права доступа +**Следующий этап:** Фаза 3.2 — Полный просмотр идеи (все поля) **Последнее обновление:** 2026-01-15 --- @@ -81,6 +81,7 @@ | 2026-01-15 | **Фаза 3.1:** AI-промпты (ТЗ и оценка) теперь учитывают комментарии к идее | | 2026-01-15 | **Планирование:** Добавлены новые требования — права доступа, аудит, WebSocket, темная тема, экспорт | | 2026-01-15 | **Документация:** Обновлены REQUIREMENTS.md, ARCHITECTURE.md, ROADMAP.md — добавлены Фазы 4-8 | +| 2026-01-15 | **Планирование:** Добавлена Фаза 3.2 — Полный просмотр идеи (все поля доступны для просмотра и редактирования) | --- @@ -89,7 +90,22 @@ > Смотри [ROADMAP.md](ROADMAP.md) для полного плана разработки **Готово:** Фазы 0-3.1 завершены ✅ -**Следующий шаг:** Фаза 4 — Права доступа 📋 +**Следующий шаг:** Фаза 3.2 — Полный просмотр идеи 📋 + +### Фаза 3.2: Полный просмотр идеи + +**Колонки в таблице:** +- [ ] Колонки pain, aiRole, verificationMethod +- [ ] Column visibility (скрытие/показ колонок, localStorage) + +**Модалка IdeaDetailModal:** +- [ ] Режим просмотра (readonly по умолчанию) +- [ ] Режим редактирования (кнопка "Редактировать") +- [ ] Кнопки "Сохранить" / "Отмена" +- [ ] Быстрый доступ к ТЗ и AI-оценке + +**E2E тесты:** +- [ ] Column visibility, модалка, редактирование, сохранение ### Новые требования (Фазы 4-8): diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index f28b9b7..5766f48 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -33,9 +33,17 @@ - Отображение автора в таблице и детальном просмотре #### 1.3 Редактирование идей +- **Полный просмотр**: пользователь может просмотреть ВСЕ поля идеи (включая pain, aiRole, verificationMethod) +- **Полное редактирование**: пользователь может отредактировать ВСЕ редактируемые поля идеи - **Inline-редактирование**: возможность редактировать любое поле прямо в таблице двойным кликом +- **Детальный просмотр**: модалка с полной информацией об идее + - Открывается в **режиме просмотра** (readonly) + - Кнопка "Редактировать" переводит в **режим редактирования** + - Кнопка "Сохранить" сохраняет изменения + - Кнопка "Отмена" отменяет изменения +- **Column visibility**: возможность скрыть/показать колонки таблицы - **Быстрое изменение статуса и приоритета** через dropdown -- **Автосохранение** изменений +- **Автосохранение** изменений (для inline-редактирования) #### 1.4 Drag & Drop - Перемещение идей в списке для ручной сортировки diff --git a/ROADMAP.md b/ROADMAP.md index 664ae94..b9fcc8e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -15,6 +15,7 @@ | 2 | Расширенный функционал | ✅ Завершена | Drag&Drop, цвета, комментарии, команда | | 3 | AI-интеграция | ✅ Завершена | Оценка времени, рекомендации | | 3.1 | Генерация мини-ТЗ | ✅ Завершена | Генерация, редактирование, история ТЗ | +| 3.2 | Полный просмотр идеи | 📋 Планируется | Просмотр и редактирование всех полей | | 4 | Права доступа | 📋 Планируется | Гранулярные права, панель админа | | 5 | Аудит и история | 📋 Планируется | Логирование действий, восстановление | | 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: Права доступа 📋 > **Гранулярная система прав доступа и панель администратора** diff --git a/frontend/src/components/IdeaDetailModal/IdeaDetailModal.tsx b/frontend/src/components/IdeaDetailModal/IdeaDetailModal.tsx new file mode 100644 index 0000000..b624913 --- /dev/null +++ b/frontend/src/components/IdeaDetailModal/IdeaDetailModal.tsx @@ -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({}); + + // Сброс при открытии/закрытии или смене идеи + 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 ( + + + + {isEditing ? ( + handleChange('title', e.target.value)} + variant="standard" + slotProps={{ + htmlInput: { 'data-testid': 'idea-detail-title-input' }, + }} + sx={{ '& input': { fontSize: '1.25rem', fontWeight: 500 } }} + /> + ) : ( + + {idea.title} + + )} + + + + + + + + + {/* Статус и приоритет */} + + + Статус + + {isEditing ? ( + + + + ) : ( + + )} + + + + + Приоритет + + {isEditing ? ( + + + + ) : ( + + )} + + + {/* Модуль */} + + + Модуль + + {isEditing ? ( + handleChange('module', e.target.value)} + slotProps={{ + htmlInput: { 'data-testid': 'idea-detail-module-input' }, + }} + /> + ) : ( + + {idea.module ?? '—'} + + )} + + + {/* Целевая аудитория */} + + + Целевая аудитория + + {isEditing ? ( + handleChange('targetAudience', e.target.value)} + slotProps={{ + htmlInput: { + 'data-testid': 'idea-detail-target-audience-input', + }, + }} + /> + ) : ( + + {idea.targetAudience ?? '—'} + + )} + + + {/* Описание */} + + + Описание + + {isEditing ? ( + handleChange('description', e.target.value)} + slotProps={{ + htmlInput: { 'data-testid': 'idea-detail-description-input' }, + }} + /> + ) : ( + + {idea.description ?? '—'} + + )} + + + {/* Боль */} + + + Какую боль решает + + {isEditing ? ( + handleChange('pain', e.target.value)} + slotProps={{ + htmlInput: { 'data-testid': 'idea-detail-pain-input' }, + }} + /> + ) : ( + + {idea.pain ?? '—'} + + )} + + + {/* Роль AI */} + + + Роль AI + + {isEditing ? ( + handleChange('aiRole', e.target.value)} + slotProps={{ + htmlInput: { 'data-testid': 'idea-detail-ai-role-input' }, + }} + /> + ) : ( + + {idea.aiRole ?? '—'} + + )} + + + {/* Способ проверки */} + + + Способ проверки + + {isEditing ? ( + + handleChange('verificationMethod', e.target.value) + } + slotProps={{ + htmlInput: { + 'data-testid': 'idea-detail-verification-method-input', + }, + }} + /> + ) : ( + + {idea.verificationMethod ?? '—'} + + )} + + + + + + + {/* AI-оценка и ТЗ */} + + + AI-оценка + + + + {formatHours(idea.estimatedHours)} + + {idea.complexity && ( + + )} + + + + + + + Техническое задание + + + + {idea.specification ? 'Есть' : 'Нет'} + + + + + + + + + + {/* Метаданные */} + + + Создано + + + {formatDate(idea.createdAt)} + + + + + + Обновлено + + + {formatDate(idea.updatedAt)} + + + + + + + {isEditing ? ( + <> + + + + ) : ( + <> + + + + )} + + + ); +} diff --git a/frontend/src/components/IdeaDetailModal/index.ts b/frontend/src/components/IdeaDetailModal/index.ts new file mode 100644 index 0000000..7106a00 --- /dev/null +++ b/frontend/src/components/IdeaDetailModal/index.ts @@ -0,0 +1 @@ +export { IdeaDetailModal } from './IdeaDetailModal'; diff --git a/frontend/src/components/IdeasTable/ColumnVisibility.tsx b/frontend/src/components/IdeasTable/ColumnVisibility.tsx new file mode 100644 index 0000000..a926eac --- /dev/null +++ b/frontend/src/components/IdeasTable/ColumnVisibility.tsx @@ -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 = { + drag: 'Перетаскивание', + color: 'Цвет', + title: 'Название', + status: 'Статус', + priority: 'Приоритет', + module: 'Модуль', + targetAudience: 'Целевая аудитория', + pain: 'Боль', + aiRole: 'Роль AI', + verificationMethod: 'Способ проверки', + description: 'Описание', + estimatedHours: 'Оценка', + actions: 'Действия', +}; + +// Колонки, которые нельзя скрыть +const alwaysVisibleColumns = ['drag', 'title', 'actions']; + +interface ColumnVisibilityProps { + table: Table; +} + +export function ColumnVisibility({ table }: ColumnVisibilityProps) { + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + // Загрузка сохранённых настроек при монтировании + useEffect(() => { + const saved = localStorage.getItem(STORAGE_KEY); + if (saved) { + try { + const visibility = JSON.parse(saved) as Record; + table.setColumnVisibility(visibility); + } catch { + // Игнорируем ошибки парсинга + } + } + }, [table]); + + const handleClick = (event: React.MouseEvent) => { + 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 = {}; + 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 ( + <> + + + + + + + + + Показать колонки + + + + {columns.map((column) => { + const isAlwaysVisible = alwaysVisibleColumns.includes(column.id); + return ( + !isAlwaysVisible && handleToggle(column.id)} + disabled={isAlwaysVisible} + dense + data-testid={`column-visibility-item-${column.id}`} + > + + + + ); + })} + + + + + + + ); +} diff --git a/frontend/src/components/IdeasTable/IdeasTable.tsx b/frontend/src/components/IdeasTable/IdeasTable.tsx index d6bab4b..f08bf8f 100644 --- a/frontend/src/components/IdeasTable/IdeasTable.tsx +++ b/frontend/src/components/IdeasTable/IdeasTable.tsx @@ -36,6 +36,7 @@ import { Collapse, } from '@mui/material'; import { Inbox } from '@mui/icons-material'; +import { ColumnVisibility } from './ColumnVisibility'; import { useIdeasQuery, useDeleteIdea, @@ -55,10 +56,11 @@ 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 } from '../../types/idea'; +import type { Idea, UpdateIdeaDto } from '../../types/idea'; -const SKELETON_COLUMNS_COUNT = 10; +const SKELETON_COLUMNS_COUNT = 13; export function IdeasTable() { const { data, isLoading, isError } = useIdeasQuery(); @@ -91,6 +93,9 @@ export function IdeasTable() { const [generatingSpecificationId, setGeneratingSpecificationId] = useState< string | null >(null); + // Детальный просмотр идеи + const [detailModalOpen, setDetailModalOpen] = useState(false); + const [detailIdea, setDetailIdea] = useState(null); // История ТЗ 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( () => createColumns({ @@ -219,6 +259,7 @@ export function IdeasTable() { onEstimate: handleEstimate, onViewEstimate: handleViewEstimate, onSpecification: handleSpecification, + onViewDetails: handleViewDetails, expandedId, estimatingId, generatingSpecificationId, @@ -230,6 +271,7 @@ export function IdeasTable() { generatingSpecificationId, handleEstimate, handleSpecification, + handleViewDetails, ], ); @@ -316,6 +358,19 @@ export function IdeasTable() { sx={{ width: '100%', overflow: 'hidden' }} data-testid="ideas-table-container" > + + + + ); } diff --git a/frontend/src/components/IdeasTable/columns.tsx b/frontend/src/components/IdeasTable/columns.tsx index b1e5165..fe45866 100644 --- a/frontend/src/components/IdeasTable/columns.tsx +++ b/frontend/src/components/IdeasTable/columns.tsx @@ -14,6 +14,7 @@ import { AutoAwesome, AccessTime, Description, + Visibility, } from '@mui/icons-material'; import type { Idea, @@ -82,6 +83,7 @@ interface ColumnsConfig { onEstimate: (id: string) => void; onViewEstimate: (idea: Idea) => void; onSpecification: (idea: Idea) => void; + onViewDetails: (idea: Idea) => void; expandedId: string | null; estimatingId: string | null; generatingSpecificationId: string | null; @@ -93,6 +95,7 @@ export const createColumns = ({ onEstimate, onViewEstimate, onSpecification, + onViewDetails, expandedId, estimatingId, generatingSpecificationId, @@ -195,6 +198,60 @@ export const createColumns = ({ ), size: 150, }), + columnHelper.accessor('pain', { + header: 'Боль', + cell: (info) => { + const value = info.getValue(); + return ( + { + 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 ( + { + 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 ( + { + if (!val) return '—'; + return val.length > 60 ? `${val.slice(0, 60)}...` : val; + }} + /> + ); + }, + size: 180, + }), columnHelper.accessor('description', { header: 'Описание', cell: (info) => { @@ -274,6 +331,16 @@ export const createColumns = ({ const hasSpecification = !!idea.specification; return ( + + onViewDetails(idea)} + data-testid="view-details-button" + sx={{ opacity: 0.6, '&:hover': { opacity: 1 } }} + > + + + @@ -341,6 +408,6 @@ export const createColumns = ({ ); }, - size: 150, + size: 180, }), ];