281 lines
9.1 KiB
PHP
281 lines
9.1 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
use App\Models\Candidate;
|
|
use App\Models\Document;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
|
|
class CandidateController extends Controller
|
|
{
|
|
public function index()
|
|
{
|
|
$candidates = Candidate::with(['user', 'documents', 'attempts', 'tenant', 'jobPosition'])->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',
|
|
'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'),
|
|
'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; })
|
|
]
|
|
];
|
|
|
|
if (auth()->user()->isSuperAdmin()) {
|
|
$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([
|
|
'cv' => 'nullable|file|mimes:pdf|max:5120',
|
|
'cover_letter' => 'nullable|file|mimes:pdf|max:5120',
|
|
'name' => 'nullable|string|max:255',
|
|
'email' => 'nullable|string|email|max:255|unique:users,email,' . $candidate->user_id,
|
|
'phone' => 'nullable|string|max:255',
|
|
'linkedin_url' => 'nullable|url|max:255',
|
|
]);
|
|
|
|
// Update User info if name or email present
|
|
if ($request->has('name') || $request->has('email')) {
|
|
$candidate->user->update($request->only(['name', 'email']));
|
|
}
|
|
|
|
// Update Candidate info
|
|
$candidate->update($request->only(['phone', 'linkedin_url']));
|
|
|
|
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:30',
|
|
]);
|
|
|
|
$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: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 updateTenant(Request $request, Candidate $candidate)
|
|
{
|
|
if (!auth()->user()->isSuperAdmin()) {
|
|
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(),
|
|
]);
|
|
}
|
|
}
|