requirePost(request()); if (!verify_csrf_token((string) request()->input('_token'))) { return $this->renderProjectWithTaskErrors((int) $projectId, ['_token' => ['The security token expired. Refresh the page and try again.']], $this->taskFormFromRequest()); } $projectRepository = new ProjectRepository(); $project = $projectRepository->findBoard((int) $projectId); if ($project === null) { return \Core\Response::notFound('Project not found.'); } $data = $this->taskDataFromRequest(); $errors = $this->validateTask($data); if ($errors !== []) { return $this->renderProjectWithTaskErrors((int) $projectId, $errors, $data); } $taskId = (new TaskRepository())->create((int) $projectId, $data); (new ActivityRepository())->record( 'task_created', 'Added task ' . $data['title'], 'Task created on the project board.', (int) $projectId, $taskId ); return $this->redirect('/projects/' . (int) $projectId . '?task=created#task-' . $taskId); } public function updateStatus(string $id) { $this->requirePost(request()); if (!verify_csrf_token((string) request()->input('_token'))) { return $this->redirect('/projects'); } $status = trim((string) request()->input('status', '')); if (!in_array($status, self::TASK_STATUS_OPTIONS, true)) { return $this->redirect('/projects'); } $projectId = (new TaskRepository())->updateStatus((int) $id, $status); if ($projectId === null) { return \Core\Response::notFound('Task not found.'); } (new ActivityRepository())->record( 'task_status_changed', 'Task updated to ' . task_status_label($status), 'Task status changed on the project board.', $projectId, (int) $id ); return $this->redirect('/projects/' . $projectId . '?task-updated=1#task-' . (int) $id); } private function taskFormFromRequest(): array { $input = request()->all(); return [ 'title' => (string) ($input['title'] ?? ''), 'description' => (string) ($input['description'] ?? ''), 'priority' => (string) ($input['priority'] ?? 'normal'), 'status' => (string) ($input['status'] ?? 'backlog'), 'assignee' => (string) ($input['assignee'] ?? ''), 'estimate_hours' => (string) ($input['estimate_hours'] ?? ''), 'due_date' => (string) ($input['due_date'] ?? ''), ]; } private function taskDataFromRequest(): array { $input = request()->all(); $estimateHours = trim((string) ($input['estimate_hours'] ?? '')); $dueDate = trim((string) ($input['due_date'] ?? '')); return [ 'title' => trim((string) ($input['title'] ?? '')), 'description' => trim((string) ($input['description'] ?? '')), 'priority' => trim((string) ($input['priority'] ?? 'normal')), 'status' => trim((string) ($input['status'] ?? 'backlog')), 'assignee' => trim((string) ($input['assignee'] ?? '')), 'estimate_hours' => $estimateHours !== '' ? (float) $estimateHours : null, 'due_date' => $dueDate !== '' ? $dueDate : null, ]; } private function validateTask(array $data): array { $validator = new Validator(); $validator->required('title', $data['title'], 'Task title is required.')->maxLength('title', $data['title'], 140); $validator->required('description', $data['description'], 'Task description is required.')->maxLength('description', $data['description'], 800); $validator->required('assignee', $data['assignee'], 'Assign the task to someone.')->maxLength('assignee', $data['assignee'], 100); if (!in_array($data['priority'], self::PRIORITY_OPTIONS, true)) { $validator->required('priority', null, 'Choose a valid task priority.'); } if (!in_array($data['status'], self::TASK_STATUS_OPTIONS, true)) { $validator->required('status', null, 'Choose a valid task status.'); } if ($data['due_date'] !== null && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $data['due_date'])) { $validator->required('due_date', null, 'Use a valid due date.'); } if ($data['estimate_hours'] !== null && $data['estimate_hours'] < 0) { $validator->required('estimate_hours', null, 'Estimate hours must be 0 or more.'); } return $validator->errors(); } private function renderProjectWithTaskErrors(int $projectId, array $errors, array $old) { $projectRepository = new ProjectRepository(); $project = $projectRepository->findBoard($projectId); if ($project === null) { return \Core\Response::notFound('Project not found.'); } return $this->view('projects.show', [ 'pageTitle' => $project['name'], 'project' => $project, 'taskStatusOptions' => self::TASK_STATUS_OPTIONS, 'priorityOptions' => self::PRIORITY_OPTIONS, 'activity' => (new ActivityRepository())->forProject($projectId, 12), 'taskErrors' => $errors, 'taskOld' => array_merge($this->taskFormFromRequest(), $old), ]); } }