diff --git a/app/Http/Controllers/CandidateController.php b/app/Http/Controllers/CandidateController.php index d36bc48..09405fb 100644 --- a/app/Http/Controllers/CandidateController.php +++ b/app/Http/Controllers/CandidateController.php @@ -237,6 +237,15 @@ class CandidateController extends Controller $this->storeDocument($candidate, $file, $type); } + public function toggleSelection(Candidate $candidate) + { + $candidate->update([ + 'is_selected' => !$candidate->is_selected + ]); + + return back()->with('success', 'Statut de sélection mis à jour.'); + } + private function storeDocument(Candidate $candidate, $file, string $type) { if (!$file) { diff --git a/app/Models/Candidate.php b/app/Models/Candidate.php index 848ad4a..f88c850 100644 --- a/app/Models/Candidate.php +++ b/app/Models/Candidate.php @@ -11,7 +11,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use App\Traits\BelongsToTenant; -#[Fillable(['user_id', 'job_position_id', 'phone', 'linkedin_url', 'status', 'notes', 'cv_score', 'motivation_score', 'interview_score', 'ai_analysis', 'tenant_id'])] +#[Fillable(['user_id', 'job_position_id', 'phone', 'linkedin_url', 'status', 'is_selected', 'notes', 'cv_score', 'motivation_score', 'interview_score', 'ai_analysis', 'tenant_id'])] class Candidate extends Model { use HasFactory, BelongsToTenant; @@ -30,6 +30,7 @@ class Candidate extends Model protected $casts = [ 'ai_analysis' => 'array', + 'is_selected' => 'boolean', ]; public function jobPosition(): BelongsTo diff --git a/database/migrations/2026_04_16_170459_add_is_selected_to_candidates_table.php b/database/migrations/2026_04_16_170459_add_is_selected_to_candidates_table.php new file mode 100644 index 0000000..970b526 --- /dev/null +++ b/database/migrations/2026_04_16_170459_add_is_selected_to_candidates_table.php @@ -0,0 +1,28 @@ +boolean('is_selected')->default(false)->after('status'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('candidates', function (Blueprint $table) { + $table->dropColumn('is_selected'); + }); + } +}; diff --git a/resources/js/Pages/Admin/Candidates/Index.vue b/resources/js/Pages/Admin/Candidates/Index.vue index 73fdf79..5ab6e3c 100644 --- a/resources/js/Pages/Admin/Candidates/Index.vue +++ b/resources/js/Pages/Admin/Candidates/Index.vue @@ -43,10 +43,14 @@ const submit = () => { const deleteCandidate = (id) => { if (confirm('Voulez-vous vraiment supprimer ce candidat ?')) { - router.delete(route('admin.candidates.destroy', id)); + router.delete(route('admin.candidates.destroy', id), { preserveScroll: true }); } }; +const toggleSelection = (id) => { + router.patch(route('admin.candidates.toggle-selection', id), {}, { preserveScroll: true }); +}; + const openPreview = (doc) => { selectedDocument.value = doc; }; @@ -69,11 +73,24 @@ const getNestedValue = (obj, path) => { }; const selectedJobPosition = ref(''); +const showOnlySelected = ref(false); const filteredCandidates = computed(() => { - if (selectedJobPosition.value === '') return props.candidates; - if (selectedJobPosition.value === 'none') return props.candidates.filter(c => !c.job_position_id); - return props.candidates.filter(c => c.job_position_id === selectedJobPosition.value); + let result = props.candidates; + + if (showOnlySelected.value) { + result = result.filter(c => c.is_selected); + } + + if (selectedJobPosition.value !== '') { + if (selectedJobPosition.value === 'none') { + result = result.filter(c => !c.job_position_id); + } else { + result = result.filter(c => c.job_position_id === selectedJobPosition.value); + } + } + + return result; }); const sortedCandidates = computed(() => { @@ -102,18 +119,26 @@ const sortedCandidates = computed(() => {

Liste des Candidats

-
- - +
+
+ +
+
+ + +
@@ -139,6 +164,7 @@ const sortedCandidates = computed(() => { + + - diff --git a/resources/js/Pages/Admin/Candidates/Show.vue b/resources/js/Pages/Admin/Candidates/Show.vue index 5b19e56..4910804 100644 --- a/resources/js/Pages/Admin/Candidates/Show.vue +++ b/resources/js/Pages/Admin/Candidates/Show.vue @@ -41,6 +41,10 @@ const updateTenant = () => { }); }; +const toggleSelection = () => { + router.patch(route('admin.candidates.toggle-selection', props.candidate.id), {}, { preserveScroll: true }); +}; + const selectedDocument = ref(null); const docForm = useForm({ @@ -312,7 +316,27 @@ const runAI = async () => {
-
+
+
+ +
{{ candidate.user.name.charAt(0) }}
diff --git a/resources/js/Pages/Dashboard.vue b/resources/js/Pages/Dashboard.vue index c7412bc..40add4d 100644 --- a/resources/js/Pages/Dashboard.vue +++ b/resources/js/Pages/Dashboard.vue @@ -46,11 +46,23 @@ const getStatusColor = (status) => {
-
+
Total Candidats
{{ stats.total_candidates }}
+
+
+
+
+ + + + Retenus +
+
{{ stats.selected_candidates }}
+
+
Tests terminés
{{ stats.finished_tests }}
@@ -61,7 +73,7 @@ const getStatusColor = (status) => {
Meilleur Score
-
{{ stats.best_score }} / 20
+
{{ stats.best_score }} / 20
diff --git a/routes/web.php b/routes/web.php index 40bc2fb..bcf3390 100644 --- a/routes/web.php +++ b/routes/web.php @@ -26,6 +26,7 @@ Route::get('/dashboard', function () { $allCandidates = Candidate::with(['attempts'])->get(); $stats = [ 'total_candidates' => Candidate::count(), + 'selected_candidates' => Candidate::where('is_selected', true)->count(), 'finished_tests' => Attempt::whereNotNull('finished_at')->count(), 'average_score' => round($allCandidates->avg('weighted_score') ?? 0, 1), 'best_score' => round($allCandidates->max('weighted_score') ?? 0, 1), @@ -86,6 +87,7 @@ Route::middleware('auth')->group(function () { Route::patch('/candidates/{candidate}/scores', [\App\Http\Controllers\CandidateController::class, 'updateScores'])->name('candidates.update-scores'); Route::patch('/candidates/{candidate}/position', [\App\Http\Controllers\CandidateController::class, 'updatePosition'])->name('candidates.update-position'); Route::patch('/candidates/{candidate}/tenant', [\App\Http\Controllers\CandidateController::class, 'updateTenant'])->name('candidates.update-tenant'); + Route::patch('/candidates/{candidate}/toggle-selection', [\App\Http\Controllers\CandidateController::class, 'toggleSelection'])->name('candidates.toggle-selection'); Route::post('/candidates/{candidate}/analyze', [\App\Http\Controllers\AIAnalysisController::class, 'analyze'])->name('candidates.analyze'); 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');
Nom @@ -201,6 +227,16 @@ const sortedCandidates = computed(() => {
+ +
{{ candidate.user.name }}
{{ candidate.phone }}
@@ -294,7 +330,7 @@ const sortedCandidates = computed(() => {
+ Aucun candidat trouvé.