feat: link quizzes to job positions and filter candidate dashboard accordingly

This commit is contained in:
jeremy bayse
2026-04-14 18:30:13 +02:00
parent 8c577cfaa7
commit ec1fe91b35
6 changed files with 92 additions and 6 deletions

View File

@@ -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.');
}

View File

@@ -22,4 +22,9 @@ class JobPosition extends Model
{
return $this->hasMany(Candidate::class);
}
public function quizzes()
{
return $this->belongsToMany(Quiz::class);
}
}

View File

@@ -19,4 +19,9 @@ class Quiz extends Model
{
return $this->hasMany(Question::class);
}
public function jobPositions()
{
return $this->belongsToMany(JobPosition::class);
}
}

View File

@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('job_position_quiz', function (Blueprint $table) {
$table->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');
}
};

View File

@@ -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) => {
<InputError :message="form.errors.ai_prompt" />
</div>
<div v-if="quizzes && quizzes.length > 0">
<label class="block text-xs font-black uppercase tracking-widest text-slate-400 mb-4">Tests techniques associés</label>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div
v-for="quiz in quizzes"
:key="quiz.id"
class="flex items-center p-3 bg-slate-50 dark:bg-slate-900 rounded-2xl cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
@click="form.quiz_ids.includes(quiz.id) ? form.quiz_ids = form.quiz_ids.filter(id => id !== quiz.id) : form.quiz_ids.push(quiz.id)"
>
<div
class="w-5 h-5 rounded-md border-2 mr-3 flex items-center justify-center transition-all"
:class="form.quiz_ids.includes(quiz.id) ? 'bg-indigo-600 border-indigo-600' : 'border-slate-300 dark:border-slate-600'"
>
<svg v-if="form.quiz_ids.includes(quiz.id)" xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
</svg>
</div>
<div>
<div class="text-xs font-bold leading-tight">{{ quiz.title }}</div>
<div class="text-[9px] text-slate-400 uppercase tracking-tighter">{{ quiz.duration_minutes }} min</div>
</div>
</div>
</div>
<InputError :message="form.errors.quiz_ids" />
</div>
<div>
<div class="flex justify-between items-center mb-4">
<label class="text-xs font-black uppercase tracking-widest text-slate-400">Compétences clés / Pré-requis</label>

View File

@@ -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;