Ajustement des scores : test technique rapporté sur 20 et moyenne pondérée corrigée
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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::table('candidates', function (Blueprint $table) {
|
||||
$table->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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main: Attempts -->
|
||||
<!-- Main: Content -->
|
||||
<div class="xl:col-span-2 space-y-8">
|
||||
<!-- Scores Dashboard -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-3xl shadow-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
<div class="p-8 bg-gradient-to-br from-slate-900 to-slate-800 text-white flex flex-col md:flex-row md:items-center justify-between gap-8">
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="relative flex items-center justify-center">
|
||||
<svg class="w-24 h-24 transform -rotate-90">
|
||||
<circle cx="48" cy="48" r="40" stroke="currentColor" stroke-width="8" fill="transparent" class="text-slate-700" />
|
||||
<circle cx="48" cy="48" r="40" stroke="currentColor" stroke-width="8" fill="transparent" class="text-indigo-500"
|
||||
:stroke-dasharray="251.2"
|
||||
:stroke-dashoffset="251.2 - (candidate.weighted_score / 20) * 251.2"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<span class="absolute text-2xl font-black">{{ candidate.weighted_score }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-2xl font-black tracking-tight mb-1">Score Global</h3>
|
||||
<p class="text-slate-400 text-sm font-medium">Note pondérée sur 20</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<PrimaryButton @click="saveScores" v-if="scoreForm.isDirty" class="!bg-indigo-500 hover:!bg-indigo-400 !border-none animate-bounce">
|
||||
Enregistrer les modifications
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-8 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<!-- CV Score -->
|
||||
<div class="group">
|
||||
<label class="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-3 block">Analyse CV /20</label>
|
||||
<div class="relative">
|
||||
<input type="number" v-model="scoreForm.cv_score" min="0" max="20" step="0.5"
|
||||
class="w-full bg-slate-50 dark:bg-slate-900 border-none rounded-2xl p-4 font-black text-xl text-indigo-600 focus:ring-2 focus:ring-indigo-500/20 transition-all" />
|
||||
<div class="absolute right-4 top-1/2 -translate-y-1/2 text-slate-300 font-bold">/ 20</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Letter Score -->
|
||||
<div class="group">
|
||||
<label class="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-3 block">Lettre Motiv. /10</label>
|
||||
<div class="relative">
|
||||
<input type="number" v-model="scoreForm.motivation_score" min="0" max="10" step="0.5"
|
||||
class="w-full bg-slate-50 dark:bg-slate-900 border-none rounded-2xl p-4 font-black text-xl text-emerald-600 focus:ring-2 focus:ring-emerald-500/20 transition-all" />
|
||||
<div class="absolute right-4 top-1/2 -translate-y-1/2 text-slate-300 font-bold">/ 10</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Interview Score -->
|
||||
<div class="group">
|
||||
<label class="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-3 block">Entretien /30</label>
|
||||
<div class="relative">
|
||||
<input type="number" v-model="scoreForm.interview_score" min="0" max="30" step="0.5"
|
||||
class="w-full bg-slate-50 dark:bg-slate-900 border-none rounded-2xl p-4 font-black text-xl text-purple-600 focus:ring-2 focus:ring-purple-500/20 transition-all" />
|
||||
<div class="absolute right-4 top-1/2 -translate-y-1/2 text-slate-300 font-bold">/ 30</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Score (Read Only) -->
|
||||
<div class="group">
|
||||
<label class="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-3 block">Test Technique /20</label>
|
||||
<div class="w-full bg-slate-100 dark:bg-slate-900/50 rounded-2xl p-4 flex items-center justify-between border-2 border-dashed border-slate-200 dark:border-slate-700">
|
||||
<span class="font-black text-xl text-slate-500">
|
||||
{{ 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' }}
|
||||
</span>
|
||||
<div class="p-2 bg-white dark:bg-slate-800 rounded-lg shadow-sm font-bold text-[10px] text-slate-400">
|
||||
/ 20
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes Section -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700 p-8">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user