feat: implementation des dossiers candidats PDF, gestion des entretiens et optimisation de l'analyse IA
This commit is contained in:
@@ -172,24 +172,26 @@ const batchAnalyze = async () => {
|
||||
Gestion des Candidats
|
||||
</template>
|
||||
|
||||
<div class="flex justify-between items-end mb-8">
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-2xl font-bold">Liste des Candidats</h3>
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="flex items-center gap-3 bg-white dark:bg-slate-800 p-2 rounded-xl border border-slate-200 dark:border-slate-700 shadow-sm">
|
||||
<div class="flex flex-col md:flex-row justify-between items-start md:items-end mb-8 gap-6">
|
||||
<div class="space-y-4 w-full md:w-auto">
|
||||
<h3 class="text-3xl font-serif font-black text-primary capitalize tracking-tight flex items-center gap-3">
|
||||
<div class="w-1.5 h-8 bg-highlight rounded-full"></div>
|
||||
Liste des Candidats
|
||||
</h3>
|
||||
<div class="flex flex-col sm:flex-row items-center gap-4">
|
||||
<div class="flex items-center gap-3 bg-white p-2 rounded-xl border border-anthracite/5 shadow-sm min-w-max">
|
||||
<label class="flex items-center gap-2 cursor-pointer px-2">
|
||||
<input type="checkbox" v-model="showOnlySelected" class="rounded border-amber-300 text-amber-500 focus:ring-amber-500/20 cursor-pointer">
|
||||
<span class="text-sm font-bold text-slate-700 dark:text-slate-300">Retenus uniquement</span>
|
||||
<input type="checkbox" v-model="showOnlySelected" class="rounded border-highlight/50 text-highlight focus:ring-highlight/20 cursor-pointer">
|
||||
<span class="text-xs font-bold text-primary uppercase tracking-widest">Retenus uniquement</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="text-sm font-medium text-slate-700 dark:text-slate-300">Filtrer par fiche de poste :</label>
|
||||
<div class="flex items-center gap-3 w-full sm:w-auto">
|
||||
<select
|
||||
v-model="selectedJobPosition"
|
||||
class="block w-64 rounded-xl border-slate-300 dark:border-slate-700 dark:bg-slate-900 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
|
||||
class="block w-full sm:w-72 rounded-xl border-anthracite/10 shadow-sm focus:border-primary focus:ring-primary/20 text-sm font-medium text-anthracite transition-all"
|
||||
>
|
||||
<option value="">Toutes les fiches de poste</option>
|
||||
<option value="none">➜ Non assigné (Candidature Spontanée)</option>
|
||||
<option value="none" class="italic">➜ Candidature Spontanée</option>
|
||||
<option v-for="jp in jobPositions" :key="jp.id" :value="jp.id">
|
||||
{{ jp.title }}
|
||||
</option>
|
||||
@@ -197,15 +199,15 @@ const batchAnalyze = async () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-4 w-full md:w-auto justify-end">
|
||||
<div v-if="selectedIds.length > 0" class="flex items-center gap-3 animate-in fade-in slide-in-from-right-4 duration-300">
|
||||
<span class="text-sm font-bold text-slate-500">{{ selectedIds.length }} sélectionné(s)</span>
|
||||
<span class="text-xs font-black uppercase tracking-widest text-primary/50">{{ selectedIds.length }} sélectionné(s)</span>
|
||||
<PrimaryButton
|
||||
@click="batchAnalyze"
|
||||
:disabled="isBatchAnalyzing"
|
||||
class="!bg-purple-600 hover:!bg-purple-500 !border-none flex items-center gap-2"
|
||||
class="!bg-primary hover:!bg-primary/90 !text-white flex items-center gap-2 shadow-primary/20"
|
||||
>
|
||||
<svg v-if="isBatchAnalyzing" class="animate-spin h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<svg v-if="isBatchAnalyzing" class="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
@@ -214,7 +216,7 @@ const batchAnalyze = async () => {
|
||||
</svg>
|
||||
{{ isBatchAnalyzing ? `Analyse ${analysisProgress.current}/${analysisProgress.total}...` : 'Analyse IA groupée' }}
|
||||
</PrimaryButton>
|
||||
<div class="h-8 w-px bg-slate-200 dark:bg-slate-700 mx-2"></div>
|
||||
<div class="h-8 w-px bg-anthracite/10 mx-2 hidden sm:block"></div>
|
||||
</div>
|
||||
<PrimaryButton @click="isModalOpen = true">
|
||||
Ajouter un Candidat
|
||||
@@ -223,211 +225,184 @@ const batchAnalyze = async () => {
|
||||
</div>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
<div v-if="flashSuccess" class="mb-8 p-6 bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-800 rounded-2xl flex items-center gap-4 animate-in fade-in slide-in-from-top-4 duration-500">
|
||||
<div class="p-2 bg-emerald-500 rounded-lg text-white">
|
||||
<div v-if="flashSuccess" class="mb-8 p-6 bg-emerald-50 border border-emerald-200 rounded-2xl flex items-center gap-4 animate-in fade-in slide-in-from-top-4 duration-500 shadow-sm">
|
||||
<div class="p-2 bg-emerald-500 rounded-lg text-white shadow-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-bold text-emerald-800 dark:text-emerald-400">Succès !</p>
|
||||
<p class="text-emerald-700 dark:text-emerald-500 text-sm">{{ flashSuccess }}</p>
|
||||
<p class="font-bold text-emerald-800">Succès !</p>
|
||||
<p class="text-emerald-700 text-sm font-medium">{{ flashSuccess }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Candidates Table -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
<table class="w-full text-left">
|
||||
<thead class="bg-slate-50 dark:bg-slate-700/50 border-b border-slate-200 dark:border-slate-700">
|
||||
<tr>
|
||||
<th class="w-12 px-6 py-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="selectedIds.length === sortedCandidates.length && sortedCandidates.length > 0"
|
||||
@change="toggleSelectAll"
|
||||
class="rounded border-slate-300 text-indigo-600 focus:ring-indigo-500/20 cursor-pointer"
|
||||
>
|
||||
</th>
|
||||
<th class="w-12 px-6 py-4"></th>
|
||||
<th @click="sortBy('user.name')" class="px-6 py-4 font-semibold text-slate-700 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-700/50 transition-colors">
|
||||
<div class="flex items-center gap-2">
|
||||
Nom
|
||||
<svg v-show="sortKey === 'user.name'" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" :class="{ 'rotate-180': sortOrder === -1 }" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
<th @click="sortBy('user.email')" class="px-6 py-4 font-semibold text-slate-700 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-700/50 transition-colors">
|
||||
<div class="flex items-center gap-2">
|
||||
Email
|
||||
<svg v-show="sortKey === 'user.email'" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" :class="{ 'rotate-180': sortOrder === -1 }" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
<th @click="sortBy('tenant.name')" v-if="$page.props.auth.user.role === 'super_admin'" class="px-6 py-4 font-semibold text-slate-700 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-700/50 transition-colors">
|
||||
<div class="flex items-center gap-2">
|
||||
Structure
|
||||
<svg v-show="sortKey === 'tenant.name'" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" :class="{ 'rotate-180': sortOrder === -1 }" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
<th @click="sortBy('job_position.title')" class="px-6 py-4 font-semibold text-slate-700 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-700/50 transition-colors">
|
||||
<div class="flex items-center gap-2">
|
||||
Fiche de Poste
|
||||
<svg v-show="sortKey === 'job_position.title'" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" :class="{ 'rotate-180': sortOrder === -1 }" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
<th @click="sortBy('status')" class="px-6 py-4 font-semibold text-slate-700 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-700/50 transition-colors">
|
||||
<div class="flex items-center gap-2">
|
||||
Statut
|
||||
<svg v-show="sortKey === 'status'" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" :class="{ 'rotate-180': sortOrder === -1 }" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
<th @click="sortBy('weighted_score')" class="px-6 py-4 font-semibold text-slate-700 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-700/50 transition-colors">
|
||||
<div class="flex items-center gap-2">
|
||||
Score /20
|
||||
<svg v-show="sortKey === 'weighted_score'" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" :class="{ 'rotate-180': sortOrder === -1 }" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
<th @click="sortBy('ai_analysis.match_score')" class="px-6 py-4 font-semibold text-slate-700 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-700/50 transition-colors">
|
||||
<div class="flex items-center gap-2">
|
||||
Adéquation IA
|
||||
<svg v-show="sortKey === 'ai_analysis.match_score'" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" :class="{ 'rotate-180': sortOrder === -1 }" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-6 py-4 font-semibold text-slate-700 dark:text-slate-300">Documents</th>
|
||||
<th class="px-6 py-4 font-semibold text-slate-700 dark:text-slate-300 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-200 dark:divide-slate-700">
|
||||
<tr v-for="candidate in sortedCandidates" :key="candidate.id" class="hover:bg-slate-50/50 dark:hover:bg-slate-700/30 transition-colors" :class="{ 'bg-indigo-50/30 dark:bg-indigo-900/10': selectedIds.includes(candidate.id) }">
|
||||
<td class="px-6 py-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="candidate.id"
|
||||
v-model="selectedIds"
|
||||
class="rounded border-slate-300 text-indigo-600 focus:ring-indigo-500/20 cursor-pointer"
|
||||
>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<button @click="toggleSelection(candidate.id)" class="text-amber-400 hover:text-amber-500 hover:scale-110 transition-transform focus:outline-none" :title="candidate.is_selected ? 'Retirer des retenus' : 'Marquer comme retenu'">
|
||||
<svg v-if="candidate.is_selected" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-slate-300 hover:text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="font-bold text-slate-900 dark:text-white">{{ candidate.user.name }}</div>
|
||||
<div class="text-[10px] text-slate-500 font-bold uppercase tracking-tight">{{ candidate.phone }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-slate-600 dark:text-slate-400">
|
||||
{{ candidate.user.email }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-xs font-bold uppercase tracking-widest text-indigo-600 dark:text-indigo-400" v-if="$page.props.auth.user.role === 'super_admin'">
|
||||
{{ candidate.tenant ? candidate.tenant.name : 'Aucun' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm font-semibold text-slate-700 dark:text-slate-300">
|
||||
{{ candidate.job_position ? candidate.job_position.title : 'Non assigné' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-xs font-bold uppercase tracking-widest">
|
||||
<span
|
||||
class="px-3 py-1 rounded-lg"
|
||||
:class="{
|
||||
'bg-amber-50 text-amber-700 border border-amber-200 dark:bg-amber-900/20 dark:border-amber-800 dark:text-amber-400': candidate.status === 'en_attente',
|
||||
'bg-indigo-50 text-indigo-700 border border-indigo-200 dark:bg-indigo-900/20 dark:border-indigo-800 dark:text-indigo-400': candidate.status === 'en_cours',
|
||||
'bg-emerald-50 text-emerald-700 border border-emerald-200 dark:bg-emerald-900/20 dark:border-emerald-800 dark:text-emerald-400': candidate.status === 'termine'
|
||||
}"
|
||||
>
|
||||
{{ candidate.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-12 h-1.5 bg-slate-100 dark:bg-slate-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
class="h-full rounded-full transition-all duration-500"
|
||||
:style="{ width: (candidate.weighted_score / 20) * 100 + '%' }"
|
||||
:class="{
|
||||
'bg-emerald-500': candidate.weighted_score >= 14,
|
||||
'bg-amber-500': candidate.weighted_score >= 10 && candidate.weighted_score < 14,
|
||||
'bg-rose-500': candidate.weighted_score < 10
|
||||
}"
|
||||
></div>
|
||||
<div class="bg-white rounded-3xl shadow-sm border border-anthracite/5 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead class="bg-neutral/50 border-b border-anthracite/5">
|
||||
<tr>
|
||||
<th class="w-12 px-8 py-5">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="selectedIds.length === sortedCandidates.length && sortedCandidates.length > 0"
|
||||
@change="toggleSelectAll"
|
||||
class="rounded border-anthracite/20 text-primary focus:ring-primary/20 cursor-pointer"
|
||||
>
|
||||
</th>
|
||||
<th class="w-12 px-4 py-5"></th>
|
||||
<th @click="sortBy('user.name')" class="px-8 py-5 text-[10px] font-subtitle font-black uppercase tracking-[0.2em] text-anthracite/40 cursor-pointer hover:text-primary transition-colors">
|
||||
<div class="flex items-center gap-2">
|
||||
Nom
|
||||
<svg v-show="sortKey === 'user.name'" xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" :class="{ 'rotate-180': sortOrder === -1 }" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" /></svg>
|
||||
</div>
|
||||
<span class="font-black text-sm" :class="{
|
||||
'text-emerald-600': candidate.weighted_score >= 14,
|
||||
'text-amber-600': candidate.weighted_score >= 10 && candidate.weighted_score < 14,
|
||||
'text-rose-600': candidate.weighted_score < 10
|
||||
}">
|
||||
{{ candidate.weighted_score }}
|
||||
</th>
|
||||
<th @click="sortBy('user.email')" class="px-8 py-5 text-[10px] font-subtitle font-black uppercase tracking-[0.2em] text-anthracite/40 cursor-pointer hover:text-primary transition-colors">
|
||||
<div class="flex items-center gap-2">
|
||||
Contact
|
||||
<svg v-show="sortKey === 'user.email'" xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" :class="{ 'rotate-180': sortOrder === -1 }" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" /></svg>
|
||||
</div>
|
||||
</th>
|
||||
<th @click="sortBy('tenant.name')" v-if="$page.props.auth.user.role === 'super_admin'" class="px-8 py-5 text-[10px] font-subtitle font-black uppercase tracking-[0.2em] text-anthracite/40 cursor-pointer hover:text-primary transition-colors">
|
||||
<div class="flex items-center gap-2">
|
||||
Structure
|
||||
<svg v-show="sortKey === 'tenant.name'" xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" :class="{ 'rotate-180': sortOrder === -1 }" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" /></svg>
|
||||
</div>
|
||||
</th>
|
||||
<th @click="sortBy('job_position.title')" class="px-8 py-5 text-[10px] font-subtitle font-black uppercase tracking-[0.2em] text-anthracite/40 cursor-pointer hover:text-primary transition-colors">
|
||||
<div class="flex items-center gap-2">
|
||||
Poste Ciblé
|
||||
<svg v-show="sortKey === 'job_position.title'" xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" :class="{ 'rotate-180': sortOrder === -1 }" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" /></svg>
|
||||
</div>
|
||||
</th>
|
||||
<th @click="sortBy('status')" class="px-8 py-5 text-[10px] font-subtitle font-black uppercase tracking-[0.2em] text-anthracite/40 cursor-pointer hover:text-primary transition-colors">
|
||||
<div class="flex items-center gap-2">
|
||||
Statut
|
||||
<svg v-show="sortKey === 'status'" xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" :class="{ 'rotate-180': sortOrder === -1 }" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" /></svg>
|
||||
</div>
|
||||
</th>
|
||||
<th @click="sortBy('weighted_score')" class="px-8 py-5 text-[10px] font-subtitle font-black uppercase tracking-[0.2em] text-anthracite/40 cursor-pointer hover:text-primary transition-colors">
|
||||
<div class="flex items-center gap-2">
|
||||
Score
|
||||
<svg v-show="sortKey === 'weighted_score'" xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" :class="{ 'rotate-180': sortOrder === -1 }" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" /></svg>
|
||||
</div>
|
||||
</th>
|
||||
<th @click="sortBy('ai_analysis.match_score')" class="px-8 py-5 text-[10px] font-subtitle font-black uppercase tracking-[0.2em] text-anthracite/40 cursor-pointer hover:text-primary transition-colors">
|
||||
<div class="flex items-center gap-2">
|
||||
IA Match
|
||||
<svg v-show="sortKey === 'ai_analysis.match_score'" xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" :class="{ 'rotate-180': sortOrder === -1 }" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" /></svg>
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-8 py-5 text-[10px] font-subtitle font-black uppercase tracking-[0.2em] text-anthracite/40">Docs</th>
|
||||
<th class="px-8 py-5 text-[10px] font-subtitle font-black uppercase tracking-[0.2em] text-anthracite/40 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-anthracite/5">
|
||||
<tr v-for="candidate in sortedCandidates" :key="candidate.id" class="hover:bg-sand/30 transition-colors group" :class="{ 'bg-primary/5': selectedIds.includes(candidate.id) }">
|
||||
<td class="px-8 py-5">
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="candidate.id"
|
||||
v-model="selectedIds"
|
||||
class="rounded border-anthracite/20 text-primary focus:ring-primary/20 cursor-pointer"
|
||||
>
|
||||
</td>
|
||||
<td class="px-4 py-5 text-center">
|
||||
<button @click="toggleSelection(candidate.id)" class="text-anthracite/20 hover:text-highlight hover:-translate-y-0.5 transition-all focus:outline-none" :class="{ '!text-highlight drop-shadow-sm scale-110': candidate.is_selected }" :title="candidate.is_selected ? 'Retirer des retenus' : 'Marquer comme retenu'">
|
||||
<svg v-if="candidate.is_selected" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
<td class="px-8 py-5">
|
||||
<Link :href="route('admin.candidates.show', candidate.id)" class="font-black text-primary group-hover:text-highlight transition-colors block">
|
||||
{{ candidate.user.name }}
|
||||
</Link>
|
||||
<div class="text-[10px] text-anthracite/50 font-bold uppercase tracking-tight mt-0.5">{{ candidate.phone || 'Pas de numéro' }}</div>
|
||||
</td>
|
||||
<td class="px-8 py-5 text-xs text-anthracite/70 font-medium">
|
||||
{{ candidate.user.email }}
|
||||
</td>
|
||||
<td class="px-8 py-5 text-[10px] font-black uppercase tracking-widest text-primary/60" v-if="$page.props.auth.user.role === 'super_admin'">
|
||||
{{ candidate.tenant ? candidate.tenant.name : 'Aucune' }}
|
||||
</td>
|
||||
<td class="px-8 py-5 text-xs font-bold text-anthracite">
|
||||
{{ candidate.job_position ? candidate.job_position.title : 'Non assigné' }}
|
||||
</td>
|
||||
<td class="px-8 py-5">
|
||||
<span
|
||||
class="px-3 py-1 text-[10px] font-black uppercase tracking-[0.15em] rounded-full"
|
||||
:class="{
|
||||
'bg-anthracite/5 text-anthracite/60 border border-anthracite/10': candidate.status === 'en_attente',
|
||||
'bg-sky/10 text-sky border border-sky/20': candidate.status === 'en_cours',
|
||||
'bg-emerald-50 text-emerald-700 border border-emerald-200': candidate.status === 'termine',
|
||||
'bg-accent/10 text-accent border border-accent/20': candidate.status === 'refuse'
|
||||
}"
|
||||
>
|
||||
{{ candidate.status }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div v-if="candidate.ai_analysis" class="flex items-center gap-2">
|
||||
<div
|
||||
class="px-2 py-0.5 rounded text-[10px] font-black"
|
||||
:class="[
|
||||
candidate.ai_analysis.match_score >= 80 ? 'bg-emerald-100 text-emerald-700' :
|
||||
candidate.ai_analysis.match_score >= 60 ? 'bg-amber-100 text-amber-700' :
|
||||
'bg-red-100 text-red-700'
|
||||
]"
|
||||
>
|
||||
{{ candidate.ai_analysis.match_score }}%
|
||||
</td>
|
||||
<td class="px-8 py-5">
|
||||
<div class="inline-flex items-center gap-2 px-3 py-1 bg-primary/5 text-primary rounded-xl font-black text-sm border border-primary/10 shadow-sm">
|
||||
{{ candidate.weighted_score }} <span class="opacity-50 text-[10px]">/ 20</span>
|
||||
</div>
|
||||
<span class="text-[10px] font-bold text-slate-400 uppercase truncate max-w-[60px]">{{ candidate.ai_analysis.verdict }}</span>
|
||||
</div>
|
||||
<span v-else class="text-[10px] text-slate-300 italic">Non analysé</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
v-for="doc in candidate.documents"
|
||||
:key="doc.id"
|
||||
@click="openPreview(doc)"
|
||||
class="p-2 bg-slate-100 dark:bg-slate-700 rounded-lg hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors"
|
||||
:title="doc.type.toUpperCase()"
|
||||
>
|
||||
<svg v-if="doc.type === 'cv'" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-indigo-600 dark:text-indigo-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-emerald-600 dark:text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<div class="flex items-center justify-end gap-3">
|
||||
<Link :href="route('admin.candidates.show', candidate.id)" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 font-medium">Détails</Link>
|
||||
<button @click="deleteCandidate(candidate.id)" class="p-2 text-slate-400 hover:text-red-600 transition-colors" title="Supprimer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="candidates.length === 0">
|
||||
<td colspan="8" class="px-6 py-12 text-center text-slate-500 italic">
|
||||
Aucun candidat trouvé.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
<td class="px-8 py-5">
|
||||
<div v-if="candidate.ai_analysis" class="flex items-center gap-2">
|
||||
<div
|
||||
class="px-2 py-0.5 rounded-lg text-[10px] font-black shadow-sm"
|
||||
:class="[
|
||||
candidate.ai_analysis.match_score >= 80 ? 'bg-emerald-50 text-emerald-700 border border-emerald-200' :
|
||||
candidate.ai_analysis.match_score >= 60 ? 'bg-highlight/10 text-[#3a2800] border border-highlight/30' :
|
||||
'bg-accent/10 text-accent border border-accent/20'
|
||||
]"
|
||||
>
|
||||
{{ candidate.ai_analysis.match_score }}%
|
||||
</div>
|
||||
<span class="text-[9px] font-bold text-anthracite/40 uppercase truncate max-w-[60px]" :title="candidate.ai_analysis.verdict">{{ candidate.ai_analysis.verdict }}</span>
|
||||
</div>
|
||||
<span v-else class="text-[9px] font-bold uppercase tracking-widest text-anthracite/30 italic">Non analysé</span>
|
||||
</td>
|
||||
<td class="px-8 py-5">
|
||||
<div class="flex gap-1.5">
|
||||
<button
|
||||
v-for="doc in candidate.documents"
|
||||
:key="doc.id"
|
||||
@click="openPreview(doc)"
|
||||
class="p-1.5 bg-neutral text-anthracite/40 rounded-lg hover:bg-primary/10 hover:text-primary transition-colors"
|
||||
:title="doc.type.toUpperCase()"
|
||||
>
|
||||
<svg v-if="doc.type === 'cv'" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>
|
||||
</button>
|
||||
<span v-if="candidate.documents.length === 0" class="text-anthracite/20 text-xs">-</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-8 py-5 text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<Link :href="route('admin.candidates.show', candidate.id)" class="p-2 text-primary/40 hover:text-highlight hover:bg-highlight/10 rounded-xl transition-all" title="Détails">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /></svg>
|
||||
</Link>
|
||||
<button @click="deleteCandidate(candidate.id)" class="p-2 text-anthracite/20 hover:text-accent hover:bg-accent/10 rounded-xl transition-all" title="Supprimer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="candidates.length === 0">
|
||||
<td colspan="11" class="px-8 py-16 text-center">
|
||||
<div class="text-anthracite/40 italic font-medium font-subtitle">
|
||||
Aucun candidat trouvé.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Candidate Modal -->
|
||||
|
||||
@@ -21,10 +21,29 @@ const props = defineProps({
|
||||
const page = usePage();
|
||||
const flashSuccess = computed(() => page.props.flash?.success);
|
||||
|
||||
const activeTab = ref('overview');
|
||||
|
||||
const positionForm = useForm({
|
||||
job_position_id: props.candidate.job_position_id || ''
|
||||
});
|
||||
|
||||
const showEditDetailsModal = ref(false);
|
||||
const detailsForm = useForm({
|
||||
name: props.candidate.user.name,
|
||||
email: props.candidate.user.email,
|
||||
phone: props.candidate.phone || '',
|
||||
linkedin_url: props.candidate.linkedin_url || '',
|
||||
});
|
||||
|
||||
const updateDetails = () => {
|
||||
detailsForm.put(route('admin.candidates.update', props.candidate.id), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
showEditDetailsModal.value = false;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const updatePosition = () => {
|
||||
positionForm.patch(route('admin.candidates.update-position', props.candidate.id), {
|
||||
preserveScroll: true,
|
||||
@@ -53,8 +72,20 @@ const docForm = useForm({
|
||||
_method: 'PUT' // For file upload via PUT in Laravel
|
||||
});
|
||||
|
||||
const rawInterviewDetails = props.candidate.interview_details || {};
|
||||
const notesForm = useForm({
|
||||
notes: props.candidate.notes || ''
|
||||
notes: props.candidate.notes || '',
|
||||
interview_details: {
|
||||
questions: rawInterviewDetails.questions || [],
|
||||
appreciation: rawInterviewDetails.appreciation || 0,
|
||||
soft_skills: rawInterviewDetails.soft_skills || [
|
||||
{ name: 'Communication & Pédagogie', score: 0 },
|
||||
{ name: 'Esprit d\'équipe & Collaboration', score: 0 },
|
||||
{ name: 'Résolution de problèmes & Logique', score: 0 },
|
||||
{ name: 'Adaptabilité & Résilience', score: 0 },
|
||||
{ name: 'Autonomie & Proactivité', score: 0 }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const scoreForm = useForm({
|
||||
@@ -114,11 +145,6 @@ const updateDocuments = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const saveNotes = () => {
|
||||
notesForm.patch(route('admin.candidates.update-notes', props.candidate.id), {
|
||||
preserveScroll: true,
|
||||
});
|
||||
};
|
||||
|
||||
const saveScores = () => {
|
||||
scoreForm.patch(route('admin.candidates.update-scores', props.candidate.id), {
|
||||
@@ -150,12 +176,21 @@ const bestTestScore = computed(() => {
|
||||
return Math.max(...finished.map(a => (a.score / a.max_score) * 20));
|
||||
});
|
||||
|
||||
// Calculated Soft Skills average
|
||||
const softSkillsScore = computed(() => {
|
||||
const skills = notesForm.interview_details.soft_skills || [];
|
||||
if (skills.length === 0) return 0;
|
||||
const total = skills.reduce((acc, s) => acc + (parseFloat(s.score) || 0), 0);
|
||||
return Number((total / skills.length).toFixed(1));
|
||||
});
|
||||
|
||||
// Données radar normalisées en % (chaque axe / son max)
|
||||
const radarData = computed(() => ([
|
||||
Math.round((parseFloat(scoreForm.cv_score) / 20) * 100),
|
||||
Math.round((parseFloat(scoreForm.motivation_score) / 10) * 100),
|
||||
Math.round((parseFloat(scoreForm.interview_score) / 30) * 100),
|
||||
Math.round((bestTestScore.value / 20) * 100),
|
||||
Math.round((softSkillsScore.value / 10) * 100), // Max is 10 for avg soft skills
|
||||
]));
|
||||
|
||||
const buildRadarChart = () => {
|
||||
@@ -172,7 +207,7 @@ const buildRadarChart = () => {
|
||||
radarChartInstance = new Chart(radarCanvasRef.value, {
|
||||
type: 'radar',
|
||||
data: {
|
||||
labels: ['Analyse CV', 'Lettre Motiv.', 'Entretien', 'Test Technique'],
|
||||
labels: ['Analyse CV', 'Lettre Motiv.', 'Entretien', 'Test Technique', 'Soft Skills'],
|
||||
datasets: [{
|
||||
label: 'Profil Candidat (%)',
|
||||
data: radarData.value,
|
||||
@@ -239,7 +274,7 @@ onUnmounted(() => {
|
||||
|
||||
// Mise à jour du radar quand les scores changent
|
||||
watch(
|
||||
() => [scoreForm.cv_score, scoreForm.motivation_score, scoreForm.interview_score, bestTestScore.value],
|
||||
() => [scoreForm.cv_score, scoreForm.motivation_score, scoreForm.interview_score, bestTestScore.value, softSkillsScore.value],
|
||||
() => {
|
||||
if (radarChartInstance) {
|
||||
radarChartInstance.data.datasets[0].data = radarData.value;
|
||||
@@ -247,6 +282,13 @@ watch(
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Ré-initialisation du radar lors du switch d'onglet
|
||||
watch(activeTab, (newTab) => {
|
||||
if (newTab === 'overview') {
|
||||
nextTick(() => buildRadarChart());
|
||||
}
|
||||
});
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const aiAnalysis = ref(props.candidate.ai_analysis || null);
|
||||
@@ -254,6 +296,43 @@ const isAnalyzing = ref(false);
|
||||
const selectedProvider = ref(props.ai_config?.default || 'ollama');
|
||||
const forceAnalysis = ref(false);
|
||||
|
||||
// ─── Interview Scoring Logic ───────────────────────────────────────────────────
|
||||
const calculatedInterviewScore = computed(() => {
|
||||
const qScore = (notesForm.interview_details.questions || []).reduce((acc, q) => acc + (parseFloat(q.score) || 0), 0);
|
||||
const appScore = parseFloat(notesForm.interview_details.appreciation) || 0;
|
||||
return Math.min(30, qScore + appScore);
|
||||
});
|
||||
|
||||
// Auto-populate questions from AI analysis if empty
|
||||
watch(aiAnalysis, (newVal) => {
|
||||
if (newVal && newVal.questions_entretien_suggerees && (!notesForm.interview_details.questions || notesForm.interview_details.questions.length === 0)) {
|
||||
notesForm.interview_details.questions = newVal.questions_entretien_suggerees.map(q => ({
|
||||
question: q,
|
||||
score: 0,
|
||||
comment: ''
|
||||
}));
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// Sync with global score form and auto-save logic
|
||||
watch(calculatedInterviewScore, (newVal) => {
|
||||
scoreForm.interview_score = newVal;
|
||||
});
|
||||
|
||||
const saveNotes = () => {
|
||||
notesForm.transform((data) => ({
|
||||
...data,
|
||||
interview_score: calculatedInterviewScore.value
|
||||
})).patch(route('admin.candidates.update-notes', props.candidate.id), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
// Update raw candidate data to reflect the new score in computed fields if necessary
|
||||
props.candidate.interview_score = calculatedInterviewScore.value;
|
||||
props.candidate.interview_details = notesForm.interview_details;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Error Modal state
|
||||
const showErrorModal = ref(false);
|
||||
const modalErrorMessage = ref("");
|
||||
@@ -310,156 +389,161 @@ const runAI = async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-3 gap-8">
|
||||
<!-- Sidebar: Profile & Docs -->
|
||||
<div class="space-y-8">
|
||||
<!-- Profile Card -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
<div class="h-24 bg-gradient-to-r from-indigo-500 to-purple-600"></div>
|
||||
<div class="px-6 pb-6 text-center -mt-12 relative">
|
||||
<div class="absolute right-6 top-16 right-0 text-center w-full max-w-[50px] ml-auto mr-auto sm:right-6 sm:top-14 sm:w-auto">
|
||||
<button
|
||||
@click="toggleSelection"
|
||||
class="flex flex-col items-center gap-1 group focus:outline-none"
|
||||
:title="candidate.is_selected ? 'Retirer des retenus' : 'Marquer pour entretien'"
|
||||
>
|
||||
<div
|
||||
class="p-2 rounded-full transition-all"
|
||||
:class="candidate.is_selected ? 'bg-amber-100 text-amber-500 shadow-sm' : 'bg-slate-100 text-slate-400 group-hover:bg-amber-50 group-hover:text-amber-400'"
|
||||
<div class="space-y-8">
|
||||
<!-- Hero Header (En-tête de Profil) -->
|
||||
<div class="bg-white rounded-3xl shadow-sm border border-anthracite/5 overflow-visible z-10 sticky top-0 md:top-4">
|
||||
<div class="h-16 md:h-20 bg-primary rounded-t-3xl relative overflow-hidden flex items-center px-8 relative">
|
||||
<div class="absolute inset-0 bg-[url('https://www.mediterranee-agglo.fr/sites/default/files/images/banniere-CABM-3.jpg')] opacity-10 bg-cover bg-center mix-blend-overlay"></div>
|
||||
<!-- Actions globales alignées à droite et stylées Or du midi -->
|
||||
<div class="ml-auto relative z-10 flex flex-wrap items-center justify-end gap-3 pt-2">
|
||||
<a :href="route('admin.candidates.export-dossier', candidate.id)" class="px-4 py-1.5 bg-[#e0b04c] text-[#3a2800] rounded-xl text-[10px] uppercase font-black font-subtitle flex items-center gap-2 hover:bg-[#e0b04c]/80 transition-all shadow-lg active:scale-95" title="Télécharger le rapport de synthèse">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Rapport (PDF)
|
||||
</a>
|
||||
<a :href="route('admin.candidates.export-zip', candidate.id)" class="px-4 py-1.5 bg-[#e0b04c] text-[#3a2800] rounded-xl text-[10px] uppercase font-black font-subtitle flex items-center gap-2 hover:bg-[#e0b04c]/80 transition-all shadow-lg active:scale-95" title="Télécharger le dossier complet avec originaux">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
|
||||
</svg>
|
||||
Dossier Complet (ZIP)
|
||||
</a>
|
||||
<SecondaryButton @click="resetPassword" class="!px-3 !py-1 text-[10px] uppercase font-bold tracking-widest !bg-white/10 !border-none !text-white hover:!bg-white/20">Réinitialiser MDP</SecondaryButton>
|
||||
<DangerButton @click="deleteCandidate" class="!px-3 !py-1 text-[10px] uppercase font-bold tracking-widest !bg-accent hover:!bg-accent/80 !border-none">Supprimer</DangerButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-6 md:px-8 pb-6 flex flex-col md:flex-row gap-6 relative">
|
||||
<!-- Avatar flottant -->
|
||||
<div class="w-24 h-24 md:w-32 md:h-32 bg-white rounded-3xl shadow-xl border-4 border-white flex items-center justify-center text-4xl md:text-5xl font-serif font-black text-primary -mt-12 md:-mt-16 relative z-10 shrink-0">
|
||||
{{ candidate.user.name.charAt(0) }}
|
||||
</div>
|
||||
|
||||
<!-- Infos Principales -->
|
||||
<div class="flex-1 pt-2 md:pt-4 flex flex-col md:flex-row justify-between gap-6">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-2xl md:text-3xl font-serif font-black text-primary">{{ candidate.user.name }}</h3>
|
||||
<button @click="showEditDetailsModal = true" class="text-anthracite/20 hover:text-highlight transition-colors bg-neutral/50 p-1.5 rounded-lg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
@click="toggleSelection"
|
||||
class="flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-subtitle uppercase tracking-[0.2em] transition-all ml-2 border"
|
||||
:class="candidate.is_selected ? 'bg-highlight/10 text-[#3a2800] border-highlight/30' : 'bg-neutral text-anthracite/40 border-anthracite/5 hover:border-highlight hover:text-highlight'"
|
||||
:title="candidate.is_selected ? 'Retirer des retenus' : 'Marquer pour entretien'"
|
||||
>
|
||||
<svg v-if="candidate.is_selected" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 20 20" fill="currentColor">
|
||||
<svg v-if="candidate.is_selected" xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-[9px] font-black uppercase tracking-widest hidden sm:block" :class="candidate.is_selected ? 'text-amber-500' : 'text-slate-400 group-hover:text-amber-400'">Retenu</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="w-24 h-24 bg-white dark:bg-slate-900 rounded-2xl shadow-xl border-4 border-white dark:border-slate-800 flex items-center justify-center text-4xl font-black text-indigo-600 mx-auto mb-4">
|
||||
{{ candidate.user.name.charAt(0) }}
|
||||
</div>
|
||||
<h3 class="text-xl font-bold">{{ candidate.user.name }}</h3>
|
||||
<p class="text-slate-500 text-sm mb-4">{{ candidate.user.email }}</p>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-2 block text-left">Poste Cible</label>
|
||||
<select
|
||||
v-model="positionForm.job_position_id"
|
||||
@change="updatePosition"
|
||||
class="w-full bg-slate-50 dark:bg-slate-900 border-none rounded-xl py-2 px-3 text-xs font-bold text-indigo-600 focus:ring-2 focus:ring-indigo-500/20 transition-all cursor-pointer"
|
||||
>
|
||||
<option value="">Non assigné</option>
|
||||
<option v-for="pos in jobPositions" :key="pos.id" :value="pos.id">
|
||||
{{ pos.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Structure de rattachement (Super Admin only) -->
|
||||
<div v-if="page.props.auth.user.role === 'super_admin'" class="mb-6">
|
||||
<label class="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-2 block text-left">Structure de Rattachement</label>
|
||||
<select
|
||||
v-model="tenantForm.tenant_id"
|
||||
@change="updateTenant"
|
||||
class="w-full bg-slate-50 dark:bg-slate-900 border-none rounded-xl py-2 px-3 text-xs font-bold text-emerald-600 focus:ring-2 focus:ring-emerald-500/20 transition-all cursor-pointer"
|
||||
>
|
||||
<option value="">Aucune structure</option>
|
||||
<option v-for="tenant in tenants" :key="tenant.id" :value="tenant.id">
|
||||
{{ tenant.name }}
|
||||
</option>
|
||||
</select>
|
||||
<p class="text-[9px] text-slate-400 mt-1 italic text-left">Note: modifie aussi le rattachement de l'utilisateur.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3 text-left">
|
||||
<div class="flex items-center gap-3 p-3 bg-slate-50 dark:bg-slate-900 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
</svg>
|
||||
<span class="text-sm font-medium">{{ candidate.phone || 'Non renseigné' }}</span>
|
||||
{{ candidate.is_selected ? 'Retenu' : 'Sélectionner' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-4 text-xs font-medium text-anthracite/60 font-subtitle">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>
|
||||
{{ candidate.user.email }}
|
||||
</span>
|
||||
<span v-if="candidate.phone" class="flex items-center gap-1.5 relative before:content-['•'] before:absolute before:-left-3 before:text-anthracite/20 ml-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" /></svg>
|
||||
{{ candidate.phone }}
|
||||
</span>
|
||||
<a v-if="candidate.linkedin_url" :href="candidate.linkedin_url" target="_blank" class="flex items-center gap-1.5 hover:text-primary transition-colors relative before:content-['•'] before:absolute before:-left-3 before:text-anthracite/20 ml-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 opacity-50 m-0.5" fill="currentColor" viewBox="0 0 24 24"><path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/></svg>
|
||||
LinkedIn
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sélecteurs (Poste & Structure) -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 lg:min-w-[400px]">
|
||||
<div class="flex-1 space-y-1.5">
|
||||
<label class="text-[9px] font-subtitle font-black uppercase tracking-[0.2em] text-anthracite/40">Fiche de Poste ciblée</label>
|
||||
<select
|
||||
v-model="positionForm.job_position_id"
|
||||
@change="updatePosition"
|
||||
class="w-full bg-neutral/50 border border-anthracite/5 rounded-xl py-2 px-3 text-xs font-bold text-primary focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all cursor-pointer shadow-sm"
|
||||
>
|
||||
<option value="">Non assigné (Candidature spontanée)</option>
|
||||
<option v-for="pos in jobPositions" :key="pos.id" :value="pos.id">
|
||||
{{ pos.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="page.props.auth.user.role === 'super_admin'" class="flex-1 space-y-1.5">
|
||||
<label class="text-[9px] font-subtitle font-black uppercase tracking-[0.2em] text-anthracite/40">Structure (Tenant)</label>
|
||||
<select
|
||||
v-model="tenantForm.tenant_id"
|
||||
@change="updateTenant"
|
||||
class="w-full bg-neutral/50 border border-anthracite/5 rounded-xl py-2 px-3 text-xs font-bold text-primary focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all cursor-pointer shadow-sm"
|
||||
>
|
||||
<option value="">Aucune structure</option>
|
||||
<option v-for="tenant in tenants" :key="tenant.id" :value="tenant.id">
|
||||
{{ tenant.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<a v-if="candidate.linkedin_url" :href="candidate.linkedin_url" target="_blank" class="flex items-center gap-3 p-3 bg-slate-50 dark:bg-slate-900 rounded-xl hover:text-indigo-600 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-slate-400" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/>
|
||||
</svg>
|
||||
<span class="text-sm font-medium">LinkedIn Profile</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-6 py-4 bg-slate-50 dark:bg-slate-900 border-t border-slate-200 dark:border-slate-700 flex justify-between items-center gap-2">
|
||||
<SecondaryButton @click="resetPassword" class="!px-3 !py-1 text-[10px] uppercase font-bold tracking-widest">Réinitialiser MDP</SecondaryButton>
|
||||
<DangerButton @click="deleteCandidate" class="!px-3 !py-1 text-[10px] uppercase font-bold tracking-widest">Supprimer Compte</DangerButton>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- Fin Hero Header (387) -->
|
||||
|
||||
<!-- Documents Card -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700 p-6">
|
||||
<h4 class="font-bold mb-4 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
|
||||
</svg>
|
||||
Documents joints
|
||||
</h4>
|
||||
<div class="space-y-3">
|
||||
<!-- Tabs Navigation -->
|
||||
<div class="border-t border-anthracite/5 px-6 md:px-8 bg-neutral/30 rounded-b-3xl">
|
||||
<div class="flex items-center gap-8 overflow-x-auto no-scrollbar">
|
||||
<button
|
||||
v-for="doc in candidate.documents"
|
||||
:key="doc.id"
|
||||
@click="openPreview(doc)"
|
||||
class="w-full flex items-center justify-between p-4 bg-slate-100 dark:bg-slate-900 rounded-xl hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors group"
|
||||
@click="activeTab = 'overview'"
|
||||
class="px-4 py-4 text-xs font-subtitle uppercase tracking-widest transition-all relative whitespace-nowrap"
|
||||
:class="activeTab === 'overview' ? 'text-primary font-black' : 'text-anthracite/40 hover:text-primary/70 font-bold'"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 bg-white dark:bg-slate-800 rounded-lg group-hover:bg-indigo-500 group-hover:text-white transition-colors">
|
||||
<svg v-if="doc.type === 'cv'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<div class="text-sm font-bold uppercase tracking-tight">{{ doc.type }}</div>
|
||||
<div class="text-[10px] text-slate-500 truncate max-w-[150px]">{{ doc.original_name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-slate-400 group-hover:translate-x-1 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
Vue d'ensemble
|
||||
<div v-if="activeTab === 'overview'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary rounded-t-full"></div>
|
||||
</button>
|
||||
<button
|
||||
@click="activeTab = 'ai_analysis'"
|
||||
class="px-4 py-4 text-xs font-subtitle uppercase tracking-widest transition-all relative whitespace-nowrap"
|
||||
:class="activeTab === 'ai_analysis' ? 'text-primary font-black' : 'text-anthracite/40 hover:text-primary/70 font-bold'"
|
||||
>
|
||||
Analyse IA
|
||||
<div v-if="activeTab === 'ai_analysis'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary rounded-t-full"></div>
|
||||
</button>
|
||||
<button
|
||||
@click="activeTab = 'interview'"
|
||||
class="px-4 py-4 text-xs font-subtitle uppercase tracking-widest transition-all relative whitespace-nowrap"
|
||||
:class="activeTab === 'interview' ? 'text-primary font-black' : 'text-anthracite/40 hover:text-primary/70 font-bold'"
|
||||
>
|
||||
Évaluation & Entretien
|
||||
<div v-if="activeTab === 'interview'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary rounded-t-full"></div>
|
||||
</button>
|
||||
<button
|
||||
@click="activeTab = 'documents'"
|
||||
class="px-4 py-4 text-xs font-subtitle uppercase tracking-widest transition-all relative whitespace-nowrap flex items-center gap-2"
|
||||
:class="activeTab === 'documents' ? 'text-primary font-black' : 'text-anthracite/40 hover:text-primary/70 font-bold'"
|
||||
>
|
||||
Documents
|
||||
<span class="px-1.5 py-0.5 bg-anthracite/5 rounded text-[9px] min-w-[20px] text-center" :class="{ 'bg-primary/10 text-primary': activeTab === 'documents' }">{{ candidate.documents?.length || 0 }}</span>
|
||||
<div v-if="activeTab === 'documents'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary rounded-t-full"></div>
|
||||
</button>
|
||||
<button
|
||||
@click="activeTab = 'tests'"
|
||||
class="px-4 py-4 text-xs font-subtitle uppercase tracking-widest transition-all relative whitespace-nowrap"
|
||||
:class="activeTab === 'tests' ? 'text-primary font-black' : 'text-anthracite/40 hover:text-primary/70 font-bold'"
|
||||
>
|
||||
Tests
|
||||
<div v-if="activeTab === 'tests'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary rounded-t-full"></div>
|
||||
</button>
|
||||
|
||||
<div class="pt-6 mt-6 border-t border-slate-100 dark:border-slate-700">
|
||||
<h5 class="text-xs font-black uppercase text-slate-400 tracking-widest mb-4">Ajouter / Remplacer</h5>
|
||||
<form @submit.prevent="updateDocuments" class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="relative group/file">
|
||||
<label class="flex flex-col items-center justify-center p-3 bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl cursor-pointer hover:border-indigo-500 transition-colors">
|
||||
<span class="text-[10px] font-bold uppercase tracking-tight text-slate-500">CV (PDF)</span>
|
||||
<span class="text-[9px] text-slate-400 truncate w-full text-center mt-1">{{ docForm.cv ? docForm.cv.name : 'Choisir...' }}</span>
|
||||
<input type="file" class="hidden" @input="docForm.cv = $event.target.files[0]" accept="application/pdf" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="relative group/file">
|
||||
<label class="flex flex-col items-center justify-center p-3 bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl cursor-pointer hover:border-emerald-500 transition-colors">
|
||||
<span class="text-[10px] font-bold uppercase tracking-tight text-slate-500">Lettre (PDF)</span>
|
||||
<span class="text-[9px] text-slate-400 truncate w-full text-center mt-1">{{ docForm.cover_letter ? docForm.cover_letter.name : 'Choisir...' }}</span>
|
||||
<input type="file" class="hidden" @input="docForm.cover_letter = $event.target.files[0]" accept="application/pdf" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<InputError :message="docForm.errors.cv" />
|
||||
<InputError :message="docForm.errors.cover_letter" />
|
||||
<PrimaryButton class="w-full !justify-center !py-2 text-xs" :disabled="docForm.processing || (!docForm.cv && !docForm.cover_letter)">
|
||||
Mettre à jour les fichiers
|
||||
</PrimaryButton>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main: Content -->
|
||||
<div class="xl:col-span-2 space-y-8">
|
||||
<!-- Tab Content: Overview -->
|
||||
<div v-if="activeTab === 'overview'" class="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<!-- Scores Dashboard -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-3xl shadow-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
<div class="p-8 bg-gradient-to-br from-slate-900 to-slate-800 text-white flex flex-col md:flex-row md:items-center justify-between gap-8">
|
||||
@@ -611,21 +695,20 @@ const runAI = async () => {
|
||||
:style="{ width: (bestTestScore / 20 * 100) + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Score footer note -->
|
||||
<p class="text-[10px] text-slate-400 italic pt-2 border-t border-slate-100 dark:border-slate-700">
|
||||
Chaque axe est normalisé sur 100% par rapport à son barème maximum.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Score footer note -->
|
||||
<p class="text-[10px] text-slate-400 italic pt-6 border-t border-slate-100 dark:border-slate-700">
|
||||
Chaque axe est normalisé sur 100% par rapport à son barème maximum.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- Fin Overview Tab Content -->
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- AI Analysis Section (Full Width) -->
|
||||
<div class="xl:col-span-3 space-y-8">
|
||||
<!-- Tab Content: AI Analysis -->
|
||||
<div v-if="activeTab === 'ai_analysis'" class="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-3xl shadow-lg border border-slate-200 dark:border-slate-700 p-10 overflow-hidden relative">
|
||||
<div class="flex flex-col xl:flex-row xl:items-center justify-between gap-8 mb-10 border-b border-slate-100 dark:border-slate-700 pb-8">
|
||||
<div>
|
||||
@@ -798,18 +881,6 @@ const runAI = async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Interview Questions -->
|
||||
<div v-if="aiAnalysis.questions_entretien_suggerees?.length > 0" class="space-y-8">
|
||||
<h5 class="text-sm font-black uppercase tracking-widest text-indigo-500 border-l-4 border-indigo-500 pl-4">Préparation d'entretien : Questions suggérées</h5>
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div v-for="(q, idx) in aiAnalysis.questions_entretien_suggerees" :key="idx" class="flex items-center gap-6 p-6 bg-slate-50 dark:bg-slate-900/30 border border-slate-100 dark:border-slate-800 rounded-3xl group hover:bg-white dark:hover:bg-slate-800 hover:shadow-xl hover:border-indigo-300 transition-all duration-300">
|
||||
<div class="w-12 h-12 shrink-0 rounded-[1.25rem] bg-white dark:bg-slate-800 flex items-center justify-center text-lg font-black text-indigo-500 shadow-md group-hover:bg-indigo-600 group-hover:text-white transition-all">
|
||||
{{ idx + 1 }}
|
||||
</div>
|
||||
<p class="text-xl font-bold text-slate-700 dark:text-slate-200 leading-snug">{{ q }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="!isAnalyzing" class="py-24 border-4 border-dashed border-slate-100 dark:border-slate-800 rounded-[3rem] text-center bg-slate-50/50 dark:bg-slate-900/20">
|
||||
<div class="w-20 h-20 bg-white dark:bg-slate-800 rounded-3xl flex items-center justify-center mx-auto mb-6 shadow-lg">
|
||||
@@ -834,8 +905,157 @@ const runAI = async () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Notes Section (Full Width) -->
|
||||
<div class="xl:col-span-3 space-y-8">
|
||||
|
||||
<!-- Tab Content: Interview -->
|
||||
<div v-if="activeTab === 'interview'" class="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<!-- Interview Questions & Interactive Evaluation -->
|
||||
<div v-if="notesForm.interview_details.questions?.length > 0" class="space-y-10">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<h5 class="text-sm font-black uppercase tracking-widest text-[#004f82] border-l-4 border-[#004f82] pl-4">Évaluation de l'entretien</h5>
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="px-5 py-2 bg-[#004f82]/5 dark:bg-[#004f82]/30 rounded-2xl border border-[#004f82]/20 dark:border-[#004f82]/50 hidden sm:block">
|
||||
<span class="text-[10px] font-black uppercase text-[#004f82]/80 block tracking-[0.2em]">Score Questions</span>
|
||||
<span class="text-xl font-black text-[#004f82]">{{ (notesForm.interview_details.questions || []).reduce((acc, q) => acc + (parseFloat(q.score) || 0), 0) }} / 20</span>
|
||||
</div>
|
||||
<div class="px-5 py-2 bg-[#e0b04c]/10 dark:bg-[#e0b04c]/30 rounded-2xl border border-[#e0b04c]/30 dark:border-[#e0b04c]/50">
|
||||
<span class="text-[10px] font-black uppercase text-[#8b6508] block tracking-[0.2em]">Total Entretien</span>
|
||||
<span class="text-xl font-black text-[#8b6508]">{{ calculatedInterviewScore }} / 30</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<div v-for="(q, idx) in notesForm.interview_details.questions" :key="idx" class="p-8 bg-neutral/30 dark:bg-slate-900/40 border border-anthracite/5 dark:border-slate-800 rounded-[2.5rem] group hover:bg-white dark:hover:bg-slate-800 hover:shadow-2xl hover:border-primary/20 transition-all duration-500">
|
||||
<div class="flex items-start gap-6 mb-8 text-left">
|
||||
<div class="w-12 h-12 shrink-0 rounded-2xl bg-white dark:bg-slate-700 shadow-lg flex items-center justify-center text-primary group-hover:bg-primary group-hover:text-white transition-all duration-300">
|
||||
<span class="text-xl font-black">{{ idx + 1 }}</span>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<input
|
||||
v-if="!candidate.interview_details"
|
||||
v-model="q.question"
|
||||
class="w-full bg-transparent border-none p-0 text-2xl font-black text-anthracite dark:text-slate-100 tracking-tight leading-snug focus:ring-0 placeholder:text-slate-300"
|
||||
placeholder="Saisissez votre question personnalisée..."
|
||||
/>
|
||||
<p v-else class="text-2xl font-black text-anthracite dark:text-slate-100 tracking-tight leading-snug">{{ q.question }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
|
||||
<div class="lg:col-span-3">
|
||||
<label class="text-[10px] font-black uppercase tracking-[0.2em] text-anthracite/50 mb-4 block">Note Qualité / 4</label>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
v-for="score in [0, 1, 2, 3, 4]"
|
||||
:key="score"
|
||||
@click="q.score = score"
|
||||
type="button"
|
||||
class="w-full h-12 rounded-xl border-2 font-black transition-all"
|
||||
:class="[
|
||||
q.score === score
|
||||
? 'bg-primary border-primary text-white shadow-lg shadow-primary/30 dark:shadow-none'
|
||||
: 'bg-white dark:bg-slate-900 border-anthracite/10 dark:border-slate-800 text-anthracite/50 hover:border-primary/50'
|
||||
]"
|
||||
>
|
||||
{{ score }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lg:col-span-9">
|
||||
<label class="text-[10px] font-black uppercase tracking-[0.2em] text-anthracite/50 mb-4 block">Commentaires & Détails de l'échange</label>
|
||||
<textarea
|
||||
v-model="q.comment"
|
||||
rows="2"
|
||||
class="w-full bg-white dark:bg-slate-900/50 border-2 border-transparent focus:border-primary/30 focus:ring-0 rounded-3xl p-6 text-base font-medium transition-all placeholder:text-slate-300"
|
||||
:placeholder="'Analyse de la réponse pour la question ' + (idx + 1) + '...'"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Soft Skills Grid -->
|
||||
<div class="space-y-10 mt-16 pt-10 border-t-2 border-dashed border-anthracite/10 dark:border-slate-800">
|
||||
<div class="flex items-center justify-between gap-4 mb-8">
|
||||
<h5 class="text-sm font-black uppercase tracking-widest text-[#004f82] border-l-4 border-[#004f82] pl-4">Évaluation des Soft Skills</h5>
|
||||
<div class="px-5 py-2 bg-[#004f82]/5 dark:bg-[#004f82]/30 rounded-2xl border border-[#004f82]/20 dark:border-[#004f82]/50">
|
||||
<span class="text-[10px] font-black uppercase text-[#004f82]/70 block tracking-[0.2em]">Moyenne Soft Skills</span>
|
||||
<span class="text-xl font-black text-[#004f82]">{{ (notesForm.interview_details.soft_skills.reduce((acc, s) => acc + (parseFloat(s.score) || 0), 0) / Math.max(1, notesForm.interview_details.soft_skills.length)).toFixed(1) }} / 10</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div v-for="(skill, idx) in notesForm.interview_details.soft_skills" :key="idx" class="p-6 bg-neutral/30 dark:bg-slate-900/40 border border-anthracite/5 dark:border-slate-800 rounded-[2rem] hover:shadow-xl hover:border-primary/20 transition-all duration-300">
|
||||
<div class="flex justify-between items-center mb-5">
|
||||
<span class="font-bold text-anthracite dark:text-slate-200 flex-1">{{ skill.name }}</span>
|
||||
<span class="text-sm font-black px-3 py-1 rounded-xl"
|
||||
:class="skill.score >= 8 ? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30' : skill.score >= 5 ? 'bg-amber-100 text-amber-700 dark:bg-amber-900/30' : skill.score > 0 ? 'bg-red-100 text-red-700 dark:bg-red-900/30' : 'bg-anthracite/10 text-anthracite/60 dark:bg-slate-800'">
|
||||
{{ skill.score }} / 10
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-1.5 h-10 w-full group/slider">
|
||||
<button
|
||||
v-for="val in 10"
|
||||
:key="val"
|
||||
@click="skill.score = val"
|
||||
type="button"
|
||||
class="flex-1 rounded-lg transition-all border border-transparent hover:scale-110 relative"
|
||||
:class="[
|
||||
skill.score >= val
|
||||
? (skill.score >= 8 ? 'bg-[#004f82]' : skill.score >= 5 ? 'bg-[#e0b04c]' : 'bg-[#a30000]')
|
||||
: 'bg-anthracite/10 dark:bg-slate-800 hover:bg-anthracite/20 dark:hover:bg-slate-700'
|
||||
]"
|
||||
>
|
||||
<!-- Affichage du chiffre au survol sur PC -->
|
||||
<span class="absolute inset-0 flex items-center justify-center text-xs font-black opacity-0 group-hover/slider:opacity-100 hover:!opacity-100"
|
||||
:class="skill.score >= val ? 'text-white/80' : 'text-anthracite/50'">
|
||||
{{ val }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overall Appreciation -->
|
||||
<div class="relative p-10 bg-primary/5 dark:bg-primary/20 border-4 border-dashed border-primary/20 dark:border-primary/30 rounded-[3rem] mt-16 group hover:border-primary/40 transition-all duration-500">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-12 items-center">
|
||||
<div class="md:col-span-1 text-center md:text-left">
|
||||
<label class="text-[10px] font-black uppercase tracking-[0.2em] text-primary/70 mb-6 block">Note Appréciation / 10</label>
|
||||
<div class="relative inline-flex items-center">
|
||||
<input
|
||||
type="number"
|
||||
v-model="notesForm.interview_details.appreciation"
|
||||
min="0" max="10" step="0.5"
|
||||
class="w-40 bg-white dark:bg-slate-800 border-none rounded-[2rem] p-6 font-black text-4xl text-primary text-center shadow-2xl focus:ring-4 focus:ring-primary/10 transition-all"
|
||||
/>
|
||||
<span class="ml-4 text-2xl font-black text-anthracite/40">/ 10</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:col-span-3">
|
||||
<div class="flex flex-col md:flex-row items-center justify-end gap-10">
|
||||
<div class="text-right">
|
||||
<span class="text-[10px] font-black uppercase tracking-[0.2em] text-anthracite/60 block mb-2">Pondération Totale Entretien</span>
|
||||
<div class="text-6xl font-black text-highlight tracking-tighter">
|
||||
{{ calculatedInterviewScore }}<span class="text-2xl text-anthracite/30 font-normal ml-3">/ 30</span>
|
||||
</div>
|
||||
</div>
|
||||
<PrimaryButton
|
||||
@click="saveNotes"
|
||||
:disabled="notesForm.processing"
|
||||
class="!px-12 !py-6 !rounded-[2rem] shadow-2xl transition-all hover:-translate-y-1 active:scale-95 flex flex-col items-center gap-1 group"
|
||||
>
|
||||
<span class="text-lg font-black tracking-tight">Enregistrer l'évaluation</span>
|
||||
<span class="text-[10px] font-black uppercase tracking-widest opacity-60">Calcul des scores & profil radar</span>
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes Section (Full Width) -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700 p-8">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h4 class="text-xl font-bold flex items-center gap-2">
|
||||
@@ -913,7 +1133,10 @@ const runAI = async () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- Fin Interview Tab -->
|
||||
|
||||
<!-- Tab Content: Tests -->
|
||||
<div v-if="activeTab === 'tests'" class="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<!-- Historique des Tests (Full Width) -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700 p-8">
|
||||
<h3 class="text-xl font-bold mb-8 flex items-center justify-between">
|
||||
@@ -1027,8 +1250,84 @@ const runAI = async () => {
|
||||
Ce candidat n'a pas encore terminé de test.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- Fin Tests Tab -->
|
||||
|
||||
<!-- Tab Content: Documents -->
|
||||
<div v-if="activeTab === 'documents'" class="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700 p-8">
|
||||
<h4 class="font-bold mb-6 flex items-center gap-2 text-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-[#004f82]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
|
||||
</svg>
|
||||
Gestion des Documents
|
||||
</h4>
|
||||
|
||||
<div v-if="candidate.documents?.length === 0" class="py-8 text-center text-slate-400 italic bg-neutral/10 rounded-xl border border-dashed border-anthracite/10">
|
||||
Aucun document disponible pour ce candidat.
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<button
|
||||
v-for="doc in candidate.documents"
|
||||
:key="doc.id"
|
||||
@click="openPreview(doc)"
|
||||
class="flex items-center justify-between p-5 bg-neutral/20 border border-anthracite/5 rounded-2xl hover:bg-neutral/50 transition-colors group text-left"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="p-3 bg-white rounded-xl shadow-sm group-hover:bg-primary group-hover:text-white transition-colors">
|
||||
<svg v-if="doc.type === 'cv'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-black uppercase tracking-tight text-primary">{{ doc.type === 'cv' ? 'Curriculum Vitae' : 'Lettre de Motivation' }}</div>
|
||||
<div class="text-[11px] text-slate-500 font-medium mt-1 truncate">{{ doc.original_name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-slate-400 group-hover:translate-x-1 group-hover:text-primary transition-all" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="pt-8 border-t border-anthracite/5">
|
||||
<h5 class="text-sm font-black uppercase text-anthracite/60 tracking-widest mb-6">Ajouter ou Remplacer les documents</h5>
|
||||
<form @submit.prevent="updateDocuments" class="space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="relative group/file">
|
||||
<label class="flex flex-col items-center justify-center p-6 bg-neutral/20 border-2 border-dashed border-anthracite/10 rounded-2xl cursor-pointer hover:border-primary hover:bg-neutral/40 transition-all">
|
||||
<div class="mb-3 p-2 bg-white rounded-lg shadow-sm group-hover/file:bg-primary group-hover/file:text-white transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-slate-400 group-hover/file:text-white transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" /></svg>
|
||||
</div>
|
||||
<span class="text-xs font-black uppercase tracking-tight text-primary mb-1">Nouveau CV (PDF)</span>
|
||||
<span class="text-[10px] text-slate-400 truncate w-full text-center">{{ docForm.cv ? docForm.cv.name : 'Cliquer pour parcourir...' }}</span>
|
||||
<input type="file" class="hidden" @input="docForm.cv = $event.target.files[0]" accept="application/pdf" />
|
||||
</label>
|
||||
<InputError :message="docForm.errors.cv" class="mt-2" />
|
||||
</div>
|
||||
<div class="relative group/file">
|
||||
<label class="flex flex-col items-center justify-center p-6 bg-neutral/20 border-2 border-dashed border-anthracite/10 rounded-2xl cursor-pointer hover:border-accent hover:bg-neutral/40 transition-all">
|
||||
<div class="mb-3 p-2 bg-white rounded-lg shadow-sm group-hover/file:bg-accent group-hover/file:text-white transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-slate-400 group-hover/file:text-white transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" /></svg>
|
||||
</div>
|
||||
<span class="text-xs font-black uppercase tracking-tight text-accent mb-1">Nouvelle Lettre (PDF)</span>
|
||||
<span class="text-[10px] text-slate-400 truncate w-full text-center">{{ docForm.cover_letter ? docForm.cover_letter.name : 'Cliquer pour parcourir...' }}</span>
|
||||
<input type="file" class="hidden" @input="docForm.cover_letter = $event.target.files[0]" accept="application/pdf" />
|
||||
</label>
|
||||
<InputError :message="docForm.errors.cover_letter" class="mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
<PrimaryButton class="!px-8 py-3 text-sm" :disabled="docForm.processing || (!docForm.cv && !docForm.cover_letter)">
|
||||
Enregistrer les nouveaux documents
|
||||
</PrimaryButton>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- Fin Documents Tab Content (1241) -->
|
||||
|
||||
|
||||
<!-- Document Preview Modal -->
|
||||
<Modal :show="!!selectedDocument" @close="selectedDocument = null" max-width="4xl">
|
||||
@@ -1048,6 +1347,68 @@ const runAI = async () => {
|
||||
</Modal>
|
||||
</AdminLayout>
|
||||
|
||||
<!-- Edit Details Modal -->
|
||||
<Modal :show="showEditDetailsModal" @close="showEditDetailsModal = false">
|
||||
<div class="p-6">
|
||||
<h2 class="text-lg font-black uppercase tracking-tight text-slate-900 dark:text-white mb-6">Modifier les informations</h2>
|
||||
|
||||
<form @submit.prevent="updateDetails" class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-[10px] font-black uppercase tracking-widest text-slate-400 mb-1">Nom complet</label>
|
||||
<input
|
||||
v-model="detailsForm.name"
|
||||
type="text"
|
||||
class="w-full bg-slate-50 dark:bg-slate-900 border-slate-200 dark:border-slate-700 rounded-xl focus:ring-indigo-500/20 focus:border-indigo-500 transition-all font-bold text-sm"
|
||||
placeholder="Ex: Jean Dupont"
|
||||
/>
|
||||
<InputError :message="detailsForm.errors.name" class="mt-1" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] font-black uppercase tracking-widest text-slate-400 mb-1">Email</label>
|
||||
<input
|
||||
v-model="detailsForm.email"
|
||||
type="email"
|
||||
class="w-full bg-slate-50 dark:bg-slate-900 border-slate-200 dark:border-slate-700 rounded-xl focus:ring-indigo-500/20 focus:border-indigo-500 transition-all font-bold text-sm"
|
||||
placeholder="Email candidat"
|
||||
/>
|
||||
<InputError :message="detailsForm.errors.email" class="mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-[10px] font-black uppercase tracking-widest text-slate-400 mb-1">Téléphone</label>
|
||||
<input
|
||||
v-model="detailsForm.phone"
|
||||
type="text"
|
||||
class="w-full bg-slate-50 dark:bg-slate-900 border-slate-200 dark:border-slate-700 rounded-xl focus:ring-indigo-500/20 focus:border-indigo-500 transition-all font-bold text-sm"
|
||||
placeholder="Ex: 06 12 34 56 78"
|
||||
/>
|
||||
<InputError :message="detailsForm.errors.phone" class="mt-1" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] font-black uppercase tracking-widest text-slate-400 mb-1">URL LinkedIn</label>
|
||||
<input
|
||||
v-model="detailsForm.linkedin_url"
|
||||
type="url"
|
||||
class="w-full bg-slate-50 dark:bg-slate-900 border-slate-200 dark:border-slate-700 rounded-xl focus:ring-indigo-500/20 focus:border-indigo-500 transition-all font-bold text-sm"
|
||||
placeholder="https://linkedin.com/in/..."
|
||||
/>
|
||||
<InputError :message="detailsForm.errors.linkedin_url" class="mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
<SecondaryButton @click="showEditDetailsModal = false" type="button">Annuler</SecondaryButton>
|
||||
<PrimaryButton :class="{ 'opacity-25': detailsForm.processing }" :disabled="detailsForm.processing">
|
||||
Enregistrer les modifications
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<!-- Error Modal -->
|
||||
<Modal :show="showErrorModal" @close="showErrorModal = false" maxWidth="md">
|
||||
<div class="p-6">
|
||||
|
||||
@@ -31,69 +31,77 @@ const submit = () => {
|
||||
|
||||
<template>
|
||||
<GuestLayout>
|
||||
<Head title="Log in" />
|
||||
<Head title="Connexion" />
|
||||
|
||||
<div v-if="status" class="mb-4 text-sm font-medium text-green-600">
|
||||
<div v-if="status" class="mb-4 text-sm font-medium text-emerald-600 bg-emerald-50 p-3 rounded-lg">
|
||||
{{ status }}
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div class="mb-8 text-center space-y-1">
|
||||
<h2 class="text-2xl font-serif font-black text-primary">Bon retour !</h2>
|
||||
<p class="text-anthracite/60 text-sm">Veuillez entrer vos identifiants.</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit" class="space-y-5">
|
||||
<div>
|
||||
<InputLabel for="email" value="Email" />
|
||||
<InputLabel for="email" value="Adresse Email" class="!font-subtitle !text-xs !uppercase !tracking-widest !text-anthracite/60 !mb-1" />
|
||||
|
||||
<TextInput
|
||||
id="email"
|
||||
type="email"
|
||||
class="mt-1 block w-full"
|
||||
class="mt-1 block w-full !rounded-xl !border-anthracite/10 focus:!border-primary focus:!ring-primary/20 shadow-sm transition-colors text-sm"
|
||||
v-model="form.email"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="username"
|
||||
placeholder="prenom.nom@exemple.com"
|
||||
/>
|
||||
|
||||
<InputError class="mt-2" :message="form.errors.email" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<InputLabel for="password" value="Password" />
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<InputLabel for="password" value="Mot de passe" class="!font-subtitle !text-xs !uppercase !tracking-widest !text-anthracite/60 !mb-0" />
|
||||
|
||||
<Link
|
||||
v-if="canResetPassword"
|
||||
:href="route('password.request')"
|
||||
class="text-[10px] font-bold text-accent hover:text-accent/80 transition-colors uppercase tracking-wider"
|
||||
>
|
||||
Oublié ?
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<TextInput
|
||||
id="password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
class="mt-1 block w-full !rounded-xl !border-anthracite/10 focus:!border-primary focus:!ring-primary/20 shadow-sm transition-colors text-sm"
|
||||
v-model="form.password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
|
||||
<InputError class="mt-2" :message="form.errors.password" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4 block">
|
||||
<label class="flex items-center">
|
||||
<Checkbox name="remember" v-model:checked="form.remember" />
|
||||
<span class="ms-2 text-sm text-gray-600"
|
||||
>Remember me</span
|
||||
>
|
||||
<div class="block pt-2">
|
||||
<label class="flex items-center group cursor-pointer w-max">
|
||||
<Checkbox name="remember" v-model:checked="form.remember" class="!rounded !border-anthracite/20 text-primary focus:ring-primary shadow-sm group-hover:border-primary transition-colors" />
|
||||
<span class="ms-2 text-sm text-anthracite/60 group-hover:text-anthracite transition-colors">Rester connecté</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-end">
|
||||
<Link
|
||||
v-if="canResetPassword"
|
||||
:href="route('password.request')"
|
||||
class="rounded-md text-sm text-gray-600 underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
>
|
||||
Forgot your password?
|
||||
</Link>
|
||||
|
||||
<PrimaryButton
|
||||
class="ms-4"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
<div class="pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': form.processing }"
|
||||
:disabled="form.processing"
|
||||
class="w-full flex justify-center py-3.5 px-4 bg-highlight text-[#3a2800] rounded-xl font-subtitle font-bold shadow-md shadow-highlight/20 hover:brightness-110 hover:-translate-y-0.5 hover:shadow-lg hover:shadow-highlight/30 transition-all text-sm uppercase tracking-widest focus:outline-none focus:ring-2 focus:ring-highlight/50 focus:ring-offset-2"
|
||||
>
|
||||
Log in
|
||||
</PrimaryButton>
|
||||
Se connecter
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</GuestLayout>
|
||||
|
||||
@@ -44,88 +44,109 @@ const getStatusColor = (status) => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="isAdmin" class="p-8 space-y-8">
|
||||
<div v-if="isAdmin" class="space-y-8 font-sans text-anthracite">
|
||||
<!-- KPI Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
|
||||
<div class="bg-white dark:bg-slate-800 p-6 rounded-3xl shadow-sm border border-slate-200 dark:border-slate-700 hover:shadow-xl transition-all duration-300">
|
||||
<div class="text-slate-500 dark:text-slate-400 text-[10px] font-black uppercase tracking-widest">Total Candidats</div>
|
||||
<div class="text-4xl font-black mt-2 text-indigo-600 dark:text-indigo-400">{{ stats.total_candidates }}</div>
|
||||
<!-- Total Candidats -->
|
||||
<div class="bg-white p-6 rounded-3xl shadow-sm border border-anthracite/5 hover:-translate-y-1 hover:shadow-xl hover:shadow-primary/5 transition-all duration-300 relative overflow-hidden group">
|
||||
<div class="text-[10px] font-subtitle font-black uppercase tracking-widest text-anthracite/40">Total Candidats</div>
|
||||
<div class="text-4xl font-black mt-3 text-primary">{{ stats.total_candidates }}</div>
|
||||
<div class="absolute bottom-0 right-0 w-24 h-24 bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-primary/10 to-transparent"></div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-slate-800 p-6 rounded-3xl shadow-sm border border-slate-200 dark:border-slate-700 hover:shadow-xl transition-all duration-300 relative overflow-hidden group">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-amber-50 to-orange-50 dark:from-amber-900/20 dark:to-orange-900/20 opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
|
||||
<!-- Candidats Retenus -->
|
||||
<div class="bg-white p-6 rounded-3xl shadow-sm border border-anthracite/5 hover:-translate-y-1 hover:shadow-xl hover:shadow-highlight/20 transition-all duration-300 relative overflow-hidden group">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-highlight/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="text-amber-600 dark:text-amber-500 text-[10px] font-black uppercase tracking-widest flex items-center gap-1.5">
|
||||
<div class="text-highlight text-[10px] font-subtitle font-black uppercase tracking-widest flex items-center gap-1.5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
Retenus
|
||||
</div>
|
||||
<div class="text-4xl font-black mt-2 text-amber-600 dark:text-amber-400">{{ stats.selected_candidates }}</div>
|
||||
<div class="text-4xl font-black mt-3 text-highlight drop-shadow-sm">{{ stats.selected_candidates }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-slate-800 p-6 rounded-3xl shadow-sm border border-slate-200 dark:border-slate-700 hover:shadow-xl transition-all duration-300">
|
||||
<div class="text-slate-500 dark:text-slate-400 text-[10px] font-black uppercase tracking-widest">Tests terminés</div>
|
||||
<div class="text-4xl font-black mt-2 text-emerald-600 dark:text-emerald-400">{{ stats.finished_tests }}</div>
|
||||
|
||||
<!-- Tests terminés -->
|
||||
<div class="bg-white p-6 rounded-3xl shadow-sm border border-anthracite/5 hover:-translate-y-1 hover:shadow-xl hover:shadow-emerald-500/10 transition-all duration-300 relative overflow-hidden group">
|
||||
<div class="text-[10px] font-subtitle font-black uppercase tracking-widest text-anthracite/40">Tests terminés</div>
|
||||
<div class="text-4xl font-black mt-3 text-emerald-500">{{ stats.finished_tests }}</div>
|
||||
<div class="absolute bottom-0 right-0 w-24 h-24 bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-emerald-500/10 to-transparent"></div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-slate-800 p-6 rounded-3xl shadow-sm border border-slate-200 dark:border-slate-700 hover:shadow-xl transition-all duration-300">
|
||||
<div class="text-slate-500 dark:text-slate-400 text-[10px] font-black uppercase tracking-widest">Moyenne Générale</div>
|
||||
<div class="text-4xl font-black mt-2 text-blue-600 dark:text-blue-400">{{ stats.average_score }} / 20</div>
|
||||
|
||||
<!-- Moyenne Générale -->
|
||||
<div class="bg-white p-6 rounded-3xl shadow-sm border border-anthracite/5 hover:-translate-y-1 hover:shadow-xl hover:shadow-sky/10 transition-all duration-300 relative overflow-hidden group">
|
||||
<div class="text-[10px] font-subtitle font-black uppercase tracking-widest text-anthracite/40">Moyenne Générale</div>
|
||||
<div class="text-4xl font-black mt-3 text-sky">{{ stats.average_score }} <span class="text-lg opacity-50 font-bold">/ 20</span></div>
|
||||
<div class="absolute bottom-0 right-0 w-24 h-24 bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-sky/10 to-transparent"></div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-slate-800 p-6 rounded-3xl shadow-sm border border-slate-200 dark:border-slate-700 hover:shadow-xl transition-all duration-300">
|
||||
<div class="text-slate-500 dark:text-slate-400 text-[10px] font-black uppercase tracking-widest">Meilleur Score</div>
|
||||
<div class="text-4xl font-black mt-2 text-purple-600 dark:text-purple-400">{{ stats.best_score }} / 20</div>
|
||||
|
||||
<!-- Meilleur Score -->
|
||||
<div class="bg-white p-6 rounded-3xl shadow-sm border border-anthracite/5 hover:-translate-y-1 hover:shadow-xl hover:shadow-accent/10 transition-all duration-300 relative overflow-hidden group">
|
||||
<div class="text-[10px] font-subtitle font-black uppercase tracking-widest text-anthracite/40">Meilleur Score</div>
|
||||
<div class="text-4xl font-black mt-3 text-accent">{{ stats.best_score }} <span class="text-lg opacity-50 font-bold">/ 20</span></div>
|
||||
<div class="absolute bottom-0 right-0 w-24 h-24 bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-accent/10 to-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Candidates Table -->
|
||||
<div class="bg-white dark:bg-slate-800 shadow-sm border border-slate-200 dark:border-slate-700 rounded-3xl overflow-hidden">
|
||||
<div class="px-8 py-6 border-b border-slate-100 dark:border-slate-700 flex justify-between items-center bg-slate-50/50 dark:bg-slate-900/50">
|
||||
<h3 class="text-xl font-black uppercase tracking-tight">Top 10 Candidats</h3>
|
||||
<Link :href="route('admin.candidates.index')" class="text-xs font-bold uppercase tracking-widest text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 transition-colors">
|
||||
Voir tous les candidats →
|
||||
<div class="bg-white shadow-sm border border-anthracite/5 rounded-3xl overflow-hidden mt-8">
|
||||
<div class="px-8 py-6 border-b border-anthracite/5 flex justify-between items-center bg-sand/30">
|
||||
<h3 class="text-xl font-serif font-black text-primary capitalize tracking-tight flex items-center gap-3">
|
||||
<div class="w-1.5 h-6 bg-highlight rounded-full hidden md:block"></div>
|
||||
Top 10 Candidats
|
||||
</h3>
|
||||
<Link :href="route('admin.candidates.index')" class="text-xs font-subtitle font-bold uppercase tracking-widest text-primary hover:text-highlight transition-colors flex items-center gap-1">
|
||||
Voir tous <span class="hidden sm:inline">les candidats</span> →
|
||||
</Link>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-slate-50/50 dark:bg-slate-900/30">
|
||||
<th class="px-8 py-4 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400">Candidat</th>
|
||||
<th class="px-8 py-4 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400">Score Pondéré</th>
|
||||
<th class="px-8 py-4 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400">Adéquation IA</th>
|
||||
<th class="px-8 py-4 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400">Statut</th>
|
||||
<th class="px-8 py-4 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 text-right">Actions</th>
|
||||
<tr class="bg-neutral/50">
|
||||
<th class="px-8 py-4 text-[10px] font-subtitle font-black uppercase tracking-[0.2em] text-anthracite/40">Candidat</th>
|
||||
<th class="px-8 py-4 text-[10px] font-subtitle font-black uppercase tracking-[0.2em] text-anthracite/40">Score Pondéré</th>
|
||||
<th class="px-8 py-4 text-[10px] font-subtitle font-black uppercase tracking-[0.2em] text-anthracite/40">Adéquation IA</th>
|
||||
<th class="px-8 py-4 text-[10px] font-subtitle font-black uppercase tracking-[0.2em] text-anthracite/40">Statut</th>
|
||||
<th class="px-8 py-4 text-[10px] font-subtitle font-black uppercase tracking-[0.2em] text-anthracite/40 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100 dark:divide-slate-800">
|
||||
<tr v-for="candidate in top_candidates" :key="candidate.id" class="hover:bg-slate-50 dark:hover:bg-slate-900/50 transition-colors group">
|
||||
<tbody class="divide-y divide-anthracite/5">
|
||||
<tr v-for="candidate in top_candidates" :key="candidate.id" class="hover:bg-sand/30 transition-colors group">
|
||||
<td class="px-8 py-5">
|
||||
<div class="font-bold text-slate-900 dark:text-slate-100 group-hover:text-indigo-600 transition-colors">{{ candidate.name }}</div>
|
||||
<div class="text-xs text-slate-500 dark:text-slate-400">{{ candidate.email }}</div>
|
||||
<div class="font-bold text-primary group-hover:text-highlight transition-colors block">{{ candidate.name }}</div>
|
||||
<div class="text-xs text-anthracite/50 font-subtitle tracking-wide mt-0.5">{{ candidate.email }}</div>
|
||||
</td>
|
||||
<td class="px-8 py-5">
|
||||
<div class="inline-flex items-center gap-2 px-4 py-1.5 bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 rounded-full font-black text-sm border border-indigo-100 dark:border-indigo-800">
|
||||
{{ candidate.weighted_score }} / 20
|
||||
<div class="inline-flex items-center gap-2 px-4 py-1.5 bg-primary/5 text-primary rounded-xl font-black text-sm border border-primary/10 shadow-sm">
|
||||
{{ candidate.weighted_score }} <span class="opacity-50 text-xs">/ 20</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-8 py-5">
|
||||
<div v-if="candidate.ai_analysis" class="flex items-center gap-2">
|
||||
<div
|
||||
class="px-2 py-0.5 rounded text-[10px] font-black"
|
||||
class="px-3 py-1 rounded-lg text-xs font-black shadow-sm"
|
||||
:class="[
|
||||
candidate.ai_analysis.match_score >= 80 ? 'bg-emerald-100 text-emerald-700' :
|
||||
candidate.ai_analysis.match_score >= 60 ? 'bg-amber-100 text-amber-700' :
|
||||
'bg-red-100 text-red-700'
|
||||
candidate.ai_analysis.match_score >= 80 ? 'bg-emerald-50 text-emerald-700 border border-emerald-200' :
|
||||
candidate.ai_analysis.match_score >= 60 ? 'bg-highlight/10 text-[#3a2800] border border-highlight/30' :
|
||||
'bg-accent/10 text-accent border border-accent/20'
|
||||
]"
|
||||
>
|
||||
{{ candidate.ai_analysis.match_score }}%
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="text-[10px] text-slate-300 italic font-medium">Non analysé</span>
|
||||
<span v-else class="text-[10px] uppercase tracking-widest text-anthracite/30 italic font-bold">Non analysé</span>
|
||||
</td>
|
||||
<td class="px-8 py-5">
|
||||
<span
|
||||
class="px-3 py-1 text-[10px] font-black uppercase tracking-widest rounded-full"
|
||||
:class="getStatusColor(candidate.status)"
|
||||
class="px-3 py-1 text-[10px] font-black uppercase tracking-[0.15em] rounded-full"
|
||||
:class="{
|
||||
'bg-anthracite/5 text-anthracite/60 border border-anthracite/10': candidate.status === 'en_attente',
|
||||
'bg-sky/10 text-sky border border-sky/20': candidate.status === 'en_cours',
|
||||
'bg-emerald-50 text-emerald-700 border border-emerald-200': candidate.status === 'termine',
|
||||
'bg-accent/10 text-accent border border-accent/20': candidate.status === 'refuse'
|
||||
}"
|
||||
>
|
||||
{{ candidate.status }}
|
||||
</span>
|
||||
@@ -133,19 +154,20 @@ const getStatusColor = (status) => {
|
||||
<td class="px-8 py-5 text-right">
|
||||
<Link
|
||||
:href="route('admin.candidates.show', candidate.id)"
|
||||
class="inline-flex items-center justify-center p-2 text-slate-400 hover:text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/30 rounded-xl transition-all"
|
||||
class="inline-flex items-center justify-center p-2 text-primary/40 hover:text-highlight hover:bg-highlight/10 rounded-xl transition-all"
|
||||
title="Détails"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||
</svg>
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="top_candidates.length === 0">
|
||||
<td colspan="4" class="px-8 py-12 text-center text-slate-400 italic font-medium">
|
||||
Aucun candidat pour le moment.
|
||||
<td colspan="5" class="px-8 py-16 text-center">
|
||||
<div class="text-anthracite/40 italic font-medium font-subtitle">
|
||||
Aucun candidat pour le moment.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -154,19 +176,19 @@ const getStatusColor = (status) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Candidate Dashboard: LIGHT ONLY, high contrast, no dark: classes -->
|
||||
<div v-else style="background: linear-gradient(135deg, #f8faff 0%, #eef2ff 100%); min-height: calc(100vh - 4rem);" class="flex flex-col items-center justify-center px-4 py-16">
|
||||
<!-- Candidate Dashboard: LIGHT ONLY, matched with new graphic charter -->
|
||||
<div v-else class="flex flex-col items-center justify-center px-4 py-16 bg-neutral min-h-[calc(100vh-4rem)] font-sans text-anthracite selection:bg-highlight selection:text-anthracite">
|
||||
<div class="w-full max-w-4xl">
|
||||
|
||||
<!-- Welcome Section -->
|
||||
<div class="mb-12 text-center">
|
||||
<div class="inline-flex items-center gap-2 px-4 py-1.5 rounded-full text-[10px] font-black uppercase tracking-widest mb-6 border" style="background:#eef2ff; color:#4f46e5; border-color:#c7d2fe;">
|
||||
<div class="inline-flex items-center gap-2 px-5 py-2 rounded-full text-xs font-subtitle font-bold uppercase tracking-widest mb-6 bg-primary/10 text-primary border border-primary/20">
|
||||
✦ Espace Candidat
|
||||
</div>
|
||||
<h3 class="font-black mb-5 tracking-tight" style="font-size: clamp(2rem, 5vw, 3.5rem); color: #1e1b4b; line-height: 1.1;">
|
||||
Bienvenue, <span style="color:#4f46e5;">{{ user.name }}</span> !
|
||||
<h3 class="text-4xl md:text-5xl font-serif font-black mb-5 tracking-tight text-primary leading-tight">
|
||||
Bienvenue, <span class="text-accent">{{ user.name }}</span> !
|
||||
</h3>
|
||||
<p style="color:#6b7280; font-size:1.1rem; max-width:40rem; margin:0 auto; line-height:1.7;">
|
||||
<p class="text-anthracite/70 text-lg max-w-2xl mx-auto leading-relaxed">
|
||||
Voici les tests techniques préparés pour votre candidature. Installez-vous confortablement avant de commencer.
|
||||
</p>
|
||||
</div>
|
||||
@@ -176,34 +198,31 @@ const getStatusColor = (status) => {
|
||||
<div
|
||||
v-for="quiz in quizzes"
|
||||
:key="quiz.id"
|
||||
class="group"
|
||||
style="background: white; border-radius: 2rem; padding: 2.5rem; box-shadow: 0 4px 24px rgba(79,70,229,0.08); border: 1.5px solid #e0e7ff; transition: all 0.4s ease; position: relative; overflow: hidden;"
|
||||
@mouseenter="$event.currentTarget.style.borderColor='#6366f1'; $event.currentTarget.style.boxShadow='0 12px 40px rgba(79,70,229,0.15)'; $event.currentTarget.style.transform='translateY(-4px)'"
|
||||
@mouseleave="$event.currentTarget.style.borderColor='#e0e7ff'; $event.currentTarget.style.boxShadow='0 4px 24px rgba(79,70,229,0.08)'; $event.currentTarget.style.transform='translateY(0)'"
|
||||
class="group bg-white rounded-3xl p-8 shadow-sm border-b-4 border-transparent hover:border-highlight hover:-translate-y-2 hover:shadow-xl hover:shadow-highlight/10 transition-all duration-300 relative overflow-hidden"
|
||||
>
|
||||
<!-- Decorative blob -->
|
||||
<div style="position:absolute; top:-2rem; right:-2rem; width:8rem; height:8rem; background:radial-gradient(circle, #818cf820 0%, transparent 70%); border-radius:50%;"></div>
|
||||
<div class="absolute -top-8 -right-8 w-32 h-32 bg-[radial-gradient(circle,_#1a4b8c20_0%,_transparent_70%)] rounded-full"></div>
|
||||
|
||||
<!-- Icon badge -->
|
||||
<div style="display:inline-flex; padding:0.75rem; background:#eef2ff; border-radius:1rem; margin-bottom:1.5rem;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="width:1.75rem;height:1.75rem;color:#4f46e5;stroke:#4f46e5;" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<div class="inline-flex p-3 bg-sky/15 rounded-xl mb-6">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-7 h-7 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h4 style="font-size:1.25rem; font-weight:800; color:#1e1b4b; margin-bottom:0.75rem; line-height:1.3;">{{ quiz.title }}</h4>
|
||||
<p style="color:#6b7280; font-size:0.875rem; line-height:1.6; margin-bottom:2rem; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">
|
||||
<h4 class="text-xl font-subtitle font-bold text-primary mb-3 leading-tight">{{ quiz.title }}</h4>
|
||||
<p class="text-anthracite/70 text-sm leading-relaxed mb-8 line-clamp-2">
|
||||
{{ quiz.description }}
|
||||
</p>
|
||||
|
||||
<div style="border-top:1.5px solid #f1f5f9; padding-top:1.5rem; display:flex; align-items:center; justify-content:space-between; gap:1rem;">
|
||||
<div class="pt-6 border-t border-anthracite/10 flex items-center justify-between gap-4 relative z-10">
|
||||
<div>
|
||||
<div style="font-size:0.65rem; font-weight:900; text-transform:uppercase; letter-spacing:0.1em; color:#9ca3af; margin-bottom:0.2rem;">Durée</div>
|
||||
<div style="font-size:0.95rem; font-weight:800; color:#374151;">{{ quiz.duration_minutes }} min</div>
|
||||
<div class="text-[10px] font-black uppercase tracking-[0.1em] text-anthracite/40 mb-1">Durée</div>
|
||||
<div class="text-base font-bold text-anthracite">{{ quiz.duration_minutes }} min</div>
|
||||
</div>
|
||||
|
||||
<div v-if="quiz.has_finished_attempt" style="display:flex; align-items:center; gap:0.5rem; background:#ecfdf5; color:#059669; font-weight:800; font-size:0.75rem; text-transform:uppercase; letter-spacing:0.08em; padding:0.625rem 1.25rem; border-radius:0.75rem; border:1.5px solid #a7f3d0;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="width:1rem;height:1rem;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
|
||||
<div v-if="quiz.has_finished_attempt" class="flex items-center gap-2 bg-[#ecfdf5] text-[#059669] font-bold text-xs uppercase tracking-wider px-5 py-2.5 rounded-xl border-2 border-[#a7f3d0]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Terminé
|
||||
@@ -211,9 +230,7 @@ const getStatusColor = (status) => {
|
||||
<Link
|
||||
v-else
|
||||
:href="route('quizzes.take', quiz.id)"
|
||||
style="display:inline-flex; align-items:center; justify-content:center; padding:0.75rem 2rem; background:#4f46e5; color:white; border-radius:0.875rem; font-weight:800; font-size:0.875rem; text-decoration:none; box-shadow:0 4px 14px rgba(79,70,229,0.35); transition:all 0.2s ease; white-space:nowrap;"
|
||||
@mouseenter="$event.currentTarget.style.background='#4338ca'; $event.currentTarget.style.transform='scale(0.98)'"
|
||||
@mouseleave="$event.currentTarget.style.background='#4f46e5'; $event.currentTarget.style.transform='scale(1)'"
|
||||
class="inline-flex items-center justify-center px-8 py-3 bg-highlight text-[#3a2800] rounded-xl font-subtitle font-bold text-sm shadow-md shadow-highlight/20 hover:brightness-110 hover:-translate-y-0.5 hover:shadow-lg hover:shadow-highlight/30 transition-all whitespace-nowrap"
|
||||
>
|
||||
Démarrer →
|
||||
</Link>
|
||||
@@ -222,21 +239,21 @@ const getStatusColor = (status) => {
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else style="text-align:center; padding:5rem 2rem; background:white; border-radius:2rem; box-shadow:0 4px 24px rgba(0,0,0,0.06); border:1.5px solid #e0e7ff;">
|
||||
<div style="display:inline-flex; padding:1.5rem; background:#fff7ed; border-radius:9999px; margin-bottom:1.5rem;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="width:3rem;height:3rem;stroke:#f97316;" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<div v-else class="text-center p-20 bg-white rounded-3xl shadow-sm border border-anthracite/5">
|
||||
<div class="inline-flex p-6 bg-accent/10 rounded-full mb-6">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-12 h-12 text-accent" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h4 style="font-size:1.5rem; font-weight:900; color:#1e1b4b; margin-bottom:0.75rem;">Aucun test assigné</h4>
|
||||
<p style="color:#6b7280; max-width:28rem; margin:0 auto; line-height:1.7; font-size:0.95rem;">
|
||||
<h4 class="text-2xl font-serif font-black text-primary mb-3">Aucun test assigné</h4>
|
||||
<p class="text-anthracite/70 max-w-lg mx-auto leading-relaxed text-sm">
|
||||
Votre dossier est en cours de traitement. Un administrateur vous assignera bientôt vos tests techniques.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div style="margin-top:3rem; text-align:center;">
|
||||
<p style="font-size:0.65rem; font-weight:900; text-transform:uppercase; letter-spacing:0.15em; color:#d1d5db;">RecruitQuizz Platform • v{{ $page.props.app_version }}</p>
|
||||
<div class="mt-12 text-center text-primary/50 text-[10px] font-subtitle font-bold uppercase tracking-widest">
|
||||
© {{ new Date().getFullYear() }} — Communauté d'Agglomération Béziers Méditerranée
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -2,184 +2,185 @@
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
|
||||
defineProps({
|
||||
canLogin: Boolean,
|
||||
canRegister: Boolean,
|
||||
canLogin: {
|
||||
type: Boolean,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Bienvenue sur RecruitQuizz" />
|
||||
<Head title="Recru IT" />
|
||||
|
||||
<div class="min-h-screen bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100 selection:bg-indigo-500 selection:text-white font-sans overflow-x-hidden">
|
||||
<div class="min-h-screen bg-neutral text-anthracite font-sans overflow-x-hidden selection:bg-highlight selection:text-anthracite">
|
||||
|
||||
<!-- Animated Background Blobs -->
|
||||
<div class="fixed inset-0 pointer-events-none overflow-hidden">
|
||||
<div class="absolute -top-[10%] -left-[10%] w-[40%] h-[40%] bg-indigo-500/10 dark:bg-indigo-500/5 rounded-full blur-[120px] animate-pulse"></div>
|
||||
<div class="absolute top-[20%] -right-[10%] w-[35%] h-[35%] bg-purple-500/10 dark:bg-purple-500/5 rounded-full blur-[120px] animate-pulse" style="animation-delay: 2s;"></div>
|
||||
<div class="absolute -bottom-[10%] left-[20%] w-[30%] h-[30%] bg-emerald-500/10 dark:bg-emerald-500/5 rounded-full blur-[120px] animate-pulse" style="animation-delay: 4s;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="relative z-50 flex items-center justify-between px-6 py-8 md:px-12 max-w-7xl mx-auto">
|
||||
<div class="flex items-center gap-2 group cursor-default">
|
||||
<div class="w-10 h-10 bg-indigo-600 rounded-xl flex items-center justify-center shadow-lg shadow-indigo-600/20 group-hover:scale-110 transition-transform duration-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<div class="flex items-center gap-3 group cursor-default">
|
||||
<div class="w-12 h-12 bg-primary rounded-lg flex items-center justify-center shadow-lg shadow-primary/30 group-hover:scale-105 transition-transform duration-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9l-.707.707M12 18v3m4.95-4.95l.707.707M12 3c-4.418 0-8 3.582-8 8 0 2.209.895 4.209 2.343 5.657L12 21l5.657-5.343A7.994 7.994 0 0020 11c0-4.418-3.582-8-8-8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-2xl font-black tracking-tighter uppercase italic text-slate-900 dark:text-white">RECRU<span class="text-indigo-600">IT</span></span>
|
||||
<span class="text-3xl font-serif font-bold text-primary">RECRU<span class="text-accent italic px-1">IT</span></span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<template v-if="$page.props.auth.user">
|
||||
<Link :href="route('dashboard')" class="px-6 py-2.5 bg-slate-900 dark:bg-white text-white dark:text-slate-900 rounded-full font-bold text-sm hover:scale-105 transition-all shadow-xl shadow-slate-900/10 dark:shadow-none">
|
||||
Aller au Dashboard
|
||||
<Link :href="route('dashboard')" class="px-8 py-3 bg-highlight text-[#3a2800] rounded-lg font-subtitle font-bold text-sm hover:brightness-110 hover:shadow-xl hover:shadow-highlight/20 transition-all duration-300">
|
||||
Accéder au Tableau de bord
|
||||
</Link>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Link :href="route('login')" class="text-slate-600 dark:text-slate-400 font-bold text-sm hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors hidden md:block px-4">
|
||||
<Link :href="route('login')" class="px-8 py-3 bg-highlight text-[#3a2800] rounded-lg font-subtitle font-bold text-sm hover:brightness-110 hover:-translate-y-0.5 hover:shadow-xl hover:shadow-highlight/20 transition-all duration-300">
|
||||
Connexion
|
||||
</Link>
|
||||
<Link
|
||||
v-if="canRegister"
|
||||
:href="route('register')"
|
||||
class="px-8 py-3 bg-indigo-600 text-white rounded-full font-bold text-sm hover:bg-indigo-700 hover:scale-105 hover:shadow-2xl hover:shadow-indigo-600/30 transition-all duration-300"
|
||||
>
|
||||
Créer un compte
|
||||
</Link>
|
||||
</template>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<main class="relative z-10 max-w-7xl mx-auto px-6 pt-20 pb-32 md:px-12 md:pt-32">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-16 items-center">
|
||||
<!-- Fond Institutionnel Hero Section -->
|
||||
<main class="relative z-10 w-full mt-4">
|
||||
<div class="max-w-[95%] mx-auto bg-primary rounded-[2.5rem] overflow-hidden shadow-2xl relative px-8 py-20 pb-32 md:px-16 md:py-32">
|
||||
|
||||
<!-- Hero Content -->
|
||||
<div class="lg:col-span-7 space-y-10">
|
||||
<div class="inline-flex items-center gap-2 px-4 py-2 bg-indigo-50 dark:bg-indigo-900/20 border border-indigo-100 dark:border-indigo-800 rounded-full text-indigo-600 dark:text-indigo-400 text-xs font-black uppercase tracking-widest animate-bounce">
|
||||
<span class="relative flex h-2 w-2">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-indigo-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-indigo-500"></span>
|
||||
</span>
|
||||
Tests de recrutements
|
||||
<!-- Graphic Elements -->
|
||||
<div class="absolute top-0 right-0 w-[50%] h-full bg-gradient-to-l from-sky/40 to-transparent pointer-events-none"></div>
|
||||
<div class="absolute -bottom-24 -left-24 w-96 h-96 bg-accent/20 rounded-full blur-[100px] pointer-events-none"></div>
|
||||
|
||||
<div class="relative z-10 grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
|
||||
|
||||
<!-- Hero Content -->
|
||||
<div class="space-y-10 text-white">
|
||||
<div class="inline-flex items-center gap-3 px-5 py-2.5 bg-white/10 backdrop-blur-md border border-white/20 rounded-full text-white text-sm font-subtitle font-bold uppercase tracking-widest shadow-inner">
|
||||
<span class="relative flex h-3 w-3">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-highlight opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-3 w-3 bg-highlight"></span>
|
||||
</span>
|
||||
Évaluation des candidats
|
||||
</div>
|
||||
|
||||
<h1 class="text-5xl md:text-7xl font-serif leading-[1.1] text-white">
|
||||
Découvrez le potentiel de vos <span class="text-highlight">futures équipes</span>.
|
||||
</h1>
|
||||
|
||||
<p class="text-lg md:text-xl text-sand font-sans font-light max-w-xl leading-relaxed">
|
||||
Recru.IT simplifie le processus d'évaluation.
|
||||
Générez des tests sur-mesure pour chaque poste et accédez à une analyse de compétences claire, précise et équitable.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 pt-4">
|
||||
<Link
|
||||
v-if="!$page.props.auth.user"
|
||||
:href="route('login')"
|
||||
class="inline-flex items-center justify-center px-10 py-4 bg-highlight text-[#3a2800] rounded-xl font-subtitle font-bold uppercase tracking-wider text-sm hover:brightness-110 hover:-translate-y-1 hover:shadow-2xl hover:shadow-highlight/40 transition-all duration-300"
|
||||
>
|
||||
S'identifier
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right illustration (abstract or UI mockup) -->
|
||||
<div class="hidden lg:flex justify-end relative h-full">
|
||||
<div class="w-full max-w-md bg-sand rounded-3xl p-8 shadow-2xl relative transform rotate-2 hover:rotate-0 transition-transform duration-500 border border-white/10">
|
||||
<!-- Fake dashboard element -->
|
||||
<div class="space-y-6">
|
||||
<div class="flex justify-between items-center border-b border-anthracite/10 pb-4">
|
||||
<div class="h-6 w-32 bg-primary/20 rounded"></div>
|
||||
<div class="h-6 w-12 bg-accent/20 rounded-full"></div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="h-24 bg-white rounded-xl shadow-sm border border-anthracite/5 p-4 flex flex-col justify-between">
|
||||
<div class="h-3 w-20 bg-neutral rounded"></div>
|
||||
<div class="h-8 w-16 bg-primary/10 rounded"></div>
|
||||
</div>
|
||||
<div class="h-24 bg-white rounded-xl shadow-sm border border-anthracite/5 p-4 flex flex-col justify-between">
|
||||
<div class="h-3 w-24 bg-neutral rounded"></div>
|
||||
<div class="h-8 w-full bg-highlight/20 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-32 bg-white rounded-xl shadow-sm border border-anthracite/5 p-4 flex gap-4">
|
||||
<div class="w-16 h-16 bg-sky/20 rounded-full shrink-0"></div>
|
||||
<div class="flex-1 space-y-3 py-2">
|
||||
<div class="h-3 w-[60%] bg-anthracite/20 rounded"></div>
|
||||
<div class="h-2 w-[80%] bg-neutral rounded"></div>
|
||||
<div class="h-2 w-[40%] bg-neutral rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Floating badge -->
|
||||
<div class="absolute -left-12 bottom-12 bg-white p-5 rounded-2xl shadow-xl flex items-center gap-4 border border-sand/50 animate-bounce" style="animation-duration: 3s">
|
||||
<div class="w-12 h-12 bg-accent rounded-full flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-anthracite/50 font-subtitle font-bold uppercase tracking-wider">Candidat</p>
|
||||
<p class="text-anthracite font-bold">Approuvé</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="text-6xl md:text-8xl font-black tracking-tight leading-[0.9] text-slate-900 dark:text-white">
|
||||
Evualuation <br>
|
||||
<span class="text-transparent bg-clip-text bg-gradient-to-r from-indigo-600 to-purple-600">des candidats.</span>
|
||||
</h1>
|
||||
|
||||
<p class="text-xl text-slate-600 dark:text-slate-400 max-w-xl leading-relaxed">
|
||||
Recru.IT transforme le processus de sélection technique. Testez les candidats avec des parcours personnalisés et des évaluations précises en quelques minutes.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row items-center gap-6">
|
||||
<Link
|
||||
:href="route('login')"
|
||||
class="group relative w-full sm:w-auto px-10 py-5 bg-slate-900 dark:bg-white text-white dark:text-slate-900 rounded-3xl font-black uppercase tracking-widest text-sm text-center overflow-hidden hover:scale-105 transition-all duration-300"
|
||||
>
|
||||
<span class="relative z-10">Démarrer maintenant</span>
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-indigo-600 to-purple-600 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
</Link>
|
||||
|
||||
<div class="flex -space-x-4">
|
||||
<img class="w-12 h-12 rounded-full border-4 border-white dark:border-slate-950 shadow-xl" src="https://i.pravatar.cc/150?u=1" alt="User 1">
|
||||
<img class="w-12 h-12 rounded-full border-4 border-white dark:border-slate-950 shadow-xl" src="https://i.pravatar.cc/150?u=2" alt="User 2">
|
||||
<img class="w-12 h-12 rounded-full border-4 border-white dark:border-slate-950 shadow-xl" src="https://i.pravatar.cc/150?u=3" alt="User 3">
|
||||
<div class="w-12 h-12 rounded-full border-4 border-white dark:border-slate-950 bg-indigo-600 flex items-center justify-center text-white text-xs font-bold shadow-xl">
|
||||
:)
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs font-bold text-slate-400"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hero Illustration / Mockup -->
|
||||
<div class="lg:col-span-5 relative hidden lg:block">
|
||||
<div class="absolute -inset-4 bg-gradient-to-tr from-indigo-600 to-purple-600 rounded-[4rem] blur-3xl opacity-20 animate-pulse"></div>
|
||||
<div class="relative bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-[3rem] shadow-2xl overflow-hidden aspect-[4/5] p-2">
|
||||
<div class="bg-slate-50 dark:bg-slate-950 rounded-[2.5rem] h-full w-full p-8 border border-slate-100 dark:border-slate-800 flex flex-col justify-center gap-12 text-center">
|
||||
<div class="w-24 h-24 bg-indigo-600/10 rounded-3xl mx-auto flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div class="text-3xl font-black uppercase tracking-tighter">Félicitations !</div>
|
||||
<p class="text-slate-500 text-sm">Votre score est de 95%. <br> Vous êtes prêt pour la suite.</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="h-4 w-full bg-slate-100 dark:bg-slate-800 rounded-full overflow-hidden">
|
||||
<div class="h-full w-[95%] bg-indigo-600 rounded-full shadow-lg shadow-indigo-600/30"></div>
|
||||
</div>
|
||||
<div class="flex justify-between text-[10px] font-black uppercase text-slate-400">
|
||||
<span>Rang S+</span>
|
||||
<span>Recruté</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Features Grid -->
|
||||
<section class="relative z-10 bg-white dark:bg-slate-900 border-y border-slate-200 dark:border-slate-800 px-6 py-24 md:px-12">
|
||||
<!-- Features -->
|
||||
<section class="relative z-10 bg-neutral px-6 py-24 md:px-12 -mt-16 pt-32">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="text-center mb-20 space-y-4">
|
||||
<h2 class="text-4xl font-black uppercase tracking-tight text-slate-900 dark:text-white">Conçu pour les recruteurs</h2>
|
||||
<p class="text-slate-500 dark:text-slate-400 max-w-xl mx-auto">Une plateforme intuitive pour automatiser vos entretiens techniques et valoriser le potentiel de chaque candidat.</p>
|
||||
<div class="text-center mb-16 space-y-4">
|
||||
<h2 class="text-3xl md:text-5xl font-serif text-primary">Un processus optimisé</h2>
|
||||
<p class="text-anthracite/70 font-sans max-w-2xl mx-auto text-lg pt-2">Pensé pour offrir la meilleure expérience d'évaluation technique aux communautés d'agglomération et leurs candidats.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<!-- Feature 1 -->
|
||||
<div class="p-10 bg-slate-50 dark:bg-slate-950 rounded-[2.5rem] border border-slate-100 dark:border-slate-800 hover:border-indigo-500 transition-colors group">
|
||||
<div class="w-14 h-14 bg-indigo-600/10 rounded-2xl flex items-center justify-center mb-8 group-hover:scale-110 transition-transform">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<div class="p-8 bg-white rounded-2xl shadow-md shadow-anthracite/5 border-b-4 border-primary hover:-translate-y-2 transition-transform duration-300">
|
||||
<div class="w-14 h-14 bg-sky/10 rounded-xl flex items-center justify-center mb-6">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold mb-4">Quiz Dynamiques</h3>
|
||||
<p class="text-slate-500 dark:text-slate-400 text-sm leading-relaxed">Questions à choix multiples ou réponses ouvertes, adaptez vos tests au poste visé en quelques clics.</p>
|
||||
<h3 class="text-xl font-subtitle font-bold text-primary mb-3">Quiz Dynamiques</h3>
|
||||
<p class="text-anthracite/70 font-sans text-sm leading-relaxed">Une génération intelligente de questions basées sur l'Intelligence Artificielle pour cibler les attentes du poste.</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature 2 -->
|
||||
<div class="p-10 bg-slate-50 dark:bg-slate-950 rounded-[2.5rem] border border-slate-100 dark:border-slate-800 hover:border-indigo-500 transition-colors group">
|
||||
<div class="w-14 h-14 bg-emerald-600/10 rounded-2xl flex items-center justify-center mb-8 group-hover:scale-110 transition-transform">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<div class="p-8 bg-white rounded-2xl shadow-md shadow-anthracite/5 border-b-4 border-highlight hover:-translate-y-2 transition-transform duration-300">
|
||||
<div class="w-14 h-14 bg-highlight/10 rounded-xl flex items-center justify-center mb-6">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 text-highlight" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold mb-4">Audit & Sécurité</h3>
|
||||
<p class="text-slate-500 dark:text-slate-400 text-sm leading-relaxed">Chaque action critique est journalisée pour une transparence totale sur vos recrutements.</p>
|
||||
<h3 class="text-xl font-subtitle font-bold text-anthracite mb-3">Sécurisé & Traçable</h3>
|
||||
<p class="text-anthracite/70 font-sans text-sm leading-relaxed">Respect de l'intégrité des compétences et de la RGPD, sans biais lors de l'analyse des profils.</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature 3 -->
|
||||
<div class="p-10 bg-slate-50 dark:bg-slate-950 rounded-[2.5rem] border border-slate-100 dark:border-slate-800 hover:border-indigo-500 transition-colors group">
|
||||
<div class="w-14 h-14 bg-purple-600/10 rounded-2xl flex items-center justify-center mb-8 group-hover:scale-110 transition-transform">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<div class="p-8 bg-white rounded-2xl shadow-md shadow-anthracite/5 border-b-4 border-accent hover:-translate-y-2 transition-transform duration-300">
|
||||
<div class="w-14 h-14 bg-accent/10 rounded-xl flex items-center justify-center mb-6">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 text-accent" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold mb-4">Mobile First</h3>
|
||||
<p class="text-slate-500 dark:text-slate-400 text-sm leading-relaxed">Les candidats passent leurs tests sur mobile ou desktop avec un confort inégalé.</p>
|
||||
<h3 class="text-xl font-subtitle font-bold text-accent mb-3">Expérience fluide</h3>
|
||||
<p class="text-anthracite/70 font-sans text-sm leading-relaxed">Une interface candidate repensée pour valoriser la marque employeur et simplifier la passation d'examens.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="relative z-10 px-6 py-20 text-center text-slate-400 text-xs font-black uppercase tracking-[0.2em]">
|
||||
© 2026 RecruitQuizz — Advanced Recruitment Intelligence
|
||||
<footer class="relative bg-sand px-6 py-12 text-center border-t border-anthracite/10">
|
||||
<p class="text-primary font-subtitle font-bold text-xs uppercase tracking-[0.1em]">
|
||||
© {{ new Date().getFullYear() }} — Communauté d'Agglomération Béziers Méditerranée — Tous droits réservés
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;700;900&display=swap');
|
||||
|
||||
.font-sans {
|
||||
font-family: 'Outfit', sans-serif;
|
||||
}
|
||||
/* Import custom typographies */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Merriweather:ital,wght@0,400;0,700;1,400;1,700&family=Nunito:ital,wght@0,400;0,600;0,700;1,400&display=swap');
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user