From ec1fe91b351122d3647d5d2df3ef04257c9f196d Mon Sep 17 00:00:00 2001 From: jeremy bayse Date: Tue, 14 Apr 2026 18:30:13 +0200 Subject: [PATCH] feat: link quizzes to job positions and filter candidate dashboard accordingly --- .../Controllers/JobPositionController.php | 19 ++++++++++-- app/Models/JobPosition.php | 5 +++ app/Models/Quiz.php | 5 +++ ..._162814_create_job_position_quiz_table.php | 29 +++++++++++++++++ .../js/Pages/Admin/JobPositions/Index.vue | 31 ++++++++++++++++++- routes/web.php | 9 ++++-- 6 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 database/migrations/2026_04_14_162814_create_job_position_quiz_table.php diff --git a/app/Http/Controllers/JobPositionController.php b/app/Http/Controllers/JobPositionController.php index 263b7d0..5467385 100644 --- a/app/Http/Controllers/JobPositionController.php +++ b/app/Http/Controllers/JobPositionController.php @@ -13,8 +13,9 @@ class JobPositionController extends Controller $this->authorizeAdmin(); return Inertia::render('Admin/JobPositions/Index', [ - 'jobPositions' => JobPosition::with('tenant')->get(), - 'tenants' => \App\Models\Tenant::orderBy('name')->get() + 'jobPositions' => JobPosition::with(['tenant', 'quizzes'])->get(), + 'tenants' => \App\Models\Tenant::orderBy('name')->get(), + 'quizzes' => \App\Models\Quiz::all() ]); } @@ -28,9 +29,11 @@ class JobPositionController extends Controller 'requirements' => 'nullable|array', 'ai_prompt' => 'nullable|string', 'tenant_id' => 'nullable|exists:tenants,id', + 'quiz_ids' => 'nullable|array', + 'quiz_ids.*' => 'exists:quizzes,id', ]); - JobPosition::create([ + $jobPosition = JobPosition::create([ 'title' => $request->title, 'description' => $request->description, 'requirements' => $request->requirements, @@ -38,6 +41,10 @@ class JobPositionController extends Controller 'tenant_id' => auth()->user()->isSuperAdmin() ? $request->tenant_id : auth()->user()->tenant_id, ]); + if ($request->has('quiz_ids')) { + $jobPosition->quizzes()->sync($request->quiz_ids); + } + return back()->with('success', 'Fiche de poste créée avec succès.'); } @@ -51,6 +58,8 @@ class JobPositionController extends Controller 'requirements' => 'nullable|array', 'ai_prompt' => 'nullable|string', 'tenant_id' => 'nullable|exists:tenants,id', + 'quiz_ids' => 'nullable|array', + 'quiz_ids.*' => 'exists:quizzes,id', ]); $jobPosition->update([ @@ -61,6 +70,10 @@ class JobPositionController extends Controller 'tenant_id' => auth()->user()->isSuperAdmin() ? $request->tenant_id : auth()->user()->tenant_id, ]); + if ($request->has('quiz_ids')) { + $jobPosition->quizzes()->sync($request->quiz_ids); + } + return back()->with('success', 'Fiche de poste mise à jour.'); } diff --git a/app/Models/JobPosition.php b/app/Models/JobPosition.php index 3374c71..0d029c0 100644 --- a/app/Models/JobPosition.php +++ b/app/Models/JobPosition.php @@ -22,4 +22,9 @@ class JobPosition extends Model { return $this->hasMany(Candidate::class); } + + public function quizzes() + { + return $this->belongsToMany(Quiz::class); + } } diff --git a/app/Models/Quiz.php b/app/Models/Quiz.php index 32de4d7..e75932f 100644 --- a/app/Models/Quiz.php +++ b/app/Models/Quiz.php @@ -19,4 +19,9 @@ class Quiz extends Model { return $this->hasMany(Question::class); } + + public function jobPositions() + { + return $this->belongsToMany(JobPosition::class); + } } diff --git a/database/migrations/2026_04_14_162814_create_job_position_quiz_table.php b/database/migrations/2026_04_14_162814_create_job_position_quiz_table.php new file mode 100644 index 0000000..041f0a9 --- /dev/null +++ b/database/migrations/2026_04_14_162814_create_job_position_quiz_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('job_position_id')->constrained()->cascadeOnDelete(); + $table->foreignId('quiz_id')->constrained()->cascadeOnDelete(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('job_position_quiz'); + } +}; diff --git a/resources/js/Pages/Admin/JobPositions/Index.vue b/resources/js/Pages/Admin/JobPositions/Index.vue index 0ba49cc..04118a9 100644 --- a/resources/js/Pages/Admin/JobPositions/Index.vue +++ b/resources/js/Pages/Admin/JobPositions/Index.vue @@ -10,7 +10,8 @@ import InputError from '@/Components/InputError.vue'; const props = defineProps({ jobPositions: Array, - tenants: Array + tenants: Array, + quizzes: Array }); const showingModal = ref(false); @@ -22,6 +23,7 @@ const form = useForm({ requirements: [], ai_prompt: '', tenant_id: '', + quiz_ids: [], }); const openModal = (position = null) => { @@ -32,6 +34,7 @@ const openModal = (position = null) => { form.requirements = position.requirements || []; form.ai_prompt = position.ai_prompt || ''; form.tenant_id = position.tenant_id || ''; + form.quiz_ids = position.quizzes ? position.quizzes.map(q => q.id) : []; } else { form.reset(); } @@ -203,6 +206,32 @@ const removeRequirement = (index) => { +
+ +
+
+
+ + + +
+
+
{{ quiz.title }}
+
{{ quiz.duration_minutes }} min
+
+
+
+ +
+
diff --git a/routes/web.php b/routes/web.php index 8d6e562..ab9733d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -48,8 +48,13 @@ Route::get('/dashboard', function () { ->values() ->all(); } else { - $candidate = auth()->user()->candidate; - $quizzes = \App\Models\Quiz::all()->map(function($quiz) use ($candidate) { + $candidate = auth()->user()->candidate?->load('jobPosition.quizzes'); + + $quizzes = ($candidate && $candidate->jobPosition) + ? $candidate->jobPosition->quizzes + : collect(); + + $quizzes = $quizzes->map(function($quiz) use ($candidate) { $quiz->has_finished_attempt = $candidate ? $candidate->attempts()->where('quiz_id', $quiz->id)->whereNotNull('finished_at')->exists() : false;