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.'); } }