authorize($request); $filters = $request->validate([ 'search' => ['nullable', 'string', 'max:160'], 'type' => ['nullable', Rule::in($this->schema->types())], 'difficulty' => ['nullable', Rule::in(['beginner', 'intermediate', 'advanced'])], 'topic' => ['nullable', 'string', 'max:160'], 'categoryId' => ['nullable', 'string', Rule::exists('question_categories', 'id')->where(fn ($query) => $query->where('organization_id', $this->tenant->id())->whereNull('parent_id'))], 'subcategoryId' => ['nullable', 'string', Rule::exists('question_categories', 'id')->where(fn ($query) => $query->where('organization_id', $this->tenant->id())->whereNotNull('parent_id'))], ]); $items = Question::query()->where('organization_id', $this->tenant->id())->where('is_bank_item', true) ->when($filters['search'] ?? null, fn ($query, string $search) => $query->where(fn ($inner) => $inner->where('prompt', 'like', "%{$search}%")->orWhere('topic', 'like', "%{$search}%"))) ->when($filters['type'] ?? null, fn ($query, string $type) => $query->where('type', $type)) ->when($filters['difficulty'] ?? null, fn ($query, string $difficulty) => $query->where('difficulty', $difficulty)) ->when($filters['topic'] ?? null, fn ($query, string $topic) => $query->where('topic', $topic)) ->when($filters['subcategoryId'] ?? null, fn ($query, string $id) => $query->where('question_category_id', $id)) ->when(($filters['categoryId'] ?? null) && ! ($filters['subcategoryId'] ?? null), fn ($query) => $query->whereHas('category', fn ($category) => $category->where('parent_id', $filters['categoryId'])->orWhere('id', $filters['categoryId']))) ->with('category.parent')->latest()->limit(300)->get()->map(fn (Question $question) => $this->payload($question)); return response()->json(['data' => $items]); } public function store(Request $request): JsonResponse { $this->authorize($request); $data = $this->data($request, true); $question = Question::query()->create([ 'organization_id' => $this->tenant->id(), 'course_version_id' => null, 'assessment_id' => null, 'is_bank_item' => true, 'type' => $data['type'], 'prompt' => $data['prompt'], 'configuration' => $this->schema->validate($data['type'], $data['configuration']), 'difficulty' => $data['difficulty'] ?? null, 'topic' => $data['topic'] ?? null, 'question_category_id' => $data['subcategoryId'] ?? $data['categoryId'] ?? null, 'tags' => $data['tags'] ?? [], 'explanation' => $data['explanation'] ?? null, 'position' => 0, ]); return response()->json(['data' => $this->payload($question)], 201); } public function update(Request $request, string $question): JsonResponse { $this->authorize($request); $model = $this->model($question); $data = $this->data($request, false); if (isset($data['configuration'])) { $data['configuration'] = $this->schema->validate($model->type, $data['configuration']); } if (array_key_exists('subcategoryId', $data) || array_key_exists('categoryId', $data)) { $data['question_category_id'] = $data['subcategoryId'] ?? $data['categoryId'] ?? null; } unset($data['type'], $data['categoryId'], $data['subcategoryId']); $model->update($data); return response()->json(['data' => $this->payload($model->fresh())]); } public function destroy(Request $request, string $question): JsonResponse { $this->authorize($request); $model = $this->model($question); abort_if(Question::query()->where('source_question_id', $model->getKey())->exists(), 409, 'Question is used by an assessment and cannot be deleted.'); $model->delete(); return response()->json(status: 204); } /** @return array */ private function data(Request $request, bool $creating): array { $data = $request->validate([ 'type' => [$creating ? 'required' : 'sometimes', Rule::in($this->schema->types())], 'prompt' => [$creating ? 'required' : 'sometimes', 'string', 'max:5000'], 'configuration' => [$creating ? 'required' : 'sometimes', 'array'], 'difficulty' => ['nullable', Rule::in(['beginner', 'intermediate', 'advanced'])], 'topic' => ['nullable', 'string', 'max:160'], 'tags' => ['nullable', 'array', 'max:20'], 'tags.*' => ['string', 'max:80'], 'categoryId' => ['nullable', 'string', Rule::exists('question_categories', 'id')->where(fn ($query) => $query->where('organization_id', $this->tenant->id())->whereNull('parent_id'))], 'subcategoryId' => ['nullable', 'string', Rule::exists('question_categories', 'id')->where(fn ($query) => $query->where('organization_id', $this->tenant->id())->whereNotNull('parent_id'))], 'explanation' => ['nullable', 'string', 'max:5000'], ]); if (($data['subcategoryId'] ?? null) && ($data['categoryId'] ?? null)) { $belongs = QuestionCategory::query()->where('organization_id', $this->tenant->id())->whereKey($data['subcategoryId'])->where('parent_id', $data['categoryId'])->exists(); if (! $belongs) { throw ValidationException::withMessages(['subcategoryId' => ['زیردسته‌بندی به دسته‌بندی انتخاب‌شده تعلق ندارد.']]); } } return $data; } private function model(string $id): Question { return Question::query()->where('organization_id', $this->tenant->id())->where('is_bank_item', true)->findOrFail($id); } private function authorize(Request $request): void { abort_unless($this->permissions->allows($request->user(), Permission::CoursesAuthor), 403); } /** @return array */ private function payload(Question $question): array { $usage = Question::query()->where('source_question_id', $question->getKey())->count(); $copyIds = Question::query()->where('source_question_id', $question->getKey())->pluck('id'); $performance = $copyIds->isEmpty() ? null : QuestionResult::query()->whereIn('question_id', $copyIds)->avg('normalized_value'); $categoryId = $question->category?->parent_id ?? $question->category?->getKey(); return [ 'id' => $question->getKey(), 'type' => $question->type, 'prompt' => $question->prompt, 'configuration' => $question->configuration, 'difficulty' => $question->difficulty, 'topic' => $question->topic, 'tags' => $question->tags ?? [], 'explanation' => $question->explanation, 'usageCount' => $usage, 'categoryId' => $categoryId, 'categoryName' => $question->category?->parent?->name ?? $question->category?->name, 'subcategoryId' => $question->category?->parent_id ? $question->category?->getKey() : null, 'subcategoryName' => $question->category?->parent_id ? $question->category?->name : null, 'performance' => $performance === null ? null : round(((float) $performance) * 100, 1), ]; } }