607 lines
23 KiB
Markdown
607 lines
23 KiB
Markdown
# Design Document: Custom Task Status Management
|
|
|
|
## Overview
|
|
|
|
This design document outlines the implementation of a custom task status management system that allows project managers, coordinators, and administrators to define project-specific task statuses with custom names, colors, and ordering. The system will maintain backward compatibility with existing hardcoded statuses while providing flexibility for different production workflows.
|
|
|
|
The implementation follows the existing pattern established by the custom task type management system, adapting it for status management with additional features for color customization, ordering, and default status designation.
|
|
|
|
## Architecture
|
|
|
|
### High-Level Architecture
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Frontend (Vue 3) │
|
|
│ ┌──────────────────────────────────────────────────────┐ │
|
|
│ │ ProjectSettingsView (Tasks Tab) │ │
|
|
│ │ └── CustomTaskStatusManager Component │ │
|
|
│ │ ├── Status List Display │ │
|
|
│ │ ├── Add/Edit Status Dialog │ │
|
|
│ │ ├── Delete Confirmation Dialog │ │
|
|
│ │ └── Drag-and-Drop Reordering │ │
|
|
│ └──────────────────────────────────────────────────────┘ │
|
|
│ ┌──────────────────────────────────────────────────────┐ │
|
|
│ │ Status Display Components (Throughout App) │ │
|
|
│ │ ├── TaskStatusBadge (with custom colors) │ │
|
|
│ │ ├── EditableTaskStatus (dropdowns) │ │
|
|
│ │ └── TaskStatusFilter (filter controls) │ │
|
|
│ └──────────────────────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
│ HTTP/REST API
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Backend (FastAPI) │
|
|
│ ┌──────────────────────────────────────────────────────┐ │
|
|
│ │ /api/projects/{id}/custom-task-statuses │ │
|
|
│ │ ├── GET - List all statuses │ │
|
|
│ │ ├── POST - Create new status │ │
|
|
│ │ ├── PUT - Update status │ │
|
|
│ │ ├── DELETE - Delete status (with validation) │ │
|
|
│ │ └── PATCH - Reorder statuses │ │
|
|
│ └──────────────────────────────────────────────────────┘ │
|
|
│ ┌──────────────────────────────────────────────────────┐ │
|
|
│ │ Database Models │ │
|
|
│ │ ├── Project (custom_task_statuses JSON field) │ │
|
|
│ │ └── Task (status field - string reference) │ │
|
|
│ └──────────────────────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌───────────────┐
|
|
│ SQLite DB │
|
|
└───────────────┘
|
|
```
|
|
|
|
### Data Flow
|
|
|
|
1. **Status Creation**: User creates status → Frontend validates → API creates status → Database updated → UI refreshed
|
|
2. **Status Display**: Task loaded → Status resolved (system or custom) → Color applied → Badge rendered
|
|
3. **Status Update**: User changes task status → API validates → Task updated → Activity logged → UI updated
|
|
4. **Status Deletion**: User deletes status → API checks usage → If in use, require reassignment → Delete → Update tasks
|
|
|
|
## Components and Interfaces
|
|
|
|
### Backend Components
|
|
|
|
#### 1. Database Schema Changes
|
|
|
|
**Project Model Extension** (`backend/models/project.py`):
|
|
```python
|
|
class Project(Base):
|
|
__tablename__ = "projects"
|
|
|
|
# ... existing fields ...
|
|
|
|
# New field for custom task statuses
|
|
custom_task_statuses = Column(JSON, nullable=True)
|
|
# Structure: [
|
|
# {
|
|
# "id": "custom_status_1",
|
|
# "name": "In Review",
|
|
# "color": "#FFA500",
|
|
# "order": 0,
|
|
# "is_default": false
|
|
# },
|
|
# ...
|
|
# ]
|
|
```
|
|
|
|
**Task Model** (`backend/models/task.py`):
|
|
```python
|
|
class Task(Base):
|
|
__tablename__ = "tasks"
|
|
|
|
# ... existing fields ...
|
|
|
|
# Change status from Enum to String to support custom statuses
|
|
status = Column(String, nullable=False, default="not_started")
|
|
# Will store either system status keys or custom status IDs
|
|
```
|
|
|
|
#### 2. Pydantic Schemas
|
|
|
|
**Custom Task Status Schemas** (`backend/schemas/custom_task_status.py`):
|
|
```python
|
|
from pydantic import BaseModel, Field, validator
|
|
from typing import List, Optional
|
|
import re
|
|
|
|
class CustomTaskStatus(BaseModel):
|
|
"""Schema for a custom task status"""
|
|
id: str = Field(..., description="Unique identifier for the status")
|
|
name: str = Field(..., min_length=1, max_length=50, description="Display name")
|
|
color: str = Field(..., pattern=r'^#[0-9A-Fa-f]{6}$', description="Hex color code")
|
|
order: int = Field(..., ge=0, description="Display order")
|
|
is_default: bool = Field(default=False, description="Whether this is the default status")
|
|
|
|
class CustomTaskStatusCreate(BaseModel):
|
|
"""Schema for creating a new custom task status"""
|
|
name: str = Field(..., min_length=1, max_length=50)
|
|
color: Optional[str] = Field(None, pattern=r'^#[0-9A-Fa-f]{6}$')
|
|
|
|
@validator('name')
|
|
def validate_name(cls, v):
|
|
# Trim whitespace
|
|
v = v.strip()
|
|
if not v:
|
|
raise ValueError('Status name cannot be empty')
|
|
return v
|
|
|
|
class CustomTaskStatusUpdate(BaseModel):
|
|
"""Schema for updating a custom task status"""
|
|
name: Optional[str] = Field(None, min_length=1, max_length=50)
|
|
color: Optional[str] = Field(None, pattern=r'^#[0-9A-Fa-f]{6}$')
|
|
is_default: Optional[bool] = None
|
|
|
|
class CustomTaskStatusReorder(BaseModel):
|
|
"""Schema for reordering statuses"""
|
|
status_ids: List[str] = Field(..., description="Ordered list of status IDs")
|
|
|
|
class CustomTaskStatusDelete(BaseModel):
|
|
"""Schema for deleting a status with reassignment"""
|
|
reassign_to_status_id: Optional[str] = Field(None, description="Status ID to reassign tasks to")
|
|
|
|
class AllTaskStatusesResponse(BaseModel):
|
|
"""Schema for response containing all task statuses"""
|
|
statuses: List[CustomTaskStatus]
|
|
system_statuses: List[dict] # [{id: "not_started", name: "Not Started", color: "#gray"}]
|
|
default_status_id: str
|
|
|
|
class TaskStatusInUseError(BaseModel):
|
|
"""Schema for error when trying to delete a status in use"""
|
|
error: str
|
|
status_id: str
|
|
task_count: int
|
|
task_ids: List[int]
|
|
```
|
|
|
|
#### 3. API Endpoints
|
|
|
|
**Router** (`backend/routers/projects.py`):
|
|
|
|
```python
|
|
# System statuses (read-only, for backward compatibility)
|
|
SYSTEM_TASK_STATUSES = [
|
|
{"id": "not_started", "name": "Not Started", "color": "#6B7280"},
|
|
{"id": "in_progress", "name": "In Progress", "color": "#3B82F6"},
|
|
{"id": "submitted", "name": "Submitted", "color": "#F59E0B"},
|
|
{"id": "approved", "name": "Approved", "color": "#10B981"},
|
|
{"id": "retake", "name": "Retake", "color": "#EF4444"}
|
|
]
|
|
|
|
DEFAULT_STATUS_COLORS = [
|
|
"#3B82F6", "#10B981", "#F59E0B", "#EF4444", "#8B5CF6",
|
|
"#EC4899", "#14B8A6", "#F97316", "#06B6D4", "#84CC16"
|
|
]
|
|
|
|
@router.get("/{project_id}/custom-task-statuses")
|
|
async def get_all_task_statuses(
|
|
project_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user_with_db)
|
|
):
|
|
"""Get all task statuses (system + custom) for a project"""
|
|
# Implementation details...
|
|
|
|
@router.post("/{project_id}/custom-task-statuses", status_code=201)
|
|
async def create_custom_task_status(
|
|
project_id: int,
|
|
status_data: CustomTaskStatusCreate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_coordinator_or_admin)
|
|
):
|
|
"""Create a new custom task status"""
|
|
# Implementation details...
|
|
|
|
@router.put("/{project_id}/custom-task-statuses/{status_id}")
|
|
async def update_custom_task_status(
|
|
project_id: int,
|
|
status_id: str,
|
|
status_data: CustomTaskStatusUpdate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_coordinator_or_admin)
|
|
):
|
|
"""Update a custom task status"""
|
|
# Implementation details...
|
|
|
|
@router.delete("/{project_id}/custom-task-statuses/{status_id}")
|
|
async def delete_custom_task_status(
|
|
project_id: int,
|
|
status_id: str,
|
|
delete_data: CustomTaskStatusDelete,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_coordinator_or_admin)
|
|
):
|
|
"""Delete a custom task status"""
|
|
# Implementation details...
|
|
|
|
@router.patch("/{project_id}/custom-task-statuses/reorder")
|
|
async def reorder_custom_task_statuses(
|
|
project_id: int,
|
|
reorder_data: CustomTaskStatusReorder,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(require_coordinator_or_admin)
|
|
):
|
|
"""Reorder custom task statuses"""
|
|
# Implementation details...
|
|
```
|
|
|
|
### Frontend Components
|
|
|
|
#### 1. Custom Task Status Manager Component
|
|
|
|
**Component** (`frontend/src/components/settings/CustomTaskStatusManager.vue`):
|
|
|
|
```vue
|
|
<template>
|
|
<div class="space-y-6">
|
|
<div>
|
|
<h3 class="text-lg font-semibold">Task Statuses</h3>
|
|
<p class="text-sm text-muted-foreground mt-1">
|
|
Customize task statuses to match your production workflow
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Status List with Drag-and-Drop -->
|
|
<div class="space-y-2">
|
|
<draggable
|
|
v-model="statusList"
|
|
@end="handleReorder"
|
|
handle=".drag-handle"
|
|
item-key="id"
|
|
>
|
|
<template #item="{ element: status }">
|
|
<div class="status-item">
|
|
<!-- Drag handle, status badge, edit/delete buttons -->
|
|
</div>
|
|
</template>
|
|
</draggable>
|
|
</div>
|
|
|
|
<!-- Add Status Button -->
|
|
<Button @click="openAddDialog">
|
|
<Plus class="h-4 w-4 mr-2" />
|
|
Add Status
|
|
</Button>
|
|
|
|
<!-- Add/Edit Dialog -->
|
|
<Dialog v-model:open="isDialogOpen">
|
|
<!-- Status name input, color picker -->
|
|
</Dialog>
|
|
|
|
<!-- Delete Confirmation Dialog -->
|
|
<AlertDialog v-model:open="isDeleteDialogOpen">
|
|
<!-- Confirmation with task count and reassignment option -->
|
|
</AlertDialog>
|
|
</div>
|
|
</template>
|
|
```
|
|
|
|
#### 2. Status Display Components
|
|
|
|
**TaskStatusBadge Enhancement** (`frontend/src/components/task/TaskStatusBadge.vue`):
|
|
```vue
|
|
<template>
|
|
<Badge
|
|
:style="{
|
|
backgroundColor: statusColor,
|
|
color: getContrastColor(statusColor)
|
|
}"
|
|
>
|
|
{{ statusName }}
|
|
</Badge>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
// Resolve status from system or custom statuses
|
|
// Apply custom color
|
|
// Calculate contrast color for text
|
|
</script>
|
|
```
|
|
|
|
#### 3. Services
|
|
|
|
**Custom Task Status Service** (`frontend/src/services/customTaskStatus.ts`):
|
|
```typescript
|
|
export interface CustomTaskStatus {
|
|
id: string
|
|
name: string
|
|
color: string
|
|
order: number
|
|
is_default: boolean
|
|
}
|
|
|
|
export interface AllTaskStatusesResponse {
|
|
statuses: CustomTaskStatus[]
|
|
system_statuses: Array<{id: string, name: string, color: string}>
|
|
default_status_id: string
|
|
}
|
|
|
|
export const customTaskStatusService = {
|
|
async getAllStatuses(projectId: number): Promise<AllTaskStatusesResponse> {
|
|
// GET /api/projects/{projectId}/custom-task-statuses
|
|
},
|
|
|
|
async createStatus(projectId: number, data: {name: string, color?: string}): Promise<AllTaskStatusesResponse> {
|
|
// POST /api/projects/{projectId}/custom-task-statuses
|
|
},
|
|
|
|
async updateStatus(projectId: number, statusId: string, data: Partial<CustomTaskStatus>): Promise<AllTaskStatusesResponse> {
|
|
// PUT /api/projects/{projectId}/custom-task-statuses/{statusId}
|
|
},
|
|
|
|
async deleteStatus(projectId: number, statusId: string, reassignTo?: string): Promise<AllTaskStatusesResponse> {
|
|
// DELETE /api/projects/{projectId}/custom-task-statuses/{statusId}
|
|
},
|
|
|
|
async reorderStatuses(projectId: number, statusIds: string[]): Promise<AllTaskStatusesResponse> {
|
|
// PATCH /api/projects/{projectId}/custom-task-statuses/reorder
|
|
}
|
|
}
|
|
```
|
|
|
|
## Data Models
|
|
|
|
### Custom Task Status Data Structure
|
|
|
|
```typescript
|
|
interface CustomTaskStatus {
|
|
id: string // Unique identifier (e.g., "custom_status_1")
|
|
name: string // Display name (e.g., "In Review")
|
|
color: string // Hex color code (e.g., "#FFA500")
|
|
order: number // Display order (0-based)
|
|
is_default: boolean // Whether this is the default status for new tasks
|
|
}
|
|
```
|
|
|
|
### Project Custom Statuses Storage
|
|
|
|
Stored in `Project.custom_task_statuses` as JSON:
|
|
```json
|
|
[
|
|
{
|
|
"id": "custom_status_1",
|
|
"name": "In Review",
|
|
"color": "#FFA500",
|
|
"order": 0,
|
|
"is_default": false
|
|
},
|
|
{
|
|
"id": "custom_status_2",
|
|
"name": "Client Feedback",
|
|
"color": "#9333EA",
|
|
"order": 1,
|
|
"is_default": false
|
|
}
|
|
]
|
|
```
|
|
|
|
### Task Status Reference
|
|
|
|
Tasks will store status as a string that can be either:
|
|
- System status key: `"not_started"`, `"in_progress"`, `"submitted"`, `"approved"`, `"retake"`
|
|
- Custom status ID: `"custom_status_1"`, `"custom_status_2"`, etc.
|
|
|
|
## Correctness Properties
|
|
|
|
*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
|
|
|
|
### Property 1: Status name uniqueness within project
|
|
*For any* project and any two statuses within that project, the status names must be unique (case-insensitive comparison)
|
|
**Validates: Requirements 1.3, 2.4**
|
|
|
|
### Property 2: Status color format validity
|
|
*For any* custom status, if a color is specified, it must be a valid 6-digit hexadecimal color code starting with #
|
|
**Validates: Requirements 1.4**
|
|
|
|
### Property 3: Default status uniqueness
|
|
*For any* project, at most one status can be marked as the default status
|
|
**Validates: Requirements 5.2**
|
|
|
|
### Property 4: Status deletion with task reassignment
|
|
*For any* status deletion where tasks exist, all tasks using the deleted status must be reassigned to another valid status before deletion completes
|
|
**Validates: Requirements 3.4, 3.5**
|
|
|
|
### Property 5: Status order consistency
|
|
*For any* project's status list, the order values must form a continuous sequence from 0 to n-1 where n is the number of statuses
|
|
**Validates: Requirements 4.1, 4.3**
|
|
|
|
### Property 6: Task status reference validity
|
|
*For any* task, the status field must reference either a valid system status or a valid custom status from the task's project
|
|
**Validates: Requirements 6.4, 6.5**
|
|
|
|
### Property 7: Status update propagation
|
|
*For any* status name or color update, all UI components displaying that status must reflect the new values without requiring a page refresh
|
|
**Validates: Requirements 2.3, 7.5**
|
|
|
|
### Property 8: Project isolation
|
|
*For any* two different projects, custom statuses defined in one project must not be accessible or visible in the other project
|
|
**Validates: Requirements 9.1, 9.2, 9.3**
|
|
|
|
### Property 9: Backward compatibility
|
|
*For any* existing task with a system status, the task must continue to display and function correctly after the custom status feature is deployed
|
|
**Validates: Requirements 6.1, 6.2**
|
|
|
|
### Property 10: Bulk status update validity
|
|
*For any* bulk status update operation, all selected tasks must belong to the same project and the target status must be valid for that project
|
|
**Validates: Requirements 10.1, 10.2**
|
|
|
|
## Error Handling
|
|
|
|
### Validation Errors
|
|
|
|
1. **Duplicate Status Name**
|
|
- HTTP 409 Conflict
|
|
- Message: "A status with the name '{name}' already exists in this project"
|
|
- Frontend: Display inline error in dialog
|
|
|
|
2. **Invalid Color Format**
|
|
- HTTP 422 Unprocessable Entity
|
|
- Message: "Color must be a valid hex code (e.g., #FF5733)"
|
|
- Frontend: Validate on input, show error message
|
|
|
|
3. **Status In Use**
|
|
- HTTP 422 Unprocessable Entity
|
|
- Response includes: task_count, task_ids
|
|
- Frontend: Show reassignment dialog with task count
|
|
|
|
4. **Invalid Status Reference**
|
|
- HTTP 404 Not Found
|
|
- Message: "Status '{status_id}' not found"
|
|
- Frontend: Refresh status list, show error toast
|
|
|
|
### Business Logic Errors
|
|
|
|
1. **Cannot Delete Default Status**
|
|
- Automatically reassign default to first remaining status
|
|
- Notify user of the change
|
|
|
|
2. **Cannot Delete Last Status**
|
|
- HTTP 400 Bad Request
|
|
- Message: "Cannot delete the last status. At least one status must exist."
|
|
|
|
3. **Reorder with Missing Status IDs**
|
|
- HTTP 400 Bad Request
|
|
- Message: "Reorder operation must include all existing status IDs"
|
|
|
|
### Database Errors
|
|
|
|
1. **Concurrent Modification**
|
|
- Use optimistic locking or retry logic
|
|
- HTTP 409 Conflict
|
|
- Message: "Status was modified by another user. Please refresh and try again."
|
|
|
|
2. **JSON Field Corruption**
|
|
- Validate JSON structure on read
|
|
- Fall back to empty array if corrupted
|
|
- Log error for investigation
|
|
|
|
## Testing Strategy
|
|
|
|
### Unit Tests
|
|
|
|
1. **Backend Unit Tests** (`backend/test_custom_task_status.py`):
|
|
- Test status CRUD operations
|
|
- Test validation logic (name uniqueness, color format)
|
|
- Test status deletion with task reassignment
|
|
- Test reordering logic
|
|
- Test default status management
|
|
- Test project isolation
|
|
|
|
2. **Frontend Unit Tests** (`frontend/src/components/settings/CustomTaskStatusManager.test.ts`):
|
|
- Test component rendering
|
|
- Test dialog interactions
|
|
- Test drag-and-drop reordering
|
|
- Test color picker functionality
|
|
- Test validation error display
|
|
|
|
### Integration Tests
|
|
|
|
1. **API Integration Tests**:
|
|
- Test complete status lifecycle (create → update → delete)
|
|
- Test status usage in task creation and updates
|
|
- Test bulk status updates with custom statuses
|
|
- Test status display across different views
|
|
|
|
2. **E2E Tests** (`frontend/test-custom-task-status.html`):
|
|
- Test creating a custom status and using it on a task
|
|
- Test editing a status and verifying UI updates
|
|
- Test deleting a status with reassignment
|
|
- Test reordering statuses via drag-and-drop
|
|
- Test setting default status
|
|
|
|
### Property-Based Tests
|
|
|
|
Property-based tests will use Python's `hypothesis` library for backend testing and `fast-check` for frontend testing where applicable.
|
|
|
|
1. **Property Test: Status name uniqueness** (Property 1)
|
|
- Generate random status names
|
|
- Attempt to create statuses with duplicate names
|
|
- Verify all rejections are correct
|
|
|
|
2. **Property Test: Color format validity** (Property 2)
|
|
- Generate random color strings (valid and invalid)
|
|
- Verify only valid hex codes are accepted
|
|
|
|
3. **Property Test: Status order consistency** (Property 5)
|
|
- Generate random reorder operations
|
|
- Verify order values remain continuous
|
|
|
|
4. **Property Test: Task status reference validity** (Property 6)
|
|
- Generate random task status assignments
|
|
- Verify all references resolve correctly
|
|
|
|
### Manual Testing Checklist
|
|
|
|
- [ ] Create custom status with auto-assigned color
|
|
- [ ] Create custom status with specific color
|
|
- [ ] Edit status name and verify UI updates everywhere
|
|
- [ ] Edit status color and verify badge updates
|
|
- [ ] Set a status as default and create new task
|
|
- [ ] Reorder statuses via drag-and-drop
|
|
- [ ] Delete unused status
|
|
- [ ] Attempt to delete status in use (verify error)
|
|
- [ ] Delete status with reassignment
|
|
- [ ] Use custom status in bulk update
|
|
- [ ] Verify status isolation between projects
|
|
- [ ] Verify backward compatibility with existing tasks
|
|
|
|
## Implementation Notes
|
|
|
|
### Migration Strategy
|
|
|
|
1. **Phase 1: Database Schema**
|
|
- Add `custom_task_statuses` JSON column to Project model
|
|
- Create migration script to add column with default empty array
|
|
- No changes to existing task statuses
|
|
|
|
2. **Phase 2: Backend API**
|
|
- Implement custom status CRUD endpoints
|
|
- Update task endpoints to support custom status references
|
|
- Maintain backward compatibility with system statuses
|
|
|
|
3. **Phase 3: Frontend Components**
|
|
- Create CustomTaskStatusManager component
|
|
- Update TaskStatusBadge to support custom colors
|
|
- Update status dropdowns to include custom statuses
|
|
- Update filters to include custom statuses
|
|
|
|
4. **Phase 4: Testing & Rollout**
|
|
- Run comprehensive test suite
|
|
- Deploy to staging environment
|
|
- Conduct user acceptance testing
|
|
- Deploy to production
|
|
|
|
### Backward Compatibility
|
|
|
|
- Existing tasks with system statuses will continue to work
|
|
- System statuses will always be available alongside custom statuses
|
|
- Status resolution logic: Check if status is system status first, then check custom statuses
|
|
- If a task has an invalid status reference, fall back to "not_started"
|
|
|
|
### Performance Considerations
|
|
|
|
- Custom statuses stored as JSON in Project table (minimal overhead)
|
|
- Status resolution happens in-memory (no additional queries)
|
|
- Consider caching project statuses in frontend store
|
|
- Bulk operations should batch database updates
|
|
|
|
### Security Considerations
|
|
|
|
- Only coordinators, project managers, and admins can manage statuses
|
|
- Validate all status references before updating tasks
|
|
- Sanitize status names to prevent XSS
|
|
- Rate limit status management endpoints
|
|
|
|
### UI/UX Considerations
|
|
|
|
- Use color picker with predefined palette for consistency
|
|
- Show visual preview of status badge while editing
|
|
- Provide clear feedback when status is in use
|
|
- Use drag handles for intuitive reordering
|
|
- Display task count prominently when deleting
|
|
- Auto-select reassignment status when deleting
|
|
- Show loading states during async operations
|