Files
GSB-Construction/Modules/TimelineScheduling/app/Http/Controllers/MilestoneController.php
Christopher Boyles 64d4b331a8
Some checks failed
Tests / PHP 8.3 (push) Has been cancelled
Tests / PHP 8.4 (push) Has been cancelled
Tests / PHP 8.5 (push) Has been cancelled
Additional Changes
2026-06-02 22:04:03 +08:00

164 lines
6.7 KiB
PHP

<?php
namespace Modules\TimelineScheduling\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Modules\ProjectManagement\Enums\WeatherCondition;
use Modules\ProjectManagement\Models\Project;
use Modules\ProjectManagement\Models\ProjectMilestone;
class MilestoneController extends Controller
{
public function index(Request $request)
{
$projectUlid = $request->query('project');
$project = null;
if ($projectUlid) {
$project = Project::where('ulid', $projectUlid)->firstOrFail();
$project->load([
'tasks' => fn ($q) => $q->select('id', 'project_id')->with('delays'),
'milestones' => fn ($q) => $q->with(['tasks.taskMaterials.material']),
]);
}
return \Inertia\Inertia::render('TimelineScheduling::Timeline/Index', [
'project' => $project,
'projects' => Project::with('parentProject:id,ulid,name,code')->get(['id', 'ulid', 'name', 'code', 'project_type', 'parent_project_id']),
'milestones' => $project ? $project->milestones->map(function ($m) {
$tasks = $m->tasks;
// Job done calculation
if ($tasks->isEmpty()) {
$jobDone = $m->actual_date !== null ? 100.0 : 0.0;
} else {
$jobDone = round($tasks->avg('completion_percentage') ?? 0, 2);
}
// Materials Spent and Planned calculations
$totalPlannedCost = 0.0;
$totalActualCost = 0.0;
$materialsMap = [];
foreach ($tasks as $task) {
foreach ($task->taskMaterials as $tm) {
$plannedCost = (float) $tm->planned_qty * (float) $tm->unit_cost;
$actualCost = (float) $tm->actual_qty * (float) $tm->unit_cost;
$totalPlannedCost += $plannedCost;
$totalActualCost += $actualCost;
$matId = $tm->material_id;
$matName = $tm->material->name ?? 'Unknown Material';
$matUnit = $tm->material->unit ?? 'pcs';
if (!isset($materialsMap[$matId])) {
$materialsMap[$matId] = [
'material_id' => $matId,
'name' => $matName,
'unit' => $matUnit,
'planned_qty' => 0.0,
'actual_qty' => 0.0,
'planned_cost' => 0.0,
'actual_cost' => 0.0,
];
}
$materialsMap[$matId]['planned_qty'] += (float) $tm->planned_qty;
$materialsMap[$matId]['actual_qty'] += (float) $tm->actual_qty;
$materialsMap[$matId]['planned_cost'] += $plannedCost;
$materialsMap[$matId]['actual_cost'] += $actualCost;
}
}
$materialsDetail = array_values($materialsMap);
$materialsSpentPercentage = $totalPlannedCost > 0
? round(($totalActualCost / $totalPlannedCost) * 100, 2)
: 0.0;
return array_merge($m->toArray(), [
'is_completed' => $m->is_completed,
'is_overdue' => $m->is_overdue,
'status' => $m->status,
'status_color' => $m->status_color,
'days_delayed' => $m->days_delayed,
'job_done_percentage' => $jobDone,
'total_planned_materials_cost' => $totalPlannedCost,
'total_actual_materials_cost' => $totalActualCost,
'materials_spent_percentage' => $materialsSpentPercentage,
'materials_detail' => $materialsDetail,
'has_tasks_or_materials' => !$tasks->isEmpty() || $totalPlannedCost > 0,
]);
}) : [],
'milestoneStats' => $project ? [
'total' => $project->milestones->count(),
'completed' => $project->milestones->where('actual_date', '!=', null)->count(),
'completion_percentage' => $project->milestone_completion,
'weather_delay_days' => $project->weather_delay_days,
] : null,
'weatherConditions' => collect(WeatherCondition::cases())->map(fn ($c) => [
'value' => $c->value,
'label' => $c->label(),
'icon' => $c->icon(),
]),
'allowedTransitions' => $project ? collect($project->status->allowedTransitions())->map(fn ($s) => [
'value' => $s->value,
'label' => $s->label(),
]) : [],
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'project_id' => 'required|exists:projects,id',
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'planned_date' => 'nullable|date',
'weight_percentage' => 'required|numeric|min:0|max:100',
]);
$project = Project::findOrFail($validated['project_id']);
$validated['sort_order'] = $project->milestones()->count();
$validated['is_default'] = false;
ProjectMilestone::create($validated);
return back()->with('success', 'Milestone added.');
}
public function update(Request $request, ProjectMilestone $milestone)
{
$validated = $request->validate([
'name' => 'sometimes|required|string|max:255',
'description' => 'nullable|string',
'planned_date' => 'nullable|date',
'actual_date' => 'nullable|date',
'weight_percentage' => 'sometimes|numeric|min:0|max:100',
'weather_impacted' => 'sometimes|boolean',
'weather_condition' => 'nullable|string',
'weather_delay_days' => 'sometimes|integer|min:0',
'weather_notes' => 'nullable|string|max:2000',
]);
// Clear weather fields if not impacted
if (isset($validated['weather_impacted']) && !$validated['weather_impacted']) {
$validated['weather_condition'] = null;
$validated['weather_delay_days'] = 0;
$validated['weather_notes'] = null;
}
$milestone->update($validated);
return back()->with('success', 'Milestone updated.');
}
public function destroy(ProjectMilestone $milestone)
{
$milestone->delete();
return back()->with('success', 'Milestone removed.');
}
}