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, ], ]); } }