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 store(Request $request) { $request->validate([ 'name' => 'required|string|max:255', 'email' => 'required|string|email|max:255|unique:users', 'phone' => 'nullable|string|max:20', 'linkedin_url' => 'nullable|url|max:255', '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); $user = User::create([ 'name' => $request->name, 'email' => $request->email, 'password' => Hash::make(Str::random(12)), 'role' => 'candidate', 'tenant_id' => auth()->user()->isSuperAdmin() ? $request->tenant_id : auth()->user()->tenant_id, ]); $candidate = $user->candidate()->create([ 'phone' => $request->phone, 'linkedin_url' => $request->linkedin_url, 'status' => 'en_attente', 'tenant_id' => auth()->user()->isSuperAdmin() ? $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', 'documents', 'attempts.quiz', 'attempts.answers.question', 'attempts.answers.option', 'jobPosition' ]); return \Inertia\Inertia::render('Admin/Candidates/Show', [ 'candidate' => $candidate, 'jobPositions' => \App\Models\JobPosition::all(), 'ai_config' => [ 'default' => env('AI_DEFAULT_PROVIDER', 'ollama'), 'enabled_providers' => array_filter([ 'ollama' => true, // Toujours dispo car local ou simulé 'openai' => !empty(env('OPENAI_API_KEY')), 'anthropic' => !empty(env('ANTHROPIC_API_KEY')), 'gemini' => !empty(env('GEMINI_API_KEY')), ], function($v) { return $v; }) ] ]); } 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([ 'cv' => 'nullable|file|mimes:pdf|max:5120', 'cover_letter' => 'nullable|file|mimes:pdf|max:5120', ]); 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', 'Documents mis à jour avec succès.'); } public function updateNotes(Request $request, Candidate $candidate) { $request->validate([ 'notes' => 'nullable|string', ]); $candidate->update([ 'notes' => $request->notes, ]); 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 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 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); } 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(), ]); } }