feat(backend): enhance allocation and project management
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.
This commit is contained in:
@@ -3,14 +3,18 @@
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\ProjectMonthPlanResource;
|
||||
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.
|
||||
@@ -18,7 +22,7 @@ class ProjectMonthPlanController extends Controller
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$year = $request->query('year', date('Y'));
|
||||
|
||||
|
||||
$startDate = "{$year}-01-01";
|
||||
$endDate = "{$year}-12-01";
|
||||
|
||||
@@ -27,19 +31,28 @@ class ProjectMonthPlanController extends Controller
|
||||
->get()
|
||||
->groupBy('project_id');
|
||||
|
||||
// Get all active projects for the year
|
||||
$projects = \App\Models\Project::where('active', true)->get();
|
||||
// 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 = $projectPlans->firstWhere('month', $monthDate);
|
||||
|
||||
$months[$monthDate] = $plan
|
||||
$plan = $planByMonth->get($monthDate);
|
||||
|
||||
$months[$monthDate] = $plan
|
||||
? [
|
||||
'id' => $plan->id,
|
||||
'planned_hours' => $plan->planned_hours,
|
||||
@@ -50,32 +63,22 @@ class ProjectMonthPlanController extends Controller
|
||||
|
||||
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
|
||||
$data->each(function (&$project) {
|
||||
$project['plan_sum'] = collect($project['months'])
|
||||
->filter(fn ($m) => $m !== null && $m['planned_hours'] !== null)
|
||||
->sum('planned_hours');
|
||||
|
||||
$approved = $project['approved_estimate'] ?? 0;
|
||||
if ($approved > 0) {
|
||||
if ($project['plan_sum'] > $approved) {
|
||||
$project['reconciliation_status'] = 'OVER';
|
||||
} elseif ($project['plan_sum'] < $approved) {
|
||||
$project['reconciliation_status'] = 'UNDER';
|
||||
} elseif ($project['plan_sum'] == $approved) {
|
||||
$project['reconciliation_status'] = 'MATCH';
|
||||
} else {
|
||||
$project['reconciliation_status'] = 'UNDER';
|
||||
}
|
||||
} else {
|
||||
$project['reconciliation_status'] = 'UNDER'; // No estimate = under
|
||||
}
|
||||
// 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([
|
||||
@@ -115,7 +118,7 @@ class ProjectMonthPlanController extends Controller
|
||||
|
||||
foreach ($items as $item) {
|
||||
$projectId = $item['project_id'];
|
||||
$month = $item['month'] . '-01'; // Convert YYYY-MM to YYYY-MM-01
|
||||
$month = $item['month'].'-01'; // Convert YYYY-MM to YYYY-MM-01
|
||||
$plannedHours = $item['planned_hours']; // Can be null to clear
|
||||
|
||||
$plan = ProjectMonthPlan::firstOrNew([
|
||||
@@ -130,8 +133,8 @@ class ProjectMonthPlanController extends Controller
|
||||
} elseif ($plannedHours !== null) {
|
||||
$plan->planned_hours = $plannedHours;
|
||||
$plan->save();
|
||||
|
||||
if (!$plan->wasRecentlyCreated) {
|
||||
|
||||
if (! $plan->wasRecentlyCreated) {
|
||||
$updated++;
|
||||
} else {
|
||||
$created++;
|
||||
|
||||
Reference in New Issue
Block a user