authorize('viewAny', Task::class); $query = Task::with(['project', 'assignee', 'reporter']); // Filter by project if ($request->filled('project_id')) { $query->where('project_id', $request->project_id); } // Filter by status if ($request->filled('status')) { $query->byStatus($request->status); } // Filter by priority if ($request->filled('priority')) { $query->byPriority($request->priority); } // Filter by assignee if ($request->filled('assignee_id')) { $query->where('assignee_id', $request->assignee_id); } // Only top-level tasks by default $query->topLevel(); $query->orderBy('order')->orderBy('due_date', 'asc'); $tasks = $query->paginate(20)->appends($request->query()); $projects = Project::whereIn('status', ['planning', 'active'])->get(); $users = User::where('tenant_id', $this->getTenantId())->where('is_active', true)->get(); $statuses = ['pending', 'in_progress', 'review', 'done', 'cancelled']; $priorities = ['low', 'medium', 'high', 'urgent']; return view('tasks.index', compact('tasks', 'projects', 'users', 'statuses', 'priorities')); } /** * Show the form for creating a new task. */ public function create(Request $request) { $this->authorize('create', Task::class); $projects = Project::whereIn('status', ['planning', 'active'])->get(); $users = User::where('tenant_id', $this->getTenantId())->where('is_active', true)->get(); $statuses = ['pending', 'in_progress', 'review', 'done', 'cancelled']; $priorities = ['low', 'medium', 'high', 'urgent']; $preselectedProject = $request->get('project_id'); return view('tasks.form', compact('projects', 'users', 'statuses', 'priorities', 'preselectedProject')); } /** * Store a newly created task in storage. */ public function store(StoreTaskRequest $request) { $this->authorize('create', Task::class); $data = $request->validated(); $data['tenant_id'] = $this->getTenantId(); $data['reporter_id'] = Auth::id(); // Convert jalali dates if needed if (!empty($data['due_date'])) { $data['due_date'] = \App\Helpers\CalendarHelper::parseDate($data['due_date']); } if (!empty($data['start_date'])) { $data['start_date'] = \App\Helpers\CalendarHelper::parseDate($data['start_date']); } $task = Task::create($data); // Log the creation TaskLog::create([ 'task_id' => $task->id, 'user_id' => Auth::id(), 'field_changed' => 'created', 'old_value' => null, 'new_value' => ['status' => $task->status], 'comment' => __('task.log_created'), ]); return redirect() ->route('tasks.show', $task) ->with('success', __('task.created_successfully')); } /** * Display the specified task with logs. */ public function show(Task $task) { $this->authorize('view', $task); $task->load([ 'project', 'assignee', 'reporter', 'logs.user', 'children.assignee', 'workLogs.employee', ]); return view('tasks.show', compact('task')); } /** * Show the form for editing the specified task. */ public function edit(Task $task) { $this->authorize('update', $task); $projects = Project::whereIn('status', ['planning', 'active'])->get(); $users = User::where('tenant_id', $this->getTenantId())->where('is_active', true)->get(); $statuses = ['pending', 'in_progress', 'review', 'done', 'cancelled']; $priorities = ['low', 'medium', 'high', 'urgent']; return view('tasks.form', compact('task', 'projects', 'users', 'statuses', 'priorities')); } /** * Update the specified task in storage. */ public function update(StoreTaskRequest $request, Task $task) { $this->authorize('update', $task); $data = $request->validated(); // Convert jalali dates if needed if (!empty($data['due_date'])) { $data['due_date'] = \App\Helpers\CalendarHelper::parseDate($data['due_date']); } if (!empty($data['start_date'])) { $data['start_date'] = \App\Helpers\CalendarHelper::parseDate($data['start_date']); } // Track changes for TaskLog $changedFields = []; $watchFields = ['status', 'priority', 'assignee_id', 'title', 'due_date', 'project_id']; foreach ($watchFields as $field) { if (isset($data[$field]) && $task->$field != $data[$field]) { $changedFields[$field] = [ 'old' => $task->$field, 'new' => $data[$field], ]; } } $task->update($data); // Create TaskLog entries for changed fields foreach ($changedFields as $field => $changes) { TaskLog::create([ 'task_id' => $task->id, 'user_id' => Auth::id(), 'field_changed' => $field, 'old_value' => $changes['old'], 'new_value' => $changes['new'], ]); } return redirect() ->route('tasks.show', $task) ->with('success', __('task.updated_successfully')); } /** * Remove the specified task from storage. */ public function destroy(Task $task) { $this->authorize('delete', $task); $task->delete(); return redirect() ->route('tasks.index') ->with('success', __('task.deleted_successfully')); } /** * Update task status via AJAX. */ public function updateStatus(Request $request, Task $task) { $this->authorize('updateStatus', $task); $request->validate([ 'status' => 'required|in:pending,in_progress,review,done,cancelled', ]); $oldStatus = $task->status; $task->status = $request->status; if ($request->status === 'done') { $task->completed_at = now(); } elseif ($oldStatus === 'done' && $request->status !== 'done') { $task->completed_at = null; } $task->save(); // Log the status change TaskLog::create([ 'task_id' => $task->id, 'user_id' => Auth::id(), 'field_changed' => 'status', 'old_value' => $oldStatus, 'new_value' => $request->status, ]); return response()->json([ 'success' => true, 'message' => __('task.status_updated'), 'status' => $task->status, 'status_label' => $task->status_label, ]); } /** * Display tasks in Kanban board view. */ public function kanban(Request $request) { $this->authorize('viewAny', Task::class); $query = Task::with(['project', 'assignee']); if ($request->filled('project_id')) { $query->where('project_id', $request->project_id); } $query->topLevel(); $allTasks = $query->get(); $kanbanColumns = [ 'pending' => $allTasks->where('status', 'pending'), 'in_progress' => $allTasks->where('status', 'in_progress'), 'review' => $allTasks->where('status', 'review'), 'done' => $allTasks->where('status', 'done'), ]; $projects = Project::whereIn('status', ['planning', 'active'])->get(); $selectedProject = $request->get('project_id'); return view('tasks.kanban', compact('kanbanColumns', 'projects', 'selectedProject')); } /** * Display tasks in Gantt chart view. */ public function gantt(Request $request, ?Task $task = null) { $this->authorize('viewAny', Task::class); $query = Task::with(['project', 'assignee']); if ($task && $task->exists) { $query->where('project_id', $task->project_id); } if ($request->filled('project_id')) { $query->where('project_id', $request->project_id); } $tasks = $query->get(); $projects = Project::whereIn('status', ['planning', 'active'])->get(); // Group tasks by project for virtual project rows $ganttData = []; $projectIdCounter = 10000; $grouped = $tasks->groupBy('project_id'); foreach ($grouped as $projectId => $projectTasks) { $project = $projectTasks->first()?->project; if (!$project) continue; // Calculate project date range from tasks or use project dates $minStart = $projectTasks->filter(fn($t) => $t->start_date)->min('start_date') ?? $project->start_date ?? now(); $maxDue = $projectTasks->filter(fn($t) => $t->due_date)->max('due_date') ?? $project->end_date ?? now()->addDays(30); // Add virtual project row $ganttData[] = [ 'id' => $projectIdCounter + $projectId, 'text' => $project->name, 'start_date' => \Carbon\Carbon::parse($minStart)->format('d-m-Y'), 'duration' => max(1, \Carbon\Carbon::parse($minStart)->diffInDays(\Carbon\Carbon::parse($maxDue))), 'progress' => 0, 'open' => true, 'type' => 'project', 'project_id' => $projectId, ]; // Add task rows — handle missing dates foreach ($projectTasks as $t) { $startDate = $t->start_date ? \Carbon\Carbon::parse($t->start_date) : \Carbon\Carbon::parse($minStart); $dueDate = $t->due_date ? \Carbon\Carbon::parse($t->due_date) : $startDate->copy()->addDays(7); $duration = max(1, $startDate->diffInDays($dueDate)); $statusColor = match($t->status) { 'done' => '#10b981', 'in_progress' => '#3b82f6', 'review' => '#f59e0b', 'cancelled' => '#ef4444', default => '#14b8a6', }; $ganttData[] = [ 'id' => $t->id, 'text' => $t->title, 'start_date' => $startDate->format('d-m-Y'), 'duration' => $duration, 'progress' => (float) ($t->progress ?? 0) / 100, 'parent' => $projectIdCounter + $projectId, 'color' => $statusColor, 'project_id' => $projectId, 'status' => $t->status, 'assignee' => $t->assignee?->name ?? '', ]; } } return view('tasks.gantt', compact('ganttData', 'projects')); } }