diff --git a/app/Http/Controllers/SurveyQuestionController.php b/app/Http/Controllers/SurveyQuestionController.php index 37bd231..1a23a8f 100644 --- a/app/Http/Controllers/SurveyQuestionController.php +++ b/app/Http/Controllers/SurveyQuestionController.php @@ -193,48 +193,21 @@ public function destroy($id) return response()->json(['message' => 'Question deleted successfully']); } - // USER submits their answer(s) for a question. + // USER submits their answer(s) for a single question. + // An empty option_ids array (or omitting it) means "no answer" / skip. public function answer(Request $request, $id) { $question = SurveyQuestion::findOrFail($id); $data = $request->validate([ - 'option_ids' => 'required|array|min:1', + 'option_ids' => 'nullable|array', 'option_ids.*' => 'integer', ]); - $optionIds = array_values(array_unique($data['option_ids'])); - - // Every selected option must belong to this question. - $validOptionIds = $question->options()->pluck('id')->all(); - if (array_diff($optionIds, $validOptionIds)) { - throw ValidationException::withMessages([ - 'option_ids' => ['One or more options do not belong to this question.'], - ]); - } - - // Enforce single vs multiple selection. - if ($question->type === 'single' && count($optionIds) > 1) { - throw ValidationException::withMessages([ - 'option_ids' => ['This question allows only a single option.'], - ]); - } - $userId = auth()->id(); - DB::transaction(function () use ($question, $optionIds, $userId) { - // Replace any previous answer for this user + question. - $question->answers()->where('user_id', $userId)->delete(); - - $rows = array_map(fn ($optionId) => [ - 'user_id' => $userId, - 'survey_question_id' => $question->id, - 'survey_option_id' => $optionId, - 'created_at' => now(), - 'updated_at' => now(), - ], $optionIds); - - $question->answers()->getRelated()->insert($rows); + DB::transaction(function () use ($question, $data, $userId) { + $this->syncAnswerFor($question, $data['option_ids'] ?? [], $userId); }); return response()->json([ @@ -243,6 +216,82 @@ public function answer(Request $request, $id) ]); } + // USER submits answers for many questions at once. Each item may carry an + // empty option_ids (skip), and the whole answers array may be empty too. + public function bulkAnswer(Request $request) + { + $data = $request->validate([ + 'answers' => 'present|array', + 'answers.*.question_id' => 'required|integer|exists:survey_questions,id', + 'answers.*.option_ids' => 'nullable|array', + 'answers.*.option_ids.*' => 'integer', + ]); + + $userId = auth()->id(); + $items = $data['answers'] ?? []; + + $questions = SurveyQuestion::with('options') + ->whereIn('id', collect($items)->pluck('question_id')->unique()->all()) + ->get() + ->keyBy('id'); + + DB::transaction(function () use ($items, $questions, $userId) { + foreach ($items as $item) { + $question = $questions->get($item['question_id']); + if (!$question) { + continue; + } + $this->syncAnswerFor($question, $item['option_ids'] ?? [], $userId); + } + }); + + return response()->json([ + 'message' => 'Answers submitted successfully', + 'questions' => SurveyQuestion::with(['options', 'userAnswers']) + ->where('is_active', true) + ->orderBy('order') + ->get(), + ]); + } + + // Validate and replace a single user's answer set for one question. + // Empty $optionIds clears the answer (the user chose not to answer it). + private function syncAnswerFor(SurveyQuestion $question, array $optionIds, int $userId): void + { + $optionIds = array_values(array_unique(array_map('intval', $optionIds))); + + // Every selected option must belong to this question. + $validOptionIds = $question->options()->pluck('id')->all(); + if (array_diff($optionIds, $validOptionIds)) { + throw ValidationException::withMessages([ + 'option_ids' => ["One or more options do not belong to question #{$question->id}."], + ]); + } + + // Enforce single vs multiple selection. + if ($question->type === 'single' && count($optionIds) > 1) { + throw ValidationException::withMessages([ + 'option_ids' => ["Question #{$question->id} allows only a single option."], + ]); + } + + // Replace any previous answer for this user + question. + $question->answers()->where('user_id', $userId)->delete(); + + if ($optionIds) { + $now = now(); + $rows = array_map(fn ($optionId) => [ + 'user_id' => $userId, + 'survey_question_id' => $question->id, + 'survey_option_id' => $optionId, + 'created_at' => $now, + 'updated_at' => $now, + ], $optionIds); + + $question->answers()->getRelated()->insert($rows); + } + } + // USER: suggest media based on the tags attached to the options this user has chosen. public function suggestedMedia(Request $request) { diff --git a/routes/api.php b/routes/api.php index 9f45b46..1d0ff16 100644 --- a/routes/api.php +++ b/routes/api.php @@ -150,6 +150,7 @@ }); // User: answer + read questions with their own answers only. Route::get('/survey-questions/suggested-media', [SurveyQuestionController::class, 'suggestedMedia']); + Route::post('/survey-questions/answers', [SurveyQuestionController::class, 'bulkAnswer']); Route::post('/survey-questions/{id}/answer', [SurveyQuestionController::class, 'answer']); Route::apiResource('survey-questions', SurveyQuestionController::class);