init
This commit is contained in:
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Dependencies
|
||||||
|
**/*/node_modules/
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
**/*/dist/
|
||||||
|
**/dist-ssr/
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
1
.serena/.gitignore
vendored
Normal file
1
.serena/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/cache
|
||||||
84
.serena/project.yml
Normal file
84
.serena/project.yml
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
# list of languages for which language servers are started; choose from:
|
||||||
|
# al bash clojure cpp csharp csharp_omnisharp
|
||||||
|
# dart elixir elm erlang fortran go
|
||||||
|
# haskell java julia kotlin lua markdown
|
||||||
|
# nix perl php python python_jedi r
|
||||||
|
# rego ruby ruby_solargraph rust scala swift
|
||||||
|
# terraform typescript typescript_vts yaml zig
|
||||||
|
# Note:
|
||||||
|
# - For C, use cpp
|
||||||
|
# - For JavaScript, use typescript
|
||||||
|
# Special requirements:
|
||||||
|
# - csharp: Requires the presence of a .sln file in the project folder.
|
||||||
|
# When using multiple languages, the first language server that supports a given file will be used for that file.
|
||||||
|
# The first language is the default language and the respective language server will be used as a fallback.
|
||||||
|
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||||
|
languages:
|
||||||
|
- typescript
|
||||||
|
|
||||||
|
# the encoding used by text files in the project
|
||||||
|
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||||
|
encoding: "utf-8"
|
||||||
|
|
||||||
|
# whether to use the project's gitignore file to ignore files
|
||||||
|
# Added on 2025-04-07
|
||||||
|
ignore_all_files_in_gitignore: true
|
||||||
|
|
||||||
|
# list of additional paths to ignore
|
||||||
|
# same syntax as gitignore, so you can use * and **
|
||||||
|
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
||||||
|
# Added (renamed) on 2025-04-07
|
||||||
|
ignored_paths: []
|
||||||
|
|
||||||
|
# whether the project is in read-only mode
|
||||||
|
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||||
|
# Added on 2025-04-18
|
||||||
|
read_only: false
|
||||||
|
|
||||||
|
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||||
|
# Below is the complete list of tools for convenience.
|
||||||
|
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||||
|
# execute `uv run scripts/print_tool_overview.py`.
|
||||||
|
#
|
||||||
|
# * `activate_project`: Activates a project by name.
|
||||||
|
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||||
|
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||||
|
# * `delete_lines`: Deletes a range of lines within a file.
|
||||||
|
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||||
|
# * `execute_shell_command`: Executes a shell command.
|
||||||
|
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||||
|
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||||
|
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||||
|
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||||
|
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||||
|
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||||
|
# Should only be used in settings where the system prompt cannot be set,
|
||||||
|
# e.g. in clients you have no control over, like Claude Desktop.
|
||||||
|
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||||
|
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||||
|
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||||
|
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||||
|
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||||
|
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||||
|
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||||
|
# * `read_file`: Reads a file within the project directory.
|
||||||
|
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||||
|
# * `remove_project`: Removes a project from the Serena configuration.
|
||||||
|
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||||
|
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||||
|
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||||
|
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||||
|
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||||
|
# * `switch_modes`: Activates modes by providing a list of their names
|
||||||
|
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||||
|
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||||
|
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||||
|
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||||
|
excluded_tools: []
|
||||||
|
|
||||||
|
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||||
|
# (contrary to the memories, which are loaded on demand).
|
||||||
|
initial_prompt: ""
|
||||||
|
|
||||||
|
project_name: "team-planner"
|
||||||
|
included_optional_tools: []
|
||||||
988
ARCHITECTURE.md
Normal file
988
ARCHITECTURE.md
Normal file
@ -0,0 +1,988 @@
|
|||||||
|
# Архитектура Team Planner
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. C4 Model
|
||||||
|
|
||||||
|
### 1.1 Level 1: System Context
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph external [" "]
|
||||||
|
User["👤 Пользователь<br/><i>Член команды разработки</i>"]
|
||||||
|
AI["🤖 AI Proxy Service<br/><i>LLM для оценки задач</i>"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph system ["Team Planner"]
|
||||||
|
TP["🎯 Team Planner<br/><i>Приложение для управления<br/>бэклогом идей команды</i>"]
|
||||||
|
end
|
||||||
|
|
||||||
|
User -->|"Управляет идеями,<br/>командой, комментариями<br/>[HTTPS]"| TP
|
||||||
|
TP -->|"Запросы на оценку<br/>трудозатрат<br/>[HTTPS/REST]"| AI
|
||||||
|
|
||||||
|
style TP fill:#1168bd,color:#fff
|
||||||
|
style User fill:#08427b,color:#fff
|
||||||
|
style AI fill:#999,color:#fff
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 Level 2: Container Diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
User["👤 Пользователь<br/><i>Член команды разработки</i>"]
|
||||||
|
|
||||||
|
subgraph TeamPlanner ["Team Planner"]
|
||||||
|
SPA["📱 Frontend SPA<br/><i>React, TypeScript, MUI</i><br/><br/>Веб-интерфейс для<br/>работы с идеями"]
|
||||||
|
API["⚙️ Backend API<br/><i>NestJS, TypeScript</i><br/><br/>REST API + WebSocket"]
|
||||||
|
DB[("🗄️ Database<br/><i>PostgreSQL</i><br/><br/>Хранение идей,<br/>команды, комментариев")]
|
||||||
|
end
|
||||||
|
|
||||||
|
AI["🤖 AI Proxy Service<br/><i>LLM для оценки задач</i>"]
|
||||||
|
|
||||||
|
User -->|"Использует<br/>[HTTPS]"| SPA
|
||||||
|
SPA -->|"API запросы<br/>[REST/WebSocket]"| API
|
||||||
|
API -->|"Читает/пишет<br/>[TypeORM]"| DB
|
||||||
|
API -->|"Оценка трудозатрат<br/>[HTTPS/REST]"| AI
|
||||||
|
|
||||||
|
style SPA fill:#438dd5,color:#fff
|
||||||
|
style API fill:#438dd5,color:#fff
|
||||||
|
style DB fill:#438dd5,color:#fff
|
||||||
|
style User fill:#08427b,color:#fff
|
||||||
|
style AI fill:#999,color:#fff
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Sequence Diagrams
|
||||||
|
|
||||||
|
### 2.1 Создание идеи
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
autonumber
|
||||||
|
actor User as Пользователь
|
||||||
|
participant FE as Frontend<br/>(React)
|
||||||
|
participant BE as Backend<br/>(NestJS)
|
||||||
|
participant DB as PostgreSQL
|
||||||
|
|
||||||
|
User->>FE: Заполняет форму идеи
|
||||||
|
FE->>FE: Валидация на клиенте
|
||||||
|
FE->>BE: POST /api/ideas
|
||||||
|
BE->>BE: Валидация DTO
|
||||||
|
BE->>DB: INSERT INTO ideas
|
||||||
|
DB-->>BE: idea record
|
||||||
|
BE-->>FE: 201 Created { idea }
|
||||||
|
FE->>FE: Добавляет в store
|
||||||
|
FE-->>User: Показывает новую идею в списке
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Inline-редактирование идеи
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
autonumber
|
||||||
|
actor User as Пользователь
|
||||||
|
participant FE as Frontend<br/>(React)
|
||||||
|
participant BE as Backend<br/>(NestJS)
|
||||||
|
participant DB as PostgreSQL
|
||||||
|
|
||||||
|
User->>FE: Double-click на ячейку
|
||||||
|
FE->>FE: Переключает в режим редактирования
|
||||||
|
User->>FE: Изменяет значение, blur/Enter
|
||||||
|
FE->>FE: Оптимистичное обновление store
|
||||||
|
FE->>BE: PATCH /api/ideas/:id
|
||||||
|
BE->>BE: Валидация DTO
|
||||||
|
BE->>DB: UPDATE ideas SET ...
|
||||||
|
DB-->>BE: updated idea
|
||||||
|
BE-->>FE: 200 OK { idea }
|
||||||
|
FE->>FE: Подтверждает изменение в store
|
||||||
|
|
||||||
|
alt Ошибка
|
||||||
|
BE-->>FE: 4xx/5xx error
|
||||||
|
FE->>FE: Откат оптимистичного обновления
|
||||||
|
FE-->>User: Показывает ошибку
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Drag & Drop (изменение порядка)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
autonumber
|
||||||
|
actor User as Пользователь
|
||||||
|
participant FE as Frontend<br/>(React)
|
||||||
|
participant BE as Backend<br/>(NestJS)
|
||||||
|
participant DB as PostgreSQL
|
||||||
|
|
||||||
|
User->>FE: Перетаскивает идею
|
||||||
|
FE->>FE: dnd-kit обновляет UI
|
||||||
|
FE->>FE: Оптимистичное обновление порядка
|
||||||
|
FE->>BE: PATCH /api/ideas/reorder
|
||||||
|
Note right of FE: { ids: [id1, id2, ...] }
|
||||||
|
BE->>DB: UPDATE ideas SET position = ...
|
||||||
|
DB-->>BE: OK
|
||||||
|
BE-->>FE: 200 OK
|
||||||
|
FE-->>User: Порядок сохранён
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 AI-оценка трудозатрат
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
autonumber
|
||||||
|
actor User as Пользователь
|
||||||
|
participant FE as Frontend<br/>(React)
|
||||||
|
participant BE as Backend<br/>(NestJS)
|
||||||
|
participant AI as AI Proxy
|
||||||
|
participant DB as PostgreSQL
|
||||||
|
|
||||||
|
User->>FE: Нажимает "Оценить"
|
||||||
|
FE->>FE: Показывает loader
|
||||||
|
FE->>BE: POST /api/ai/estimate
|
||||||
|
Note right of FE: { ideaId }
|
||||||
|
BE->>DB: SELECT idea, team_members
|
||||||
|
DB-->>BE: idea + team data
|
||||||
|
BE->>BE: Формирует промпт
|
||||||
|
BE->>AI: POST /chat/completions
|
||||||
|
Note right of BE: prompt с описанием идеи<br/>и составом команды
|
||||||
|
AI-->>BE: LLM response
|
||||||
|
BE->>BE: Парсит ответ
|
||||||
|
BE->>DB: UPDATE ideas SET estimate = ...
|
||||||
|
DB-->>BE: OK
|
||||||
|
BE-->>FE: 200 OK { estimate }
|
||||||
|
FE->>FE: Обновляет store
|
||||||
|
FE-->>User: Показывает оценку
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.5 Добавление комментария
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
autonumber
|
||||||
|
actor User as Пользователь
|
||||||
|
participant FE as Frontend<br/>(React)
|
||||||
|
participant BE as Backend<br/>(NestJS)
|
||||||
|
participant DB as PostgreSQL
|
||||||
|
|
||||||
|
User->>FE: Пишет комментарий
|
||||||
|
FE->>BE: POST /api/ideas/:id/comments
|
||||||
|
Note right of FE: { text, parentId? }
|
||||||
|
BE->>BE: Валидация
|
||||||
|
BE->>DB: INSERT INTO comments
|
||||||
|
DB-->>BE: comment record
|
||||||
|
BE-->>FE: 201 Created { comment }
|
||||||
|
FE->>FE: Добавляет в store
|
||||||
|
FE-->>User: Показывает комментарий
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.6 Загрузка списка идей с фильтрами
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
autonumber
|
||||||
|
actor User as Пользователь
|
||||||
|
participant FE as Frontend<br/>(React)
|
||||||
|
participant BE as Backend<br/>(NestJS)
|
||||||
|
participant DB as PostgreSQL
|
||||||
|
|
||||||
|
User->>FE: Открывает страницу / меняет фильтры
|
||||||
|
FE->>FE: Показывает skeleton loader
|
||||||
|
FE->>BE: GET /api/ideas?status=...&priority=...
|
||||||
|
BE->>DB: SELECT * FROM ideas WHERE ... ORDER BY ...
|
||||||
|
DB-->>BE: ideas[]
|
||||||
|
BE-->>FE: 200 OK { data, total, page }
|
||||||
|
FE->>FE: Сохраняет в store
|
||||||
|
FE-->>User: Отображает таблицу
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. API Contracts
|
||||||
|
|
||||||
|
### 3.1 Ideas
|
||||||
|
|
||||||
|
#### GET /api/ideas
|
||||||
|
Получение списка идей с пагинацией и фильтрами.
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
page?: number; // default: 1
|
||||||
|
limit?: number; // default: 50
|
||||||
|
status?: IdeaStatus; // фильтр по статусу
|
||||||
|
priority?: Priority; // фильтр по приоритету
|
||||||
|
module?: Module; // фильтр по модулю
|
||||||
|
color?: string; // фильтр по цвету
|
||||||
|
search?: string; // поиск по тексту
|
||||||
|
sortBy?: string; // поле для сортировки
|
||||||
|
sortOrder?: 'asc' | 'desc';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response 200:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
data: Idea[];
|
||||||
|
meta: {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### POST /api/ideas
|
||||||
|
Создание новой идеи.
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
title: string; // обязательное
|
||||||
|
description?: string;
|
||||||
|
status: IdeaStatus; // default: 'new'
|
||||||
|
priority: Priority; // default: 'medium'
|
||||||
|
module: Module[];
|
||||||
|
targetAudience?: string;
|
||||||
|
painPoint?: string;
|
||||||
|
aiRole?: string;
|
||||||
|
validationMethod?: string;
|
||||||
|
color?: string;
|
||||||
|
position?: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response 201:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string | null;
|
||||||
|
status: IdeaStatus;
|
||||||
|
priority: Priority;
|
||||||
|
module: Module[];
|
||||||
|
targetAudience: string | null;
|
||||||
|
painPoint: string | null;
|
||||||
|
aiRole: string | null;
|
||||||
|
validationMethod: string | null;
|
||||||
|
color: string | null;
|
||||||
|
position: number;
|
||||||
|
estimate: Estimate | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### PATCH /api/ideas/:id
|
||||||
|
Обновление идеи (partial update).
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```typescript
|
||||||
|
Partial<{
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
status: IdeaStatus;
|
||||||
|
priority: Priority;
|
||||||
|
module: Module[];
|
||||||
|
targetAudience: string;
|
||||||
|
painPoint: string;
|
||||||
|
aiRole: string;
|
||||||
|
validationMethod: string;
|
||||||
|
color: string;
|
||||||
|
}>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response 200:** `Idea`
|
||||||
|
|
||||||
|
#### DELETE /api/ideas/:id
|
||||||
|
Удаление идеи.
|
||||||
|
|
||||||
|
**Response 204:** No Content
|
||||||
|
|
||||||
|
#### PATCH /api/ideas/reorder
|
||||||
|
Изменение порядка идей.
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
ids: string[]; // ID идей в новом порядке
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response 200:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
success: true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Comments
|
||||||
|
|
||||||
|
#### GET /api/ideas/:ideaId/comments
|
||||||
|
Получение комментариев к идее.
|
||||||
|
|
||||||
|
**Response 200:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
data: Comment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comment
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
ideaId: string;
|
||||||
|
parentId: string | null; // для тредов
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
replies?: Comment[]; // вложенные ответы
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### POST /api/ideas/:ideaId/comments
|
||||||
|
Добавление комментария.
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
text: string;
|
||||||
|
parentId?: string; // для ответа на комментарий
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response 201:** `Comment`
|
||||||
|
|
||||||
|
#### DELETE /api/comments/:id
|
||||||
|
Удаление комментария.
|
||||||
|
|
||||||
|
**Response 204:** No Content
|
||||||
|
|
||||||
|
### 3.3 Team
|
||||||
|
|
||||||
|
#### GET /api/team/members
|
||||||
|
Получение списка членов команды.
|
||||||
|
|
||||||
|
**Response 200:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
data: TeamMember[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TeamMember
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
role: TeamRole;
|
||||||
|
productivity: {
|
||||||
|
trivial: number; // часы
|
||||||
|
easy: number;
|
||||||
|
medium: number;
|
||||||
|
hard: number;
|
||||||
|
epic: number;
|
||||||
|
};
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### POST /api/team/members
|
||||||
|
Добавление члена команды.
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
role: TeamRole;
|
||||||
|
productivity?: {
|
||||||
|
trivial?: number;
|
||||||
|
easy?: number;
|
||||||
|
medium?: number;
|
||||||
|
hard?: number;
|
||||||
|
epic?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response 201:** `TeamMember`
|
||||||
|
|
||||||
|
#### PATCH /api/team/members/:id
|
||||||
|
Обновление члена команды.
|
||||||
|
|
||||||
|
**Request Body:** `Partial<TeamMember>`
|
||||||
|
|
||||||
|
**Response 200:** `TeamMember`
|
||||||
|
|
||||||
|
#### DELETE /api/team/members/:id
|
||||||
|
Удаление члена команды.
|
||||||
|
|
||||||
|
**Response 204:** No Content
|
||||||
|
|
||||||
|
#### GET /api/team/summary
|
||||||
|
Сводка по команде.
|
||||||
|
|
||||||
|
**Response 200:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
totalMembers: number;
|
||||||
|
byRole: Record<TeamRole, number>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 AI
|
||||||
|
|
||||||
|
#### POST /api/ai/estimate
|
||||||
|
AI-оценка трудозатрат для идеи.
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
ideaId: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response 200:**
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
ideaId: string;
|
||||||
|
estimate: {
|
||||||
|
totalHours: number;
|
||||||
|
totalDays: number;
|
||||||
|
complexity: 'trivial' | 'easy' | 'medium' | 'hard' | 'epic';
|
||||||
|
breakdown: {
|
||||||
|
role: TeamRole;
|
||||||
|
hours: number;
|
||||||
|
complexity: Complexity;
|
||||||
|
description: string;
|
||||||
|
}[];
|
||||||
|
recommendations?: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 Enums
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
enum IdeaStatus {
|
||||||
|
NEW = 'new',
|
||||||
|
DISCUSSING = 'discussing',
|
||||||
|
APPROVED = 'approved',
|
||||||
|
IN_PROGRESS = 'in_progress',
|
||||||
|
DONE = 'done',
|
||||||
|
REJECTED = 'rejected'
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Priority {
|
||||||
|
CRITICAL = 'critical',
|
||||||
|
HIGH = 'high',
|
||||||
|
MEDIUM = 'medium',
|
||||||
|
LOW = 'low'
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Module {
|
||||||
|
FRONTEND = 'frontend',
|
||||||
|
BACKEND = 'backend',
|
||||||
|
AI = 'ai',
|
||||||
|
MOBILE = 'mobile',
|
||||||
|
INFRASTRUCTURE = 'infrastructure',
|
||||||
|
OTHER = 'other'
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TeamRole {
|
||||||
|
BACKEND = 'backend',
|
||||||
|
FRONTEND = 'frontend',
|
||||||
|
AI_ML = 'ai_ml',
|
||||||
|
ANALYST = 'analyst',
|
||||||
|
QA = 'qa',
|
||||||
|
DEVOPS = 'devops',
|
||||||
|
DESIGNER = 'designer',
|
||||||
|
PM = 'pm'
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Complexity {
|
||||||
|
TRIVIAL = 'trivial',
|
||||||
|
EASY = 'easy',
|
||||||
|
MEDIUM = 'medium',
|
||||||
|
HARD = 'hard',
|
||||||
|
EPIC = 'epic'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. UI Prototypes
|
||||||
|
|
||||||
|
### 4.1 Главная страница - Список идей
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 🎯 Team Planner [Команда] [+ Идея] │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌─ Фильтры ─────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ [Статус ▼] [Приоритет ▼] [Модуль ▼] [Цвет ▼] [🔍 Поиск... ] │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ Таблица идей ────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ ⋮⋮ │ Статус │ ⚡ │ Модуль │ Идея │ Для кого │ Оценка │ │
|
||||||
|
│ ├────┼───────────┼────┼──────────┼────────────────┼──────────┼─────────┤ │
|
||||||
|
│ │ ⋮⋮ │ 🟢 Новая │ 🔴 │ Frontend │ Добавить темну │ Все поль │ 3д │ │
|
||||||
|
│ │ ⋮⋮ │ 🟡 В обсу │ 🟡 │ Backend │ API кэширован │ Разработ │ 5д │ │
|
||||||
|
│ │ ⋮⋮ │ 🔵 Одобре │ 🟢 │ AI │ Автоматическа │ Менеджер │ 10д │ │
|
||||||
|
│ │ ⋮⋮ │ 🟣 В рабо │ 🔴 │ Backend │ Оптимизация з │ Все │ 7д │ │
|
||||||
|
│ │ │ │ │ │ │ │ │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Показано 4 из 24 [< Пред] 1 2 3 [След >] │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Легенда:
|
||||||
|
⋮⋮ - drag handle для перетаскивания
|
||||||
|
⚡ - приоритет (иконка молнии)
|
||||||
|
🔴🟡🟢 - цветовые индикаторы приоритета
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Расширенная строка с комментариями
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ⋮⋮ │ 🟢 Новая │ 🔴 │ Frontend │ Добавить тёмную тему │ Все │ 3д [▼]│
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌─ Детали ──────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Боль: Пользователи жалуются на яркий интерфейс вечером │ │
|
||||||
|
│ │ AI роль: — │ │
|
||||||
|
│ │ Проверка: A/B тест с 10% пользователей │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ Комментарии (3) ─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 💬 Иван: Нужно согласовать палитру с дизайнером 2ч назад │ │
|
||||||
|
│ │ └─ 💬 Мария: Уже в процессе, будет готово завтра 1ч назад │ │
|
||||||
|
│ │ 💬 Петр: Предлагаю использовать CSS variables 30м назад │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ [Написать комментарий... ] [Отправить] │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [🤖 Оценить AI] [✏️ Редактировать] [🗑️ Удалить] │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Модальное окно создания/редактирования идеи
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Новая идея [✕] │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Суть идеи * │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────┐ ┌─────────────────────┐ ┌──────────────────┐ │
|
||||||
|
│ │ Статус │ │ Приоритет │ │ Модуль │ │
|
||||||
|
│ │ [Новая ▼] │ │ [Средний ▼] │ │ [Frontend ▼] │ │
|
||||||
|
│ └─────────────────────┘ └─────────────────────┘ └──────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Для кого эта идея │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Какую боль решает │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Роль AI (если применимо) │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Быстрый способ проверить │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Цвет строки │
|
||||||
|
│ [⬜][🟥][🟧][🟨][🟩][🟦][🟪] │
|
||||||
|
│ │
|
||||||
|
│ [Отмена] [💾 Сохранить] │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 Страница управления командой
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 🎯 Team Planner [Команда] [+ Идея] │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ◀ Назад к идеям │
|
||||||
|
│ │
|
||||||
|
│ ┌─ Состав команды ──────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 👨💻 Backend: 3 👩💻 Frontend: 2 🤖 AI/ML: 1 📊 Аналитик: 1 │ │
|
||||||
|
│ │ 🧪 QA: 2 🔧 DevOps: 1 🎨 Дизайн: 1 📋 PM: 1 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ Участники ───────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Имя │ Роль │ Trivial │ Easy │ Med │ Hard │ Epic │ │
|
||||||
|
│ ├──────────────────┼────────────┼─────────┼──────┼─────┼──────┼────────┤ │
|
||||||
|
│ │ Иван Петров │ Backend │ 1ч │ 4ч │ 16ч │ 40ч │ 80ч │ │
|
||||||
|
│ │ Мария Сидорова │ Frontend │ 1ч │ 4ч │ 16ч │ 40ч │ 80ч │ │
|
||||||
|
│ │ Алексей Козлов │ AI/ML │ 2ч │ 8ч │ 24ч │ 60ч │ 120ч │ │
|
||||||
|
│ │ ... │ │ │ │ │ │ │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [+ Добавить участника] │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 Модальное окно AI-оценки
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 🤖 AI-оценка трудозатрат [✕] │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Идея: Добавить тёмную тему │
|
||||||
|
│ │
|
||||||
|
│ ┌─ Общая оценка ────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ⏱️ 24 часа (~3 рабочих дня) │ │
|
||||||
|
│ │ 📊 Сложность: Средняя │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ Разбивка по ролям ───────────────────────────────────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 👩💻 Frontend │ 16ч │ ████████████████░░░░ │ Средняя │ │
|
||||||
|
│ │ 🎨 Дизайнер │ 4ч │ ████░░░░░░░░░░░░░░░░ │ Лёгкая │ │
|
||||||
|
│ │ 🧪 QA │ 4ч │ ████░░░░░░░░░░░░░░░░ │ Лёгкая │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ Рекомендации ────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ • Использовать CSS custom properties для цветовой схемы │ │
|
||||||
|
│ │ • Добавить переключатель в настройки пользователя │ │
|
||||||
|
│ │ • Учесть системные настройки (prefers-color-scheme) │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [Сохранить оценку] │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. UI Specification
|
||||||
|
|
||||||
|
### 5.1 Цветовая палитра
|
||||||
|
|
||||||
|
#### Основные цвета
|
||||||
|
```
|
||||||
|
Primary: #1976D2 (MUI Blue 700)
|
||||||
|
Primary Light: #42A5F5 (MUI Blue 400)
|
||||||
|
Primary Dark: #1565C0 (MUI Blue 800)
|
||||||
|
|
||||||
|
Secondary: #9C27B0 (MUI Purple 500)
|
||||||
|
|
||||||
|
Background: #FFFFFF
|
||||||
|
Surface: #F5F5F5 (Grey 100)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Статусы идей
|
||||||
|
```
|
||||||
|
New: #4CAF50 (Green 500) 🟢
|
||||||
|
Discussing: #FF9800 (Orange 500) 🟡
|
||||||
|
Approved: #2196F3 (Blue 500) 🔵
|
||||||
|
In Progress: #9C27B0 (Purple 500) 🟣
|
||||||
|
Done: #607D8B (Blue Grey 500) ⚫
|
||||||
|
Rejected: #F44336 (Red 500) 🔴
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Приоритеты
|
||||||
|
```
|
||||||
|
Critical: #D32F2F (Red 700) фон: #FFEBEE
|
||||||
|
High: #F57C00 (Orange 700) фон: #FFF3E0
|
||||||
|
Medium: #FBC02D (Yellow 700) фон: #FFFDE7
|
||||||
|
Low: #388E3C (Green 700) фон: #E8F5E9
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Цвета для маркировки строк
|
||||||
|
```
|
||||||
|
Без цвета: transparent
|
||||||
|
Красный: #FFCDD2 (Red 100)
|
||||||
|
Оранжевый: #FFE0B2 (Orange 100)
|
||||||
|
Жёлтый: #FFF9C4 (Yellow 100)
|
||||||
|
Зелёный: #C8E6C9 (Green 100)
|
||||||
|
Голубой: #BBDEFB (Blue 100)
|
||||||
|
Фиолетовый: #E1BEE7 (Purple 100)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Типографика
|
||||||
|
|
||||||
|
```
|
||||||
|
Font Family: 'Roboto', 'Helvetica', 'Arial', sans-serif
|
||||||
|
|
||||||
|
H1: 24px, weight 500, line-height 1.2
|
||||||
|
H2: 20px, weight 500, line-height 1.3
|
||||||
|
H3: 16px, weight 500, line-height 1.4
|
||||||
|
Body1: 14px, weight 400, line-height 1.5
|
||||||
|
Body2: 12px, weight 400, line-height 1.43
|
||||||
|
Caption: 12px, weight 400, line-height 1.66, color: #757575
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 Компоненты
|
||||||
|
|
||||||
|
#### Кнопки
|
||||||
|
|
||||||
|
| Тип | Использование | Стиль |
|
||||||
|
|-----|---------------|-------|
|
||||||
|
| Primary | Главное действие (Сохранить, Создать) | Filled, Primary color |
|
||||||
|
| Secondary | Вторичные действия (Отмена) | Outlined, Grey |
|
||||||
|
| Text | Третичные действия (Назад) | Text only |
|
||||||
|
| Icon | Действия в строке таблицы | IconButton, 40x40px |
|
||||||
|
| Danger | Удаление | Filled, Red |
|
||||||
|
|
||||||
|
```
|
||||||
|
Border Radius: 4px
|
||||||
|
Height: 36px (medium), 40px (large)
|
||||||
|
Padding: 8px 16px
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Состояния кнопок
|
||||||
|
```
|
||||||
|
Default: opacity 1
|
||||||
|
Hover: brightness 0.95, shadow elevation 2
|
||||||
|
Active: brightness 0.9
|
||||||
|
Disabled: opacity 0.38, cursor not-allowed
|
||||||
|
Loading: показать CircularProgress (20px), disabled
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Инпуты
|
||||||
|
|
||||||
|
```
|
||||||
|
Height: 40px
|
||||||
|
Border: 1px solid #E0E0E0
|
||||||
|
Border Radius: 4px
|
||||||
|
Padding: 8px 12px
|
||||||
|
|
||||||
|
Focus: border-color: Primary, box-shadow: 0 0 0 2px Primary/20%
|
||||||
|
Error: border-color: #D32F2F, helper text red
|
||||||
|
Disabled: background: #F5F5F5, opacity 0.6
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Таблица
|
||||||
|
|
||||||
|
```
|
||||||
|
Header:
|
||||||
|
Background: #FAFAFA
|
||||||
|
Font: 14px, weight 500
|
||||||
|
Height: 48px
|
||||||
|
Border: 1px solid #E0E0E0 (bottom)
|
||||||
|
|
||||||
|
Row:
|
||||||
|
Height: 52px
|
||||||
|
Border: 1px solid #E0E0E0 (bottom)
|
||||||
|
Hover: background #F5F5F5
|
||||||
|
|
||||||
|
Row (colored):
|
||||||
|
Background: соответствующий цвет из палитры маркировки
|
||||||
|
Hover: darken 5%
|
||||||
|
|
||||||
|
Cell:
|
||||||
|
Padding: 16px
|
||||||
|
Vertical align: middle
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Dropdown/Select
|
||||||
|
|
||||||
|
```
|
||||||
|
Trigger: как Input
|
||||||
|
Menu:
|
||||||
|
Background: #FFFFFF
|
||||||
|
Shadow: 0 2px 8px rgba(0,0,0,0.15)
|
||||||
|
Border Radius: 4px
|
||||||
|
Max Height: 300px (scroll)
|
||||||
|
|
||||||
|
Item:
|
||||||
|
Height: 40px
|
||||||
|
Padding: 8px 16px
|
||||||
|
Hover: background #F5F5F5
|
||||||
|
Selected: background Primary/10%, color Primary
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Модальные окна
|
||||||
|
|
||||||
|
```
|
||||||
|
Overlay: rgba(0, 0, 0, 0.5)
|
||||||
|
Background: #FFFFFF
|
||||||
|
Border Radius: 8px
|
||||||
|
Shadow: 0 8px 32px rgba(0,0,0,0.2)
|
||||||
|
Width: 480px (small), 640px (medium), 800px (large)
|
||||||
|
Max Height: 90vh
|
||||||
|
Padding: 24px
|
||||||
|
|
||||||
|
Header:
|
||||||
|
Font: H2
|
||||||
|
Border: 1px solid #E0E0E0 (bottom)
|
||||||
|
Padding: 24px 24px 16px
|
||||||
|
|
||||||
|
Footer:
|
||||||
|
Border: 1px solid #E0E0E0 (top)
|
||||||
|
Padding: 16px 24px
|
||||||
|
Justify: flex-end
|
||||||
|
Gap: 12px
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 Состояния загрузки
|
||||||
|
|
||||||
|
#### Skeleton Loader
|
||||||
|
```
|
||||||
|
Для таблицы:
|
||||||
|
- Показать 5 строк skeleton
|
||||||
|
- Анимация: shimmer effect (gradient slide)
|
||||||
|
- Background: #E0E0E0
|
||||||
|
- Highlight: #F5F5F5
|
||||||
|
|
||||||
|
Для карточек/полей:
|
||||||
|
- Прямоугольники с закруглением 4px
|
||||||
|
- Высота соответствует контенту
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Spinner (CircularProgress)
|
||||||
|
```
|
||||||
|
Size: 24px (в кнопках), 40px (в контенте)
|
||||||
|
Color: Primary
|
||||||
|
Thickness: 3.6
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Inline Loading (в ячейках таблицы)
|
||||||
|
```
|
||||||
|
Показать маленький spinner (16px) справа от текста
|
||||||
|
Текст затемнить (opacity 0.5)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.5 Анимации
|
||||||
|
|
||||||
|
```
|
||||||
|
Duration:
|
||||||
|
Fast: 150ms (hover effects)
|
||||||
|
Normal: 250ms (transitions)
|
||||||
|
Slow: 350ms (modals, large elements)
|
||||||
|
|
||||||
|
Easing:
|
||||||
|
Standard: cubic-bezier(0.4, 0, 0.2, 1)
|
||||||
|
Enter: cubic-bezier(0.0, 0, 0.2, 1)
|
||||||
|
Exit: cubic-bezier(0.4, 0, 1, 1)
|
||||||
|
|
||||||
|
Drag & Drop:
|
||||||
|
Item lift: scale 1.02, shadow elevation 8
|
||||||
|
Drop zone: border 2px dashed Primary, background Primary/5%
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.6 Иконки
|
||||||
|
|
||||||
|
Использовать **Material Icons** (MUI Icons).
|
||||||
|
|
||||||
|
```
|
||||||
|
Размеры:
|
||||||
|
Small: 18px
|
||||||
|
Medium: 24px (default)
|
||||||
|
Large: 36px
|
||||||
|
|
||||||
|
Основные иконки:
|
||||||
|
Добавить: Add (+)
|
||||||
|
Редактировать: Edit (карандаш)
|
||||||
|
Удалить: Delete (корзина)
|
||||||
|
Drag: DragIndicator (⋮⋮)
|
||||||
|
Фильтр: FilterList
|
||||||
|
Поиск: Search (🔍)
|
||||||
|
Комментарий: ChatBubbleOutline
|
||||||
|
AI: AutoAwesome (✨) или SmartToy (🤖)
|
||||||
|
Развернуть: ExpandMore
|
||||||
|
Свернуть: ExpandLess
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.7 Отступы и сетка
|
||||||
|
|
||||||
|
```
|
||||||
|
Spacing unit: 8px
|
||||||
|
|
||||||
|
Margins/Paddings:
|
||||||
|
xs: 4px
|
||||||
|
sm: 8px
|
||||||
|
md: 16px
|
||||||
|
lg: 24px
|
||||||
|
xl: 32px
|
||||||
|
|
||||||
|
Container:
|
||||||
|
Max width: 1440px
|
||||||
|
Padding: 24px (desktop), 16px (tablet), 12px (mobile)
|
||||||
|
|
||||||
|
Table:
|
||||||
|
Min width: 1024px
|
||||||
|
Horizontal scroll на меньших экранах
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.8 Breakpoints
|
||||||
|
|
||||||
|
```
|
||||||
|
xs: 0px
|
||||||
|
sm: 600px
|
||||||
|
md: 900px
|
||||||
|
lg: 1200px
|
||||||
|
xl: 1536px
|
||||||
|
|
||||||
|
Desktop-first approach:
|
||||||
|
Основной дизайн для lg+ (1200px+)
|
||||||
|
Адаптация для md (900-1199px)
|
||||||
|
Горизонтальный скролл для sm и ниже
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Error States
|
||||||
|
|
||||||
|
### 6.1 Validation Errors
|
||||||
|
|
||||||
|
```
|
||||||
|
Inline под полем:
|
||||||
|
Color: #D32F2F
|
||||||
|
Font: 12px
|
||||||
|
Icon: ErrorOutline (слева от текста)
|
||||||
|
Margin top: 4px
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 API Errors
|
||||||
|
|
||||||
|
```
|
||||||
|
Toast notification (Snackbar):
|
||||||
|
Position: bottom-left
|
||||||
|
Duration: 5000ms (auto-hide)
|
||||||
|
Background: #D32F2F
|
||||||
|
Color: #FFFFFF
|
||||||
|
Action: "Повторить" (если применимо)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 Empty States
|
||||||
|
|
||||||
|
```
|
||||||
|
Центрированный блок:
|
||||||
|
Icon: 64px, Grey 400
|
||||||
|
Title: H2, Grey 700
|
||||||
|
Description: Body1, Grey 500
|
||||||
|
Action: Primary Button
|
||||||
|
|
||||||
|
Примеры:
|
||||||
|
Нет идей: "Список пуст. Создайте первую идею!"
|
||||||
|
Нет команды: "Добавьте членов команды для AI-оценки"
|
||||||
|
Нет результатов: "По вашему запросу ничего не найдено"
|
||||||
|
```
|
||||||
53
CLAUDE.md
Normal file
53
CLAUDE.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Team Planner
|
||||||
|
|
||||||
|
## Prompt
|
||||||
|
|
||||||
|
Ты работаешь над Team Planner — приложением для управления бэклогом идей команды.
|
||||||
|
|
||||||
|
**Порядок чтения:**
|
||||||
|
1. DEVELOPMENT.md — правила работы (обязательно!)
|
||||||
|
2. CONTEXT.md — текущий статус
|
||||||
|
3. ROADMAP.md — план и задачи
|
||||||
|
4. REQUIREMENTS.md / ARCHITECTURE.md — по необходимости
|
||||||
|
|
||||||
|
После работы обнови CONTEXT.md.
|
||||||
|
|
||||||
|
После прочтения скажи "Жду инструкций"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Важные файлы
|
||||||
|
|
||||||
|
- [DEVELOPMENT.md](DEVELOPMENT.md) — **читай первым!** Правила локальной разработки
|
||||||
|
- [CONTEXT.md](CONTEXT.md) — текущий статус, что сделано
|
||||||
|
- [ROADMAP.md](ROADMAP.md) — план разработки, задачи по фазам
|
||||||
|
- [REQUIREMENTS.md](REQUIREMENTS.md) — требования к продукту
|
||||||
|
- [ARCHITECTURE.md](ARCHITECTURE.md) — C4, sequence diagrams, API контракты, UI прототипы
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
team-planner/
|
||||||
|
├── backend/ # NestJS API
|
||||||
|
└── frontend/ # React + TypeScript
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ключевые сущности
|
||||||
|
|
||||||
|
- **Idea** — идея с полями: статус, приоритет, модуль, описание, целевая аудитория, боль, роль AI, способ проверки, цвет, комментарии
|
||||||
|
- **TeamMember** — член команды с ролью и матрицей производительности
|
||||||
|
- **Comment** — комментарий к идее
|
||||||
|
|
||||||
|
## Технологии
|
||||||
|
|
||||||
|
| Backend | Frontend |
|
||||||
|
|---------|----------|
|
||||||
|
| NestJS | React 18+ |
|
||||||
|
| TypeORM | Zustand |
|
||||||
|
| PostgreSQL | MUI + TanStack Table |
|
||||||
|
| WebSocket | dnd-kit |
|
||||||
|
|
||||||
|
## AI-интеграция
|
||||||
|
|
||||||
|
Используется ai-proxy service для оценки трудозатрат.
|
||||||
|
Гайд: `/Users/vigdorov/dev/gptunnel-service/INTEGRATION.md`
|
||||||
109
CONTEXT.md
Normal file
109
CONTEXT.md
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
# Контекст проекта Team Planner
|
||||||
|
|
||||||
|
> Этот файл обновляется агентами для передачи контекста. **Обновляй его после каждой значимой работы!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Текущий статус
|
||||||
|
|
||||||
|
**Этап:** Фаза 1 (Frontend) завершена
|
||||||
|
**Фаза MVP:** Готов к тестированию базового функционала
|
||||||
|
**Последнее обновление:** 2025-12-29
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что сделано
|
||||||
|
|
||||||
|
| Дата | Что сделано |
|
||||||
|
|------|-------------|
|
||||||
|
| 2025-12-29 | Созданы REQUIREMENTS.md, CLAUDE.md, CONTEXT.md |
|
||||||
|
| 2025-12-29 | Создан ARCHITECTURE.md (C4, sequences, API, UI prototypes, спецификация) |
|
||||||
|
| 2025-12-29 | Создан ROADMAP.md — план разработки по фазам |
|
||||||
|
| 2025-12-29 | Создан DEVELOPMENT.md — правила локальной разработки |
|
||||||
|
| 2025-12-29 | **Фаза 0:** docker-compose.yml для PostgreSQL |
|
||||||
|
| 2025-12-29 | **Фаза 0:** Backend (NestJS + TypeORM + PostgreSQL + class-validator) |
|
||||||
|
| 2025-12-29 | **Фаза 0:** Frontend (Vite + React + MUI + Zustand + TanStack + dnd-kit) |
|
||||||
|
| 2025-12-29 | **Фаза 1:** Backend Ideas module (entity, DTO, service, controller) |
|
||||||
|
| 2025-12-29 | **Фаза 1:** Frontend — API сервис (services/ideas.ts) |
|
||||||
|
| 2025-12-29 | **Фаза 1:** Frontend — Zustand store для фильтров и пагинации |
|
||||||
|
| 2025-12-29 | **Фаза 1:** Frontend — React Query хуки (useIdeas.ts) |
|
||||||
|
| 2025-12-29 | **Фаза 1:** Frontend — IdeasTable с TanStack Table |
|
||||||
|
| 2025-12-29 | **Фаза 1:** Frontend — Фильтры (статус, приоритет, модуль, поиск) |
|
||||||
|
| 2025-12-29 | **Фаза 1:** Frontend — Inline-редактирование ячеек (double-click) |
|
||||||
|
| 2025-12-29 | **Фаза 1:** Frontend — Модалка создания идеи |
|
||||||
|
| 2025-12-29 | **Фаза 1:** Frontend — Skeleton loader и empty state |
|
||||||
|
| 2025-12-29 | **Фаза 1:** Frontend — Удаление идей |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Текущая задача
|
||||||
|
|
||||||
|
> Смотри [ROADMAP.md](ROADMAP.md) для полного плана разработки
|
||||||
|
|
||||||
|
**Сейчас:** Тестирование Фазы 1, затем Фаза 2 (Drag&Drop, цвета, комментарии)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Файловая структура
|
||||||
|
|
||||||
|
```
|
||||||
|
team-planner/
|
||||||
|
├── CLAUDE.md # Точка входа для агентов
|
||||||
|
├── DEVELOPMENT.md # Правила локальной разработки
|
||||||
|
├── CONTEXT.md # Этот файл — текущий контекст
|
||||||
|
├── REQUIREMENTS.md # Требования к продукту
|
||||||
|
├── ARCHITECTURE.md # Архитектура, API, UI
|
||||||
|
├── ROADMAP.md # План разработки
|
||||||
|
├── docker-compose.yml # PostgreSQL и сервисы
|
||||||
|
├── backend/ # NestJS API
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── ideas/ # Модуль идей (готов)
|
||||||
|
│ │ ├── team/ # Модуль команды (Фаза 2)
|
||||||
|
│ │ ├── comments/ # Модуль комментариев (Фаза 2)
|
||||||
|
│ │ └── ai/ # AI-оценка (Фаза 3)
|
||||||
|
│ └── ...
|
||||||
|
└── frontend/ # React приложение
|
||||||
|
├── src/
|
||||||
|
│ ├── components/
|
||||||
|
│ │ ├── IdeasTable/ # Таблица идей с inline-редактированием
|
||||||
|
│ │ ├── IdeasFilters/ # Фильтры
|
||||||
|
│ │ └── CreateIdeaModal/ # Модалка создания
|
||||||
|
│ ├── hooks/
|
||||||
|
│ │ └── useIdeas.ts # React Query хуки
|
||||||
|
│ ├── services/
|
||||||
|
│ │ ├── api.ts # Axios instance
|
||||||
|
│ │ └── ideas.ts # API методы для идей
|
||||||
|
│ ├── store/
|
||||||
|
│ │ └── ideas.ts # Zustand store
|
||||||
|
│ └── types/
|
||||||
|
│ └── idea.ts # TypeScript типы
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ключевые решения
|
||||||
|
|
||||||
|
| Решение | Выбор | Причина |
|
||||||
|
|---------|-------|---------|
|
||||||
|
| ORM | TypeORM | Указано в требованиях |
|
||||||
|
| State Management | Zustand | Простота, минимальный boilerplate |
|
||||||
|
| UI Library | MUI | Богатый набор компонентов |
|
||||||
|
| Таблица | TanStack Table | Гибкость, виртуализация |
|
||||||
|
| Drag & Drop | dnd-kit | Современный, хорошая поддержка |
|
||||||
|
| Data Fetching | React Query | Кэширование, оптимистичные обновления |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Блокеры / Проблемы
|
||||||
|
|
||||||
|
*Пока нет*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Заметки
|
||||||
|
|
||||||
|
- AI-интеграция через ai-proxy: `/Users/vigdorov/dev/gptunnel-service/INTEGRATION.md`
|
||||||
|
- Многопользовательский режим НЕ нужен
|
||||||
|
- Экспорт и интеграции НЕ нужны
|
||||||
|
- Warning о React Compiler и TanStack Table можно игнорировать
|
||||||
148
DEVELOPMENT.md
Normal file
148
DEVELOPMENT.md
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
# Правила локальной разработки
|
||||||
|
|
||||||
|
> **Обязательно к прочтению перед началом работы!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Инструменты
|
||||||
|
|
||||||
|
### MCP серверы
|
||||||
|
- **Serena** — для работы с кодом (символьная навигация, редактирование)
|
||||||
|
- **Context7** — для получения актуальной документации по библиотекам
|
||||||
|
|
||||||
|
Используй эти инструменты для эффективной работы с кодовой базой.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Локальное окружение
|
||||||
|
|
||||||
|
### Порты
|
||||||
|
| Сервис | Порт |
|
||||||
|
|--------|------|
|
||||||
|
| Frontend (React) | 4000 |
|
||||||
|
| Backend (NestJS) | 4001 |
|
||||||
|
| PostgreSQL | 5432 |
|
||||||
|
|
||||||
|
### База данных
|
||||||
|
PostgreSQL поднимается в Docker. Файл `docker-compose.yml` в корне проекта.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск БД
|
||||||
|
docker-compose up -d postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Правила работы
|
||||||
|
|
||||||
|
### 1. Никогда не запускай сервисы самостоятельно
|
||||||
|
|
||||||
|
**ЗАПРЕЩЕНО** запускать `npm run dev`, `npm start` и подобные команды.
|
||||||
|
|
||||||
|
**Вместо этого:**
|
||||||
|
1. Убедись, что команда запуска есть в `package.json`
|
||||||
|
2. Если команды нет — создай её
|
||||||
|
3. Попроси пользователя запустить:
|
||||||
|
|
||||||
|
```
|
||||||
|
Запусти, пожалуйста:
|
||||||
|
cd backend && npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Тестирование — только ручное
|
||||||
|
|
||||||
|
После завершения задачи:
|
||||||
|
1. Опиши сценарии для проверки
|
||||||
|
2. Попроси пользователя проверить вручную
|
||||||
|
3. Дождись фидбека
|
||||||
|
|
||||||
|
**Формат:**
|
||||||
|
```
|
||||||
|
Готово! Проверь, пожалуйста:
|
||||||
|
|
||||||
|
1. Открой http://localhost:4000
|
||||||
|
2. Нажми кнопку "Создать идею"
|
||||||
|
3. Заполни форму и сохрани
|
||||||
|
4. Убедись, что идея появилась в списке
|
||||||
|
|
||||||
|
Напиши, если что-то не работает.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Работа поэтапно
|
||||||
|
|
||||||
|
Делай работу **небольшими итерациями**:
|
||||||
|
|
||||||
|
1. **Один этап = одна логическая единица**
|
||||||
|
- Один endpoint
|
||||||
|
- Один компонент
|
||||||
|
- Одна фича
|
||||||
|
|
||||||
|
2. **После каждого этапа:**
|
||||||
|
- Сообщи что сделано
|
||||||
|
- Дай сценарий проверки
|
||||||
|
- Дождись подтверждения
|
||||||
|
|
||||||
|
3. **Не делай сразу много:**
|
||||||
|
- ❌ Весь CRUD за раз
|
||||||
|
- ✅ Сначала GET, проверили, потом POST, проверили...
|
||||||
|
|
||||||
|
### 4. Команды в package.json
|
||||||
|
|
||||||
|
Все команды запуска должны быть в `package.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
// backend/package.json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nest start --watch",
|
||||||
|
"build": "nest build",
|
||||||
|
"start:prod": "node dist/main"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// frontend/package.json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --port 4000",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Чеклист перед началом работы
|
||||||
|
|
||||||
|
- [ ] Прочитал CONTEXT.md
|
||||||
|
- [ ] Прочитал ROADMAP.md (понял текущую фазу)
|
||||||
|
- [ ] Понял задачу
|
||||||
|
- [ ] Готов работать поэтапно
|
||||||
|
- [ ] Помню: не запускаю сервисы сам, прошу пользователя
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Пример правильного workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
Пользователь: Сделай endpoint для получения списка идей
|
||||||
|
|
||||||
|
Агент:
|
||||||
|
1. Читает документацию через Context7 (NestJS, TypeORM)
|
||||||
|
2. Использует Serena для навигации по коду
|
||||||
|
3. Создаёт endpoint GET /api/ideas
|
||||||
|
4. Сообщает:
|
||||||
|
|
||||||
|
"Создал endpoint GET /api/ideas.
|
||||||
|
|
||||||
|
Запусти backend:
|
||||||
|
cd backend && npm run dev
|
||||||
|
|
||||||
|
Проверь:
|
||||||
|
curl http://localhost:4001/api/ideas
|
||||||
|
|
||||||
|
Должен вернуться пустой массив: { data: [], meta: {...} }"
|
||||||
|
|
||||||
|
5. Ждёт фидбек
|
||||||
|
6. Переходит к следующему этапу
|
||||||
|
```
|
||||||
152
REQUIREMENTS.md
Normal file
152
REQUIREMENTS.md
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
# Team Planner - Требования к приложению
|
||||||
|
|
||||||
|
## Описание продукта
|
||||||
|
|
||||||
|
Приложение для команды разработки, позволяющее собирать бэклог идей, приоритизировать его, проводить брейнштормы, оценивать задачи и оставлять комментарии.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Функциональные требования
|
||||||
|
|
||||||
|
### 1. Управление идеями
|
||||||
|
|
||||||
|
#### 1.1 Список идей
|
||||||
|
Таблица со следующими полями для каждой идеи:
|
||||||
|
|
||||||
|
| Поле | Описание | Тип |
|
||||||
|
|------|----------|-----|
|
||||||
|
| Статус | Текущее состояние идеи (Новая, В обсуждении, Одобрена, В работе, Готово, Отклонена) | Enum |
|
||||||
|
| Приоритет | Важность идеи (Критический, Высокий, Средний, Низкий) | Enum |
|
||||||
|
| Модуль | Где реализуется (Frontend, Backend, AI, Mobile, Infrastructure, Other) | Enum/Multi-select |
|
||||||
|
| Суть идеи | Краткое описание идеи | Text |
|
||||||
|
| Для кого | Целевая аудитория / пользователь | Text |
|
||||||
|
| Какую боль решает | Проблема, которую решает идея | Text |
|
||||||
|
| Роль AI | Что делает ИИ в рамках этой идеи (если применимо) | Text |
|
||||||
|
| Способ проверки | Быстрый способ валидации идеи / MVP | Text |
|
||||||
|
| Комментарии | Обсуждение и заметки | Text (список комментариев) |
|
||||||
|
| Цвет | Цветовая маркировка строки | Color |
|
||||||
|
| Оценка времени | AI-генерируемая оценка трудозатрат | Calculated |
|
||||||
|
|
||||||
|
#### 1.2 Редактирование идей
|
||||||
|
- **Inline-редактирование**: возможность редактировать любое поле прямо в таблице двойным кликом
|
||||||
|
- **Быстрое изменение статуса и приоритета** через dropdown
|
||||||
|
- **Автосохранение** изменений
|
||||||
|
|
||||||
|
#### 1.3 Drag & Drop
|
||||||
|
- Перемещение идей в списке для ручной сортировки
|
||||||
|
- Визуальная индикация при перетаскивании
|
||||||
|
- Сохранение порядка после перемещения
|
||||||
|
|
||||||
|
#### 1.4 Цветовая маркировка
|
||||||
|
- Возможность назначить цвет строке для визуального выделения
|
||||||
|
- Предустановленная палитра цветов
|
||||||
|
- Фильтрация по цвету
|
||||||
|
|
||||||
|
### 2. Сортировка и фильтрация
|
||||||
|
|
||||||
|
#### 2.1 Сортировка
|
||||||
|
- По любому полю (клик на заголовок колонки)
|
||||||
|
- Множественная сортировка (Shift + клик)
|
||||||
|
- Сохранение настроек сортировки
|
||||||
|
|
||||||
|
#### 2.2 Фильтры
|
||||||
|
- Фильтр по статусу
|
||||||
|
- Фильтр по приоритету
|
||||||
|
- Фильтр по модулю
|
||||||
|
- Фильтр по цвету
|
||||||
|
- Текстовый поиск по всем полям
|
||||||
|
- Сохранение пресетов фильтров
|
||||||
|
|
||||||
|
### 3. AI-оценка времени
|
||||||
|
|
||||||
|
#### 3.1 Управление командой
|
||||||
|
- Список членов команды с ролями:
|
||||||
|
- Backend-разработчик
|
||||||
|
- Frontend-разработчик
|
||||||
|
- AI/ML-инженер
|
||||||
|
- Аналитик
|
||||||
|
- Тестировщик (QA)
|
||||||
|
- DevOps
|
||||||
|
- Дизайнер
|
||||||
|
- Project Manager
|
||||||
|
- Количество сотрудников каждой роли
|
||||||
|
|
||||||
|
#### 3.2 Матрица производительности
|
||||||
|
Для каждого сотрудника/роли указывается время на задачи разной сложности:
|
||||||
|
|
||||||
|
| Сложность | Описание | Пример времени |
|
||||||
|
|-----------|----------|----------------|
|
||||||
|
| Trivial | Тривиальная задача | 1-2 часа |
|
||||||
|
| Easy | Лёгкая задача | 0.5-1 день |
|
||||||
|
| Medium | Средняя задача | 2-3 дня |
|
||||||
|
| Hard | Сложная задача | 1-2 недели |
|
||||||
|
| Epic | Эпик / большая фича | 2-4 недели |
|
||||||
|
|
||||||
|
#### 3.3 AI-функционал
|
||||||
|
- Анализ описания идеи
|
||||||
|
- Определение необходимых ролей для реализации
|
||||||
|
- Оценка сложности для каждой роли
|
||||||
|
- Расчёт общего времени с учётом состава команды
|
||||||
|
- Рекомендации по оптимизации
|
||||||
|
|
||||||
|
### 4. Комментарии
|
||||||
|
|
||||||
|
- Добавление комментариев к идее
|
||||||
|
- Ответы на комментарии (треды)
|
||||||
|
- Упоминание участников (@mention)
|
||||||
|
- История комментариев
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Технические требования
|
||||||
|
|
||||||
|
### Backend (NestJS)
|
||||||
|
|
||||||
|
#### Стек технологий
|
||||||
|
- **Framework**: NestJS
|
||||||
|
- **Language**: TypeScript
|
||||||
|
- **Database**: PostgreSQL
|
||||||
|
- **ORM**: TypeORM
|
||||||
|
- **API**: REST + WebSocket (для real-time обновлений)
|
||||||
|
- **AI Integration**: ai-proxy service тут лежит гайд по интеграции /Users/vigdorov/dev/gptunnel-service/INTEGRATION.md
|
||||||
|
|
||||||
|
### Frontend (React + TypeScript)
|
||||||
|
|
||||||
|
#### Стек технологий
|
||||||
|
- **Framework**: React 18+
|
||||||
|
- **Language**: TypeScript
|
||||||
|
- **State Management**: Zustand
|
||||||
|
- **UI Library**: MUI
|
||||||
|
- **Table**: TanStack Table (react-table)
|
||||||
|
- **Drag & Drop**: dnd-kit / react-beautiful-dnd
|
||||||
|
- **HTTP Client**: Axios / React Query
|
||||||
|
- **Styling**: Tailwind CSS / CSS Modules
|
||||||
|
|
||||||
|
## Нефункциональные требования
|
||||||
|
|
||||||
|
### Производительность
|
||||||
|
- Виртуализация списка при большом количестве идей (1000+)
|
||||||
|
- Оптимистичные обновления UI
|
||||||
|
- Кэширование данных
|
||||||
|
|
||||||
|
### UX
|
||||||
|
- Отзывчивый интерфейс (< 100ms на действие)
|
||||||
|
- Keyboard shortcuts для частых операций
|
||||||
|
- Responsive design (desktop-first)
|
||||||
|
|
||||||
|
### Безопасность
|
||||||
|
- Валидация входных данных
|
||||||
|
- Rate limiting для AI-запросов
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Открытые вопросы
|
||||||
|
|
||||||
|
1. Нужна ли многопользовательская работа и разграничение прав?
|
||||||
|
НЕТ
|
||||||
|
2. Требуется ли история изменений (audit log)?
|
||||||
|
НЕТ
|
||||||
|
4. Нужен ли экспорт данных (CSV, Excel)?
|
||||||
|
НЕТ
|
||||||
|
5. Интеграция с внешними системами (Jira, Trello)?
|
||||||
|
НЕТ
|
||||||
161
ROADMAP.md
Normal file
161
ROADMAP.md
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
# План разработки Team Planner
|
||||||
|
|
||||||
|
> **Это основной источник истины для всех планов по проекту.**
|
||||||
|
> Обновляй этот файл при изменении планов или завершении задач.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Обзор фаз
|
||||||
|
|
||||||
|
| Фаза | Название | Статус | Описание |
|
||||||
|
|------|----------|--------|----------|
|
||||||
|
| 0 | Инициализация | ⏳ В процессе | Настройка проектов, инфраструктура |
|
||||||
|
| 1 | Базовый функционал | ⏸️ Ожидает | CRUD идей, таблица, редактирование |
|
||||||
|
| 2 | Расширенный функционал | ⏸️ Ожидает | Drag&Drop, цвета, комментарии, команда |
|
||||||
|
| 3 | AI-интеграция | ⏸️ Ожидает | Оценка времени, рекомендации |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 0: Инициализация
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- [ ] Создать NestJS проект (`nest new backend`)
|
||||||
|
- [ ] Настроить TypeORM + PostgreSQL
|
||||||
|
- [ ] Создать docker-compose для PostgreSQL
|
||||||
|
- [ ] Настроить базовую структуру модулей
|
||||||
|
- [ ] Добавить глобальную валидацию (class-validator)
|
||||||
|
- [ ] Настроить CORS
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- [ ] Создать React проект (Vite + TypeScript)
|
||||||
|
- [ ] Установить и настроить MUI
|
||||||
|
- [ ] Установить Zustand
|
||||||
|
- [ ] Установить TanStack Table
|
||||||
|
- [ ] Установить dnd-kit
|
||||||
|
- [ ] Настроить Axios + React Query
|
||||||
|
- [ ] Создать базовую структуру папок
|
||||||
|
|
||||||
|
### Инфраструктура
|
||||||
|
- [ ] Настроить ESLint + Prettier для обоих проектов
|
||||||
|
- [ ] Создать общий docker-compose
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 1: Базовый функционал
|
||||||
|
|
||||||
|
### Backend — Модуль Ideas
|
||||||
|
- [ ] Создать сущность Idea (entity)
|
||||||
|
- [ ] Создать DTO (CreateIdeaDto, UpdateIdeaDto, QueryIdeasDto)
|
||||||
|
- [ ] Реализовать IdeasService
|
||||||
|
- [ ] Реализовать IdeasController
|
||||||
|
- [ ] GET /api/ideas (с пагинацией, фильтрами, сортировкой)
|
||||||
|
- [ ] POST /api/ideas
|
||||||
|
- [ ] PATCH /api/ideas/:id
|
||||||
|
- [ ] DELETE /api/ideas/:id
|
||||||
|
- [ ] Добавить валидацию
|
||||||
|
- [ ] Написать тесты
|
||||||
|
|
||||||
|
### Frontend — Таблица идей
|
||||||
|
- [ ] Создать типы (types/idea.ts)
|
||||||
|
- [ ] Создать API-сервис (services/ideas.ts)
|
||||||
|
- [ ] Создать Zustand store (store/ideas.ts)
|
||||||
|
- [ ] Создать компонент IdeasTable
|
||||||
|
- [ ] Отображение колонок
|
||||||
|
- [ ] Пагинация
|
||||||
|
- [ ] Сортировка (клик по заголовку)
|
||||||
|
- [ ] Создать компоненты фильтров
|
||||||
|
- [ ] Фильтр по статусу
|
||||||
|
- [ ] Фильтр по приоритету
|
||||||
|
- [ ] Фильтр по модулю
|
||||||
|
- [ ] Текстовый поиск
|
||||||
|
- [ ] Inline-редактирование ячеек
|
||||||
|
- [ ] Double-click для редактирования
|
||||||
|
- [ ] Автосохранение при blur/Enter
|
||||||
|
- [ ] Оптимистичные обновления
|
||||||
|
- [ ] Создать модалку создания идеи
|
||||||
|
- [ ] Добавить skeleton loader
|
||||||
|
- [ ] Добавить empty state
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 2: Расширенный функционал
|
||||||
|
|
||||||
|
### Backend — Дополнения
|
||||||
|
- [ ] PATCH /api/ideas/reorder (изменение порядка)
|
||||||
|
- [ ] Модуль Comments
|
||||||
|
- [ ] Сущность Comment
|
||||||
|
- [ ] GET /api/ideas/:id/comments
|
||||||
|
- [ ] POST /api/ideas/:id/comments
|
||||||
|
- [ ] DELETE /api/comments/:id
|
||||||
|
- [ ] Модуль Team
|
||||||
|
- [ ] Сущность TeamMember
|
||||||
|
- [ ] CRUD endpoints
|
||||||
|
- [ ] GET /api/team/summary
|
||||||
|
|
||||||
|
### Frontend — Drag & Drop
|
||||||
|
- [ ] Интегрировать dnd-kit в таблицу
|
||||||
|
- [ ] Drag handle в первой колонке
|
||||||
|
- [ ] Визуальная индикация при перетаскивании
|
||||||
|
- [ ] Сохранение порядка на сервер
|
||||||
|
|
||||||
|
### Frontend — Цветовая маркировка
|
||||||
|
- [ ] Добавить поле color в таблицу
|
||||||
|
- [ ] Цветовой фон строки
|
||||||
|
- [ ] Picker для выбора цвета
|
||||||
|
- [ ] Фильтр по цвету
|
||||||
|
|
||||||
|
### Frontend — Комментарии
|
||||||
|
- [ ] Раскрывающаяся панель под строкой
|
||||||
|
- [ ] Список комментариев с тредами
|
||||||
|
- [ ] Форма добавления комментария
|
||||||
|
- [ ] Ответы на комментарии
|
||||||
|
|
||||||
|
### Frontend — Управление командой
|
||||||
|
- [ ] Страница /team
|
||||||
|
- [ ] Сводка по ролям
|
||||||
|
- [ ] Таблица участников
|
||||||
|
- [ ] Модалка добавления/редактирования
|
||||||
|
- [ ] Матрица производительности (время на задачи по сложности)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Фаза 3: AI-интеграция
|
||||||
|
|
||||||
|
### Backend — Модуль AI
|
||||||
|
- [ ] Интегрировать ai-proxy service
|
||||||
|
- [ ] POST /api/ai/estimate
|
||||||
|
- [ ] Получить идею и состав команды
|
||||||
|
- [ ] Сформировать промпт
|
||||||
|
- [ ] Отправить запрос в AI
|
||||||
|
- [ ] Распарсить ответ
|
||||||
|
- [ ] Сохранить оценку
|
||||||
|
- [ ] Rate limiting для AI-запросов
|
||||||
|
|
||||||
|
### Frontend — AI-оценка
|
||||||
|
- [ ] Кнопка "Оценить AI" в строке/детали идеи
|
||||||
|
- [ ] Модалка с результатом оценки
|
||||||
|
- [ ] Общее время
|
||||||
|
- [ ] Сложность
|
||||||
|
- [ ] Разбивка по ролям
|
||||||
|
- [ ] Рекомендации
|
||||||
|
- [ ] Отображение оценки в таблице
|
||||||
|
- [ ] Loading state для AI-запросов
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backlog (после MVP)
|
||||||
|
|
||||||
|
- [ ] WebSocket для real-time обновлений
|
||||||
|
- [ ] Виртуализация списка (1000+ идей)
|
||||||
|
- [ ] Keyboard shortcuts
|
||||||
|
- [ ] Сохранение пресетов фильтров
|
||||||
|
- [ ] Темная тема
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Принципы разработки
|
||||||
|
|
||||||
|
1. **Вертикальная разработка** — делаем полный flow (BE → FE) для каждой фичи
|
||||||
|
2. **Инкрементальность** — сначала базовое, потом улучшаем
|
||||||
|
3. **Тестирование** — покрываем критичный функционал
|
||||||
|
4. **Документирование** — обновляем CONTEXT.md после значимых изменений
|
||||||
9
backend/.env.example
Normal file
9
backend/.env.example
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Database
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_USERNAME=teamplanner
|
||||||
|
DB_PASSWORD=teamplanner
|
||||||
|
DB_DATABASE=teamplanner
|
||||||
|
|
||||||
|
# App
|
||||||
|
PORT=4001
|
||||||
4
backend/.prettierrc
Normal file
4
backend/.prettierrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
98
backend/README.md
Normal file
98
backend/README.md
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||||
|
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||||
|
|
||||||
|
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||||
|
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||||
|
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||||
|
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||||
|
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
||||||
|
</p>
|
||||||
|
<!--[](https://opencollective.com/nest#backer)
|
||||||
|
[](https://opencollective.com/nest#sponsor)-->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compile and run the project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# development
|
||||||
|
$ npm run start
|
||||||
|
|
||||||
|
# watch mode
|
||||||
|
$ npm run start:dev
|
||||||
|
|
||||||
|
# production mode
|
||||||
|
$ npm run start:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# unit tests
|
||||||
|
$ npm run test
|
||||||
|
|
||||||
|
# e2e tests
|
||||||
|
$ npm run test:e2e
|
||||||
|
|
||||||
|
# test coverage
|
||||||
|
$ npm run test:cov
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
|
||||||
|
|
||||||
|
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install -g @nestjs/mau
|
||||||
|
$ mau deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
Check out a few resources that may come in handy when working with NestJS:
|
||||||
|
|
||||||
|
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
|
||||||
|
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
|
||||||
|
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
|
||||||
|
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
|
||||||
|
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
|
||||||
|
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
|
||||||
|
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
|
||||||
|
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||||
|
|
||||||
|
## Stay in touch
|
||||||
|
|
||||||
|
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
||||||
|
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||||
|
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
||||||
35
backend/eslint.config.mjs
Normal file
35
backend/eslint.config.mjs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// @ts-check
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||||
|
import globals from 'globals';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: ['eslint.config.mjs'],
|
||||||
|
},
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
eslintPluginPrettierRecommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.jest,
|
||||||
|
},
|
||||||
|
sourceType: 'commonjs',
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-floating-promises': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||||
|
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
8
backend/nest-cli.json
Normal file
8
backend/nest-cli.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true
|
||||||
|
}
|
||||||
|
}
|
||||||
10678
backend/package-lock.json
generated
Normal file
10678
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
79
backend/package.json
Normal file
79
backend/package.json
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
{
|
||||||
|
"name": "backend",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"dev": "nest start --watch",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/common": "^11.0.1",
|
||||||
|
"@nestjs/config": "^4.0.2",
|
||||||
|
"@nestjs/core": "^11.0.1",
|
||||||
|
"@nestjs/mapped-types": "^2.1.0",
|
||||||
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.3",
|
||||||
|
"pg": "^8.16.3",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"typeorm": "^0.3.28"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@nestjs/cli": "^11.0.0",
|
||||||
|
"@nestjs/schematics": "^11.0.0",
|
||||||
|
"@nestjs/testing": "^11.0.1",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
|
"@types/node": "^22.10.7",
|
||||||
|
"@types/supertest": "^6.0.2",
|
||||||
|
"eslint": "^9.18.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-prettier": "^5.2.2",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"jest": "^30.0.0",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
|
"ts-loader": "^9.5.2",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"typescript-eslint": "^8.20.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
backend/src/app.controller.spec.ts
Normal file
22
backend/src/app.controller.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
describe('AppController', () => {
|
||||||
|
let appController: AppController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const app: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
appController = app.get<AppController>(AppController);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('root', () => {
|
||||||
|
it('should return "Hello World!"', () => {
|
||||||
|
expect(appController.getHello()).toBe('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
12
backend/src/app.controller.ts
Normal file
12
backend/src/app.controller.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class AppController {
|
||||||
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
getHello(): string {
|
||||||
|
return this.appService.getHello();
|
||||||
|
}
|
||||||
|
}
|
||||||
32
backend/src/app.module.ts
Normal file
32
backend/src/app.module.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
import { IdeasModule } from './ideas/ideas.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
}),
|
||||||
|
TypeOrmModule.forRootAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: (configService: ConfigService) => ({
|
||||||
|
type: 'postgres',
|
||||||
|
host: configService.get('DB_HOST', 'localhost'),
|
||||||
|
port: configService.get<number>('DB_PORT', 5432),
|
||||||
|
username: configService.get('DB_USERNAME', 'teamplanner'),
|
||||||
|
password: configService.get('DB_PASSWORD', 'teamplanner'),
|
||||||
|
database: configService.get('DB_DATABASE', 'teamplanner'),
|
||||||
|
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||||||
|
synchronize: true, // Only for development!
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
IdeasModule,
|
||||||
|
],
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
8
backend/src/app.service.ts
Normal file
8
backend/src/app.service.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AppService {
|
||||||
|
getHello(): string {
|
||||||
|
return 'Hello World!';
|
||||||
|
}
|
||||||
|
}
|
||||||
52
backend/src/ideas/dto/create-idea.dto.ts
Normal file
52
backend/src/ideas/dto/create-idea.dto.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
IsOptional,
|
||||||
|
IsEnum,
|
||||||
|
MaxLength,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { IdeaStatus, IdeaPriority } from '../entities/idea.entity';
|
||||||
|
|
||||||
|
export class CreateIdeaDto {
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(255)
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(IdeaStatus)
|
||||||
|
status?: IdeaStatus;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(IdeaPriority)
|
||||||
|
priority?: IdeaPriority;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(100)
|
||||||
|
module?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(255)
|
||||||
|
targetAudience?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
pain?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
aiRole?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
verificationMethod?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(20)
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
3
backend/src/ideas/dto/index.ts
Normal file
3
backend/src/ideas/dto/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './create-idea.dto';
|
||||||
|
export * from './update-idea.dto';
|
||||||
|
export * from './query-ideas.dto';
|
||||||
39
backend/src/ideas/dto/query-ideas.dto.ts
Normal file
39
backend/src/ideas/dto/query-ideas.dto.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { IsOptional, IsEnum, IsString, IsInt, Min, Max } from 'class-validator';
|
||||||
|
import { IdeaStatus, IdeaPriority } from '../entities/idea.entity';
|
||||||
|
|
||||||
|
export class QueryIdeasDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(IdeaStatus)
|
||||||
|
status?: IdeaStatus;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(IdeaPriority)
|
||||||
|
priority?: IdeaPriority;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
module?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
search?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
sortBy?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(['ASC', 'DESC'])
|
||||||
|
sortOrder?: 'ASC' | 'DESC';
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
page?: number = 1;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
@Max(100)
|
||||||
|
limit?: number = 20;
|
||||||
|
}
|
||||||
10
backend/src/ideas/dto/update-idea.dto.ts
Normal file
10
backend/src/ideas/dto/update-idea.dto.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { PartialType } from '@nestjs/mapped-types';
|
||||||
|
import { IsOptional, IsInt, Min } from 'class-validator';
|
||||||
|
import { CreateIdeaDto } from './create-idea.dto';
|
||||||
|
|
||||||
|
export class UpdateIdeaDto extends PartialType(CreateIdeaDto) {
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
order?: number;
|
||||||
|
}
|
||||||
75
backend/src/ideas/entities/idea.entity.ts
Normal file
75
backend/src/ideas/entities/idea.entity.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
export enum IdeaStatus {
|
||||||
|
BACKLOG = 'backlog',
|
||||||
|
TODO = 'todo',
|
||||||
|
IN_PROGRESS = 'in_progress',
|
||||||
|
DONE = 'done',
|
||||||
|
CANCELLED = 'cancelled',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum IdeaPriority {
|
||||||
|
LOW = 'low',
|
||||||
|
MEDIUM = 'medium',
|
||||||
|
HIGH = 'high',
|
||||||
|
CRITICAL = 'critical',
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity('ideas')
|
||||||
|
export class Idea {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
description: string | null;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'enum',
|
||||||
|
enum: IdeaStatus,
|
||||||
|
default: IdeaStatus.BACKLOG,
|
||||||
|
})
|
||||||
|
status: IdeaStatus;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'enum',
|
||||||
|
enum: IdeaPriority,
|
||||||
|
default: IdeaPriority.MEDIUM,
|
||||||
|
})
|
||||||
|
priority: IdeaPriority;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 100, nullable: true })
|
||||||
|
module: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'target_audience', type: 'varchar', length: 255, nullable: true })
|
||||||
|
targetAudience: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
pain: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'ai_role', type: 'text', nullable: true })
|
||||||
|
aiRole: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'verification_method', type: 'text', nullable: true })
|
||||||
|
verificationMethod: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 20, nullable: true })
|
||||||
|
color: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'int', default: 0 })
|
||||||
|
order: number;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at' })
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
51
backend/src/ideas/ideas.controller.ts
Normal file
51
backend/src/ideas/ideas.controller.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Patch,
|
||||||
|
Param,
|
||||||
|
Delete,
|
||||||
|
Query,
|
||||||
|
ParseUUIDPipe,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { IdeasService } from './ideas.service';
|
||||||
|
import { CreateIdeaDto, UpdateIdeaDto, QueryIdeasDto } from './dto';
|
||||||
|
|
||||||
|
@Controller('ideas')
|
||||||
|
export class IdeasController {
|
||||||
|
constructor(private readonly ideasService: IdeasService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
create(@Body() createIdeaDto: CreateIdeaDto) {
|
||||||
|
return this.ideasService.create(createIdeaDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
findAll(@Query() query: QueryIdeasDto) {
|
||||||
|
return this.ideasService.findAll(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('modules')
|
||||||
|
getModules() {
|
||||||
|
return this.ideasService.getModules();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
return this.ideasService.findOne(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
update(
|
||||||
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
|
@Body() updateIdeaDto: UpdateIdeaDto,
|
||||||
|
) {
|
||||||
|
return this.ideasService.update(id, updateIdeaDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
return this.ideasService.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
backend/src/ideas/ideas.module.ts
Normal file
13
backend/src/ideas/ideas.module.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { IdeasService } from './ideas.service';
|
||||||
|
import { IdeasController } from './ideas.controller';
|
||||||
|
import { Idea } from './entities/idea.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([Idea])],
|
||||||
|
controllers: [IdeasController],
|
||||||
|
providers: [IdeasService],
|
||||||
|
exports: [IdeasService],
|
||||||
|
})
|
||||||
|
export class IdeasModule {}
|
||||||
126
backend/src/ideas/ideas.service.ts
Normal file
126
backend/src/ideas/ideas.service.ts
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository, Like, FindOptionsWhere } from 'typeorm';
|
||||||
|
import { Idea } from './entities/idea.entity';
|
||||||
|
import { CreateIdeaDto, UpdateIdeaDto, QueryIdeasDto } from './dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class IdeasService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Idea)
|
||||||
|
private readonly ideasRepository: Repository<Idea>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async create(createIdeaDto: CreateIdeaDto): Promise<Idea> {
|
||||||
|
const maxOrder = await this.ideasRepository.maximum('order');
|
||||||
|
const idea = this.ideasRepository.create({
|
||||||
|
...createIdeaDto,
|
||||||
|
order: (maxOrder ?? -1) + 1,
|
||||||
|
});
|
||||||
|
return this.ideasRepository.save(idea);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(query: QueryIdeasDto) {
|
||||||
|
const {
|
||||||
|
status,
|
||||||
|
priority,
|
||||||
|
module,
|
||||||
|
search,
|
||||||
|
sortBy = 'order',
|
||||||
|
sortOrder = 'ASC',
|
||||||
|
page = 1,
|
||||||
|
limit = 20,
|
||||||
|
} = query;
|
||||||
|
|
||||||
|
const where: FindOptionsWhere<Idea> = {};
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
where.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (priority) {
|
||||||
|
where.priority = priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (module) {
|
||||||
|
where.module = module;
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryBuilder = this.ideasRepository.createQueryBuilder('idea');
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
queryBuilder.andWhere('idea.status = :status', { status });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (priority) {
|
||||||
|
queryBuilder.andWhere('idea.priority = :priority', { priority });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (module) {
|
||||||
|
queryBuilder.andWhere('idea.module = :module', { module });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
queryBuilder.andWhere(
|
||||||
|
'(idea.title ILIKE :search OR idea.description ILIKE :search)',
|
||||||
|
{ search: `%${search}%` },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const validSortFields = [
|
||||||
|
'order',
|
||||||
|
'title',
|
||||||
|
'status',
|
||||||
|
'priority',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
];
|
||||||
|
const sortField = validSortFields.includes(sortBy) ? sortBy : 'order';
|
||||||
|
|
||||||
|
queryBuilder
|
||||||
|
.orderBy(`idea.${sortField}`, sortOrder)
|
||||||
|
.skip((page - 1) * limit)
|
||||||
|
.take(limit);
|
||||||
|
|
||||||
|
const [data, total] = await queryBuilder.getManyAndCount();
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
meta: {
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string): Promise<Idea> {
|
||||||
|
const idea = await this.ideasRepository.findOne({ where: { id } });
|
||||||
|
if (!idea) {
|
||||||
|
throw new NotFoundException(`Idea with ID "${id}" not found`);
|
||||||
|
}
|
||||||
|
return idea;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, updateIdeaDto: UpdateIdeaDto): Promise<Idea> {
|
||||||
|
const idea = await this.findOne(id);
|
||||||
|
Object.assign(idea, updateIdeaDto);
|
||||||
|
return this.ideasRepository.save(idea);
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
const idea = await this.findOne(id);
|
||||||
|
await this.ideasRepository.remove(idea);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getModules(): Promise<string[]> {
|
||||||
|
const result = await this.ideasRepository
|
||||||
|
.createQueryBuilder('idea')
|
||||||
|
.select('DISTINCT idea.module', 'module')
|
||||||
|
.where('idea.module IS NOT NULL')
|
||||||
|
.getRawMany();
|
||||||
|
|
||||||
|
return result.map((r) => r.module).filter(Boolean);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
backend/src/main.ts
Normal file
32
backend/src/main.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
|
// Global prefix
|
||||||
|
app.setGlobalPrefix('api');
|
||||||
|
|
||||||
|
// CORS
|
||||||
|
app.enableCors({
|
||||||
|
origin: 'http://localhost:4000',
|
||||||
|
credentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Global validation pipe
|
||||||
|
app.useGlobalPipes(
|
||||||
|
new ValidationPipe({
|
||||||
|
whitelist: true,
|
||||||
|
transform: true,
|
||||||
|
transformOptions: {
|
||||||
|
enableImplicitConversion: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const port = process.env.PORT ?? 4001;
|
||||||
|
await app.listen(port);
|
||||||
|
console.log(`Backend running on http://localhost:${port}`);
|
||||||
|
}
|
||||||
|
bootstrap();
|
||||||
25
backend/test/app.e2e-spec.ts
Normal file
25
backend/test/app.e2e-spec.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { App } from 'supertest/types';
|
||||||
|
import { AppModule } from './../src/app.module';
|
||||||
|
|
||||||
|
describe('AppController (e2e)', () => {
|
||||||
|
let app: INestApplication<App>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/ (GET)', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
9
backend/test/jest-e2e.json
Normal file
9
backend/test/jest-e2e.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".e2e-spec.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
backend/tsconfig.build.json
Normal file
4
backend/tsconfig.build.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
25
backend/tsconfig.json
Normal file
25
backend/tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "nodenext",
|
||||||
|
"moduleResolution": "nodenext",
|
||||||
|
"resolvePackageJsonExports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2023",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"noFallthroughCasesInSwitch": false
|
||||||
|
}
|
||||||
|
}
|
||||||
22
docker-compose.yml
Normal file
22
docker-compose.yml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: team-planner-db
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: teamplanner
|
||||||
|
POSTGRES_PASSWORD: teamplanner
|
||||||
|
POSTGRES_DB: teamplanner
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U teamplanner"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
73
frontend/README.md
Normal file
73
frontend/README.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## React Compiler
|
||||||
|
|
||||||
|
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
|
||||||
|
// Remove tseslint.configs.recommended and replace with this
|
||||||
|
tseslint.configs.recommendedTypeChecked,
|
||||||
|
// Alternatively, use this for stricter rules
|
||||||
|
tseslint.configs.strictTypeChecked,
|
||||||
|
// Optionally, add this for stylistic rules
|
||||||
|
tseslint.configs.stylisticTypeChecked,
|
||||||
|
|
||||||
|
// Other configs...
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
// other options...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import reactX from 'eslint-plugin-react-x'
|
||||||
|
import reactDom from 'eslint-plugin-react-dom'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
// Enable lint rules for React
|
||||||
|
reactX.configs['recommended-typescript'],
|
||||||
|
// Enable lint rules for React DOM
|
||||||
|
reactDom.configs.recommended,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
// other options...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
23
frontend/eslint.config.js
Normal file
23
frontend/eslint.config.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
reactHooks.configs.flat.recommended,
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>frontend</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4328
frontend/package-lock.json
generated
Normal file
4328
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
frontend/package.json
Normal file
41
frontend/package.json
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@emotion/styled": "^11.14.1",
|
||||||
|
"@mui/icons-material": "^7.3.6",
|
||||||
|
"@mui/material": "^7.3.6",
|
||||||
|
"@tanstack/react-query": "^5.90.14",
|
||||||
|
"@tanstack/react-table": "^8.21.3",
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
|
"zustand": "^5.0.9"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/react": "^19.2.5",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
|
"globals": "^16.5.0",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"typescript-eslint": "^8.46.4",
|
||||||
|
"vite": "^7.2.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
frontend/public/vite.svg
Normal file
1
frontend/public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
42
frontend/src/App.tsx
Normal file
42
frontend/src/App.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Container, Typography, Box, Button } from '@mui/material';
|
||||||
|
import { Add } from '@mui/icons-material';
|
||||||
|
import { IdeasTable } from './components/IdeasTable';
|
||||||
|
import { IdeasFilters } from './components/IdeasFilters';
|
||||||
|
import { CreateIdeaModal } from './components/CreateIdeaModal';
|
||||||
|
import { useIdeasStore } from './store/ideas';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const { setCreateModalOpen } = useIdeasStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth="xl" sx={{ py: 4 }}>
|
||||||
|
<Box sx={{ mb: 4, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h4" component="h1">
|
||||||
|
Team Planner
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
Backlog management for your team
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<Add />}
|
||||||
|
onClick={() => setCreateModalOpen(true)}
|
||||||
|
>
|
||||||
|
New Idea
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<IdeasFilters />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<IdeasTable />
|
||||||
|
|
||||||
|
<CreateIdeaModal />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
1
frontend/src/assets/react.svg
Normal file
1
frontend/src/assets/react.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
186
frontend/src/components/CreateIdeaModal/CreateIdeaModal.tsx
Normal file
186
frontend/src/components/CreateIdeaModal/CreateIdeaModal.tsx
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Button,
|
||||||
|
TextField,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
Box,
|
||||||
|
Alert,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useIdeasStore } from '../../store/ideas';
|
||||||
|
import { useCreateIdea } from '../../hooks/useIdeas';
|
||||||
|
import type { CreateIdeaDto, IdeaStatus, IdeaPriority } from '../../types/idea';
|
||||||
|
|
||||||
|
const statusOptions: { value: IdeaStatus; label: string }[] = [
|
||||||
|
{ value: 'backlog', label: 'Backlog' },
|
||||||
|
{ value: 'todo', label: 'To Do' },
|
||||||
|
{ value: 'in_progress', label: 'In Progress' },
|
||||||
|
{ value: 'done', label: 'Done' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const priorityOptions: { value: IdeaPriority; label: string }[] = [
|
||||||
|
{ value: 'low', label: 'Low' },
|
||||||
|
{ value: 'medium', label: 'Medium' },
|
||||||
|
{ value: 'high', label: 'High' },
|
||||||
|
{ value: 'critical', label: 'Critical' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const initialFormData: CreateIdeaDto = {
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
status: 'backlog',
|
||||||
|
priority: 'medium',
|
||||||
|
module: '',
|
||||||
|
targetAudience: '',
|
||||||
|
pain: '',
|
||||||
|
aiRole: '',
|
||||||
|
verificationMethod: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CreateIdeaModal() {
|
||||||
|
const { createModalOpen, setCreateModalOpen } = useIdeasStore();
|
||||||
|
const createIdea = useCreateIdea();
|
||||||
|
const [formData, setFormData] = useState<CreateIdeaDto>(initialFormData);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setCreateModalOpen(false);
|
||||||
|
setFormData(initialFormData);
|
||||||
|
createIdea.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (field: keyof CreateIdeaDto, value: string) => {
|
||||||
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
await createIdea.mutateAsync(formData);
|
||||||
|
handleClose();
|
||||||
|
} catch {
|
||||||
|
// Error is handled by mutation state
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={createModalOpen} onClose={handleClose} maxWidth="sm" fullWidth>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<DialogTitle>Create New Idea</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
|
||||||
|
{createIdea.isError && (
|
||||||
|
<Alert severity="error">
|
||||||
|
Failed to create idea. Please try again.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Title"
|
||||||
|
value={formData.title}
|
||||||
|
onChange={(e) => handleChange('title', e.target.value)}
|
||||||
|
required
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Description"
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) => handleChange('description', e.target.value)}
|
||||||
|
multiline
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel>Status</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={formData.status}
|
||||||
|
label="Status"
|
||||||
|
onChange={(e) => handleChange('status', e.target.value)}
|
||||||
|
>
|
||||||
|
{statusOptions.map((opt) => (
|
||||||
|
<MenuItem key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel>Priority</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={formData.priority}
|
||||||
|
label="Priority"
|
||||||
|
onChange={(e) => handleChange('priority', e.target.value)}
|
||||||
|
>
|
||||||
|
{priorityOptions.map((opt) => (
|
||||||
|
<MenuItem key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Module"
|
||||||
|
value={formData.module}
|
||||||
|
onChange={(e) => handleChange('module', e.target.value)}
|
||||||
|
placeholder="e.g., Auth, Dashboard, API"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Target Audience"
|
||||||
|
value={formData.targetAudience}
|
||||||
|
onChange={(e) => handleChange('targetAudience', e.target.value)}
|
||||||
|
placeholder="Who is this for?"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Pain Point"
|
||||||
|
value={formData.pain}
|
||||||
|
onChange={(e) => handleChange('pain', e.target.value)}
|
||||||
|
multiline
|
||||||
|
rows={2}
|
||||||
|
placeholder="What problem does this solve?"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="AI Role"
|
||||||
|
value={formData.aiRole}
|
||||||
|
onChange={(e) => handleChange('aiRole', e.target.value)}
|
||||||
|
multiline
|
||||||
|
rows={2}
|
||||||
|
placeholder="How can AI help with this?"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Verification Method"
|
||||||
|
value={formData.verificationMethod}
|
||||||
|
onChange={(e) => handleChange('verificationMethod', e.target.value)}
|
||||||
|
multiline
|
||||||
|
rows={2}
|
||||||
|
placeholder="How to verify this is done?"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleClose}>Cancel</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
disabled={!formData.title || createIdea.isPending}
|
||||||
|
>
|
||||||
|
{createIdea.isPending ? 'Creating...' : 'Create'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
frontend/src/components/CreateIdeaModal/index.ts
Normal file
1
frontend/src/components/CreateIdeaModal/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { CreateIdeaModal } from './CreateIdeaModal';
|
||||||
126
frontend/src/components/IdeasFilters/IdeasFilters.tsx
Normal file
126
frontend/src/components/IdeasFilters/IdeasFilters.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
TextField,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
Button,
|
||||||
|
InputAdornment,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { Search, Clear } from '@mui/icons-material';
|
||||||
|
import { useIdeasStore } from '../../store/ideas';
|
||||||
|
import { useModulesQuery } from '../../hooks/useIdeas';
|
||||||
|
import type { IdeaStatus, IdeaPriority } from '../../types/idea';
|
||||||
|
|
||||||
|
const statusOptions: { value: IdeaStatus; label: string }[] = [
|
||||||
|
{ value: 'backlog', label: 'Backlog' },
|
||||||
|
{ value: 'todo', label: 'To Do' },
|
||||||
|
{ value: 'in_progress', label: 'In Progress' },
|
||||||
|
{ value: 'done', label: 'Done' },
|
||||||
|
{ value: 'cancelled', label: 'Cancelled' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const priorityOptions: { value: IdeaPriority; label: string }[] = [
|
||||||
|
{ value: 'low', label: 'Low' },
|
||||||
|
{ value: 'medium', label: 'Medium' },
|
||||||
|
{ value: 'high', label: 'High' },
|
||||||
|
{ value: 'critical', label: 'Critical' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function IdeasFilters() {
|
||||||
|
const { filters, setFilter, clearFilters } = useIdeasStore();
|
||||||
|
const { data: modules = [] } = useModulesQuery();
|
||||||
|
const [searchValue, setSearchValue] = useState(filters.search || '');
|
||||||
|
|
||||||
|
// Debounced search
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setFilter('search', searchValue || undefined);
|
||||||
|
}, 300);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [searchValue, setFilter]);
|
||||||
|
|
||||||
|
const hasFilters = filters.status || filters.priority || filters.module || filters.search;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
placeholder="Search ideas..."
|
||||||
|
value={searchValue}
|
||||||
|
onChange={(e) => setSearchValue(e.target.value)}
|
||||||
|
sx={{ minWidth: 200 }}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<Search fontSize="small" />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControl size="small" sx={{ minWidth: 120 }}>
|
||||||
|
<InputLabel>Status</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={filters.status || ''}
|
||||||
|
label="Status"
|
||||||
|
onChange={(e) => setFilter('status', e.target.value as IdeaStatus || undefined)}
|
||||||
|
>
|
||||||
|
<MenuItem value="">All</MenuItem>
|
||||||
|
{statusOptions.map((opt) => (
|
||||||
|
<MenuItem key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl size="small" sx={{ minWidth: 120 }}>
|
||||||
|
<InputLabel>Priority</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={filters.priority || ''}
|
||||||
|
label="Priority"
|
||||||
|
onChange={(e) => setFilter('priority', e.target.value as IdeaPriority || undefined)}
|
||||||
|
>
|
||||||
|
<MenuItem value="">All</MenuItem>
|
||||||
|
{priorityOptions.map((opt) => (
|
||||||
|
<MenuItem key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl size="small" sx={{ minWidth: 120 }}>
|
||||||
|
<InputLabel>Module</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={filters.module || ''}
|
||||||
|
label="Module"
|
||||||
|
onChange={(e) => setFilter('module', e.target.value || undefined)}
|
||||||
|
>
|
||||||
|
<MenuItem value="">All</MenuItem>
|
||||||
|
{modules.map((module) => (
|
||||||
|
<MenuItem key={module} value={module}>
|
||||||
|
{module}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{hasFilters && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
startIcon={<Clear />}
|
||||||
|
onClick={() => {
|
||||||
|
clearFilters();
|
||||||
|
setSearchValue('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
frontend/src/components/IdeasFilters/index.ts
Normal file
1
frontend/src/components/IdeasFilters/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { IdeasFilters } from './IdeasFilters';
|
||||||
143
frontend/src/components/IdeasTable/EditableCell.tsx
Normal file
143
frontend/src/components/IdeasTable/EditableCell.tsx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
TextField,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
Box,
|
||||||
|
ClickAwayListener,
|
||||||
|
} from '@mui/material';
|
||||||
|
import type { Idea, IdeaStatus, IdeaPriority } from '../../types/idea';
|
||||||
|
import { useUpdateIdea } from '../../hooks/useIdeas';
|
||||||
|
|
||||||
|
interface EditableCellProps {
|
||||||
|
idea: Idea;
|
||||||
|
field: keyof Idea;
|
||||||
|
value: string | null;
|
||||||
|
type?: 'text' | 'select';
|
||||||
|
options?: { value: string; label: string }[];
|
||||||
|
renderDisplay: (value: string | null) => React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditableCell({
|
||||||
|
idea,
|
||||||
|
field,
|
||||||
|
value,
|
||||||
|
type = 'text',
|
||||||
|
options,
|
||||||
|
renderDisplay,
|
||||||
|
}: EditableCellProps) {
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [editValue, setEditValue] = useState(value || '');
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const updateIdea = useUpdateIdea();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEditing && inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
inputRef.current.select();
|
||||||
|
}
|
||||||
|
}, [isEditing]);
|
||||||
|
|
||||||
|
const handleDoubleClick = () => {
|
||||||
|
setIsEditing(true);
|
||||||
|
setEditValue(value || '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setIsEditing(false);
|
||||||
|
if (editValue !== value) {
|
||||||
|
await updateIdea.mutateAsync({
|
||||||
|
id: idea.id,
|
||||||
|
dto: { [field]: editValue || null },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleSave();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
setIsEditing(false);
|
||||||
|
setEditValue(value || '');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
if (type === 'select' && options) {
|
||||||
|
return (
|
||||||
|
<ClickAwayListener onClickAway={handleSave}>
|
||||||
|
<Select
|
||||||
|
size="small"
|
||||||
|
value={editValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
setEditValue(e.target.value);
|
||||||
|
setTimeout(() => {
|
||||||
|
updateIdea.mutate({
|
||||||
|
id: idea.id,
|
||||||
|
dto: { [field]: e.target.value },
|
||||||
|
});
|
||||||
|
setIsEditing(false);
|
||||||
|
}, 0);
|
||||||
|
}}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
autoFocus
|
||||||
|
sx={{ minWidth: 100 }}
|
||||||
|
>
|
||||||
|
{options.map((opt) => (
|
||||||
|
<MenuItem key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</ClickAwayListener>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ClickAwayListener onClickAway={handleSave}>
|
||||||
|
<TextField
|
||||||
|
inputRef={inputRef}
|
||||||
|
size="small"
|
||||||
|
value={editValue}
|
||||||
|
onChange={(e) => setEditValue(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onBlur={handleSave}
|
||||||
|
sx={{ minWidth: 100 }}
|
||||||
|
/>
|
||||||
|
</ClickAwayListener>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
onDoubleClick={handleDoubleClick}
|
||||||
|
sx={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
minHeight: 24,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'action.hover',
|
||||||
|
borderRadius: 0.5,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderDisplay(value)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status options
|
||||||
|
export const statusOptions: { value: IdeaStatus; label: string }[] = [
|
||||||
|
{ value: 'backlog', label: 'Backlog' },
|
||||||
|
{ value: 'todo', label: 'To Do' },
|
||||||
|
{ value: 'in_progress', label: 'In Progress' },
|
||||||
|
{ value: 'done', label: 'Done' },
|
||||||
|
{ value: 'cancelled', label: 'Cancelled' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Priority options
|
||||||
|
export const priorityOptions: { value: IdeaPriority; label: string }[] = [
|
||||||
|
{ value: 'low', label: 'Low' },
|
||||||
|
{ value: 'medium', label: 'Medium' },
|
||||||
|
{ value: 'high', label: 'High' },
|
||||||
|
{ value: 'critical', label: 'Critical' },
|
||||||
|
];
|
||||||
182
frontend/src/components/IdeasTable/IdeasTable.tsx
Normal file
182
frontend/src/components/IdeasTable/IdeasTable.tsx
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
useReactTable,
|
||||||
|
getCoreRowModel,
|
||||||
|
flexRender,
|
||||||
|
} from '@tanstack/react-table';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Paper,
|
||||||
|
TableSortLabel,
|
||||||
|
Skeleton,
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
TablePagination,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { Inbox } from '@mui/icons-material';
|
||||||
|
import { useIdeasQuery, useDeleteIdea } from '../../hooks/useIdeas';
|
||||||
|
import { useIdeasStore } from '../../store/ideas';
|
||||||
|
import { createColumns } from './columns';
|
||||||
|
|
||||||
|
const SKELETON_COLUMNS_COUNT = 7;
|
||||||
|
|
||||||
|
export function IdeasTable() {
|
||||||
|
const { data, isLoading, isError } = useIdeasQuery();
|
||||||
|
const deleteIdea = useDeleteIdea();
|
||||||
|
const { sorting, setSorting, pagination, setPage, setLimit } = useIdeasStore();
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => createColumns((id) => deleteIdea.mutate(id)),
|
||||||
|
[deleteIdea]
|
||||||
|
);
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: data?.data ?? [],
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
manualSorting: true,
|
||||||
|
manualPagination: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSort = (columnId: string) => {
|
||||||
|
setSorting(columnId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangePage = (_: unknown, newPage: number) => {
|
||||||
|
setPage(newPage + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setLimit(parseInt(event.target.value, 10));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 4, textAlign: 'center' }}>
|
||||||
|
<Typography color="error">Failed to load ideas</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper sx={{ width: '100%', overflow: 'hidden' }}>
|
||||||
|
<TableContainer>
|
||||||
|
<Table stickyHeader size="small">
|
||||||
|
<TableHead>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => (
|
||||||
|
<TableCell
|
||||||
|
key={header.id}
|
||||||
|
sx={{
|
||||||
|
fontWeight: 600,
|
||||||
|
backgroundColor: 'grey.100',
|
||||||
|
width: header.getSize(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{header.column.getCanSort() ? (
|
||||||
|
<TableSortLabel
|
||||||
|
active={sorting.sortBy === header.id}
|
||||||
|
direction={
|
||||||
|
sorting.sortBy === header.id
|
||||||
|
? (sorting.sortOrder.toLowerCase() as 'asc' | 'desc')
|
||||||
|
: 'asc'
|
||||||
|
}
|
||||||
|
onClick={() => handleSort(header.id)}
|
||||||
|
>
|
||||||
|
{flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
</TableSortLabel>
|
||||||
|
) : (
|
||||||
|
flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{isLoading ? (
|
||||||
|
Array.from({ length: 5 }).map((_, index) => (
|
||||||
|
<TableRow key={index}>
|
||||||
|
{Array.from({ length: SKELETON_COLUMNS_COUNT }).map((_, colIndex) => (
|
||||||
|
<TableCell key={colIndex}>
|
||||||
|
<Skeleton variant="text" />
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : table.getRowModel().rows.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={SKELETON_COLUMNS_COUNT}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
py: 8,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
color: 'text.secondary',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Inbox sx={{ fontSize: 48, mb: 2, opacity: 0.5 }} />
|
||||||
|
<Typography variant="h6">No ideas yet</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
Create your first idea to get started
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
hover
|
||||||
|
sx={{
|
||||||
|
backgroundColor: row.original.color
|
||||||
|
? `${row.original.color}15`
|
||||||
|
: undefined,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: row.original.color
|
||||||
|
? `${row.original.color}25`
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext()
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
{data && (
|
||||||
|
<TablePagination
|
||||||
|
component="div"
|
||||||
|
count={data.meta.total}
|
||||||
|
page={pagination.page - 1}
|
||||||
|
rowsPerPage={pagination.limit}
|
||||||
|
onPageChange={handleChangePage}
|
||||||
|
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||||
|
rowsPerPageOptions={[10, 20, 50, 100]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
140
frontend/src/components/IdeasTable/columns.tsx
Normal file
140
frontend/src/components/IdeasTable/columns.tsx
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import { createColumnHelper } from '@tanstack/react-table';
|
||||||
|
import { Chip, Box, IconButton } from '@mui/material';
|
||||||
|
import { Delete } from '@mui/icons-material';
|
||||||
|
import type { Idea, IdeaStatus, IdeaPriority } from '../../types/idea';
|
||||||
|
import { EditableCell, statusOptions, priorityOptions } from './EditableCell';
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<Idea>();
|
||||||
|
|
||||||
|
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',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createColumns = (onDelete: (id: string) => void) => [
|
||||||
|
columnHelper.accessor('title', {
|
||||||
|
header: 'Title',
|
||||||
|
cell: (info) => (
|
||||||
|
<EditableCell
|
||||||
|
idea={info.row.original}
|
||||||
|
field="title"
|
||||||
|
value={info.getValue()}
|
||||||
|
renderDisplay={(value) => (
|
||||||
|
<Box sx={{ fontWeight: 500 }}>{value || '—'}</Box>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
size: 250,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('status', {
|
||||||
|
header: 'Status',
|
||||||
|
cell: (info) => {
|
||||||
|
const status = info.getValue();
|
||||||
|
const label = statusOptions.find((s) => s.value === status)?.label || status;
|
||||||
|
return (
|
||||||
|
<EditableCell
|
||||||
|
idea={info.row.original}
|
||||||
|
field="status"
|
||||||
|
value={status}
|
||||||
|
type="select"
|
||||||
|
options={statusOptions}
|
||||||
|
renderDisplay={() => (
|
||||||
|
<Chip label={label} color={statusColors[status]} size="small" />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
size: 140,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('priority', {
|
||||||
|
header: 'Priority',
|
||||||
|
cell: (info) => {
|
||||||
|
const priority = info.getValue();
|
||||||
|
const label = priorityOptions.find((p) => p.value === priority)?.label || priority;
|
||||||
|
return (
|
||||||
|
<EditableCell
|
||||||
|
idea={info.row.original}
|
||||||
|
field="priority"
|
||||||
|
value={priority}
|
||||||
|
type="select"
|
||||||
|
options={priorityOptions}
|
||||||
|
renderDisplay={() => (
|
||||||
|
<Chip
|
||||||
|
label={label}
|
||||||
|
color={priorityColors[priority]}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
size: 120,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('module', {
|
||||||
|
header: 'Module',
|
||||||
|
cell: (info) => (
|
||||||
|
<EditableCell
|
||||||
|
idea={info.row.original}
|
||||||
|
field="module"
|
||||||
|
value={info.getValue()}
|
||||||
|
renderDisplay={(value) => value || '—'}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
size: 120,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('targetAudience', {
|
||||||
|
header: 'Target Audience',
|
||||||
|
cell: (info) => (
|
||||||
|
<EditableCell
|
||||||
|
idea={info.row.original}
|
||||||
|
field="targetAudience"
|
||||||
|
value={info.getValue()}
|
||||||
|
renderDisplay={(value) => value || '—'}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
size: 150,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('description', {
|
||||||
|
header: 'Description',
|
||||||
|
cell: (info) => {
|
||||||
|
const value = info.getValue();
|
||||||
|
return (
|
||||||
|
<EditableCell
|
||||||
|
idea={info.row.original}
|
||||||
|
field="description"
|
||||||
|
value={value}
|
||||||
|
renderDisplay={(val) => {
|
||||||
|
if (!val) return '—';
|
||||||
|
return val.length > 80 ? `${val.slice(0, 80)}...` : val;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
size: 200,
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'actions',
|
||||||
|
header: '',
|
||||||
|
cell: (info) => (
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => onDelete(info.row.original.id)}
|
||||||
|
sx={{ opacity: 0.5, '&:hover': { opacity: 1 } }}
|
||||||
|
>
|
||||||
|
<Delete fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
),
|
||||||
|
size: 50,
|
||||||
|
}),
|
||||||
|
];
|
||||||
1
frontend/src/components/IdeasTable/index.ts
Normal file
1
frontend/src/components/IdeasTable/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { IdeasTable } from './IdeasTable';
|
||||||
71
frontend/src/hooks/useIdeas.ts
Normal file
71
frontend/src/hooks/useIdeas.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { ideasApi, type QueryIdeasParams } from '../services/ideas';
|
||||||
|
import type { CreateIdeaDto, UpdateIdeaDto } from '../types/idea';
|
||||||
|
import { useIdeasStore } from '../store/ideas';
|
||||||
|
|
||||||
|
const QUERY_KEY = 'ideas';
|
||||||
|
|
||||||
|
export function useIdeasQuery() {
|
||||||
|
const { filters, sorting, pagination } = useIdeasStore();
|
||||||
|
|
||||||
|
const params: QueryIdeasParams = {
|
||||||
|
...filters,
|
||||||
|
...sorting,
|
||||||
|
...pagination,
|
||||||
|
};
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [QUERY_KEY, params],
|
||||||
|
queryFn: () => ideasApi.getAll(params),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useIdeaQuery(id: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [QUERY_KEY, id],
|
||||||
|
queryFn: () => ideasApi.getOne(id),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useModulesQuery() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [QUERY_KEY, 'modules'],
|
||||||
|
queryFn: () => ideasApi.getModules(),
|
||||||
|
staleTime: 60000, // Cache for 1 minute
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateIdea() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (dto: CreateIdeaDto) => ideasApi.create(dto),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateIdea() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, dto }: { id: string; dto: UpdateIdeaDto }) =>
|
||||||
|
ideasApi.update(id, dto),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteIdea() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => ideasApi.delete(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
32
frontend/src/main.tsx
Normal file
32
frontend/src/main.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { ThemeProvider, CssBaseline } from '@mui/material'
|
||||||
|
import { createTheme } from '@mui/material/styles'
|
||||||
|
import App from './App.tsx'
|
||||||
|
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 5000,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const theme = createTheme({
|
||||||
|
palette: {
|
||||||
|
mode: 'light',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<CssBaseline />
|
||||||
|
<App />
|
||||||
|
</ThemeProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
8
frontend/src/services/api.ts
Normal file
8
frontend/src/services/api.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export const api = axios.create({
|
||||||
|
baseURL: '/api',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
54
frontend/src/services/ideas.ts
Normal file
54
frontend/src/services/ideas.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { api } from './api';
|
||||||
|
import type { Idea, CreateIdeaDto, UpdateIdeaDto, IdeaStatus, IdeaPriority } from '../types/idea';
|
||||||
|
|
||||||
|
export interface QueryIdeasParams {
|
||||||
|
status?: IdeaStatus;
|
||||||
|
priority?: IdeaPriority;
|
||||||
|
module?: string;
|
||||||
|
search?: string;
|
||||||
|
sortBy?: string;
|
||||||
|
sortOrder?: 'ASC' | 'DESC';
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedResponse<T> {
|
||||||
|
data: T[];
|
||||||
|
meta: {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ideasApi = {
|
||||||
|
getAll: async (params?: QueryIdeasParams): Promise<PaginatedResponse<Idea>> => {
|
||||||
|
const { data } = await api.get<PaginatedResponse<Idea>>('/ideas', { params });
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getOne: async (id: string): Promise<Idea> => {
|
||||||
|
const { data } = await api.get<Idea>(`/ideas/${id}`);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
create: async (dto: CreateIdeaDto): Promise<Idea> => {
|
||||||
|
const { data } = await api.post<Idea>('/ideas', dto);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
update: async (id: string, dto: UpdateIdeaDto): Promise<Idea> => {
|
||||||
|
const { data } = await api.patch<Idea>(`/ideas/${id}`, dto);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
delete: async (id: string): Promise<void> => {
|
||||||
|
await api.delete(`/ideas/${id}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
getModules: async (): Promise<string[]> => {
|
||||||
|
const { data } = await api.get<string[]>('/ideas/modules');
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
};
|
||||||
76
frontend/src/store/ideas.ts
Normal file
76
frontend/src/store/ideas.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import type { IdeaStatus, IdeaPriority } from '../types/idea';
|
||||||
|
|
||||||
|
interface IdeasFilters {
|
||||||
|
status?: IdeaStatus;
|
||||||
|
priority?: IdeaPriority;
|
||||||
|
module?: string;
|
||||||
|
search?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IdeasSorting {
|
||||||
|
sortBy: string;
|
||||||
|
sortOrder: 'ASC' | 'DESC';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IdeasPagination {
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IdeasStore {
|
||||||
|
// Filters
|
||||||
|
filters: IdeasFilters;
|
||||||
|
setFilter: <K extends keyof IdeasFilters>(key: K, value: IdeasFilters[K]) => void;
|
||||||
|
clearFilters: () => void;
|
||||||
|
|
||||||
|
// Sorting
|
||||||
|
sorting: IdeasSorting;
|
||||||
|
setSorting: (sortBy: string, sortOrder?: 'ASC' | 'DESC') => void;
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
pagination: IdeasPagination;
|
||||||
|
setPage: (page: number) => void;
|
||||||
|
setLimit: (limit: number) => void;
|
||||||
|
|
||||||
|
// UI State
|
||||||
|
createModalOpen: boolean;
|
||||||
|
setCreateModalOpen: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialFilters: IdeasFilters = {};
|
||||||
|
const initialSorting: IdeasSorting = { sortBy: 'createdAt', sortOrder: 'DESC' };
|
||||||
|
const initialPagination: IdeasPagination = { page: 1, limit: 20 };
|
||||||
|
|
||||||
|
export const useIdeasStore = create<IdeasStore>((set) => ({
|
||||||
|
// Filters
|
||||||
|
filters: initialFilters,
|
||||||
|
setFilter: (key, value) =>
|
||||||
|
set((state) => ({
|
||||||
|
filters: { ...state.filters, [key]: value || undefined },
|
||||||
|
pagination: { ...state.pagination, page: 1 }, // Reset page on filter change
|
||||||
|
})),
|
||||||
|
clearFilters: () =>
|
||||||
|
set({ filters: initialFilters, pagination: { ...initialPagination } }),
|
||||||
|
|
||||||
|
// Sorting
|
||||||
|
sorting: initialSorting,
|
||||||
|
setSorting: (sortBy, sortOrder) =>
|
||||||
|
set((state) => ({
|
||||||
|
sorting: {
|
||||||
|
sortBy,
|
||||||
|
sortOrder: sortOrder ?? (state.sorting.sortBy === sortBy && state.sorting.sortOrder === 'ASC' ? 'DESC' : 'ASC'),
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
pagination: initialPagination,
|
||||||
|
setPage: (page) =>
|
||||||
|
set((state) => ({ pagination: { ...state.pagination, page } })),
|
||||||
|
setLimit: (limit) =>
|
||||||
|
set((state) => ({ pagination: { ...state.pagination, limit, page: 1 } })),
|
||||||
|
|
||||||
|
// UI State
|
||||||
|
createModalOpen: false,
|
||||||
|
setCreateModalOpen: (open) => set({ createModalOpen: open }),
|
||||||
|
}));
|
||||||
36
frontend/src/types/idea.ts
Normal file
36
frontend/src/types/idea.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
export type IdeaStatus = 'backlog' | 'todo' | 'in_progress' | 'done' | 'cancelled';
|
||||||
|
export type IdeaPriority = 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
|
||||||
|
export interface Idea {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string | null;
|
||||||
|
status: IdeaStatus;
|
||||||
|
priority: IdeaPriority;
|
||||||
|
module: string | null;
|
||||||
|
targetAudience: string | null;
|
||||||
|
pain: string | null;
|
||||||
|
aiRole: string | null;
|
||||||
|
verificationMethod: string | null;
|
||||||
|
color: string | null;
|
||||||
|
order: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateIdeaDto {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
status?: IdeaStatus;
|
||||||
|
priority?: IdeaPriority;
|
||||||
|
module?: string;
|
||||||
|
targetAudience?: string;
|
||||||
|
pain?: string;
|
||||||
|
aiRole?: string;
|
||||||
|
verificationMethod?: string;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateIdeaDto extends Partial<CreateIdeaDto> {
|
||||||
|
order?: number;
|
||||||
|
}
|
||||||
28
frontend/tsconfig.app.json
Normal file
28
frontend/tsconfig.app.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
frontend/tsconfig.json
Normal file
7
frontend/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
26
frontend/tsconfig.node.json
Normal file
26
frontend/tsconfig.node.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
16
frontend/vite.config.ts
Normal file
16
frontend/vite.config.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 4000,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:4001',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
12044
package-lock.json
generated
Normal file
12044
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
package.json
Normal file
27
package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "team-planner",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"workspaces": [
|
||||||
|
"backend",
|
||||||
|
"frontend"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"dev": "concurrently -n be,fe -c blue,green \"npm run dev:backend\" \"npm run dev:frontend\"",
|
||||||
|
"dev:backend": "npm run dev -w backend",
|
||||||
|
"dev:frontend": "npm run dev -w frontend",
|
||||||
|
"build": "npm run build:backend && npm run build:frontend",
|
||||||
|
"build:backend": "npm run build -w backend",
|
||||||
|
"build:frontend": "npm run build -w frontend",
|
||||||
|
"lint": "npm run lint -w backend && npm run lint -w frontend",
|
||||||
|
"db:up": "docker-compose up -d postgres",
|
||||||
|
"db:down": "docker-compose down"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"concurrently": "^9.1.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user