validate([ 'ticket_key' => ['sometimes', 'string', 'regex:/^[A-Z0-9]+-[1-9][0-9]*$/'], 'query' => ['sometimes', 'string', 'max:255'], 'project_id' => ['sometimes', 'integer'], 'ticket_status_id' => ['sometimes', 'integer'], 'ticket_type_id' => ['sometimes', 'integer'], 'assignee_user_id' => ['sometimes', 'nullable', 'integer'], 'parent_ticket_id' => ['sometimes', 'nullable', 'integer'], 'page' => ['sometimes', 'integer', 'min:1'], 'per_page' => ['sometimes', 'integer', 'min:1', 'max:100'], ]); $user = $this->kanbanGitMcp->user($request); $this->kanbanGitMcp->projectForRead($user, $input['project_id'] ?? null); $query = Ticket::query()->whereIn('project_id', $this->permissionService->viewableProjectIds($user)); foreach (['ticket_key', 'project_id', 'ticket_status_id', 'ticket_type_id', 'assignee_user_id', 'parent_ticket_id'] as $filter) { if (Arr::exists($input, $filter)) { $query->where($filter, $input[$filter]); } } if (Arr::exists($input, 'query')) { $pattern = '%'.str_replace(['!', '%', '_'], ['!!', '!%', '!_'], $input['query']).'%'; $query->where(fn (Builder $q) => $q->whereRaw("title LIKE ? ESCAPE '!'", [$pattern])->orWhereRaw("description LIKE ? ESCAPE '!'", [$pattern])); } $tickets = $query->with('ticketStatus:id,name,slug,board_column_id')->orderBy('id') ->paginate($input['per_page'] ?? 25, ['id', 'ticket_key', 'title', 'project_id', 'ticket_status_id'], 'page', $input['page'] ?? 1); return Response::structured([ 'data' => $tickets->getCollection()->map(fn (Ticket $ticket): array => [ 'id' => $ticket->id, 'ticket_key' => $ticket->ticket_key, 'title' => $ticket->title, 'project_id' => $ticket->project_id, 'ticket_status_id' => $ticket->ticket_status_id, 'status' => $ticket->ticketStatus === null ? null : ['id' => $ticket->ticketStatus->id, 'name' => $ticket->ticketStatus->name, 'slug' => $ticket->ticketStatus->slug], ])->all(), 'meta' => $this->kanbanGitMcp->paginationPayload($tickets), ]); } /** @return array */ public function schema(JsonSchema $schema): array { return [ 'ticket_key' => $schema->string()->description('Exact full key, e.g. KAN-67.'), 'query' => $schema->string()->max(255)->description('Literal substring in title or description.'), 'project_id' => $schema->integer()->description('Visible project ID.'), 'ticket_status_id' => $schema->integer()->description('Status ID from get_project_configuration.'), 'ticket_type_id' => $schema->integer()->description('Type ID from get_project_configuration.'), 'assignee_user_id' => $schema->integer()->nullable()->description('Assignee ID; null selects unassigned tickets.'), 'parent_ticket_id' => $schema->integer()->nullable()->description('Parent ID; null selects top-level tickets.'), 'page' => $schema->integer()->min(1)->description('Page number, defaults to 1.'), 'per_page' => $schema->integer()->min(1)->max(100)->description('Page size, defaults to 25; maximum 100.'), ]; } }