feat: implementation du role Gestionnaire RH et refonte de la gestion des offres

This commit is contained in:
jeremy bayse
2026-05-09 11:21:40 +02:00
parent 97a8b9443d
commit 9edf79e8ba
23 changed files with 1223 additions and 232 deletions

View File

@@ -26,11 +26,20 @@ const positionForm = useForm({ job_position_id: props.candidate.job_position_id
const showEditDetailsModal = ref(false);
const detailsForm = useForm({
name: props.candidate.user.name,
birth_name: props.candidate.birth_name || '',
usage_name: props.candidate.usage_name || '',
first_name: props.candidate.first_name || '',
address: props.candidate.address || '',
zip_code: props.candidate.zip_code || '',
email: props.candidate.user.email,
phone: props.candidate.phone || '',
linkedin_url: props.candidate.linkedin_url || '',
city: props.candidate.city || '',
birth_date: props.candidate.birth_date || '',
birth_place: props.candidate.birth_place || '',
nationality: props.candidate.nationality || '',
current_situation: props.candidate.current_situation || '',
education_level: props.candidate.education_level || '',
has_driving_license: props.candidate.has_driving_license ? 1 : 0,
});
const updateDetails = () => {
detailsForm.put(route('admin.candidates.update', props.candidate.id), {
@@ -264,7 +273,7 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
<div class="h-16 bg-primary relative rounded-t-2xl overflow-hidden">
<div class="absolute inset-0 opacity-10" style="background: radial-gradient(circle at top right, #f5a800, transparent 70%)"></div>
<!-- Selection star -->
<button @click="toggleSelection" :title="candidate.is_selected ? 'Retirer la sélection' : 'Retenir ce candidat'"
<button v-if="$page.props.auth.user.role !== 'gestionnaire_rh'" @click="toggleSelection" :title="candidate.is_selected ? 'Retirer la sélection' : 'Retenir ce candidat'"
class="absolute top-3 right-3 p-1.5 rounded-lg transition-all"
:class="candidate.is_selected ? 'text-highlight bg-highlight/20' : 'text-white/30 hover:text-highlight hover:bg-white/10'">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
@@ -318,7 +327,7 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
Exporter
</a>
</div>
<button @click="toggleSelection"
<button v-if="$page.props.auth.user.role !== 'gestionnaire_rh'" @click="toggleSelection"
:class="['mt-2 w-full flex items-center justify-center gap-2 py-2.5 rounded-[10px] border-none text-xs font-extrabold uppercase tracking-[0.08em] transition-all duration-150',
candidate.is_selected
? 'bg-highlight/15 text-highlight border border-highlight/30 hover:bg-highlight/25'
@@ -330,7 +339,7 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
</div>
<!-- Score global card -->
<div class="bg-primary rounded-2xl p-5 relative overflow-hidden">
<div v-if="$page.props.auth.user.role !== 'gestionnaire_rh'" class="bg-primary rounded-2xl p-5 relative overflow-hidden">
<div class="absolute inset-0 opacity-10" style="background: radial-gradient(circle at bottom right, #f5a800, transparent 60%)"></div>
<div class="relative z-10">
<p class="text-[9px] font-black uppercase tracking-[0.18em] text-white/40 mb-2">Score Global Pondéré</p>
@@ -373,7 +382,7 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
</div>
<!-- AI Summary card (if analysed) -->
<div v-if="aiAnalysis" class="bg-surface rounded-2xl border border-ink/[0.07] shadow-sm p-5">
<div v-if="aiAnalysis && $page.props.auth.user.role !== 'gestionnaire_rh'" class="bg-surface rounded-2xl border border-ink/[0.07] shadow-sm p-5">
<div class="flex items-center justify-between mb-3">
<span class="text-[9px] font-black uppercase tracking-[0.16em] text-ink/35">Analyse IA</span>
<div :class="['w-9 h-9 rounded-full flex items-center justify-center text-[11px] font-black',
@@ -395,6 +404,16 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
</select>
</div>
<!-- Structure -->
<div v-if="tenants" class="bg-surface rounded-2xl border border-ink/[0.07] shadow-sm p-5">
<p class="text-[9px] font-black uppercase tracking-[0.16em] text-ink/35 mb-3">Structure de rattachement</p>
<select v-model="tenantForm.tenant_id" @change="updateTenant"
class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none transition-all">
<option value="">— Aucune structure —</option>
<option v-for="t in tenants" :key="t.id" :value="t.id">{{ t.name }}</option>
</select>
</div>
<!-- Danger zone -->
<div class="bg-surface rounded-2xl border border-ink/[0.07] shadow-sm p-5">
<p class="text-[9px] font-black uppercase tracking-[0.16em] text-ink/35 mb-3">Actions</p>
@@ -427,7 +446,13 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
{ id:'documents', label:'Documents', count: candidate.documents?.length },
{ id:'tests', label:'Tests', count: candidate.attempts?.length },
{ id:'security', label:'Sécurité', count: candidate.user.security_alerts?.length },
].filter(t => t.id !== 'security' || t.count > 0)" :key="tab.id" @click="activeTab = tab.id"
].filter(t => {
if (t.id === 'security' && t.count === 0) return false;
if ($page.props.auth.user.role === 'gestionnaire_rh') {
return !['ai_analysis', 'interview', 'tests', 'security'].includes(t.id);
}
return true;
})" :key="tab.id" @click="activeTab = tab.id"
class="relative flex items-center gap-2 px-5 py-4 text-[11px] font-black uppercase tracking-[0.1em] whitespace-nowrap transition-all duration-150"
:class="activeTab === tab.id ? 'text-primary' : 'text-ink/35 hover:text-ink/60'">
{{ tab.label }}
@@ -440,7 +465,7 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
<div v-if="activeTab === 'overview'" class="p-6 space-y-6">
<!-- Save scores button -->
<div v-if="scoreForm.isDirty" class="flex justify-end">
<div v-if="scoreForm.isDirty && $page.props.auth.user.role !== 'gestionnaire_rh'" class="flex justify-end">
<button @click="saveScores"
class="flex items-center gap-2 px-5 py-2.5 rounded-[10px] bg-highlight text-highlight-dark text-xs font-extrabold uppercase tracking-[0.08em] shadow-gold hover:brightness-105 transition-all">
<svg class="w-3.5 h-3.5 animate-pulse" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
@@ -449,7 +474,7 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
</div>
<!-- Score inputs grid -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div v-if="$page.props.auth.user.role !== 'gestionnaire_rh'" class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div v-for="(item, i) in [
{ label:'Analyse CV', key:'cv_score', max:20, color:'text-primary' },
{ label:'Lettre Motiv.', key:'motivation_score', max:10, color:'text-success' },
@@ -471,8 +496,62 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
</div>
</div>
<!-- Informations détaillées -->
<div class="p-5 rounded-2xl border border-ink/[0.07] bg-neutral/30 space-y-4">
<div class="flex items-center justify-between">
<p class="text-[9px] font-black uppercase tracking-[0.16em] text-ink/35">Identité & Situation</p>
<button @click="showEditDetailsModal = true" class="text-[10px] font-bold text-primary hover:underline uppercase tracking-widest">Modifier</button>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
<div>
<p class="text-[10px] font-black text-ink/30 uppercase mb-1">Nom de naissance</p>
<p class="text-sm font-bold text-ink">{{ candidate.birth_name || '—' }}</p>
</div>
<div>
<p class="text-[10px] font-black text-ink/30 uppercase mb-1">Nom d'usage</p>
<p class="text-sm font-bold text-ink">{{ candidate.usage_name || '—' }}</p>
</div>
<div>
<p class="text-[10px] font-black text-ink/30 uppercase mb-1">Prénom</p>
<p class="text-sm font-bold text-ink">{{ candidate.first_name || '—' }}</p>
</div>
<div>
<p class="text-[10px] font-black text-ink/30 uppercase mb-1">Nationalité</p>
<p class="text-sm font-bold text-ink">{{ candidate.nationality || '—' }}</p>
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-6 pt-2">
<div>
<p class="text-[10px] font-black text-ink/30 uppercase mb-1">Né(e) le</p>
<p class="text-sm font-bold text-ink">{{ candidate.birth_date ? new Date(candidate.birth_date).toLocaleDateString('fr-FR') : '—' }}</p>
</div>
<div>
<p class="text-[10px] font-black text-ink/30 uppercase mb-1">Lieu de naissance</p>
<p class="text-sm font-bold text-ink">{{ candidate.birth_place || '—' }}</p>
</div>
<div>
<p class="text-[10px] font-black text-ink/30 uppercase mb-1">Adresse</p>
<p class="text-sm font-bold text-ink leading-tight">{{ candidate.address || '—' }}<br/>{{ candidate.zip_code }} {{ candidate.city }}</p>
</div>
<div>
<p class="text-[10px] font-black text-ink/30 uppercase mb-1">Permis de conduire</p>
<p class="text-sm font-bold" :class="candidate.has_driving_license ? 'text-success' : 'text-accent'">{{ candidate.has_driving_license ? 'OUI' : 'NON' }}</p>
</div>
</div>
<div class="grid grid-cols-2 gap-6 pt-2 border-t border-ink/5 mt-2">
<div>
<p class="text-[10px] font-black text-ink/30 uppercase mb-1">Situation actuelle</p>
<span class="inline-block px-2 py-1 bg-primary/10 text-primary text-[10px] font-black rounded uppercase tracking-wider">{{ candidate.current_situation || '—' }}</span>
</div>
<div>
<p class="text-[10px] font-black text-ink/30 uppercase mb-1">Niveau de diplôme</p>
<span class="inline-block px-2 py-1 bg-highlight/20 text-highlight-dark text-[10px] font-black rounded uppercase tracking-wider">{{ candidate.education_level || '—' }}</span>
</div>
</div>
</div>
<!-- Radar chart -->
<div class="grid md:grid-cols-2 gap-6 items-center">
<div v-if="$page.props.auth.user.role !== 'gestionnaire_rh'" class="grid md:grid-cols-2 gap-6 items-center">
<div class="flex items-center justify-center">
<canvas ref="radarCanvasRef" class="max-h-64 w-full" />
</div>
@@ -844,22 +923,124 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
</div><!-- end flex layout -->
<!-- ─── Modal: Éditer les infos ────────────────────────────────────── -->
<Modal :show="showEditDetailsModal" @close="showEditDetailsModal = false" max-width="lg">
<div class="p-6 space-y-5">
<h3 class="font-serif font-black text-lg text-primary">Modifier les informations</h3>
<div class="grid md:grid-cols-2 gap-4">
<div v-for="(field, key) in { name:'Nom complet', email:'Email', phone:'Téléphone', city:'Ville', linkedin_url:'LinkedIn URL' }" :key="key">
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">{{ field }}</label>
<input v-model="detailsForm[key]" type="text"
class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2.5 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
<InputError :message="detailsForm.errors[key]" class="mt-1" />
<Modal :show="showEditDetailsModal" @close="showEditDetailsModal = false" max-width="4xl">
<div class="p-6 space-y-6">
<div class="border-b border-ink/10 pb-3">
<h3 class="font-serif font-black text-xl text-primary">Modifier le dossier du candidat</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- État Civil -->
<div class="space-y-4 md:col-span-3">
<p class="text-[10px] font-black uppercase tracking-widest text-primary/50">1. État Civil</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Nom de naissance</label>
<input v-model="detailsForm.birth_name" type="text" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
</div>
<div>
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Nom d'usage</label>
<input v-model="detailsForm.usage_name" type="text" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
</div>
<div>
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Prénom</label>
<input v-model="detailsForm.first_name" type="text" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Date de naissance</label>
<input v-model="detailsForm.birth_date" type="date" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
</div>
<div>
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Lieu de naissance</label>
<input v-model="detailsForm.birth_place" type="text" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
</div>
<div>
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Nationalité</label>
<input v-model="detailsForm.nationality" type="text" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
</div>
</div>
</div>
<!-- Coordonnées -->
<div class="space-y-4 md:col-span-3 pt-4 border-t border-ink/5">
<p class="text-[10px] font-black uppercase tracking-widest text-primary/50">2. Coordonnées & Contact</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Email</label>
<input v-model="detailsForm.email" type="email" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
<InputError :message="detailsForm.errors.email" class="mt-1" />
</div>
<div>
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Téléphone</label>
<input v-model="detailsForm.phone" type="text" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="md:col-span-2">
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Adresse</label>
<input v-model="detailsForm.address" type="text" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
</div>
<div>
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Code Postal</label>
<input v-model="detailsForm.zip_code" type="text" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
</div>
<div>
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Ville</label>
<input v-model="detailsForm.city" type="text" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
</div>
</div>
</div>
<!-- Situation -->
<div class="space-y-4 md:col-span-3 pt-4 border-t border-ink/5">
<p class="text-[10px] font-black uppercase tracking-widest text-primary/50">3. Profil Professionnel</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Situation actuelle</label>
<select v-model="detailsForm.current_situation" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none">
<option value="Titulaire">Titulaire</option>
<option value="Lauréat(e) d'un concours">Lauréat(e) d'un concours</option>
<option value="contractuel">Contractuel</option>
<option value="En recherche d'emplois">En recherche d'emplois</option>
</select>
</div>
<div>
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Niveau de diplôme</label>
<select v-model="detailsForm.education_level" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none">
<option value="Aucun diplome">Aucun diplome</option>
<option value="Brevet">Brevet</option>
<option value="CAP/BEP">CAP/BEP</option>
<option value="Bac">Bac</option>
<option value="Bac + 1">Bac + 1</option>
<option value="Bac + 2">Bac + 2</option>
<option value="Bac + 3">Bac + 3</option>
<option value="Bac + 4">Bac + 4</option>
<option value="Bac + 5">Bac + 5</option>
<option value="Autre">Autre</option>
</select>
</div>
<div>
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Permis de conduire</label>
<div class="flex gap-4 h-9 items-center">
<label class="flex items-center gap-2 text-xs font-bold text-ink/60 cursor-pointer">
<input type="radio" v-model="detailsForm.has_driving_license" :value="1" class="text-primary focus:ring-primary/30" /> OUI
</label>
<label class="flex items-center gap-2 text-xs font-bold text-ink/60 cursor-pointer">
<input type="radio" v-model="detailsForm.has_driving_license" :value="0" class="text-primary focus:ring-primary/30" /> NON
</label>
</div>
</div>
</div>
</div>
</div>
<div class="flex justify-end gap-3 pt-2">
<div class="flex justify-end gap-3 pt-4 border-t border-ink/10">
<SecondaryButton @click="showEditDetailsModal = false">Annuler</SecondaryButton>
<button @click="updateDetails" :disabled="detailsForm.processing"
class="px-5 py-2.5 rounded-[10px] bg-highlight text-highlight-dark text-xs font-extrabold uppercase tracking-[0.08em] shadow-gold hover:brightness-105 transition-all disabled:opacity-50">
Enregistrer
class="px-6 py-2.5 rounded-[10px] bg-highlight text-highlight-dark text-xs font-extrabold uppercase tracking-[0.08em] shadow-gold hover:brightness-105 transition-all disabled:opacity-50">
{{ detailsForm.processing ? 'Enregistrement...' : 'Mettre à jour le dossier' }}
</button>
</div>
</div>