database ??= database(); } public function create(int $projectId, array $data): int { $position = (int) ($this->database->first( 'SELECT COALESCE(MAX(position), 0) + 1 AS next_position FROM tasks WHERE project_id = :project_id AND status = :status', [ 'project_id' => $projectId, 'status' => $data['status'] ?? 'backlog', ] )['next_position'] ?? 1); $this->database->execute( 'INSERT INTO tasks ( project_id, title, description, status, priority, assignee, estimate_hours, due_date, position, completed_at ) VALUES ( :project_id, :title, :description, :status, :priority, :assignee, :estimate_hours, :due_date, :position, :completed_at )', [ 'project_id' => $projectId, 'title' => $data['title'], 'description' => $data['description'] ?? '', 'status' => $data['status'] ?? 'backlog', 'priority' => $data['priority'] ?? 'normal', 'assignee' => $data['assignee'] ?? '', 'estimate_hours' => $data['estimate_hours'] ?? null, 'due_date' => $data['due_date'] ?? null, 'position' => $position, 'completed_at' => ($data['status'] ?? 'backlog') === 'done' ? date('Y-m-d H:i:s') : null, ] ); return (int) $this->database->pdo()->lastInsertId(); } public function updateStatus(int $taskId, string $status): ?int { $task = $this->database->first('SELECT id, project_id, title, status FROM tasks WHERE id = :id', ['id' => $taskId]); if ($task === null) { return null; } $completedAt = $status === 'done' ? date('Y-m-d H:i:s') : null; $this->database->execute( 'UPDATE tasks SET status = :status, completed_at = :completed_at, updated_at = CURRENT_TIMESTAMP WHERE id = :id', [ 'status' => $status, 'completed_at' => $completedAt, 'id' => $taskId, ] ); return (int) $task['project_id']; } public function dueSoon(int $limit = 6): array { $limit = max(1, min(25, $limit)); return $this->database->query( 'SELECT t.*, p.name AS project_name, p.code AS project_code FROM tasks t INNER JOIN projects p ON p.id = t.project_id WHERE t.status != "done" AND t.due_date IS NOT NULL AND date(t.due_date) BETWEEN date("now") AND date("now", "+7 day") ORDER BY date(t.due_date) ASC, CASE t.priority WHEN "urgent" THEN 0 WHEN "high" THEN 1 WHEN "normal" THEN 2 ELSE 3 END, t.id DESC LIMIT ' . $limit ); } public function overdue(int $limit = 6): array { $limit = max(1, min(25, $limit)); return $this->database->query( 'SELECT t.*, p.name AS project_name, p.code AS project_code FROM tasks t INNER JOIN projects p ON p.id = t.project_id WHERE t.status != "done" AND t.due_date IS NOT NULL AND date(t.due_date) < date("now") ORDER BY date(t.due_date) ASC, t.id DESC LIMIT ' . $limit ); } public function countsByStatus(int $projectId): array { $rows = $this->database->query( 'SELECT status, COUNT(*) AS total FROM tasks WHERE project_id = :project_id GROUP BY status', ['project_id' => $projectId] ); $counts = []; foreach ($rows as $row) { $counts[$row['status']] = (int) $row['total']; } return $counts; } public function forProject(int $projectId): array { return $this->database->query( 'SELECT * FROM tasks WHERE project_id = :project_id ORDER BY CASE status WHEN "backlog" THEN 0 WHEN "in-progress" THEN 1 WHEN "review" THEN 2 WHEN "blocked" THEN 3 WHEN "done" THEN 4 ELSE 5 END, CASE priority WHEN "urgent" THEN 0 WHEN "high" THEN 1 WHEN "normal" THEN 2 ELSE 3 END, due_date IS NULL, due_date ASC, position ASC, id ASC', ['project_id' => $projectId] ); } }