chore: update document approval workflow and bug fixes
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\ProjectReports\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Inertia\Inertia;
|
||||
use Modules\ApprovalWorkflow\Services\ApprovalService;
|
||||
use Modules\ProjectManagement\Enums\DelayReason;
|
||||
use Modules\ProjectManagement\Enums\ReportStatus;
|
||||
use Modules\ProjectManagement\Enums\WeatherCondition;
|
||||
use Modules\ProjectManagement\Models\HseRecord;
|
||||
use Modules\ProjectManagement\Models\Project;
|
||||
use Modules\ProjectManagement\Models\TaskDelay;
|
||||
use Modules\ProjectManagement\Models\WeeklyStatusReport;
|
||||
use Modules\ProjectManagement\Models\WorkforceMetric;
|
||||
|
||||
class StatusReportController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private ApprovalService $approvalService,
|
||||
) {}
|
||||
|
||||
public function index(Request $request, Project $project)
|
||||
{
|
||||
$reports = $project->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user