user()->can('managementtools.access') || ! auth()->user()->can('managementtools.manage_employee_tasks')) { abort(403, 'Unauthorized action.'); } } protected function authorizeUserTasksPage(): void { if (! auth()->user()->can('managementtools.access') || (! auth()->user()->can('managementtools.manage_employee_tasks') && ! auth()->user()->can('managementtools.daily_report_all'))) { abort(403, 'Unauthorized action.'); } } protected function authorizeOwnOrManage(): void { if (! auth()->user()->can('managementtools.daily_report_own') && ! auth()->user()->can('managementtools.manage_employee_tasks')) { abort(403, 'Unauthorized action.'); } } protected const TABS = ['defined', 'pending', 'unreported', 'inability', 'incomplete', 'approved', 'all']; public function index(Request $request) { $this->authorizeUserTasksPage(); $business_id = (int) $request->session()->get('user.business_id'); $active_tab = $request->query('tab', 'defined'); if (! in_array($active_tab, self::TABS, true)) { $active_tab = 'defined'; } return view('managementtools::employee_tasks.index', [ 'tab_counts' => $this->buildTabCounts($business_id), 'active_tab' => $active_tab, 'can_manage' => auth()->user()->can('managementtools.manage_employee_tasks'), ]); } public function tabCounts(Request $request) { $this->authorizeUserTasksPage(); $business_id = (int) $request->session()->get('user.business_id'); return response()->json([ 'success' => true, 'counts' => $this->buildTabCounts($business_id), ]); } public function tabContent(Request $request, string $tab) { $this->authorizeUserTasksPage(); if (! in_array($tab, self::TABS, true)) { abort(404); } $business_id = (int) $request->session()->get('user.business_id'); $tasks = $this->tasksForTab($business_id, $tab, $request); $count = $tasks instanceof \Illuminate\Pagination\AbstractPaginator ? $tasks->total() : $tasks->count(); $html = view('managementtools::employee_tasks.partials.tab_pane_content', [ 'tab' => $tab, 'tasks' => $tasks, 'users' => $tab === 'approved' ? User::forDropdown($business_id, false) : [], 'filter_user_id' => $request->input('filter_user_id'), 'filter_category_id' => $request->input('filter_category_id'), 'filter_start' => $this->mtUtil->formatForInput($this->mtUtil->parseInputDate($request->filter_start)), 'filter_end' => $this->mtUtil->formatForInput($this->mtUtil->parseInputDate($request->filter_end)), 'can_manage' => auth()->user()->can('managementtools.manage_employee_tasks'), 'can_urgent_message' => auth()->user()->can('managementtools.urgent_messages'), 'can_task_report' => auth()->user()->can('managementtools.task_report'), 'has_pp_projects' => $this->ppBridge->isAvailable(), 'project_filter_options' => $this->ppBridge->isAvailable() ? $this->ppBridge->categoriesDropdown($business_id) : [], ])->render(); return response()->json([ 'success' => true, 'html' => $html, 'count' => $count, 'tab' => $tab, ]); } protected function buildTabCounts(int $business_id): array { $three_days_ago = Carbon::today()->subDays(3)->format('Y-m-d'); return [ 'defined' => MtUserTask::forBusiness($business_id)->where('task_status', 0)->count(), 'pending' => MtUserTask::forBusiness($business_id)->pendingApproval()->count(), 'unreported' => MtUserTask::forBusiness($business_id) ->where('task_status', 0) ->whereDoesntHave('dailyReports', fn ($q) => $q->where('report_date', '>=', $three_days_ago)) ->count(), 'inability' => MtUserTask::forBusiness($business_id) ->whereHas('dailyReports', fn ($q) => $q->where('task_status', 4)) ->count(), 'incomplete' => MtUserTask::forBusiness($business_id) ->where('task_status', 0) ->whereNotNull('due_date') ->where('due_date', '<', Carbon::today()) ->count(), 'approved' => MtUserTask::forBusiness($business_id)->where('task_status', 1)->count(), 'all' => MtUserTask::forBusiness($business_id)->count(), ]; } protected function applyProjectFilter($query, Request $request): void { if (! $request->filled('filter_category_id') || ! $this->ppBridge->isAvailable()) { return; } $filter = $request->input('filter_category_id'); if ($filter === 'uncategorized') { $query->whereNotNull('project_id')->whereHas('ppProject', fn ($q) => $q->uncategorized()); } elseif (is_numeric($filter)) { $query->whereHas('ppProject.categories', fn ($q) => $q->where('pp_project_categories.id', (int) $filter)); } } protected function tasksForTab(int $business_id, string $tab, Request $request) { $three_days_ago = Carbon::today()->subDays(3)->format('Y-m-d'); $per_page = 50; $with = ['user', 'ppProject.categories']; switch ($tab) { case 'defined': $query = MtUserTask::forBusiness($business_id) ->where('task_status', 0) ->with($with) ->orderByDesc('created_at'); $this->applyProjectFilter($query, $request); return $query->paginate($per_page); case 'pending': $query = MtUserTask::forBusiness($business_id) ->pendingApproval() ->with($with) ->orderByDesc('created_at'); $this->applyProjectFilter($query, $request); return $query->paginate($per_page); case 'unreported': $query = MtUserTask::forBusiness($business_id) ->where('task_status', 0) ->whereDoesntHave('dailyReports', fn ($q) => $q->where('report_date', '>=', $three_days_ago)) ->with($with) ->orderByDesc('created_at'); $this->applyProjectFilter($query, $request); return $query->paginate($per_page); case 'inability': $query = MtUserTask::forBusiness($business_id) ->whereHas('dailyReports', fn ($q) => $q->where('task_status', 4)) ->with($with) ->orderByDesc('updated_at'); $this->applyProjectFilter($query, $request); return $query->paginate($per_page); case 'incomplete': $query = MtUserTask::forBusiness($business_id) ->where('task_status', 0) ->whereNotNull('due_date') ->where('due_date', '<', Carbon::today()) ->with($with) ->orderBy('due_date'); $this->applyProjectFilter($query, $request); return $query->paginate(40); case 'approved': $query = MtUserTask::forBusiness($business_id)->where('task_status', 1)->with($with); if ($request->filled('filter_user_id')) { $query->where('user_id', (int) $request->filter_user_id); } if ($request->filled('filter_start')) { $start = $this->mtUtil->parseInputDate($request->filter_start); if ($start) { $query->whereDate('updated_at', '>=', $start); } } if ($request->filled('filter_end')) { $end = $this->mtUtil->parseInputDate($request->filter_end); if ($end) { $query->whereDate('updated_at', '<=', $end); } } $this->applyProjectFilter($query, $request); return $query->orderByDesc('updated_at')->paginate($per_page); case 'all': $query = MtUserTask::forBusiness($business_id) ->with($with) ->orderByDesc('created_at'); $this->applyProjectFilter($query, $request); return $query->paginate($per_page); default: abort(404); } } public function create() { $this->authorizeAccess(); $business_id = (int) request()->session()->get('user.business_id'); $users = User::forDropdown($business_id, true); $projects_grouped = $this->ppBridge->getProjectsGroupedByCategory($business_id, (int) request()->input('user_id', auth()->id())); return view('managementtools::employee_tasks.create', [ 'users' => $users, 'statuses' => MtUserTask::statusLabels(), 'has_pp_projects' => $this->ppBridge->isAvailable(), 'projects_grouped' => $projects_grouped, ]); } /** Admin creates a system task (pre-approved). */ public function store(Request $request) { $this->authorizeAccess(); $business_id = (int) $request->session()->get('user.business_id'); $request->validate([ 'user_id' => 'required|integer|exists:users,id', 'title' => 'required|string|max:255', 'description' => 'nullable|string|max:2000', 'due_date' => 'nullable|string|max:20', 'task_status' => 'nullable|integer|in:0,1,2', 'project_id' => 'nullable|integer', ]); if ($request->filled('project_id') && ! $this->ppBridge->validateProjectForUser($business_id, (int) $request->user_id, (int) $request->project_id)) { return response()->json(['success' => false, 'msg' => __('managementtools::lang.invalid_project')]); } MtUserTask::create([ 'business_id' => $business_id, 'user_id' => $request->user_id, 'project_id' => $request->input('project_id'), 'title' => $request->title, 'description' => $request->description, 'due_date' => $this->mtUtil->parseInputDate($request->due_date), 'task_status' => $request->input('task_status', 0), 'task_source' => MtUserTask::SOURCE_SYSTEM, 'approval_status' => MtUserTask::APPROVAL_APPROVED, 'approved_by' => auth()->id(), 'approved_at' => now(), 'created_by' => auth()->id(), ]); return response()->json(['success' => true, 'msg' => __('managementtools::lang.task_added')]); } /** Modal form for employee task submission (CRM-style). */ public function createSubmitForm() { $this->authorizeOwnOrManage(); $business_id = (int) request()->session()->get('user.business_id'); $projects_grouped = $this->ppBridge->getProjectsGroupedByCategory($business_id, auth()->id()); return view('managementtools::employee_tasks.submit_task_form', [ 'projects_grouped' => $projects_grouped, 'has_pp_projects' => $this->ppBridge->isAvailable(), ]); } /** Employee submits own task — pending manager approval. */ public function submitOwn(Request $request) { $this->authorizeOwnOrManage(); $business_id = (int) $request->session()->get('user.business_id'); $user_id = auth()->id(); $projects_grouped = $this->ppBridge->getProjectsGroupedByCategory($business_id, $user_id); $has_projects = $this->ppBridge->isAvailable() && ! empty($projects_grouped); $rules = [ 'title' => 'required|string|max:255', 'description' => 'nullable|string|max:2000', 'due_date' => 'nullable|string|max:20', 'estimated_hours' => 'required|integer|min:0|max:24', 'estimated_minutes' => 'required|integer|min:0|max:59', ]; if ($has_projects) { $rules['project_id'] = 'required|integer'; } $request->validate($rules); if ($has_projects && ! $this->ppBridge->validateProjectForUser($business_id, $user_id, (int) $request->project_id)) { return response()->json(['success' => false, 'msg' => __('managementtools::lang.invalid_project')]); } $minutes = ((int) $request->estimated_hours * 60) + (int) $request->estimated_minutes; MtUserTask::create([ 'business_id' => $business_id, 'user_id' => $user_id, 'project_id' => $request->input('project_id'), 'title' => $request->title, 'description' => $request->description, 'due_date' => $this->mtUtil->parseInputDate($request->due_date), 'estimated_minutes' => $minutes, 'task_status' => 0, 'task_source' => MtUserTask::SOURCE_USER, 'approval_status' => MtUserTask::APPROVAL_PENDING, 'created_by' => $user_id, ]); return response()->json(['success' => true, 'msg' => __('managementtools::lang.user_task_submitted')]); } /** Employee deletes own pending submission. */ public function deleteOwn($id) { $this->authorizeOwnOrManage(); $business_id = (int) request()->session()->get('user.business_id'); $task = MtUserTask::forBusiness($business_id) ->where('user_id', auth()->id()) ->userSubmitted() ->findOrFail($id); if (! $task->isPendingApproval()) { return response()->json(['success' => false, 'msg' => __('managementtools::lang.something_went_wrong')]); } $task->delete(); return response()->json(['success' => true, 'msg' => __('managementtools::lang.task_deleted')]); } protected function getProjectsForUser(int $business_id, int $user_id): array { return $this->ppBridge->getProjectsForUser($business_id, $user_id); } public function approve($id) { $this->authorizeAccess(); $business_id = (int) request()->session()->get('user.business_id'); $task = MtUserTask::forBusiness($business_id)->findOrFail($id); if (! $task->isPendingApproval()) { return response()->json(['success' => false, 'msg' => __('managementtools::lang.something_went_wrong')]); } $task->update([ 'approval_status' => MtUserTask::APPROVAL_APPROVED, 'approved_by' => auth()->id(), 'approved_at' => now(), ]); return response()->json(['success' => true, 'msg' => __('managementtools::lang.task_approved')]); } public function reject($id) { $this->authorizeAccess(); $business_id = (int) request()->session()->get('user.business_id'); $task = MtUserTask::forBusiness($business_id)->findOrFail($id); if (! $task->isPendingApproval()) { return response()->json(['success' => false, 'msg' => __('managementtools::lang.something_went_wrong')]); } $task->update([ 'approval_status' => MtUserTask::APPROVAL_REJECTED, 'approved_by' => auth()->id(), 'approved_at' => now(), 'task_status' => 2, ]); return response()->json(['success' => true, 'msg' => __('managementtools::lang.task_rejected')]); } public function edit($id) { $this->authorizeAccess(); $business_id = (int) request()->session()->get('user.business_id'); $task = MtUserTask::forBusiness($business_id)->findOrFail($id); $users = User::forDropdown($business_id, true); $projects_grouped = $this->ppBridge->getProjectsGroupedByCategory($business_id, (int) $task->user_id); return view('managementtools::employee_tasks.edit', [ 'task' => $task, 'users' => $users, 'statuses' => MtUserTask::statusLabels(), 'has_pp_projects' => $this->ppBridge->isAvailable(), 'projects_grouped' => $projects_grouped, ]); } public function update(Request $request, $id) { $this->authorizeAccess(); $business_id = (int) $request->session()->get('user.business_id'); $task = MtUserTask::forBusiness($business_id)->findOrFail($id); $request->validate([ 'user_id' => 'required|integer|exists:users,id', 'title' => 'required|string|max:255', 'description' => 'nullable|string|max:2000', 'due_date' => 'nullable|string|max:20', 'task_status' => 'nullable|integer|in:0,1,2', 'project_id' => 'nullable|integer', ]); if ($request->filled('project_id') && ! $this->ppBridge->validateProjectForUser($business_id, (int) $request->user_id, (int) $request->project_id)) { return response()->json(['success' => false, 'msg' => __('managementtools::lang.invalid_project')]); } $task->update([ 'user_id' => $request->user_id, 'project_id' => $request->input('project_id'), 'title' => $request->title, 'description' => $request->description, 'due_date' => $this->mtUtil->parseInputDate($request->due_date), 'task_status' => $request->input('task_status', 0), ]); return response()->json(['success' => true, 'msg' => __('managementtools::lang.task_updated')]); } public function destroy($id) { $this->authorizeAccess(); $business_id = (int) request()->session()->get('user.business_id'); $task = MtUserTask::forBusiness($business_id)->findOrFail($id); $task->delete(); return response()->json(['success' => true, 'msg' => __('managementtools::lang.task_deleted')]); } }