statusReports() ->with(['workforceMetric', 'hseRecord', 'submitter:id,name']) ->when($request->status, fn ($q, $s) => $q->where('status', $s)) ->paginate(15) ->withQueryString(); return Inertia::render('ProjectReports::Reports/Index', [ 'project' => $project->load('status'), // we need status for transitions if we want to show it 'reports' => $reports, 'filters' => $request->only(['status']), 'statuses' => collect(ReportStatus::cases())->map(fn ($s) => [ 'value' => $s->value, 'label' => $s->label(), ]), 'allowedTransitions' => collect($project->status->allowedTransitions())->map(fn ($s) => [ 'value' => $s->value, 'label' => $s->label(), ]), ]); } public function create(Project $project) { $latestReport = $project->statusReports()->first(); $defaultStart = $latestReport ? $latestReport->period_end->addDay()->format('Y-m-d') : ($project->start_date?->format('Y-m-d') ?? now()->format('Y-m-d')); return Inertia::render('ProjectManagement::Reports/Create', [ 'project' => $project->only('id', 'name', 'code'), 'defaultPeriodStart' => $defaultStart, 'defaultPeriodEnd' => now()->format('Y-m-d'), ]); } public function store(Request $request, Project $project) { $validated = $this->validateReport($request); $report = DB::transaction(function () use ($validated, $project, $request) { $report = $project->statusReports()->create([ 'period_start' => $validated['period_start'], 'period_end' => $validated['period_end'], 'narrative_status' => $validated['narrative_status'] ?? null, 'narrative_weather' => $validated['narrative_weather'] ?? null, 'narrative_compliance' => $validated['narrative_compliance'] ?? null, 'submitted_by' => $request->user()->id, ]); $periodManHours = (float) ($validated['period_man_hours'] ?? 0); $cumulative = WorkforceMetric::calculateCumulative( $project->id, $validated['period_start'], $periodManHours, ); $report->workforceMetric()->create([ 'active_workforce' => $validated['active_workforce'] ?? 0, 'period_man_hours' => $periodManHours, 'cumulative_man_hours' => $cumulative, 'logistics_km' => $validated['logistics_km'] ?? 0, ]); $report->hseRecord()->create([ 'toolbox_meetings' => $validated['toolbox_meetings'] ?? 0, 'safety_observations' => $validated['safety_observations'] ?? 0, 'fatalities' => $validated['fatalities'] ?? 0, 'major_injuries' => $validated['major_injuries'] ?? 0, 'first_aid_cases' => $validated['first_aid_cases'] ?? 0, 'medical_cases' => $validated['medical_cases'] ?? 0, 'near_misses' => $validated['near_misses'] ?? 0, 'environmental_damage' => $validated['environmental_damage'] ?? 0, 'property_damage' => $validated['property_damage'] ?? 0, 'fines' => $validated['fines'] ?? 0, ]); return $report; }); // Auto-populate weather from task delays if user left fields empty $this->autoPopulateWeather($report, $project); return redirect()->route('projects.reports.show', [$project, $report]) ->with('success', 'Status report created.'); } public function show(Project $project, WeeklyStatusReport $report) { $report->load([ 'workforceMetric', 'hseRecord', 'submitter:id,name', 'approver:id,name', 'approvalChains.steps.approver:id,name', ]); return Inertia::render('ProjectManagement::Reports/Show', [ 'project' => $project->load(['customer:id,name', 'contractor:id,company_name']), 'report' => $report, ]); } public function edit(Project $project, WeeklyStatusReport $report) { if (!in_array($report->status, [ReportStatus::Draft, ReportStatus::Rejected])) { return back()->with('error', 'Only draft or rejected reports can be edited.'); } $report->load(['workforceMetric', 'hseRecord']); return Inertia::render('ProjectManagement::Reports/Edit', [ 'project' => $project->only('id', 'name', 'code'), 'report' => $report, ]); } public function update(Request $request, Project $project, WeeklyStatusReport $report) { if (!in_array($report->status, [ReportStatus::Draft, ReportStatus::Rejected])) { return back()->with('error', 'Only draft or rejected reports can be edited.'); } $validated = $this->validateReport($request); DB::transaction(function () use ($validated, $project, $report) { $report->update([ 'period_start' => $validated['period_start'], 'period_end' => $validated['period_end'], 'narrative_status' => $validated['narrative_status'] ?? null, 'narrative_weather' => $validated['narrative_weather'] ?? null, 'narrative_compliance' => $validated['narrative_compliance'] ?? null, 'status' => ReportStatus::Draft, // reset to draft on edit ]); $periodManHours = (float) ($validated['period_man_hours'] ?? 0); $cumulative = WorkforceMetric::calculateCumulative( $project->id, $validated['period_start'], $periodManHours, $report->id, ); $report->workforceMetric()->updateOrCreate( ['weekly_status_report_id' => $report->id], [ 'active_workforce' => $validated['active_workforce'] ?? 0, 'period_man_hours' => $periodManHours, 'cumulative_man_hours' => $cumulative, 'logistics_km' => $validated['logistics_km'] ?? 0, ] ); $report->hseRecord()->updateOrCreate( ['weekly_status_report_id' => $report->id], [ 'toolbox_meetings' => $validated['toolbox_meetings'] ?? 0, 'safety_observations' => $validated['safety_observations'] ?? 0, 'fatalities' => $validated['fatalities'] ?? 0, 'major_injuries' => $validated['major_injuries'] ?? 0, 'first_aid_cases' => $validated['first_aid_cases'] ?? 0, 'medical_cases' => $validated['medical_cases'] ?? 0, 'near_misses' => $validated['near_misses'] ?? 0, 'environmental_damage' => $validated['environmental_damage'] ?? 0, 'property_damage' => $validated['property_damage'] ?? 0, 'fines' => $validated['fines'] ?? 0, ] ); }); // Auto-populate weather from task delays if user left fields empty $this->autoPopulateWeather($report, $project); return redirect()->route('projects.reports.show', [$project, $report]) ->with('success', 'Status report updated.'); } public function submitForApproval(Request $request, Project $project, WeeklyStatusReport $report) { if ($report->status !== ReportStatus::Draft) { return back()->with('error', 'Only draft reports can be submitted for approval.'); } $report->transitionTo(ReportStatus::Submitted); // Get PM as the approver $pm = $project->personnel()->wherePivot('role', 'pm')->first(); if (!$pm) { return back()->with('error', 'No Project Manager assigned. Cannot submit for approval.'); } $this->approvalService->createChain( approvable: $report, approverIds: [$pm->id], type: 'weekly_status_report', initiatedBy: $request->user()->id, notes: "Weekly status report for {$report->period_label}", ); $report->transitionTo(ReportStatus::InReview); return back()->with('success', 'Report submitted for approval.'); } public function destroy(Project $project, WeeklyStatusReport $report) { if ($report->status !== ReportStatus::Draft) { return back()->with('error', 'Only draft reports can be deleted.'); } $report->delete(); return redirect()->route('projects.reports.index', $project) ->with('success', 'Report deleted.'); } private function validateReport(Request $request): array { return $request->validate([ 'period_start' => 'required|date', 'period_end' => 'required|date|after_or_equal:period_start', // Narrative 'narrative_status' => 'nullable|string|max:2000', 'narrative_weather' => 'nullable|string|max:2000', 'narrative_compliance' => 'nullable|string|max:2000', // Structured weather 'weather_work_days_lost' => 'nullable|integer|min:0', 'weather_conditions' => 'nullable|array', 'weather_conditions.*' => 'string', // Workforce 'active_workforce' => 'nullable|integer|min:0', 'period_man_hours' => 'nullable|numeric|min:0', 'logistics_km' => 'nullable|numeric|min:0', // HSE Proactive 'toolbox_meetings' => 'nullable|integer|min:0', 'safety_observations' => 'nullable|integer|min:0', // HSE Reactive 'fatalities' => 'nullable|integer|min:0', 'major_injuries' => 'nullable|integer|min:0', 'first_aid_cases' => 'nullable|integer|min:0', 'medical_cases' => 'nullable|integer|min:0', 'near_misses' => 'nullable|integer|min:0', 'environmental_damage' => 'nullable|integer|min:0', 'property_damage' => 'nullable|integer|min:0', 'fines' => 'nullable|numeric|min:0', ]); } private function autoPopulateWeather(WeeklyStatusReport $report, Project $project): void { $delays = TaskDelay::whereIn('task_id', $project->tasks()->pluck('id')) ->where('reason_type', DelayReason::Weather) ->whereBetween('delay_date', [$report->period_start, $report->period_end]) ->get(); if ($delays->isEmpty()) return; $totalHours = (float) $delays->sum('lost_hours'); $daysLost = (int) ceil($totalHours / 8); $conditions = $delays->pluck('weather_condition') ->filter() ->map(fn ($c) => $c instanceof WeatherCondition ? $c->value : $c) ->unique() ->values() ->toArray(); $updates = []; if ($report->weather_work_days_lost === 0 && $daysLost > 0) { $updates['weather_work_days_lost'] = $daysLost; } if (empty($report->weather_conditions) && !empty($conditions)) { $updates['weather_conditions'] = $conditions; } // Auto-generate narrative if empty if (empty($report->narrative_weather) && !empty($conditions)) { $labels = collect($conditions)->map(fn ($v) => WeatherCondition::tryFrom($v)?->label() ?? $v)->join(', '); $updates['narrative_weather'] = "Weather delays recorded: {$labels}. Total {$daysLost} work day(s) lost ({$totalHours} hours)."; } if (!empty($updates)) { $report->update($updates); } } }