latest()->get(); $jobPositions = \App\Models\JobPosition::orderBy('title')->get(); $tenants = \App\Models\Tenant::orderBy('name')->get(); return \Inertia\Inertia::render('Admin/Candidates/Index', [ 'candidates' => $candidates, 'jobPositions' => $jobPositions, 'tenants' => $tenants ]); } public function comparative() { $candidates = Candidate::with(['user', 'attempts.quiz']) ->whereHas('attempts', function($query) { $query->whereNotNull('finished_at'); }) ->get(); return \Inertia\Inertia::render('Admin/Comparative', [ 'candidates' => $candidates ]); } public function selectedCandidates() { $candidates = Candidate::with(['user', 'jobPosition', 'attempts.quiz', 'documents']) ->where('is_selected', true) ->orderBy('sort_order') ->get() ->map(function($candidate) { $candidate->weighted_score = $candidate->weighted_score; return $candidate; }); return \Inertia\Inertia::render('Admin/Candidates/Selected', [ 'candidates' => $candidates ]); } public function map() { $candidates = Candidate::with(['user', 'jobPosition']) ->whereNotNull('city') ->where('city', '!=', '') ->get() ->map(function($c) { return [ 'id' => $c->id, 'name' => $c->user->name, 'city' => $c->city, 'job' => $c->jobPosition?->title, 'score' => $c->weighted_score ]; }); return \Inertia\Inertia::render('Admin/Candidates/Map', [ 'candidates' => $candidates ]); } public function updateOrder(Request $request) { $request->validate([ 'ids' => 'required|array', 'ids.*' => 'exists:candidates,id', ]); foreach ($request->ids as $index => $id) { Candidate::where('id', $id)->update(['sort_order' => $index]); } return back()->with('success', 'Classement enregistré.'); } public function store(Request $request) { $request->validate([ 'birth_name' => 'nullable|string|max:255', 'usage_name' => 'nullable|string|max:255', 'first_name' => 'nullable|string|max:255', 'address' => 'nullable|string|max:255', 'zip_code' => 'nullable|string|max:10', 'email' => 'required|string|email|max:255|unique:users', 'phone' => 'nullable|string|max:20', 'city' => 'nullable|string|max:255', 'birth_date' => 'nullable|date', 'birth_place' => 'nullable|string|max:255', 'nationality' => 'nullable|string|max:255', 'current_situation' => 'nullable|string|max:255', 'education_level' => 'nullable|string|max:255', 'has_driving_license' => 'nullable|boolean', 'cv' => 'nullable|mimes:pdf|max:5120', 'cover_letter' => 'nullable|mimes:pdf|max:5120', 'tenant_id' => 'nullable|exists:tenants,id', 'job_position_id' => 'nullable|exists:job_positions,id', ]); $password = Str::random(10); $name = $request->first_name ? ($request->first_name . ' ' . ($request->usage_name ?? '')) : $request->name; $user = User::create([ 'name' => $name, 'email' => $request->email, 'password' => Hash::make($password), 'role' => 'candidate', 'tenant_id' => (auth()->user()->isSuperAdmin() || auth()->user()->isGestionnaireRH()) ? $request->tenant_id : auth()->user()->tenant_id, ]); $candidate = $user->candidate()->create([ 'birth_name' => $request->birth_name, 'usage_name' => $request->usage_name, 'first_name' => $request->first_name, 'address' => $request->address, 'zip_code' => $request->zip_code, 'phone' => $request->phone, 'city' => $request->city, 'birth_date' => $request->birth_date, 'birth_place' => $request->birth_place, 'nationality' => $request->nationality, 'current_situation' => $request->current_situation, 'education_level' => $request->education_level, 'has_driving_license' => $request->has_driving_license ?? false, 'status' => 'en_attente', 'tenant_id' => (auth()->user()->isSuperAdmin() || auth()->user()->isGestionnaireRH()) ? $request->tenant_id : auth()->user()->tenant_id, 'job_position_id' => $request->job_position_id, ]); $this->storeDocument($candidate, $request->file('cv'), 'cv'); $this->storeDocument($candidate, $request->file('cover_letter'), 'cover_letter'); return back()->with('success', 'Candidat créé avec succès. Mot de passe généré: ' . $password); } public function show(Candidate $candidate) { $candidate->load([ 'user.securityAlerts', 'documents', 'jobPosition', 'tenant' ]); // Load attempts with quiz bypassing tenant scope // (admin may view candidates whose quizzes belong to other tenants) $candidate->setRelation( 'attempts', $candidate->attempts() ->with([ 'quiz' => fn($q) => $q->withoutGlobalScopes(), 'answers.question', 'answers.option', ]) ->get() ); $data = [ 'candidate' => $candidate, 'jobPositions' => \App\Models\JobPosition::all(), 'ai_config' => [ 'default' => env('AI_DEFAULT_PROVIDER', 'ollama'), 'providers' => array_keys(array_filter([ 'ollama' => true, 'openai' => !empty(env('OPENAI_API_KEY')), 'anthropic' => !empty(env('ANTHROPIC_API_KEY')), 'gemini' => !empty(env('GEMINI_API_KEY')), ])), ] ]; if (auth()->user()->isSuperAdmin() || auth()->user()->isGestionnaireRH()) { $data['tenants'] = \App\Models\Tenant::orderBy('name')->get(); } return \Inertia\Inertia::render('Admin/Candidates/Show', $data); } public function destroy(Candidate $candidate) { $user = $candidate->user; // Delete files foreach ($candidate->documents as $doc) { Storage::disk('local')->delete($doc->file_path); } // Delete user (cascades to candidate, documents, attempts via DB constraints usually) $user->delete(); return redirect()->route('admin.candidates.index')->with('success', 'Candidat supprimé avec succès.'); } public function update(Request $request, Candidate $candidate) { $request->validate([ 'birth_name' => 'nullable|string|max:255', 'usage_name' => 'nullable|string|max:255', 'first_name' => 'nullable|string|max:255', 'address' => 'nullable|string|max:255', 'zip_code' => 'nullable|string|max:10', 'phone' => 'nullable|string|max:255', 'city' => 'nullable|string|max:255', 'birth_date' => 'nullable|date', 'birth_place' => 'nullable|string|max:255', 'nationality' => 'nullable|string|max:255', 'current_situation' => 'nullable|string|max:255', 'education_level' => 'nullable|string|max:255', 'has_driving_license' => 'nullable|boolean', 'email' => 'nullable|string|email|max:255|unique:users,email,' . $candidate->user_id, 'linkedin_url' => 'nullable|url|max:255', 'cv' => 'nullable|file|mimes:pdf|max:5120', 'cover_letter' => 'nullable|file|mimes:pdf|max:5120', ]); // Update User info if name or email present if ($request->has('email')) { $candidate->user->update(['email' => $request->email]); } if ($request->has('first_name') || $request->has('usage_name')) { $firstName = $request->first_name ?? $candidate->first_name; $usageName = $request->usage_name ?? $candidate->usage_name; $candidate->user->update(['name' => $firstName . ' ' . $usageName]); } // Update Candidate info $candidate->update($request->only([ 'birth_name', 'usage_name', 'first_name', 'address', 'zip_code', 'phone', 'linkedin_url', 'city', 'birth_date', 'birth_place', 'nationality', 'current_situation', 'education_level', 'has_driving_license' ])); if ($request->hasFile('cv')) { $this->replaceDocument($candidate, $request->file('cv'), 'cv'); } if ($request->hasFile('cover_letter')) { $this->replaceDocument($candidate, $request->file('cover_letter'), 'cover_letter'); } return back()->with('success', 'Profil mis à jour avec succès.'); } public function updateNotes(Request $request, Candidate $candidate) { $request->validate([ 'notes' => 'nullable|string', 'interview_details' => 'nullable|array', 'interview_score' => 'nullable|numeric|min:0|max:25', ]); $candidate->update([ 'notes' => $request->notes, 'interview_details' => $request->interview_details, 'interview_score' => $request->has('interview_score') ? $request->interview_score : $candidate->interview_score, ]); return back()->with('success', 'Entretien mis à 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:25', ]); $candidate->update($request->only(['cv_score', 'motivation_score', 'interview_score'])); return back()->with('success', 'Notes mises à jour avec succès.'); } public function updatePosition(Request $request, Candidate $candidate) { $request->validate([ 'job_position_id' => 'nullable|exists:job_positions,id', ]); $candidate->update([ 'job_position_id' => $request->job_position_id, ]); return back()->with('success', 'Fiche de poste associée au candidat.'); } public function updateTenant(Request $request, Candidate $candidate) { if (!auth()->user()->isSuperAdmin() && !auth()->user()->isGestionnaireRH()) { abort(403); } $request->validate([ 'tenant_id' => 'nullable|exists:tenants,id', ]); $candidate->update([ 'tenant_id' => $request->tenant_id, ]); // Also update the associated user's tenant_id if it exists if ($candidate->user) { $candidate->user->update([ 'tenant_id' => $request->tenant_id, ]); } return back()->with('success', 'Structure de rattachement mise à jour avec succès.'); } public function resetPassword(Candidate $candidate) { $password = Str::random(10); $candidate->user->update([ 'password' => Hash::make($password) ]); return back()->with('success', 'Nouveau mot de passe généré: ' . $password); } private function replaceDocument(Candidate $candidate, $file, string $type) { // Delete old one if exists $oldDoc = $candidate->documents()->where('type', $type)->first(); if ($oldDoc) { Storage::disk('local')->delete($oldDoc->file_path); $oldDoc->delete(); } $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) { return; } $path = $file->store('private/documents/' . $candidate->id, 'local'); Document::create([ 'candidate_id' => $candidate->id, 'type' => $type, 'file_path' => $path, 'original_name' => $file->getClientOriginalName(), ]); } }