projectService = $projectService; } /** * List all projects * * Get a list of all projects with optional filtering by status and type. * * @authenticated * * @queryParam status_id integer Filter by status ID. Example: 1 * @queryParam type_id integer Filter by type ID. Example: 2 * * @response 200 [ * { * "id": "550e8400-e29b-41d4-a716-446655440000", * "code": "PROJ-001", * "title": "Client Dashboard Redesign", * "status_id": 1, * "status": {"id": 1, "name": "Pre-sales"}, * "type_id": 2, * "type": {"id": 2, "name": "Support"}, * "approved_estimate": "120.00", * "forecasted_effort": {"2024-02": 40, "2024-03": 60, "2024-04": 20}, * "created_at": "2024-01-15T10:00:00.000000Z", * "updated_at": "2024-01-15T10:00:00.000000Z" * } * ] */ public function index(Request $request): JsonResponse { $statusId = $request->query('status_id') ? (int) $request->query('status_id') : null; $typeId = $request->query('type_id') ? (int) $request->query('type_id') : null; $projects = $this->projectService->getAll($statusId, $typeId); return response()->json($projects); } /** * Create a new project * * Create a new project with code, title, and type. * * @authenticated * * @bodyParam code string required Project code (must be unique). Example: PROJ-001 * @bodyParam title string required Project title. Example: Client Dashboard Redesign * @bodyParam type_id integer required Project type ID. Example: 1 * * @response 201 { * "id": "550e8400-e29b-41d4-a716-446655440000", * "code": "PROJ-001", * "title": "Client Dashboard Redesign", * "status_id": 1, * "status": {"id": 1, "name": "Pre-sales"}, * "type_id": 1, * "type": {"id": 1, "name": "Project"} * } * @response 422 {"message":"Validation failed","errors":{"code":["Project code must be unique"],"title":["The title field is required."]}} */ public function store(Request $request): JsonResponse { try { $project = $this->projectService->create($request->all()); return response()->json($project, 201); } catch (ValidationException $e) { return response()->json([ 'message' => 'Validation failed', 'errors' => $e->validator->errors(), ], 422); } } /** * Get a single project * * Get details of a specific project by ID. * * @authenticated * * @urlParam id string required Project UUID. Example: 550e8400-e29b-41d4-a716-446655440000 * * @response 200 { * "id": "550e8400-e29b-41d4-a716-446655440000", * "code": "PROJ-001", * "title": "Client Dashboard Redesign", * "status": {"id": 1, "name": "Pre-sales"}, * "type": {"id": 1, "name": "Project"}, * "approved_estimate": "120.00", * "forecasted_effort": {"2024-02": 40, "2024-03": 60} * } * @response 404 {"message":"Project not found"} */ public function show(string $id): JsonResponse { $project = $this->projectService->findById($id); if (! $project) { return response()->json([ 'message' => 'Project not found', ], 404); } return response()->json($project); } /** * Update a project * * Update details of an existing project. * * @authenticated * * @urlParam id string required Project UUID. Example: 550e8400-e29b-41d4-a716-446655440000 * * @bodyParam code string Project code (must be unique). Example: PROJ-002 * @bodyParam title string Project title. Example: Updated Title * @bodyParam type_id integer Project type ID. Example: 2 * * @response 200 { * "id": "550e8400-e29b-41d4-a716-446655440000", * "code": "PROJ-002", * "title": "Updated Title", * "type_id": 2 * } * @response 404 {"message":"Project not found"} * @response 422 {"message":"Validation failed","errors":{"type_id":["The selected type id is invalid."]}} */ public function update(Request $request, string $id): JsonResponse { $project = Project::find($id); if (! $project) { return response()->json([ 'message' => 'Project not found', ], 404); } try { $project = $this->projectService->update($project, $request->only([ 'code', 'title', 'type_id', ])); return response()->json($project); } catch (ValidationException $e) { return response()->json([ 'message' => 'Validation failed', 'errors' => $e->validator->errors(), ], 422); } } /** * Transition project status * * Transition project to a new status following the state machine rules. * * @authenticated * * @urlParam id string required Project UUID. Example: 550e8400-e29b-41d4-a716-446655440000 * * @bodyParam status_id integer required Target status ID. Example: 2 * * @response 200 { * "id": "550e8400-e29b-41d4-a716-446655440000", * "status": {"id": 2, "name": "SOW Approval"} * } * @response 404 {"message":"Project not found"} * @response 422 {"message":"Cannot transition from Pre-sales to Done"} */ public function updateStatus(Request $request, string $id): JsonResponse { $project = Project::with('status')->find($id); if (! $project) { return response()->json([ 'message' => 'Project not found', ], 404); } $request->validate([ 'status_id' => 'required|integer|exists:project_statuses,id', ]); try { $project = $this->projectService->transitionStatus( $project, (int) $request->input('status_id') ); return response()->json($project); } catch (\RuntimeException $e) { return response()->json([ 'message' => $e->getMessage(), ], 422); } } /** * Set approved estimate * * Set the approved billable hours estimate for a project. * * @authenticated * * @urlParam id string required Project UUID. Example: 550e8400-e29b-41d4-a716-446655440000 * * @bodyParam approved_estimate number required Approved estimate hours (must be > 0). Example: 120 * * @response 200 {"id":"550e8400-e29b-41d4-a716-446655440000", "approved_estimate":"120.00"} * @response 404 {"message":"Project not found"} * @response 422 {"message":"Approved estimate must be greater than 0"} */ public function setEstimate(Request $request, string $id): JsonResponse { $project = Project::find($id); if (! $project) { return response()->json([ 'message' => 'Project not found', ], 404); } $request->validate([ 'approved_estimate' => 'required|numeric', ]); try { $project = $this->projectService->setApprovedEstimate( $project, (float) $request->input('approved_estimate') ); return response()->json($project); } catch (\RuntimeException $e) { return response()->json([ 'message' => $e->getMessage(), ], 422); } } /** * Set forecasted effort * * Set the month-by-month forecasted effort breakdown. * * @authenticated * * @urlParam id string required Project UUID. Example: 550e8400-e29b-41d4-a716-446655440000 * * @bodyParam forecasted_effort object required Monthly effort breakdown. Example: {"2024-02": 40, "2024-03": 60} * * @response 200 {"id":"550e8400-e29b-41d4-a716-446655440000", "forecasted_effort":{"2024-02":40,"2024-03":60}} * @response 404 {"message":"Project not found"} * @response 422 {"message":"Forecasted effort exceeds approved estimate by more than 5%"} */ public function setForecast(Request $request, string $id): JsonResponse { $project = Project::find($id); if (! $project) { return response()->json([ 'message' => 'Project not found', ], 404); } $request->validate([ 'forecasted_effort' => 'required|array', ]); try { $project = $this->projectService->setForecastedEffort( $project, $request->input('forecasted_effort') ); return response()->json($project); } catch (\RuntimeException $e) { return response()->json([ 'message' => $e->getMessage(), ], 422); } } /** * Get all project types * * @authenticated * * @response 200 [ * {"id": 1, "name": "Project"}, * {"id": 2, "name": "Support"}, * {"id": 3, "name": "Engagement"} * ] */ public function types(): JsonResponse { $types = ProjectType::orderBy('name')->get(['id', 'name']); return response()->json($types); } /** * Get all project statuses * * @authenticated * * @response 200 [ * {"id": 1, "name": "Pre-sales", "order": 1}, * {"id": 2, "name": "SOW Approval", "order": 2}, * {"id": 3, "name": "Gathering Estimates", "order": 3} * ] */ public function statuses(): JsonResponse { $statuses = ProjectStatus::orderBy('order')->get(['id', 'name', 'order']); return response()->json($statuses); } /** * Delete a project * * Delete a project. Cannot delete if project has allocations or actuals. * * @authenticated * * @urlParam id string required Project UUID. Example: 550e8400-e29b-41d4-a716-446655440000 * * @response 200 {"message":"Project deleted successfully"} * @response 404 {"message":"Project not found"} * @response 422 {"message":"Cannot delete project with allocations"} */ public function destroy(string $id): JsonResponse { $project = Project::find($id); if (! $project) { return response()->json([ 'message' => 'Project not found', ], 404); } $canDelete = $this->projectService->canDelete($project); if (! $canDelete['canDelete']) { return response()->json([ 'message' => "Cannot delete project with {$canDelete['reason']}", ], 422); } $project->delete(); return response()->json([ 'message' => 'Project deleted successfully', ]); } }