From 5c2bcb0169e2af03155a0e209b653533c9815130 Mon Sep 17 00:00:00 2001 From: jeremy bayse Date: Fri, 20 Mar 2026 17:46:42 +0100 Subject: [PATCH] =?UTF-8?q?Ajustement=20des=20scores=20:=20test=20techniqu?= =?UTF-8?q?e=20rapport=C3=A9=20sur=2020=20et=20moyenne=20pond=C3=A9r=C3=A9?= =?UTF-8?q?e=20corrig=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/CandidateController.php | 13 +++ app/Models/Candidate.php | 21 ++++- ...dd_detailed_scores_to_candidates_table.php | 30 +++++++ resources/js/Pages/Admin/Candidates/Show.vue | 87 ++++++++++++++++++- routes/web.php | 1 + 5 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 database/migrations/2026_03_20_164315_add_detailed_scores_to_candidates_table.php diff --git a/app/Http/Controllers/CandidateController.php b/app/Http/Controllers/CandidateController.php index f343603..c860088 100644 --- a/app/Http/Controllers/CandidateController.php +++ b/app/Http/Controllers/CandidateController.php @@ -129,6 +129,19 @@ class CandidateController extends Controller return back()->with('success', 'Notes mises à jour avec succès.'); } + public function updateScores(Request $request, Candidate $candidate) + { + $request->validate([ + 'cv_score' => 'nullable|numeric|min:0|max:20', + 'motivation_score' => 'nullable|numeric|min:0|max:10', + 'interview_score' => 'nullable|numeric|min:0|max:30', + ]); + + $candidate->update($request->only(['cv_score', 'motivation_score', 'interview_score'])); + + return back()->with('success', 'Notes mises à jour avec succès.'); + } + public function resetPassword(Candidate $candidate) { $password = Str::random(10); diff --git a/app/Models/Candidate.php b/app/Models/Candidate.php index 277e31f..feb1d95 100644 --- a/app/Models/Candidate.php +++ b/app/Models/Candidate.php @@ -9,11 +9,30 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Factories\HasFactory; -#[Fillable(['user_id', 'phone', 'linkedin_url', 'status', 'notes'])] +#[Fillable(['user_id', 'phone', 'linkedin_url', 'status', 'notes', 'cv_score', 'motivation_score', 'interview_score'])] class Candidate extends Model { use HasFactory; + protected $appends = ['weighted_score']; + + public function getWeightedScoreAttribute(): float + { + $cv = (float)$this->cv_score; + $motivation = (float)$this->motivation_score; + $interview = (float)$this->interview_score; + + // On récupère le meilleur test (ramené sur 20) + $bestAttempt = $this->attempts()->whereNotNull('finished_at')->get()->map(function($a) { + return $a->max_score > 0 ? ($a->score / $a->max_score) * 20 : 0; + })->max() ?? 0; + + $totalPoints = $cv + $motivation + $interview + $bestAttempt; + $maxPoints = 20 + 10 + 30 + 20; // Total potentiel = 80 + + return round(($totalPoints / $maxPoints) * 20, 2); + } + public function user(): BelongsTo { return $this->belongsTo(User::class); diff --git a/database/migrations/2026_03_20_164315_add_detailed_scores_to_candidates_table.php b/database/migrations/2026_03_20_164315_add_detailed_scores_to_candidates_table.php new file mode 100644 index 0000000..76fcae9 --- /dev/null +++ b/database/migrations/2026_03_20_164315_add_detailed_scores_to_candidates_table.php @@ -0,0 +1,30 @@ +decimal('cv_score', 5, 2)->nullable()->after('notes'); + $table->decimal('motivation_score', 5, 2)->nullable()->after('cv_score'); + $table->decimal('interview_score', 5, 2)->nullable()->after('motivation_score'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('candidates', function (Blueprint $table) { + $table->dropColumn(['cv_score', 'motivation_score', 'interview_score']); + }); + } +}; diff --git a/resources/js/Pages/Admin/Candidates/Show.vue b/resources/js/Pages/Admin/Candidates/Show.vue index 1c0ea1d..b9e0cd9 100644 --- a/resources/js/Pages/Admin/Candidates/Show.vue +++ b/resources/js/Pages/Admin/Candidates/Show.vue @@ -28,6 +28,12 @@ const notesForm = useForm({ notes: props.candidate.notes || '' }); +const scoreForm = useForm({ + cv_score: props.candidate.cv_score || 0, + motivation_score: props.candidate.motivation_score || 0, + interview_score: props.candidate.interview_score || 0, +}); + const isPreview = ref(false); const renderedNotes = computed(() => marked.parse(notesForm.notes || '')); @@ -85,6 +91,12 @@ const saveNotes = () => { }); }; +const saveScores = () => { + scoreForm.patch(route('admin.candidates.update-scores', props.candidate.id), { + preserveScroll: true, + }); +}; + const openPreview = (doc) => { selectedDocument.value = doc; }; @@ -216,8 +228,81 @@ const openPreview = (doc) => { - +
+ +
+
+
+
+ + + + + {{ candidate.weighted_score }} +
+
+

Score Global

+

Note pondérée sur 20

+
+
+
+ + Enregistrer les modifications + +
+
+ +
+ +
+ +
+ +
/ 20
+
+
+ + +
+ +
+ +
/ 10
+
+
+ + +
+ +
+ +
/ 30
+
+
+ + +
+ +
+ + {{ candidate.attempts.length > 0 ? (Math.max(...candidate.attempts.map(a => a.max_score > 0 ? a.score/a.max_score : 0)) * 20).toFixed(2) : '0.00' }} + +
+ / 20 +
+
+
+
+
+
diff --git a/routes/web.php b/routes/web.php index e07d6ed..8f02d8b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -53,6 +53,7 @@ Route::middleware('auth')->group(function () { Route::get('/comparative', [\App\Http\Controllers\CandidateController::class, 'comparative'])->name('comparative'); Route::resource('candidates', \App\Http\Controllers\CandidateController::class)->only(['index', 'store', 'show', 'destroy', 'update']); Route::patch('/candidates/{candidate}/notes', [\App\Http\Controllers\CandidateController::class, 'updateNotes'])->name('candidates.update-notes'); + Route::patch('/candidates/{candidate}/scores', [\App\Http\Controllers\CandidateController::class, 'updateScores'])->name('candidates.update-scores'); Route::post('/candidates/{candidate}/reset-password', [\App\Http\Controllers\CandidateController::class, 'resetPassword'])->name('candidates.reset-password'); Route::get('/documents/{document}', [\App\Http\Controllers\DocumentController::class, 'show'])->name('documents.show');