Update controllers and services for allocation fidelity: - AllocationController: variance data in responses, bulk operations - ProjectController: include plan data in responses - ProjectMonthPlanController: planning grid API - AllocationMatrixService: support untracked allocations - ProjectResource/TeamMemberResource: include reconciliation data Improved test coverage for allocation flows.
155 lines
5.0 KiB
PHP
155 lines
5.0 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\ProjectMonthPlan;
|
|
use App\Services\ReconciliationCalculator;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Validator;
|
|
|
|
class ProjectMonthPlanController extends Controller
|
|
{
|
|
public function __construct(
|
|
private ReconciliationCalculator $reconciliationCalculator
|
|
) {}
|
|
|
|
/**
|
|
* GET /api/project-month-plans?year=2026
|
|
* Returns month-plan grid payload by project/month for the year.
|
|
*/
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
$year = $request->query('year', date('Y'));
|
|
|
|
$startDate = "{$year}-01-01";
|
|
$endDate = "{$year}-12-01";
|
|
|
|
$plans = ProjectMonthPlan::whereBetween('month', [$startDate, $endDate])
|
|
->with('project')
|
|
->get()
|
|
->groupBy('project_id');
|
|
|
|
// Get all projects for the year with status relationship
|
|
$projects = \App\Models\Project::with('status')->get();
|
|
|
|
// Build grid payload
|
|
$data = $projects->map(function ($project) use ($plans, $year) {
|
|
$projectPlans = $plans->get($project->id, collect());
|
|
$planByMonth = $projectPlans->mapWithKeys(function ($plan) {
|
|
$monthKey = $plan->month?->format('Y-m-01');
|
|
|
|
if ($monthKey === null) {
|
|
return [];
|
|
}
|
|
|
|
return [$monthKey => $plan];
|
|
});
|
|
|
|
$months = [];
|
|
for ($month = 1; $month <= 12; $month++) {
|
|
$monthDate = sprintf('%04d-%02d-01', $year, $month);
|
|
$plan = $planByMonth->get($monthDate);
|
|
|
|
$months[$monthDate] = $plan
|
|
? [
|
|
'id' => $plan->id,
|
|
'planned_hours' => $plan->planned_hours,
|
|
'is_blank' => $plan->planned_hours === null,
|
|
]
|
|
: null;
|
|
}
|
|
|
|
return [
|
|
'project_id' => $project->id,
|
|
'project_code' => $project->code,
|
|
'project_name' => $project->title,
|
|
'project_status' => $project->status?->name,
|
|
'approved_estimate' => $project->approved_estimate,
|
|
'months' => $months,
|
|
];
|
|
});
|
|
|
|
// Calculate reconciliation status for each project using the service
|
|
$reconciliationResults = $this->reconciliationCalculator->calculateForProjects($projects, (int) $year);
|
|
|
|
$data = $data->map(function ($project) use ($reconciliationResults) {
|
|
$project['plan_sum'] = $reconciliationResults[$project['project_id']]['plan_sum'] ?? 0;
|
|
$project['reconciliation_status'] = $reconciliationResults[$project['project_id']]['status'] ?? 'UNDER';
|
|
|
|
return $project;
|
|
});
|
|
|
|
return response()->json([
|
|
'data' => $data,
|
|
'meta' => [
|
|
'year' => (int) $year,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* PUT /api/project-month-plans/bulk
|
|
* Bulk upsert month plan cells.
|
|
*/
|
|
public function bulkUpdate(Request $request): JsonResponse
|
|
{
|
|
$validator = Validator::make($request->all(), [
|
|
'year' => 'required|integer|min:2020|max:2100',
|
|
'items' => 'required|array',
|
|
'items.*.project_id' => 'required|uuid|exists:projects,id',
|
|
'items.*.month' => 'required|date_format:Y-m',
|
|
'items.*.planned_hours' => 'nullable|numeric|min:0',
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
return response()->json([
|
|
'message' => 'Validation failed',
|
|
'errors' => $validator->errors(),
|
|
], 422);
|
|
}
|
|
|
|
$year = $request->input('year');
|
|
$items = $request->input('items');
|
|
$created = 0;
|
|
$updated = 0;
|
|
$cleared = 0;
|
|
|
|
foreach ($items as $item) {
|
|
$projectId = $item['project_id'];
|
|
$month = $item['month'].'-01'; // Convert YYYY-MM to YYYY-MM-01
|
|
$plannedHours = $item['planned_hours']; // Can be null to clear
|
|
|
|
$plan = ProjectMonthPlan::firstOrNew([
|
|
'project_id' => $projectId,
|
|
'month' => $month,
|
|
]);
|
|
|
|
if ($plannedHours === null && $plan->exists) {
|
|
// Clear semantics: delete the row to represent blank
|
|
$plan->delete();
|
|
$cleared++;
|
|
} elseif ($plannedHours !== null) {
|
|
$plan->planned_hours = $plannedHours;
|
|
$plan->save();
|
|
|
|
if (! $plan->wasRecentlyCreated) {
|
|
$updated++;
|
|
} else {
|
|
$created++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return response()->json([
|
|
'message' => 'Bulk update complete',
|
|
'summary' => [
|
|
'created' => $created,
|
|
'updated' => $updated,
|
|
'cleared' => $cleared,
|
|
],
|
|
]);
|
|
}
|
|
}
|