feat: implement task management module

Backend (FastAPI):
- Database migration for spaces, projects, task_statuses, tasks tables
- SQLAlchemy models with relationships
- Pydantic schemas for CRUD operations
- Spaces API: CRUD with soft delete
- Projects API: CRUD with auto-created default statuses
- Tasks API: CRUD, status change, assign, subtask support
- Permission middleware with Security Level filtering
- Subtask depth limit (max 2 levels)

Frontend (React + Vite):
- Layout component with navigation
- Spaces list page
- Projects list page
- Tasks list page with status management

Fixes:
- auth_client.py: use 'username' field for external API
- config.py: extend JWT expiry to 7 days
- auth/router.py: sync Redis session with JWT expiry

Tests: 36 passed (unit + integration)
E2E: All APIs verified with real authentication

OpenSpec: add-task-management archived

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beabigegg
2025-12-29 00:31:34 +08:00
parent 1fda7da2c2
commit daca7798e3
41 changed files with 3616 additions and 13 deletions

View File

@@ -2,6 +2,15 @@ from app.schemas.auth import LoginRequest, LoginResponse, TokenPayload
from app.schemas.user import UserCreate, UserUpdate, UserResponse, UserInDB
from app.schemas.department import DepartmentCreate, DepartmentUpdate, DepartmentResponse
from app.schemas.role import RoleResponse
from app.schemas.space import SpaceCreate, SpaceUpdate, SpaceResponse, SpaceWithOwner
from app.schemas.project import (
ProjectCreate, ProjectUpdate, ProjectResponse, ProjectWithDetails, SecurityLevel
)
from app.schemas.task_status import TaskStatusCreate, TaskStatusUpdate, TaskStatusResponse
from app.schemas.task import (
TaskCreate, TaskUpdate, TaskResponse, TaskWithDetails, TaskListResponse,
TaskStatusUpdate as TaskStatusChangeUpdate, TaskAssignUpdate, Priority
)
__all__ = [
"LoginRequest",
@@ -15,4 +24,24 @@ __all__ = [
"DepartmentUpdate",
"DepartmentResponse",
"RoleResponse",
"SpaceCreate",
"SpaceUpdate",
"SpaceResponse",
"SpaceWithOwner",
"ProjectCreate",
"ProjectUpdate",
"ProjectResponse",
"ProjectWithDetails",
"SecurityLevel",
"TaskStatusCreate",
"TaskStatusUpdate",
"TaskStatusResponse",
"TaskCreate",
"TaskUpdate",
"TaskResponse",
"TaskWithDetails",
"TaskListResponse",
"TaskStatusChangeUpdate",
"TaskAssignUpdate",
"Priority",
]

View File

@@ -0,0 +1,55 @@
from pydantic import BaseModel
from typing import Optional
from datetime import datetime, date
from decimal import Decimal
from enum import Enum
class SecurityLevel(str, Enum):
PUBLIC = "public"
DEPARTMENT = "department"
CONFIDENTIAL = "confidential"
class ProjectBase(BaseModel):
title: str
description: Optional[str] = None
budget: Optional[Decimal] = None
start_date: Optional[date] = None
end_date: Optional[date] = None
security_level: SecurityLevel = SecurityLevel.DEPARTMENT
class ProjectCreate(ProjectBase):
department_id: Optional[str] = None
class ProjectUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
budget: Optional[Decimal] = None
start_date: Optional[date] = None
end_date: Optional[date] = None
security_level: Optional[SecurityLevel] = None
status: Optional[str] = None
department_id: Optional[str] = None
class ProjectResponse(ProjectBase):
id: str
space_id: str
owner_id: str
status: str
department_id: Optional[str] = None
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class ProjectWithDetails(ProjectResponse):
owner_name: Optional[str] = None
space_name: Optional[str] = None
department_name: Optional[str] = None
task_count: int = 0

View File

@@ -0,0 +1,32 @@
from pydantic import BaseModel
from typing import Optional
from datetime import datetime
class SpaceBase(BaseModel):
name: str
description: Optional[str] = None
class SpaceCreate(SpaceBase):
pass
class SpaceUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
class SpaceResponse(SpaceBase):
id: str
owner_id: str
is_active: bool
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class SpaceWithOwner(SpaceResponse):
owner_name: Optional[str] = None

View File

@@ -0,0 +1,74 @@
from pydantic import BaseModel
from typing import Optional, List
from datetime import datetime
from decimal import Decimal
from enum import Enum
class Priority(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
URGENT = "urgent"
class TaskBase(BaseModel):
title: str
description: Optional[str] = None
priority: Priority = Priority.MEDIUM
original_estimate: Optional[Decimal] = None
due_date: Optional[datetime] = None
class TaskCreate(TaskBase):
parent_task_id: Optional[str] = None
assignee_id: Optional[str] = None
status_id: Optional[str] = None
class TaskUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
priority: Optional[Priority] = None
original_estimate: Optional[Decimal] = None
time_spent: Optional[Decimal] = None
due_date: Optional[datetime] = None
position: Optional[int] = None
class TaskStatusUpdate(BaseModel):
status_id: str
class TaskAssignUpdate(BaseModel):
assignee_id: Optional[str] = None
class TaskResponse(TaskBase):
id: str
project_id: str
parent_task_id: Optional[str] = None
assignee_id: Optional[str] = None
status_id: Optional[str] = None
time_spent: Decimal
blocker_flag: bool
position: int
created_by: str
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class TaskWithDetails(TaskResponse):
assignee_name: Optional[str] = None
status_name: Optional[str] = None
status_color: Optional[str] = None
creator_name: Optional[str] = None
subtask_count: int = 0
class TaskListResponse(BaseModel):
tasks: List[TaskWithDetails]
total: int

View File

@@ -0,0 +1,30 @@
from pydantic import BaseModel
from typing import Optional
from datetime import datetime
class TaskStatusBase(BaseModel):
name: str
color: str = "#808080"
position: int = 0
is_done: bool = False
class TaskStatusCreate(TaskStatusBase):
pass
class TaskStatusUpdate(BaseModel):
name: Optional[str] = None
color: Optional[str] = None
position: Optional[int] = None
is_done: Optional[bool] = None
class TaskStatusResponse(TaskStatusBase):
id: str
project_id: str
created_at: datetime
class Config:
from_attributes = True