query('project'); $project = null; $availableMaterials = []; $employees = []; if ($projectUlid) { $project = Project::where('ulid', $projectUlid)->firstOrFail(); $project->load([ 'tasks' => fn ($q) => $q->with([ 'users:id,ulid,name', 'taskMaterials.material' => function ($query) { $query->select('id', 'ulid', 'name', 'unit', 'unit_cost', 'type') ->with('components.component:id,name,unit'); }, 'delays.reporter:id,name', 'activities.user:id,name' ])->orderBy('sort_order'), ]); $project->tasks->each(function ($task) { $task->append(['material_cost', 'total_cost']); }); $employees = User::whereIn('user_type', ['admin', 'employee']) ->with('employeeProfile') ->select('id', 'ulid', 'name', 'email') ->get(); $availableMaterials = \Modules\MaterialLogistics\Models\ProjectInventory::where('project_id', $project->id) ->where('on_hand_qty', '>', 0) ->with([ 'material:id,ulid,name,sku,unit,unit_cost,category,type', 'material.components.component:id,name,unit' ]) ->get() ->map(fn ($inv) => [ 'id' => $inv->material->id, 'ulid' => $inv->material->ulid, 'name' => $inv->material->name, 'sku' => $inv->material->sku, 'unit' => $inv->material->unit, 'unit_cost' => $inv->material->unit_cost, 'category' => $inv->material->category, 'type' => $inv->material->type, 'components' => $inv->material->components, 'available_qty' => $inv->available_qty, 'on_hand_qty' => (float) $inv->on_hand_qty, 'allocated_qty' => (float) $inv->allocated_qty, ]); } return Inertia::render('TaskManagement::Tasks/Index', [ 'project' => $project, 'projects' => Project::select('id', 'ulid', 'name')->get(), 'employees' => $employees, 'availableMaterials' => $availableMaterials, 'delayReasons' => collect(\Modules\ProjectManagement\Enums\DelayReason::cases())->map(fn ($r) => [ 'value' => $r->value, 'label' => $r->label(), ]), 'weatherConditions' => collect(\Modules\ProjectManagement\Enums\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', 'assigned_users' => 'nullable|array', 'assigned_users.*' => 'string', 'labor_cost' => 'nullable|numeric|min:0', 'estimated_hours' => 'nullable|numeric|min:0', 'start_date' => 'nullable|date', 'end_date' => 'nullable|date|after_or_equal:start_date', ]); $project = Project::findOrFail($validated['project_id']); $userIds = []; if (!empty($validated['assigned_users'])) { foreach ($validated['assigned_users'] as $ulid) { $userIds[] = User::resolveUlidToId($ulid); } } $validated['sort_order'] = $project->tasks()->count(); $task = Task::create($validated); $task->users()->sync($userIds); if (!empty($userIds)) { $task->activities()->create([ 'user_id' => auth()->id(), 'description' => 'Assigned ' . count($userIds) . ' user(s) to the task.', 'type' => 'assignment', ]); } $this->updateProjectCompletion($project); $project->load('tasks.taskMaterials'); $project->recalculateCapitalization(); return back()->with('success', 'Task created.'); } public function update(Request $request, Task $task) { $project = $task->project; $validated = $request->validate([ 'name' => 'required|string|max:255', 'description' => 'nullable|string', 'assigned_users' => 'nullable|array', 'assigned_users.*' => 'string', 'labor_cost' => 'nullable|numeric|min:0', 'estimated_hours' => 'nullable|numeric|min:0', 'actual_hours' => 'nullable|numeric|min:0', 'start_date' => 'nullable|date', 'end_date' => 'nullable|date|after_or_equal:start_date', 'completion_percentage' => 'nullable|numeric|min:0|max:100', ]); $userIds = []; if (!empty($validated['assigned_users'])) { foreach ($validated['assigned_users'] as $ulid) { $userIds[] = User::resolveUlidToId($ulid); } } $oldUserIds = $task->users()->pluck('users.id')->toArray(); $task->update($validated); $task->users()->sync($userIds); $addedUsers = array_diff($userIds, $oldUserIds); $removedUsers = array_diff($oldUserIds, $userIds); if (!empty($addedUsers) || !empty($removedUsers)) { $changes = []; if (!empty($addedUsers)) $changes[] = 'assigned ' . count($addedUsers) . ' user(s)'; if (!empty($removedUsers)) $changes[] = 'removed ' . count($removedUsers) . ' user(s)'; $task->activities()->create([ 'user_id' => auth()->id(), 'description' => ucfirst(implode(' and ', $changes)) . '.', 'type' => 'assignment', ]); } $project->load('tasks.taskMaterials'); $project->recalculateCapitalization(); return back()->with('success', 'Task updated.'); } public function destroy(Task $task) { $project = $task->project; $task->delete(); $this->updateProjectCompletion($project); $project->load('tasks.taskMaterials'); $project->recalculateCapitalization(); return back()->with('success', 'Task deleted.'); } public function transition(Request $request, Task $task) { $project = $task->project; $request->validate(['status' => 'required|string']); $newStatus = TaskStatus::from($request->status); try { $task->transitionTo($newStatus); if ($newStatus === TaskStatus::Completed) { TaskCompleted::dispatch($task); } $this->updateProjectCompletion($project); } catch (\InvalidArgumentException $e) { return back()->with('error', $e->getMessage()); } return back()->with('success', "Task status changed to {$newStatus->label()}."); } public function reorder(Request $request) { $validated = $request->validate([ 'tasks' => 'required|array', 'tasks.*.ulid' => 'required|string|exists:tasks,ulid', 'tasks.*.status' => 'required|string', 'tasks.*.sort_order' => 'required|integer|min:0', ]); $project = null; foreach ($validated['tasks'] as $taskData) { $task = Task::where('ulid', $taskData['ulid'])->first(); if ($task) { if (!$project) { $project = $task->project; } $newStatus = TaskStatus::tryFrom($taskData['status']); if ($newStatus && $task->status !== $newStatus) { try { $task->transitionTo($newStatus); if ($newStatus === TaskStatus::Completed) { TaskCompleted::dispatch($task); } } catch (\InvalidArgumentException $e) { // Skip invalid transitions, just update order } } $task->update(['sort_order' => $taskData['sort_order']]); } } if ($project) { $this->updateProjectCompletion($project); } return back()->with('success', 'Tasks reordered successfully.'); } private function updateProjectCompletion(Project $project): void { $project->refresh(); $tasks = $project->tasks; $total = $tasks->count(); $completed = $total > 0 ? $tasks->where('status', 'completed')->count() : 0; $project->update([ 'completion_percentage' => $total > 0 ? round(($completed / $total) * 100, 2) : 0, ]); } }