From 3d0904f630415629e7490d9fc7273d22217b456b Mon Sep 17 00:00:00 2001 From: AmirmahdiNourkazemi Date: Mon, 1 Jun 2026 09:25:25 +0330 Subject: [PATCH] feat: add tags for options --- .../Controllers/SurveyQuestionController.php | 70 +++++++++++++++++-- app/Models/SurveyOption.php | 7 ++ ..._110000_create_survey_option_tag_table.php | 31 ++++++++ routes/api.php | 1 + 4 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 database/migrations/2026_06_01_110000_create_survey_option_tag_table.php diff --git a/app/Http/Controllers/SurveyQuestionController.php b/app/Http/Controllers/SurveyQuestionController.php index f7e32bd..37bd231 100644 --- a/app/Http/Controllers/SurveyQuestionController.php +++ b/app/Http/Controllers/SurveyQuestionController.php @@ -2,7 +2,10 @@ namespace App\Http\Controllers; +use App\Models\Media; +use App\Models\SurveyAnswer; use App\Models\SurveyQuestion; +use App\Models\Tag; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Validation\ValidationException; @@ -12,7 +15,7 @@ class SurveyQuestionController extends Controller // USER: list active questions with their options and the current user's own answers. public function index(Request $request) { - $questions = SurveyQuestion::with(['options', 'userAnswers']) + $questions = SurveyQuestion::with(['options.tags', 'userAnswers']) ->where('is_active', true) ->orderBy('order') ->get(); @@ -23,7 +26,7 @@ public function index(Request $request) // USER: a single question with its options and the current user's own answers. public function show($id) { - $question = SurveyQuestion::with(['options', 'userAnswers'])->findOrFail($id); + $question = SurveyQuestion::with(['options.tags', 'userAnswers'])->findOrFail($id); return response()->json($question); } @@ -32,7 +35,7 @@ public function show($id) public function adminIndex(Request $request) { $questions = SurveyQuestion::with([ - 'options' => fn ($q) => $q->withCount('answers'), + 'options' => fn ($q) => $q->withCount('answers')->with('tags'), 'answers' => fn ($q) => $q->with(['user', 'option']), ]) ->orderBy('order') @@ -45,7 +48,7 @@ public function adminIndex(Request $request) public function adminShow($id) { $question = SurveyQuestion::with([ - 'options' => fn ($q) => $q->withCount('answers'), + 'options' => fn ($q) => $q->withCount('answers')->with('tags'), 'answers' => fn ($q) => $q->with(['user', 'option']), ]) ->findOrFail($id); @@ -113,6 +116,8 @@ public function store(Request $request) 'options.*.label' => 'required|string|max:255', 'options.*.value' => 'nullable|string|max:255', 'options.*.order' => 'nullable|integer', + 'options.*.tags' => 'nullable|array', + 'options.*.tags.*' => 'string', ]); $question = DB::transaction(function () use ($data) { @@ -131,7 +136,7 @@ public function store(Request $request) return response()->json([ 'message' => 'Question created successfully', - 'question' => $question->load('options'), + 'question' => $question->load('options.tags'), ], 201); } @@ -150,6 +155,8 @@ public function update(Request $request, $id) 'options.*.label' => 'required_with:options|string|max:255', 'options.*.value' => 'nullable|string|max:255', 'options.*.order' => 'nullable|integer', + 'options.*.tags' => 'nullable|array', + 'options.*.tags.*' => 'string', ]); DB::transaction(function () use ($question, $data) { @@ -174,7 +181,7 @@ public function update(Request $request, $id) return response()->json([ 'message' => 'Question updated successfully', - 'question' => $question->load('options'), + 'question' => $question->load('options.tags'), ]); } @@ -236,14 +243,63 @@ public function answer(Request $request, $id) ]); } + // USER: suggest media based on the tags attached to the options this user has chosen. + public function suggestedMedia(Request $request) + { + $userId = auth()->id(); + + $answersQuery = SurveyAnswer::where('user_id', $userId); + if ($request->filled('question_id')) { + $answersQuery->where('survey_question_id', $request->question_id); + } + $optionIds = $answersQuery->pluck('survey_option_id'); + + if ($optionIds->isEmpty()) { + return response()->json([]); + } + + // Collect the tags behind the chosen options. + $tagIds = DB::table('survey_option_tag') + ->whereIn('survey_option_id', $optionIds) + ->pluck('tag_id') + ->unique() + ->values(); + + if ($tagIds->isEmpty()) { + return response()->json([]); + } + + // Media sharing those tags, ranked by how many of them match. + $media = Media::query() + ->whereHas('tags', fn ($q) => $q->whereIn('tags.id', $tagIds)) + ->withCount(['tags as match_count' => fn ($q) => $q->whereIn('tags.id', $tagIds)]) + ->where(function ($q) use ($userId) { + $q->where('visibility', 'public')->orWhere('user_id', $userId); + }) + ->with(['image', 'categories', 'subCategories', 'tags']) + ->orderByDesc('match_count') + ->orderByDesc('created_at') + ->get(); + + return response()->json($media); + } + private function syncOptions(SurveyQuestion $question, array $options): void { foreach (array_values($options) as $i => $option) { - $question->options()->create([ + $created = $question->options()->create([ 'label' => $option['label'], 'value' => $option['value'] ?? null, 'order' => $option['order'] ?? $i, ]); + + if (!empty($option['tags'])) { + $tagIds = collect($option['tags']) + ->map(fn ($name) => Tag::firstOrCreate(['name' => $name])->id) + ->all(); + + $created->tags()->sync($tagIds); + } } } } diff --git a/app/Models/SurveyOption.php b/app/Models/SurveyOption.php index d8a0dcb..ccdf7e3 100644 --- a/app/Models/SurveyOption.php +++ b/app/Models/SurveyOption.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; class SurveyOption extends Model @@ -23,4 +24,10 @@ public function answers(): HasMany { return $this->hasMany(SurveyAnswer::class); } + + // Tags used to suggest media when a user picks this option. + public function tags(): BelongsToMany + { + return $this->belongsToMany(Tag::class, 'survey_option_tag'); + } } diff --git a/database/migrations/2026_06_01_110000_create_survey_option_tag_table.php b/database/migrations/2026_06_01_110000_create_survey_option_tag_table.php new file mode 100644 index 0000000..2ee43f5 --- /dev/null +++ b/database/migrations/2026_06_01_110000_create_survey_option_tag_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('survey_option_id')->constrained('survey_options')->cascadeOnDelete(); + $table->foreignId('tag_id')->constrained('tags')->cascadeOnDelete(); + $table->timestamps(); + + $table->unique(['survey_option_id', 'tag_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('survey_option_tag'); + } +}; diff --git a/routes/api.php b/routes/api.php index 7a2c1eb..ada22f1 100644 --- a/routes/api.php +++ b/routes/api.php @@ -127,6 +127,7 @@ Route::get('/admin/survey-questions/{id}', [SurveyQuestionController::class, 'adminShow']); }); // User: answer + read questions with their own answers only. + Route::get('/survey-questions/suggested-media', [SurveyQuestionController::class, 'suggestedMedia']); Route::post('/survey-questions/{id}/answer', [SurveyQuestionController::class, 'answer']); Route::apiResource('survey-questions', SurveyQuestionController::class);