Files
GSB-Construction/Modules/TaskManagement/app/Http/Controllers/TaskController.php
Ajjj ccbd23d474
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
feat: enhance module RBAC permissions, financial billing workflows, task evidence uploading, and role analytics
2026-08-13 18:46:26 +08:00

381 lines
14 KiB
PHP

<?php
namespace Modules\TaskManagement\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Modules\ProjectManagement\Enums\TaskStatus;
use Modules\ProjectManagement\Events\TaskCompleted;
use Modules\ProjectManagement\Models\Project;
use Modules\ProjectManagement\Models\Task;
class TaskController extends Controller
{
public function index(Request $request)
{
$projectUlid = $request->query('project');
$project = null;
$availableMaterials = [];
$employees = [];
if ($projectUlid) {
$project = Project::where('ulid', $projectUlid)->firstOrFail();
$project->load([
'milestones' => fn ($q) => $q->orderBy('sort_order'),
'tasks' => fn ($q) => $q->with([
'users:id,ulid,name,profile_picture',
'milestone: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'])
->whereHas('projects', function ($q) use ($project) {
$q->where('projects.id', $project->id);
})
->with('employeeProfile')
->select('id', 'ulid', 'name', 'email', 'profile_picture')
->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::with('parentProject:id,ulid,name,code')->get(['id', 'ulid', 'name', 'code', 'project_type', 'parent_project_id']),
'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',
'equipment_cost' => 'nullable|numeric|min:0',
'estimated_hours' => 'nullable|numeric|min:0',
'start_date' => 'nullable|date',
'end_date' => 'nullable|date|after_or_equal:start_date',
'milestone_id' => 'nullable|string|exists:project_milestones,ulid',
]);
$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();
if (empty($validated['labor_cost'])) {
$validated['labor_cost'] = 0;
}
if (empty($validated['equipment_cost'])) {
$validated['equipment_cost'] = 0;
}
if (empty($validated['estimated_hours'])) {
$validated['estimated_hours'] = 0;
}
$validated['milestone_id'] = !empty($validated['milestone_id'])
? \Modules\ProjectManagement\Models\ProjectMilestone::resolveUlidToId($validated['milestone_id'])
: null;
$task = Task::create($validated);
$task->users()->sync($userIds);
if ($request->hasFile('evidence')) {
foreach ($request->file('evidence') as $file) {
$task->addMedia($file)->toMediaCollection('evidence');
}
}
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',
'equipment_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',
'milestone_id' => 'nullable|string|exists:project_milestones,ulid',
]);
$userIds = [];
if (!empty($validated['assigned_users'])) {
foreach ($validated['assigned_users'] as $ulid) {
$userIds[] = User::resolveUlidToId($ulid);
}
}
$oldUserIds = $task->users()->pluck('users.id')->toArray();
if (array_key_exists('labor_cost', $validated) && empty($validated['labor_cost'])) {
$validated['labor_cost'] = 0;
}
if (array_key_exists('equipment_cost', $validated) && empty($validated['equipment_cost'])) {
$validated['equipment_cost'] = 0;
}
if (array_key_exists('estimated_hours', $validated) && empty($validated['estimated_hours'])) {
$validated['estimated_hours'] = 0;
}
if (array_key_exists('actual_hours', $validated) && empty($validated['actual_hours'])) {
$validated['actual_hours'] = 0;
}
if (array_key_exists('milestone_id', $validated)) {
$validated['milestone_id'] = !empty($validated['milestone_id'])
? \Modules\ProjectManagement\Models\ProjectMilestone::resolveUlidToId($validated['milestone_id'])
: null;
}
if (array_key_exists('completion_percentage', $validated)) {
$pct = (float) $validated['completion_percentage'];
if ($pct >= 100) {
$validated['status'] = 'completed';
} elseif ($pct > 0 && $task->status === TaskStatus::Pending) {
$validated['status'] = 'in_progress';
}
}
$task->update($validated);
$this->updateProjectCompletion($project);
$task->users()->sync($userIds);
if ($request->hasFile('evidence')) {
foreach ($request->file('evidence') as $file) {
$task->addMedia($file)->toMediaCollection('evidence');
}
}
$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 uploadEvidence(Request $request, Task $task)
{
$request->validate([
'evidence' => 'required|array',
'evidence.*' => 'image|max:10240',
]);
foreach ($request->file('evidence') as $file) {
$task->addMedia($file)->toMediaCollection('evidence');
}
return back()->with('success', 'Evidence uploaded successfully.');
}
public function deleteEvidence(Task $task, $mediaId)
{
$media = $task->media()->findOrFail($mediaId);
$media->delete();
return back()->with('success', 'Evidence photo deleted.');
}
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);
$completionPct = match ($newStatus->value) {
'completed', 'closed' => 100,
'in_progress' => 50,
default => 0,
};
$task->update([
'status' => $newStatus->value,
'completion_percentage' => $completionPct,
]);
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
}
}
$completionPct = match ($taskData['status']) {
'completed', 'closed' => 100,
'in_progress' => 50,
default => 0,
};
$task->update([
'status' => $taskData['status'],
'sort_order' => $taskData['sort_order'],
'completion_percentage' => $completionPct,
]);
}
}
if ($project) {
$this->updateProjectCompletion($project);
}
return back()->with('success', 'Tasks updated 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,
]);
}
}