169 lines
7.0 KiB
PHP
169 lines
7.0 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(), [
|
|
'tasks' => $tasks->map(fn ($task) => [
|
|
'ulid' => $task->ulid,
|
|
'name' => $task->name,
|
|
'completion_percentage' => (float) $task->completion_percentage,
|
|
])->values(),
|
|
'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.');
|
|
}
|
|
}
|