feat: implementation du role Gestionnaire RH et refonte de la gestion des offres
This commit is contained in:
@@ -74,7 +74,9 @@ const getNestedValue = (obj, path) => {
|
||||
return path.split('.').reduce((o, i) => (o ? o[i] : null), obj);
|
||||
};
|
||||
|
||||
const selectedJobPosition = ref('');
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const initialJobPositionParam = urlParams.get('job_position');
|
||||
const selectedJobPosition = ref(initialJobPositionParam === 'none' ? 'none' : (initialJobPositionParam ? parseInt(initialJobPositionParam) : ''));
|
||||
const showOnlySelected = ref(false);
|
||||
|
||||
const filteredCandidates = computed(() => {
|
||||
@@ -181,7 +183,7 @@ const batchAnalyze = async () => {
|
||||
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">
|
||||
<div v-if="$page.props.auth.user.role !== 'gestionnaire_rh'" 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-highlight/50 text-highlight focus:ring-highlight/20 cursor-pointer">
|
||||
<span class="text-xs font-bold text-primary uppercase tracking-widest">Retenus uniquement</span>
|
||||
@@ -202,7 +204,7 @@ const batchAnalyze = async () => {
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<div v-if="selectedIds.length > 0 && $page.props.auth.user.role !== 'gestionnaire_rh'" class="flex items-center gap-3 animate-in fade-in slide-in-from-right-4 duration-300">
|
||||
<span class="text-xs font-black uppercase tracking-widest text-primary/50">{{ selectedIds.length }} sélectionné(s)</span>
|
||||
<PrimaryButton
|
||||
@click="batchAnalyze"
|
||||
@@ -246,7 +248,7 @@ const batchAnalyze = async () => {
|
||||
<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">
|
||||
<th v-if="$page.props.auth.user.role !== 'gestionnaire_rh'" class="w-12 px-8 py-5">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="selectedIds.length === sortedCandidates.length && sortedCandidates.length > 0"
|
||||
@@ -254,7 +256,7 @@ const batchAnalyze = async () => {
|
||||
class="rounded border-anthracite/20 text-primary focus:ring-primary/20 cursor-pointer"
|
||||
>
|
||||
</th>
|
||||
<th class="w-12 px-4 py-5"></th>
|
||||
<th v-if="$page.props.auth.user.role !== 'gestionnaire_rh'" 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
|
||||
@@ -291,13 +293,13 @@ const batchAnalyze = async () => {
|
||||
<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">
|
||||
<th v-if="$page.props.auth.user.role !== 'gestionnaire_rh'" @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">
|
||||
<th v-if="$page.props.auth.user.role !== 'gestionnaire_rh'" @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>
|
||||
@@ -309,7 +311,7 @@ const batchAnalyze = async () => {
|
||||
</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">
|
||||
<td v-if="$page.props.auth.user.role !== 'gestionnaire_rh'" class="px-8 py-5">
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="candidate.id"
|
||||
@@ -317,7 +319,7 @@ const batchAnalyze = async () => {
|
||||
class="rounded border-anthracite/20 text-primary focus:ring-primary/20 cursor-pointer"
|
||||
>
|
||||
</td>
|
||||
<td class="px-4 py-5 text-center">
|
||||
<td v-if="$page.props.auth.user.role !== 'gestionnaire_rh'" 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" />
|
||||
@@ -358,12 +360,12 @@ const batchAnalyze = async () => {
|
||||
{{ candidate.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-8 py-5">
|
||||
<td v-if="$page.props.auth.user.role !== 'gestionnaire_rh'" 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>
|
||||
</td>
|
||||
<td class="px-8 py-5">
|
||||
<td v-if="$page.props.auth.user.role !== 'gestionnaire_rh'" 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"
|
||||
@@ -458,7 +460,7 @@ const batchAnalyze = async () => {
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-if="$page.props.auth.user.role === 'super_admin'">
|
||||
<div v-if="$page.props.auth.user.role === 'super_admin' || $page.props.auth.user.role === 'gestionnaire_rh'">
|
||||
<InputLabel for="tenant_id" value="Structure de rattachement" />
|
||||
<select id="tenant_id" v-model="form.tenant_id" class="mt-1 block w-full rounded-md border-slate-300 dark:border-slate-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-slate-900">
|
||||
<option value="">Aucune</option>
|
||||
|
||||
@@ -26,11 +26,20 @@ const positionForm = useForm({ job_position_id: props.candidate.job_position_id
|
||||
|
||||
const showEditDetailsModal = ref(false);
|
||||
const detailsForm = useForm({
|
||||
name: props.candidate.user.name,
|
||||
birth_name: props.candidate.birth_name || '',
|
||||
usage_name: props.candidate.usage_name || '',
|
||||
first_name: props.candidate.first_name || '',
|
||||
address: props.candidate.address || '',
|
||||
zip_code: props.candidate.zip_code || '',
|
||||
email: props.candidate.user.email,
|
||||
phone: props.candidate.phone || '',
|
||||
linkedin_url: props.candidate.linkedin_url || '',
|
||||
city: props.candidate.city || '',
|
||||
birth_date: props.candidate.birth_date || '',
|
||||
birth_place: props.candidate.birth_place || '',
|
||||
nationality: props.candidate.nationality || '',
|
||||
current_situation: props.candidate.current_situation || '',
|
||||
education_level: props.candidate.education_level || '',
|
||||
has_driving_license: props.candidate.has_driving_license ? 1 : 0,
|
||||
});
|
||||
const updateDetails = () => {
|
||||
detailsForm.put(route('admin.candidates.update', props.candidate.id), {
|
||||
@@ -264,7 +273,7 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
|
||||
<div class="h-16 bg-primary relative rounded-t-2xl overflow-hidden">
|
||||
<div class="absolute inset-0 opacity-10" style="background: radial-gradient(circle at top right, #f5a800, transparent 70%)"></div>
|
||||
<!-- Selection star -->
|
||||
<button @click="toggleSelection" :title="candidate.is_selected ? 'Retirer la sélection' : 'Retenir ce candidat'"
|
||||
<button v-if="$page.props.auth.user.role !== 'gestionnaire_rh'" @click="toggleSelection" :title="candidate.is_selected ? 'Retirer la sélection' : 'Retenir ce candidat'"
|
||||
class="absolute top-3 right-3 p-1.5 rounded-lg transition-all"
|
||||
:class="candidate.is_selected ? 'text-highlight bg-highlight/20' : 'text-white/30 hover:text-highlight hover:bg-white/10'">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
|
||||
@@ -318,7 +327,7 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
|
||||
Exporter
|
||||
</a>
|
||||
</div>
|
||||
<button @click="toggleSelection"
|
||||
<button v-if="$page.props.auth.user.role !== 'gestionnaire_rh'" @click="toggleSelection"
|
||||
:class="['mt-2 w-full flex items-center justify-center gap-2 py-2.5 rounded-[10px] border-none text-xs font-extrabold uppercase tracking-[0.08em] transition-all duration-150',
|
||||
candidate.is_selected
|
||||
? 'bg-highlight/15 text-highlight border border-highlight/30 hover:bg-highlight/25'
|
||||
@@ -330,7 +339,7 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
|
||||
</div>
|
||||
|
||||
<!-- Score global card -->
|
||||
<div class="bg-primary rounded-2xl p-5 relative overflow-hidden">
|
||||
<div v-if="$page.props.auth.user.role !== 'gestionnaire_rh'" class="bg-primary rounded-2xl p-5 relative overflow-hidden">
|
||||
<div class="absolute inset-0 opacity-10" style="background: radial-gradient(circle at bottom right, #f5a800, transparent 60%)"></div>
|
||||
<div class="relative z-10">
|
||||
<p class="text-[9px] font-black uppercase tracking-[0.18em] text-white/40 mb-2">Score Global Pondéré</p>
|
||||
@@ -373,7 +382,7 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
|
||||
</div>
|
||||
|
||||
<!-- AI Summary card (if analysed) -->
|
||||
<div v-if="aiAnalysis" class="bg-surface rounded-2xl border border-ink/[0.07] shadow-sm p-5">
|
||||
<div v-if="aiAnalysis && $page.props.auth.user.role !== 'gestionnaire_rh'" class="bg-surface rounded-2xl border border-ink/[0.07] shadow-sm p-5">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-[9px] font-black uppercase tracking-[0.16em] text-ink/35">Analyse IA</span>
|
||||
<div :class="['w-9 h-9 rounded-full flex items-center justify-center text-[11px] font-black',
|
||||
@@ -395,6 +404,16 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Structure -->
|
||||
<div v-if="tenants" class="bg-surface rounded-2xl border border-ink/[0.07] shadow-sm p-5">
|
||||
<p class="text-[9px] font-black uppercase tracking-[0.16em] text-ink/35 mb-3">Structure de rattachement</p>
|
||||
<select v-model="tenantForm.tenant_id" @change="updateTenant"
|
||||
class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none transition-all">
|
||||
<option value="">— Aucune structure —</option>
|
||||
<option v-for="t in tenants" :key="t.id" :value="t.id">{{ t.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Danger zone -->
|
||||
<div class="bg-surface rounded-2xl border border-ink/[0.07] shadow-sm p-5">
|
||||
<p class="text-[9px] font-black uppercase tracking-[0.16em] text-ink/35 mb-3">Actions</p>
|
||||
@@ -427,7 +446,13 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
|
||||
{ id:'documents', label:'Documents', count: candidate.documents?.length },
|
||||
{ id:'tests', label:'Tests', count: candidate.attempts?.length },
|
||||
{ id:'security', label:'Sécurité', count: candidate.user.security_alerts?.length },
|
||||
].filter(t => t.id !== 'security' || t.count > 0)" :key="tab.id" @click="activeTab = tab.id"
|
||||
].filter(t => {
|
||||
if (t.id === 'security' && t.count === 0) return false;
|
||||
if ($page.props.auth.user.role === 'gestionnaire_rh') {
|
||||
return !['ai_analysis', 'interview', 'tests', 'security'].includes(t.id);
|
||||
}
|
||||
return true;
|
||||
})" :key="tab.id" @click="activeTab = tab.id"
|
||||
class="relative flex items-center gap-2 px-5 py-4 text-[11px] font-black uppercase tracking-[0.1em] whitespace-nowrap transition-all duration-150"
|
||||
:class="activeTab === tab.id ? 'text-primary' : 'text-ink/35 hover:text-ink/60'">
|
||||
{{ tab.label }}
|
||||
@@ -440,7 +465,7 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
|
||||
<div v-if="activeTab === 'overview'" class="p-6 space-y-6">
|
||||
|
||||
<!-- Save scores button -->
|
||||
<div v-if="scoreForm.isDirty" class="flex justify-end">
|
||||
<div v-if="scoreForm.isDirty && $page.props.auth.user.role !== 'gestionnaire_rh'" class="flex justify-end">
|
||||
<button @click="saveScores"
|
||||
class="flex items-center gap-2 px-5 py-2.5 rounded-[10px] bg-highlight text-highlight-dark text-xs font-extrabold uppercase tracking-[0.08em] shadow-gold hover:brightness-105 transition-all">
|
||||
<svg class="w-3.5 h-3.5 animate-pulse" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
|
||||
@@ -449,7 +474,7 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
|
||||
</div>
|
||||
|
||||
<!-- Score inputs grid -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<div v-if="$page.props.auth.user.role !== 'gestionnaire_rh'" class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<div v-for="(item, i) in [
|
||||
{ label:'Analyse CV', key:'cv_score', max:20, color:'text-primary' },
|
||||
{ label:'Lettre Motiv.', key:'motivation_score', max:10, color:'text-success' },
|
||||
@@ -471,8 +496,62 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Informations détaillées -->
|
||||
<div class="p-5 rounded-2xl border border-ink/[0.07] bg-neutral/30 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-[9px] font-black uppercase tracking-[0.16em] text-ink/35">Identité & Situation</p>
|
||||
<button @click="showEditDetailsModal = true" class="text-[10px] font-bold text-primary hover:underline uppercase tracking-widest">Modifier</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-ink/30 uppercase mb-1">Nom de naissance</p>
|
||||
<p class="text-sm font-bold text-ink">{{ candidate.birth_name || '—' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-ink/30 uppercase mb-1">Nom d'usage</p>
|
||||
<p class="text-sm font-bold text-ink">{{ candidate.usage_name || '—' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-ink/30 uppercase mb-1">Prénom</p>
|
||||
<p class="text-sm font-bold text-ink">{{ candidate.first_name || '—' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-ink/30 uppercase mb-1">Nationalité</p>
|
||||
<p class="text-sm font-bold text-ink">{{ candidate.nationality || '—' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-6 pt-2">
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-ink/30 uppercase mb-1">Né(e) le</p>
|
||||
<p class="text-sm font-bold text-ink">{{ candidate.birth_date ? new Date(candidate.birth_date).toLocaleDateString('fr-FR') : '—' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-ink/30 uppercase mb-1">Lieu de naissance</p>
|
||||
<p class="text-sm font-bold text-ink">{{ candidate.birth_place || '—' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-ink/30 uppercase mb-1">Adresse</p>
|
||||
<p class="text-sm font-bold text-ink leading-tight">{{ candidate.address || '—' }}<br/>{{ candidate.zip_code }} {{ candidate.city }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-ink/30 uppercase mb-1">Permis de conduire</p>
|
||||
<p class="text-sm font-bold" :class="candidate.has_driving_license ? 'text-success' : 'text-accent'">{{ candidate.has_driving_license ? 'OUI' : 'NON' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-6 pt-2 border-t border-ink/5 mt-2">
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-ink/30 uppercase mb-1">Situation actuelle</p>
|
||||
<span class="inline-block px-2 py-1 bg-primary/10 text-primary text-[10px] font-black rounded uppercase tracking-wider">{{ candidate.current_situation || '—' }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-ink/30 uppercase mb-1">Niveau de diplôme</p>
|
||||
<span class="inline-block px-2 py-1 bg-highlight/20 text-highlight-dark text-[10px] font-black rounded uppercase tracking-wider">{{ candidate.education_level || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Radar chart -->
|
||||
<div class="grid md:grid-cols-2 gap-6 items-center">
|
||||
<div v-if="$page.props.auth.user.role !== 'gestionnaire_rh'" class="grid md:grid-cols-2 gap-6 items-center">
|
||||
<div class="flex items-center justify-center">
|
||||
<canvas ref="radarCanvasRef" class="max-h-64 w-full" />
|
||||
</div>
|
||||
@@ -844,22 +923,124 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
|
||||
</div><!-- end flex layout -->
|
||||
|
||||
<!-- ─── Modal: Éditer les infos ────────────────────────────────────── -->
|
||||
<Modal :show="showEditDetailsModal" @close="showEditDetailsModal = false" max-width="lg">
|
||||
<div class="p-6 space-y-5">
|
||||
<h3 class="font-serif font-black text-lg text-primary">Modifier les informations</h3>
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div v-for="(field, key) in { name:'Nom complet', email:'Email', phone:'Téléphone', city:'Ville', linkedin_url:'LinkedIn URL' }" :key="key">
|
||||
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">{{ field }}</label>
|
||||
<input v-model="detailsForm[key]" type="text"
|
||||
class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2.5 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
|
||||
<InputError :message="detailsForm.errors[key]" class="mt-1" />
|
||||
<Modal :show="showEditDetailsModal" @close="showEditDetailsModal = false" max-width="4xl">
|
||||
<div class="p-6 space-y-6">
|
||||
<div class="border-b border-ink/10 pb-3">
|
||||
<h3 class="font-serif font-black text-xl text-primary">Modifier le dossier du candidat</h3>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<!-- État Civil -->
|
||||
<div class="space-y-4 md:col-span-3">
|
||||
<p class="text-[10px] font-black uppercase tracking-widest text-primary/50">1. État Civil</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Nom de naissance</label>
|
||||
<input v-model="detailsForm.birth_name" type="text" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Nom d'usage</label>
|
||||
<input v-model="detailsForm.usage_name" type="text" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Prénom</label>
|
||||
<input v-model="detailsForm.first_name" type="text" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Date de naissance</label>
|
||||
<input v-model="detailsForm.birth_date" type="date" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Lieu de naissance</label>
|
||||
<input v-model="detailsForm.birth_place" type="text" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Nationalité</label>
|
||||
<input v-model="detailsForm.nationality" type="text" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Coordonnées -->
|
||||
<div class="space-y-4 md:col-span-3 pt-4 border-t border-ink/5">
|
||||
<p class="text-[10px] font-black uppercase tracking-widest text-primary/50">2. Coordonnées & Contact</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Email</label>
|
||||
<input v-model="detailsForm.email" type="email" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
|
||||
<InputError :message="detailsForm.errors.email" class="mt-1" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Téléphone</label>
|
||||
<input v-model="detailsForm.phone" type="text" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div class="md:col-span-2">
|
||||
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Adresse</label>
|
||||
<input v-model="detailsForm.address" type="text" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Code Postal</label>
|
||||
<input v-model="detailsForm.zip_code" type="text" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Ville</label>
|
||||
<input v-model="detailsForm.city" type="text" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Situation -->
|
||||
<div class="space-y-4 md:col-span-3 pt-4 border-t border-ink/5">
|
||||
<p class="text-[10px] font-black uppercase tracking-widest text-primary/50">3. Profil Professionnel</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Situation actuelle</label>
|
||||
<select v-model="detailsForm.current_situation" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none">
|
||||
<option value="Titulaire">Titulaire</option>
|
||||
<option value="Lauréat(e) d'un concours">Lauréat(e) d'un concours</option>
|
||||
<option value="contractuel">Contractuel</option>
|
||||
<option value="En recherche d'emplois">En recherche d'emplois</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Niveau de diplôme</label>
|
||||
<select v-model="detailsForm.education_level" class="w-full rounded-[10px] border border-ink/10 bg-neutral px-3 py-2 text-sm font-semibold text-ink focus:border-primary focus:ring-2 focus:ring-primary/15 outline-none">
|
||||
<option value="Aucun diplome">Aucun diplome</option>
|
||||
<option value="Brevet">Brevet</option>
|
||||
<option value="CAP/BEP">CAP/BEP</option>
|
||||
<option value="Bac">Bac</option>
|
||||
<option value="Bac + 1">Bac + 1</option>
|
||||
<option value="Bac + 2">Bac + 2</option>
|
||||
<option value="Bac + 3">Bac + 3</option>
|
||||
<option value="Bac + 4">Bac + 4</option>
|
||||
<option value="Bac + 5">Bac + 5</option>
|
||||
<option value="Autre">Autre</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[9px] font-black uppercase tracking-[0.14em] text-ink/35 mb-1.5 block">Permis de conduire</label>
|
||||
<div class="flex gap-4 h-9 items-center">
|
||||
<label class="flex items-center gap-2 text-xs font-bold text-ink/60 cursor-pointer">
|
||||
<input type="radio" v-model="detailsForm.has_driving_license" :value="1" class="text-primary focus:ring-primary/30" /> OUI
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-xs font-bold text-ink/60 cursor-pointer">
|
||||
<input type="radio" v-model="detailsForm.has_driving_license" :value="0" class="text-primary focus:ring-primary/30" /> NON
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3 pt-2">
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4 border-t border-ink/10">
|
||||
<SecondaryButton @click="showEditDetailsModal = false">Annuler</SecondaryButton>
|
||||
<button @click="updateDetails" :disabled="detailsForm.processing"
|
||||
class="px-5 py-2.5 rounded-[10px] bg-highlight text-highlight-dark text-xs font-extrabold uppercase tracking-[0.08em] shadow-gold hover:brightness-105 transition-all disabled:opacity-50">
|
||||
Enregistrer
|
||||
class="px-6 py-2.5 rounded-[10px] bg-highlight text-highlight-dark text-xs font-extrabold uppercase tracking-[0.08em] shadow-gold hover:brightness-105 transition-all disabled:opacity-50">
|
||||
{{ detailsForm.processing ? 'Enregistrement...' : 'Mettre à jour le dossier' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue';
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
import { ref } from 'vue';
|
||||
import { Head, useForm, Link } from '@inertiajs/vue3';
|
||||
import { ref, computed } from 'vue';
|
||||
import Modal from '@/Components/Modal.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
||||
@@ -14,6 +14,49 @@ const props = defineProps({
|
||||
quizzes: Array
|
||||
});
|
||||
|
||||
const viewMode = ref('grid');
|
||||
const sortKey = ref('created_at');
|
||||
const sortOrder = ref(-1); // -1 = desc, 1 = asc
|
||||
const filterStatus = ref('active'); // active, expired, all
|
||||
|
||||
const filteredAndSortedPositions = computed(() => {
|
||||
let result = [...props.jobPositions];
|
||||
const now = new Date();
|
||||
|
||||
// Filtering
|
||||
if (filterStatus.value === 'active') {
|
||||
result = result.filter(p => !p.expires_at || new Date(p.expires_at) >= now);
|
||||
} else if (filterStatus.value === 'expired') {
|
||||
result = result.filter(p => p.expires_at && new Date(p.expires_at) < now);
|
||||
}
|
||||
|
||||
// Sorting
|
||||
result.sort((a, b) => {
|
||||
let valA = a[sortKey.value] || '';
|
||||
let valB = b[sortKey.value] || '';
|
||||
|
||||
if (sortKey.value.includes('at')) {
|
||||
valA = valA ? new Date(valA).getTime() : 0;
|
||||
valB = valB ? new Date(valB).getTime() : 0;
|
||||
}
|
||||
|
||||
if (valA < valB) return -1 * sortOrder.value;
|
||||
if (valA > valB) return 1 * sortOrder.value;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const toggleSort = (key) => {
|
||||
if (sortKey.value === key) {
|
||||
sortOrder.value *= -1;
|
||||
} else {
|
||||
sortKey.value = key;
|
||||
sortOrder.value = -1;
|
||||
}
|
||||
};
|
||||
|
||||
const showingModal = ref(false);
|
||||
const editingPosition = ref(null);
|
||||
|
||||
@@ -26,6 +69,7 @@ const form = useForm({
|
||||
tenant_id: '',
|
||||
quiz_ids: [],
|
||||
fpt_metadata: null,
|
||||
expires_at: '',
|
||||
});
|
||||
|
||||
const isGeneratingFpt = ref(false);
|
||||
@@ -41,6 +85,7 @@ const openModal = (position = null) => {
|
||||
form.tenant_id = position.tenant_id || '';
|
||||
form.quiz_ids = position.quizzes ? position.quizzes.map(q => q.id) : [];
|
||||
form.fpt_metadata = position.fpt_metadata || null;
|
||||
form.expires_at = position.expires_at ? position.expires_at.split('T')[0] : '';
|
||||
} else {
|
||||
form.reset();
|
||||
}
|
||||
@@ -88,7 +133,7 @@ const submit = () => {
|
||||
};
|
||||
|
||||
const deletePosition = (id) => {
|
||||
if (confirm('Voulez-vous vraiment supprimer cette fiche de poste ?')) {
|
||||
if (confirm('Voulez-vous vraiment supprimer cette offre d\'emploi ?')) {
|
||||
form.delete(route('admin.job-positions.destroy', id));
|
||||
}
|
||||
};
|
||||
@@ -110,41 +155,125 @@ const copyLink = (position) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Fiches de Poste" />
|
||||
<Head title="Offres d'emploi" />
|
||||
|
||||
<AdminLayout>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center gap-8">
|
||||
<h2 class="text-xl font-semibold leading-tight capitalize">
|
||||
Fiches de Poste
|
||||
Offres d'emploi
|
||||
</h2>
|
||||
<PrimaryButton @click="openModal()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Nouvelle Fiche
|
||||
Nouvelle Offre
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="p-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<!-- Filter & Sort Bar -->
|
||||
<div class="mb-8 flex flex-col md:flex-row gap-6 justify-between items-start md:items-center bg-white dark:bg-slate-800 p-6 rounded-[2rem] shadow-sm border border-slate-100 dark:border-slate-700">
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<div class="flex bg-slate-100 dark:bg-slate-900 p-1 rounded-xl">
|
||||
<button
|
||||
@click="filterStatus = 'active'"
|
||||
:class="filterStatus === 'active' ? 'bg-white dark:bg-slate-800 shadow-sm text-indigo-600' : 'text-slate-500 hover:text-indigo-400'"
|
||||
class="px-4 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all"
|
||||
>
|
||||
En cours
|
||||
</button>
|
||||
<button
|
||||
@click="filterStatus = 'expired'"
|
||||
:class="filterStatus === 'expired' ? 'bg-white dark:bg-slate-800 shadow-sm text-indigo-600' : 'text-slate-500 hover:text-indigo-400'"
|
||||
class="px-4 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all"
|
||||
>
|
||||
Expirées
|
||||
</button>
|
||||
<button
|
||||
@click="filterStatus = 'all'"
|
||||
:class="filterStatus === 'all' ? 'bg-white dark:bg-slate-800 shadow-sm text-indigo-600' : 'text-slate-500 hover:text-indigo-400'"
|
||||
class="px-4 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all"
|
||||
>
|
||||
Toutes
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700 mx-2 hidden md:block"></div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[10px] font-black uppercase tracking-widest text-slate-400">Trier par :</span>
|
||||
<select
|
||||
v-model="sortKey"
|
||||
class="bg-transparent border-none text-xs font-bold text-slate-700 dark:text-slate-300 focus:ring-0 cursor-pointer py-0 pl-0"
|
||||
>
|
||||
<option value="created_at">Date de création</option>
|
||||
<option value="expires_at">Date d'expiration</option>
|
||||
<option value="title">Titre</option>
|
||||
</select>
|
||||
<button @click="sortOrder *= -1" class="p-1 hover:bg-slate-100 dark:hover:bg-slate-900 rounded-lg transition-colors">
|
||||
<svg v-if="sortOrder === 1" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12" /></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M3 4h13M3 8h9m-9 4h6m4 0l4 4m0 0l4-4m-4 4v-12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 bg-slate-100 dark:bg-slate-900 p-1 rounded-xl">
|
||||
<button
|
||||
@click="viewMode = 'grid'"
|
||||
:class="viewMode === 'grid' ? 'bg-white dark:bg-slate-800 shadow-sm text-indigo-600' : 'text-slate-400 hover:text-indigo-400'"
|
||||
class="p-2 rounded-lg transition-all"
|
||||
>
|
||||
<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.5" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" /></svg>
|
||||
</button>
|
||||
<button
|
||||
@click="viewMode = 'list'"
|
||||
:class="viewMode === 'list' ? 'bg-white dark:bg-slate-800 shadow-sm text-indigo-600' : 'text-slate-400 hover:text-indigo-400'"
|
||||
class="p-2 rounded-lg transition-all"
|
||||
>
|
||||
<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.5" d="M4 6h16M4 10h16M4 14h16M4 18h16" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid View -->
|
||||
<div v-if="viewMode === 'grid'" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<div
|
||||
v-for="position in jobPositions"
|
||||
v-for="position in filteredAndSortedPositions"
|
||||
:key="position.id"
|
||||
class="bg-white dark:bg-slate-800 rounded-3xl p-8 shadow-sm border border-slate-200 dark:border-slate-700 hover:shadow-2xl transition-all duration-300 group flex flex-col h-full"
|
||||
>
|
||||
<div class="mb-6 flex-1">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<div class="text-[10px] font-black uppercase tracking-widest text-indigo-500">Poste / Compétences</div>
|
||||
<div v-if="$page.props.auth.user.role === 'super_admin'" class="text-[9px] font-black uppercase tracking-widest px-2 py-0.5 rounded bg-indigo-50 text-indigo-600 dark:bg-indigo-900/30">
|
||||
<div v-if="['super_admin', 'gestionnaire_rh'].includes($page.props.auth.user.role)" class="text-[9px] font-black uppercase tracking-widest px-2 py-0.5 rounded bg-indigo-50 text-indigo-600 dark:bg-indigo-900/30">
|
||||
{{ position.tenant ? position.tenant.name : 'Global' }}
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-2xl font-black mb-3 group-hover:text-indigo-600 transition-colors">{{ position.title }}</h3>
|
||||
<p class="text-slate-500 dark:text-slate-400 text-sm line-clamp-3 leading-relaxed">
|
||||
<h3 class="text-2xl font-black mb-1 group-hover:text-indigo-600 transition-colors">{{ position.title }}</h3>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="text-[10px] font-black bg-primary/10 text-primary px-2 py-0.5 rounded-full uppercase tracking-tighter">
|
||||
{{ position.candidates_count }} {{ position.candidates_count > 1 ? 'candidats' : 'candidat' }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-slate-500 dark:text-slate-400 text-sm line-clamp-3 leading-relaxed mb-4">
|
||||
{{ position.description }}
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
<div v-if="position.expires_at" class="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider px-2 py-1 rounded-lg w-fit" :class="new Date(position.expires_at) < new Date() ? 'text-red-500 bg-red-50 dark:bg-red-900/20' : 'text-emerald-500 bg-emerald-50 dark:bg-emerald-900/20'">
|
||||
<svg 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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{{ new Date(position.expires_at) < new Date() ? 'Expirée le' : 'Expire le' }} : {{ new Date(position.expires_at).toLocaleDateString() }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider px-2 py-1 rounded-lg w-fit text-slate-400 bg-slate-50 dark:bg-slate-900/40 border border-slate-100 dark:border-slate-700">
|
||||
<svg 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="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Créée le : {{ new Date(position.created_at).toLocaleDateString() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mb-6" v-if="position.requirements?.length">
|
||||
@@ -160,8 +289,13 @@ const copyLink = (position) => {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="pt-6 border-t border-slate-100 dark:border-slate-700 flex justify-between gap-3">
|
||||
<SecondaryButton @click="openModal(position)" class="flex-1 !justify-center !py-2 text-xs">Modifier</SecondaryButton>
|
||||
<div class="pt-6 border-t border-slate-100 dark:border-slate-700 flex flex-col gap-3">
|
||||
<div class="flex gap-3">
|
||||
<Link :href="route('admin.candidates.index', { job_position: position.id })" class="flex-1 inline-flex items-center justify-center py-2 rounded-xl bg-primary/5 text-primary text-xs font-extrabold uppercase tracking-widest hover:bg-primary/10 transition-all">
|
||||
Voir Candidats
|
||||
</Link>
|
||||
<SecondaryButton @click="openModal(position)" class="flex-1 !justify-center !py-2 text-xs">Modifier</SecondaryButton>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
@click="copyLink(position)"
|
||||
@@ -184,28 +318,74 @@ const copyLink = (position) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="jobPositions.length === 0" class="col-span-full py-32 text-center">
|
||||
<div class="inline-flex p-6 bg-slate-100 dark:bg-slate-800 rounded-full mb-6">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-slate-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>
|
||||
<!-- List View -->
|
||||
<div v-else class="space-y-4">
|
||||
<div
|
||||
v-for="position in filteredAndSortedPositions"
|
||||
:key="position.id"
|
||||
class="bg-white dark:bg-slate-800 rounded-2xl p-6 shadow-sm border border-slate-200 dark:border-slate-700 hover:shadow-md transition-all flex flex-col md:flex-row items-center gap-6"
|
||||
>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<h3 class="text-lg font-black truncate">{{ position.title }}</h3>
|
||||
<span v-if="['super_admin', 'gestionnaire_rh'].includes($page.props.auth.user.role)" class="text-[9px] font-black uppercase tracking-widest px-2 py-0.5 rounded bg-indigo-50 text-indigo-600 dark:bg-indigo-900/30 shrink-0">
|
||||
{{ position.tenant ? position.tenant.name : 'Global' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-4 text-[10px] font-bold uppercase tracking-widest text-slate-400">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-indigo-400"></span>
|
||||
{{ position.candidates_count }} candidats
|
||||
</div>
|
||||
<div v-if="position.expires_at" class="flex items-center gap-1.5" :class="new Date(position.expires_at) < new Date() ? 'text-red-500' : 'text-emerald-500'">
|
||||
<svg 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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
{{ new Date(position.expires_at) < new Date() ? 'Expirée' : 'Expire' }} : {{ new Date(position.expires_at).toLocaleDateString() }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
Créée le : {{ new Date(position.created_at).toLocaleDateString() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<Link :href="route('admin.candidates.index', { job_position: position.id })" title="Voir candidats" class="p-3 bg-primary/5 text-primary rounded-xl hover:bg-primary/10 transition-all">
|
||||
<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="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
|
||||
</Link>
|
||||
<button @click="openModal(position)" title="Modifier" class="p-3 bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 rounded-xl hover:bg-slate-200 dark:hover:bg-slate-600 transition-all">
|
||||
<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.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" /></svg>
|
||||
</button>
|
||||
<button @click="copyLink(position)" title="Lien de candidature" class="p-3 text-slate-400 hover:text-indigo-500 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 rounded-xl transition-all">
|
||||
<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="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" /></svg>
|
||||
</button>
|
||||
<button @click="deletePosition(position.id)" title="Supprimer" class="p-3 text-slate-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-xl transition-all">
|
||||
<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>
|
||||
<h3 class="text-2xl font-black mb-2">Aucune fiche de poste</h3>
|
||||
<p class="text-slate-500 mb-8">Créez votre première fiche de poste pour permettre l'analyse IA.</p>
|
||||
<PrimaryButton @click="openModal()">Créer une fiche</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="filteredAndSortedPositions.length === 0" class="col-span-full py-32 text-center">
|
||||
<div class="inline-flex p-6 bg-slate-100 dark:bg-slate-800 rounded-full mb-6">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-slate-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>
|
||||
</div>
|
||||
<h3 class="text-2xl font-black mb-2">Aucune offre d'emploi</h3>
|
||||
<p class="text-slate-500 mb-8">Créez votre première offre d'emploi pour permettre l'analyse IA.</p>
|
||||
<PrimaryButton @click="openModal()">Créer une offre</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Create/Edit -->
|
||||
<Modal :show="showingModal" @close="closeModal">
|
||||
<div class="p-8">
|
||||
<h3 class="text-2xl font-black mb-8">{{ editingPosition ? 'Modifier' : 'Créer' }} la Fiche de Poste</h3>
|
||||
<h3 class="text-2xl font-black mb-8">{{ editingPosition ? 'Modifier' : 'Créer' }} l'Offre d'emploi</h3>
|
||||
|
||||
<form @submit.prevent="submit" class="space-y-6">
|
||||
<div v-if="$page.props.auth.user.role === 'super_admin'" class="mb-4">
|
||||
<div v-if="['super_admin', 'gestionnaire_rh'].includes($page.props.auth.user.role)" class="mb-4">
|
||||
<label class="block text-xs font-black uppercase tracking-widest text-slate-400 mb-2">Structure de rattachement</label>
|
||||
<select
|
||||
v-model="form.tenant_id"
|
||||
@@ -231,7 +411,18 @@ const copyLink = (position) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-black uppercase tracking-widest text-slate-400 mb-2">Description / Fiche de Poste</label>
|
||||
<label class="block text-xs font-black uppercase tracking-widest text-slate-400 mb-2">Date limite de candidature (Expiration)</label>
|
||||
<input
|
||||
v-model="form.expires_at"
|
||||
type="date"
|
||||
class="w-full bg-slate-50 dark:bg-slate-900 border-none rounded-2xl p-4 focus:ring-2 focus:ring-indigo-500/20 transition-all font-bold"
|
||||
>
|
||||
<p class="mt-1 text-[10px] text-slate-400 font-bold uppercase tracking-tight">L'offre ne sera plus visible sur le site après cette date.</p>
|
||||
<InputError :message="form.errors.expires_at" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-black uppercase tracking-widest text-slate-400 mb-2">Description / Détail de l'offre</label>
|
||||
<textarea
|
||||
v-model="form.description"
|
||||
rows="8"
|
||||
|
||||
@@ -113,6 +113,7 @@ const cancel = () => {
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300">Rôle</label>
|
||||
<select v-model="form.role" class="mt-1 block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6">
|
||||
<option value="admin">Administrateur Standard (SaaS)</option>
|
||||
<option value="gestionnaire_rh">Gestionnaire RH (Restreint)</option>
|
||||
<option value="super_admin">Super Administrateur (Global)</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -152,10 +153,11 @@ const cancel = () => {
|
||||
<td class="py-3 px-6 text-slate-500">{{ user.email }}</td>
|
||||
<td class="py-3 px-6">
|
||||
<span v-if="user.role === 'super_admin'" class="inline-flex items-center rounded-md bg-purple-50 px-2 py-1 text-xs font-medium text-purple-700 ring-1 ring-inset ring-purple-700/10">Super Admin</span>
|
||||
<span v-else-if="user.role === 'gestionnaire_rh'" class="inline-flex items-center rounded-md bg-amber-50 px-2 py-1 text-xs font-medium text-amber-700 ring-1 ring-inset ring-amber-700/10">Gestionnaire RH</span>
|
||||
<span v-else class="inline-flex items-center rounded-md bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10">Admin Site</span>
|
||||
</td>
|
||||
<td class="py-3 px-6 text-slate-500">
|
||||
{{ user.tenant ? user.tenant.name : (user.role === 'super_admin' ? 'Toutes les structures' : 'Aucun rattachement') }}
|
||||
{{ user.tenant ? user.tenant.name : (user.role === 'super_admin' || user.role === 'gestionnaire_rh' ? 'Toutes les structures' : 'Aucun rattachement') }}
|
||||
</td>
|
||||
<td class="py-3 px-6 text-right space-x-2">
|
||||
<button v-if="page.props.auth.user.role === 'super_admin'" @click="resetPassword(user)" class="text-orange-600 hover:text-orange-900 px-3 py-1 rounded bg-orange-50 hover:bg-orange-100 transition-colors" title="Réinitialiser le mot de passe">
|
||||
|
||||
Reference in New Issue
Block a user