|
- <?php
-
- declare(strict_types=1);
-
- namespace App\Controllers;
-
- use App\Repositories\ActivityRepository;
- use App\Repositories\ProjectRepository;
- use Core\Controller;
- use Core\Validator;
-
- class ProjectController extends Controller
- {
- private const STATUS_OPTIONS = ['planned', 'active', 'at-risk', 'paused', 'done'];
- private const TASK_STATUS_OPTIONS = ['backlog', 'in-progress', 'review', 'blocked', 'done'];
- private const PRIORITY_OPTIONS = ['low', 'normal', 'high', 'urgent'];
-
- public function index()
- {
- $search = trim((string) request()->input('search', ''));
- $status = trim((string) request()->input('status', ''));
-
- $repository = new ProjectRepository();
-
- return $this->view('projects.index', [
- 'pageTitle' => 'Projects',
- 'projects' => $repository->all($search, $status),
- 'summary' => $repository->dashboardSummary(),
- 'search' => $search,
- 'status' => $status,
- 'statusOptions' => self::STATUS_OPTIONS,
- ]);
- }
-
- public function create()
- {
- return $this->view('projects.create', [
- 'pageTitle' => 'Create project',
- 'statusOptions' => self::STATUS_OPTIONS,
- 'old' => $this->defaultProjectForm(),
- 'errors' => [],
- ]);
- }
-
- public function store()
- {
- $this->requirePost(request());
-
- if (!verify_csrf_token((string) request()->input('_token'))) {
- return $this->renderCreateWithErrors([
- '_token' => ['The security token expired. Refresh the page and try again.'],
- ], $this->defaultProjectForm(request()->all()));
- }
-
- $data = $this->projectDataFromRequest();
- $errors = $this->validateProject($data);
-
- if ($errors !== []) {
- return $this->renderCreateWithErrors($errors, $data);
- }
-
- $projectId = (new ProjectRepository())->create($data);
-
- return $this->redirect('/projects/' . $projectId);
- }
-
- public function show(string $id)
- {
- $projectId = (int) $id;
- $repository = new ProjectRepository();
- $project = $repository->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' => [],
- 'taskOld' => $this->defaultTaskForm(),
- ]);
- }
-
- public function updateStatus(string $id)
- {
- $this->requirePost(request());
-
- if (!verify_csrf_token((string) request()->input('_token'))) {
- return $this->redirect('/projects/' . (int) $id . '?error=token');
- }
-
- $status = trim((string) request()->input('status', ''));
-
- if (!in_array($status, self::STATUS_OPTIONS, true)) {
- return $this->redirect('/projects/' . (int) $id . '?error=status');
- }
-
- (new ProjectRepository())->updateStatus((int) $id, $status);
-
- return $this->redirect('/projects/' . (int) $id . '?updated=1');
- }
-
- private function renderCreateWithErrors(array $errors, array $old)
- {
- return $this->view('projects.create', [
- 'pageTitle' => 'Create project',
- 'statusOptions' => self::STATUS_OPTIONS,
- 'errors' => $errors,
- 'old' => array_merge($this->defaultProjectForm(), $old),
- ]);
- }
-
- private function defaultProjectForm(array $input = []): array
- {
- return [
- 'name' => (string) ($input['name'] ?? ''),
- 'code' => (string) ($input['code'] ?? ''),
- 'client_name' => (string) ($input['client_name'] ?? ''),
- 'description' => (string) ($input['description'] ?? ''),
- 'status' => (string) ($input['status'] ?? 'planned'),
- 'start_date' => (string) ($input['start_date'] ?? date('Y-m-d')),
- 'due_date' => (string) ($input['due_date'] ?? ''),
- 'budget_cents' => (string) ($input['budget_cents'] ?? '0'),
- 'owner_name' => (string) ($input['owner_name'] ?? ''),
- 'color_token' => (string) ($input['color_token'] ?? 'teal'),
- 'members_text' => (string) ($input['members_text'] ?? ''),
- ];
- }
-
- private function defaultTaskForm(array $input = []): array
- {
- 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 projectDataFromRequest(): array
- {
- $input = request()->all();
-
- return [
- 'name' => trim((string) ($input['name'] ?? '')),
- 'code' => trim((string) ($input['code'] ?? '')),
- 'client_name' => trim((string) ($input['client_name'] ?? '')),
- 'description' => trim((string) ($input['description'] ?? '')),
- 'status' => trim((string) ($input['status'] ?? 'planned')),
- 'start_date' => trim((string) ($input['start_date'] ?? '')),
- 'due_date' => trim((string) ($input['due_date'] ?? '')),
- 'budget_cents' => max(0, (int) preg_replace('/[^0-9]/', '', (string) ($input['budget_cents'] ?? '0'))),
- 'owner_name' => trim((string) ($input['owner_name'] ?? '')),
- 'color_token' => trim((string) ($input['color_token'] ?? 'teal')),
- 'members' => $this->parseMembers((string) ($input['members_text'] ?? '')),
- 'members_text' => (string) ($input['members_text'] ?? ''),
- ];
- }
-
- private function validateProject(array $data): array
- {
- $validator = new Validator();
-
- $validator->required('name', $data['name'], 'Project name is required.')->maxLength('name', $data['name'], 120);
- $validator->required('client_name', $data['client_name'], 'Client name is required.')->maxLength('client_name', $data['client_name'], 120);
- $validator->required('owner_name', $data['owner_name'], 'Project owner is required.')->maxLength('owner_name', $data['owner_name'], 120);
- $validator->required('description', $data['description'], 'Project summary is required.')->maxLength('description', $data['description'], 800);
- $validator->required('status', $data['status'], 'Choose a project status.');
- $validator->required('start_date', $data['start_date'], 'Start date is required.');
-
- if ($data['due_date'] !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $data['due_date'])) {
- $validator->required('due_date', null, 'Use a valid due date.');
- }
-
- if (!in_array($data['status'], self::STATUS_OPTIONS, true)) {
- $validator->required('status', null, 'Choose a valid project status.');
- }
-
- return $validator->errors();
- }
-
- private function parseMembers(string $value): array
- {
- $lines = preg_split('/[\r\n,]+/', $value) ?: [];
- $members = [];
-
- foreach ($lines as $index => $line) {
- $name = trim($line);
- if ($name === '') {
- continue;
- }
-
- $members[] = [
- 'full_name' => $name,
- 'role' => $index === 0 ? 'Project Lead' : 'Contributor',
- 'allocation_percent' => $index === 0 ? 40 : 20,
- ];
- }
-
- return $members;
- }
- }
|