feat(project): Complete Project Lifecycle capability with full TDD workflow

- Implement ProjectController with CRUD, status transitions, estimate/forecast
- Add ProjectService with state machine validation
- Extract ProjectStatusService for reusable state machine logic
- Add ProjectPolicy for role-based authorization
- Create ProjectSeeder with test data
- Implement frontend project management UI with modal forms
- Add projectService API client
- Complete all 9 incomplete unit tests (ProjectModelTest, ProjectForecastTest, ProjectPolicyTest)
- Fix E2E test timing issues with loading state waits
- Add Scribe API documentation annotations
- Improve forecasted effort validation messages with detailed feedback

Test Results:
- Backend: 49 passed (182 assertions)
- Frontend Unit: 32 passed
- E2E: 134 passed (Chromium + Firefox)

Phase 3 Refactor:
- Extract ProjectStatusService for state machine
- Optimize project list query with status joins
- Improve forecasted effort validation messages

Phase 4 Document:
- Add Scribe annotations to ProjectController
- Generate API documentation
This commit is contained in:
2026-02-19 02:43:05 -05:00
parent 8f70e81d29
commit 8ed56c9f7c
19 changed files with 5126 additions and 173 deletions

View File

@@ -0,0 +1,102 @@
import { api } from './api';
export interface ProjectType {
id: number;
name: string;
}
export interface ProjectStatus {
id: number;
name: string;
order?: number;
}
export interface Project {
id: string;
code: string;
title: string;
type_id: number;
status_id: number;
type?: ProjectType;
status?: ProjectStatus;
approved_estimate?: string | number | null;
forecasted_effort?: Record<string, number> | null;
created_at?: string;
updated_at?: string;
}
export interface CreateProjectRequest {
code: string;
title: string;
type_id: number;
}
export interface UpdateProjectRequest {
code?: string;
title?: string;
type_id?: number;
}
export const projectService = {
/**
* Get all projects
*/
getAll: (statusId?: number, typeId?: number) => {
const params = new URLSearchParams();
if (statusId) params.append('status_id', String(statusId));
if (typeId) params.append('type_id', String(typeId));
const query = params.toString() ? `?${params.toString()}` : '';
return api.get<Project[]>(`/projects${query}`);
},
/**
* Get a single project by ID
*/
getById: (id: string) => api.get<Project>(`/projects/${id}`),
/**
* Create a new project
*/
create: (data: CreateProjectRequest) => api.post<Project>('/projects', data),
/**
* Update project basic info (code, title, type)
*/
update: (id: string, data: UpdateProjectRequest) =>
api.put<Project>(`/projects/${id}`, data),
/**
* Delete a project
*/
delete: (id: string) => api.delete<{ message: string }>(`/projects/${id}`),
/**
* Transition project status
*/
updateStatus: (id: string, statusId: number) =>
api.put<Project>(`/projects/${id}/status`, { status_id: statusId }),
/**
* Set approved estimate
*/
setEstimate: (id: string, estimate: number) =>
api.put<Project>(`/projects/${id}/estimate`, { approved_estimate: estimate }),
/**
* Set forecasted effort
*/
setForecast: (id: string, forecast: Record<string, number>) =>
api.put<Project>(`/projects/${id}/forecast`, { forecasted_effort: forecast }),
/**
* Get all project types
*/
getTypes: () => api.get<ProjectType[]>('/projects/types'),
/**
* Get all project statuses
*/
getStatuses: () => api.get<ProjectStatus[]>('/projects/statuses'),
};
export default projectService;