New_Micro_Learning/backend/app/Modules/Assessments/Http/QuestionBankController.php

138 خطوط
8.0 KiB
PHP

<?php
namespace App\Modules\Assessments\Http;
use App\Http\Controllers\Controller;
use App\Modules\Assessments\Application\QuestionSchema;
use App\Modules\Assessments\Domain\Question;
use App\Modules\Assessments\Domain\QuestionCategory;
use App\Modules\Assessments\Domain\QuestionResult;
use App\Modules\Identity\Application\RolePermissions;
use App\Modules\Identity\Domain\Enums\Permission;
use App\Modules\Tenancy\Application\TenantContext;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
final class QuestionBankController extends Controller
{
public function __construct(private readonly TenantContext $tenant, private readonly RolePermissions $permissions, private readonly QuestionSchema $schema) {}
public function index(Request $request): JsonResponse
{
$this->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<string, mixed> */
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<string, mixed> */
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),
];
}
}