Enhance candidates management: add city field, drag-and-drop ranking persistence, AI analysis popover, and CV preview
This commit is contained in:
@@ -26,7 +26,8 @@ const form = useForm({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
linkedin_url: '',
|
||||
linkedin_url: '',
|
||||
city: '',
|
||||
cv: null,
|
||||
cover_letter: null,
|
||||
tenant_id: '',
|
||||
@@ -272,6 +273,12 @@ const batchAnalyze = async () => {
|
||||
<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('city')" 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">
|
||||
Ville
|
||||
<svg v-show="sortKey === 'city'" 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é
|
||||
@@ -332,6 +339,9 @@ const batchAnalyze = async () => {
|
||||
<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-[10px] font-black uppercase tracking-widest text-anthracite/60">
|
||||
{{ candidate.city || '--' }}
|
||||
</td>
|
||||
<td class="px-8 py-5 text-xs font-bold text-anthracite">
|
||||
{{ candidate.job_position ? candidate.job_position.title : 'Non assigné' }}
|
||||
</td>
|
||||
@@ -358,8 +368,8 @@ const batchAnalyze = async () => {
|
||||
<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' :
|
||||
candidate.ai_analysis.match_score >= 90 ? 'bg-emerald-50 text-emerald-700 border border-emerald-200' :
|
||||
candidate.ai_analysis.match_score >= 80 ? 'bg-highlight/10 text-[#3a2800] border border-highlight/30' :
|
||||
'bg-accent/10 text-accent border border-accent/20'
|
||||
]"
|
||||
>
|
||||
@@ -396,7 +406,7 @@ const batchAnalyze = async () => {
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="candidates.length === 0">
|
||||
<td colspan="11" class="px-8 py-16 text-center">
|
||||
<td colspan="12" class="px-8 py-16 text-center">
|
||||
<div class="text-anthracite/40 italic font-medium font-subtitle">
|
||||
Aucun candidat trouvé.
|
||||
</div>
|
||||
@@ -432,6 +442,14 @@ const batchAnalyze = async () => {
|
||||
<TextInput id="phone" type="text" class="mt-1 block w-full" v-model="form.phone" />
|
||||
<InputError class="mt-2" :message="form.errors.phone" />
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel for="city" value="Ville" />
|
||||
<TextInput id="city" type="text" class="mt-1 block w-full" v-model="form.city" />
|
||||
<InputError class="mt-2" :message="form.errors.city" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<InputLabel for="linkedin_url" value="LinkedIn URL" />
|
||||
<TextInput id="linkedin_url" type="url" class="mt-1 block w-full" v-model="form.linkedin_url" />
|
||||
|
||||
302
resources/js/Pages/Admin/Candidates/Selected.vue
Normal file
302
resources/js/Pages/Admin/Candidates/Selected.vue
Normal file
@@ -0,0 +1,302 @@
|
||||
<script setup>
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue';
|
||||
import { Head, Link, router } from '@inertiajs/vue3';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import draggable from 'vuedraggable';
|
||||
import Modal from '@/Components/Modal.vue';
|
||||
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
||||
|
||||
const props = defineProps({
|
||||
candidates: Array
|
||||
});
|
||||
|
||||
const searchQuery = ref('');
|
||||
|
||||
// Maintain local state for dragging
|
||||
const localCandidates = ref([]);
|
||||
|
||||
// We initialize sorting based on the server data
|
||||
watch(() => props.candidates, (newVal) => {
|
||||
localCandidates.value = [...newVal];
|
||||
}, { immediate: true });
|
||||
|
||||
const filteredCandidates = computed({
|
||||
get() {
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
if (!query) return localCandidates.value;
|
||||
|
||||
return localCandidates.value.filter(c =>
|
||||
c.user.name.toLowerCase().includes(query) ||
|
||||
c.user.email.toLowerCase().includes(query) ||
|
||||
(c.job_position && c.job_position.title.toLowerCase().includes(query))
|
||||
);
|
||||
},
|
||||
set(val) {
|
||||
if (!searchQuery.value) {
|
||||
localCandidates.value = val;
|
||||
saveOrder();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const saveOrder = () => {
|
||||
router.post(route('admin.candidates.update-order'), {
|
||||
ids: localCandidates.value.map(c => c.id)
|
||||
}, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
// Optional: Show a toast or notification
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const hoveredCandidateId = ref(null);
|
||||
const popoverPosition = ref({ top: 0, left: 0 });
|
||||
|
||||
const handleMouseEnter = (event, id) => {
|
||||
hoveredCandidateId.value = id;
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
popoverPosition.value = {
|
||||
top: rect.bottom + window.scrollY + 10,
|
||||
left: rect.left + window.scrollX
|
||||
};
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
hoveredCandidateId.value = null;
|
||||
};
|
||||
|
||||
const selectedDocument = ref(null);
|
||||
const openCvPreview = (candidate) => {
|
||||
const cv = (candidate.documents || []).find(d => d.type === 'cv');
|
||||
if (cv) {
|
||||
selectedDocument.value = cv;
|
||||
} else {
|
||||
alert("Aucun CV n'a été trouvé pour ce candidat.");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Candidats Sélectionnés" />
|
||||
|
||||
<AdminLayout>
|
||||
<template #header>
|
||||
Candidats Sélectionnés (Comparateur)
|
||||
</template>
|
||||
|
||||
<div class="mb-8 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div>
|
||||
<h3 class="text-2xl font-bold">Listes de Candidats retenus</h3>
|
||||
<p class="text-sm text-slate-500 mt-1">
|
||||
Faites glisser les candidats sur la poignée (⋮⋮) pour modifier manuellement le classement.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="relative w-full sm:w-64">
|
||||
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-slate-400">
|
||||
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Rechercher..."
|
||||
class="block w-full pl-10 pr-3 py-2 border border-slate-300 dark:border-slate-700 rounded-lg leading-5 bg-white dark:bg-slate-800 placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm transition-all"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700 overflow-hidden text-sm">
|
||||
<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="px-4 py-3 font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider text-xs">Ordre</th>
|
||||
<th class="px-4 py-3 font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider text-xs">Candidat</th>
|
||||
<th class="px-4 py-3 font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider text-xs">Poste</th>
|
||||
<th class="px-4 py-3 font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider text-xs text-center">Ville</th>
|
||||
<th class="px-4 py-3 font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider text-xs text-center">Score Global (/20)</th>
|
||||
<th class="px-4 py-3 font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider text-xs text-center">IA Match</th>
|
||||
<th class="px-4 py-3 font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider text-xs text-center">CV (/20)</th>
|
||||
<th class="px-4 py-3 font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider text-xs text-center">Motiv (/10)</th>
|
||||
<th class="px-4 py-3 font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider text-xs text-center">Test QCM (/20)</th>
|
||||
<th class="px-4 py-3 font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider text-xs text-center">Entretien (/30)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<draggable
|
||||
v-model="filteredCandidates"
|
||||
tag="tbody"
|
||||
item-key="id"
|
||||
class="divide-y divide-slate-200 dark:divide-slate-700"
|
||||
handle=".drag-handle"
|
||||
:animation="200"
|
||||
:disabled="searchQuery.length > 0"
|
||||
>
|
||||
<template #item="{ element: candidate, index }">
|
||||
<tr class="hover:bg-slate-50/50 dark:hover:bg-slate-700/30 transition-colors group bg-white dark:bg-slate-800">
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="drag-handle text-slate-400 transition-colors"
|
||||
:class="searchQuery.length > 0 ? 'opacity-30 cursor-not-allowed' : 'cursor-grab active:cursor-grabbing hover:text-slate-600'"
|
||||
:title="searchQuery.length > 0 ? 'Désactivé pendant la recherche' : 'Glisser pour réordonner'"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="9" cy="12" r="1"/><circle cx="9" cy="5" r="1"/><circle cx="9" cy="19" r="1"/>
|
||||
<circle cx="15" cy="12" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="19" r="1"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm"
|
||||
:class="[
|
||||
index === 0 && !searchQuery ? 'bg-yellow-100 text-yellow-700 shadow-sm' :
|
||||
index === 1 && !searchQuery ? 'bg-slate-200 text-slate-700 shadow-sm' :
|
||||
index === 2 && !searchQuery ? 'bg-orange-100 text-orange-700 shadow-sm' : 'bg-slate-50 text-slate-500'
|
||||
]"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 relative">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="font-bold text-slate-900 dark:text-slate-100"
|
||||
@mouseenter="handleMouseEnter($event, candidate.id)"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<Link :href="route('admin.candidates.show', candidate.id)" class="hover:text-indigo-600 transition-colors">
|
||||
{{ candidate.user.name }}
|
||||
</Link>
|
||||
</div>
|
||||
<button
|
||||
v-if="(candidate.documents || []).some(d => d.type === 'cv')"
|
||||
@click="openCvPreview(candidate)"
|
||||
class="p-1 text-slate-400 hover:text-indigo-600 transition-colors"
|
||||
title="Voir le CV"
|
||||
>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-xs text-slate-500">{{ candidate.user.email }}</div>
|
||||
|
||||
<!-- AI Analysis Popover -->
|
||||
<div
|
||||
v-if="hoveredCandidateId === candidate.id && candidate.ai_analysis"
|
||||
class="fixed z-[100] w-80 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-xl shadow-2xl p-4 pointer-events-none transition-all animate-in fade-in zoom-in-95 duration-200"
|
||||
:style="{ top: popoverPosition.top + 'px', left: popoverPosition.left + 'px' }"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-[10px] font-black uppercase tracking-widest text-slate-400">Synthèse IA</span>
|
||||
<div
|
||||
class="px-2 py-0.5 rounded-lg text-[10px] font-black"
|
||||
:class="[
|
||||
candidate.ai_analysis.match_score >= 90 ? 'bg-emerald-50 text-emerald-700' :
|
||||
candidate.ai_analysis.match_score >= 80 ? 'bg-amber-50 text-amber-700' :
|
||||
'bg-rose-50 text-rose-700'
|
||||
]"
|
||||
>
|
||||
{{ candidate.ai_analysis.match_score }}% Match
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-slate-600 dark:text-slate-300 italic mb-4 leading-relaxed">
|
||||
"{{ candidate.ai_analysis.synthese || candidate.ai_analysis.summary }}"
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div v-if="candidate.ai_analysis.points_forts || candidate.ai_analysis.strengths">
|
||||
<p class="text-[9px] font-bold uppercase text-emerald-600 mb-1">Forces</p>
|
||||
<ul class="text-[10px] text-slate-500 space-y-0.5">
|
||||
<li v-for="s in (candidate.ai_analysis.points_forts || candidate.ai_analysis.strengths).slice(0,3)" :key="s">• {{ s }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="candidate.ai_analysis.points_faibles || candidate.ai_analysis.gaps">
|
||||
<p class="text-[9px] font-bold uppercase text-rose-600 mb-1">Points d'attention</p>
|
||||
<ul class="text-[10px] text-slate-500 space-y-0.5">
|
||||
<li v-for="g in (candidate.ai_analysis.points_faibles || candidate.ai_analysis.gaps).slice(0,3)" :key="g">• {{ g }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="candidate.ai_analysis.verdict" class="mt-3 pt-3 border-t border-slate-100 dark:border-slate-800">
|
||||
<span class="text-[9px] font-bold uppercase text-slate-400">Verdict final :</span>
|
||||
<span class="ml-2 text-[10px] font-black text-indigo-600">{{ candidate.ai_analysis.verdict }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-slate-600">
|
||||
{{ candidate.job_position ? candidate.job_position.title : '--' }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center text-slate-500 font-medium">
|
||||
{{ candidate.city ?? '--' }}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-lg font-black text-indigo-600 dark:text-indigo-400">{{ candidate.weighted_score }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div v-if="candidate.ai_analysis" class="flex flex-col items-center">
|
||||
<div
|
||||
class="px-2 py-0.5 rounded-lg text-[10px] font-black shadow-sm"
|
||||
:class="[
|
||||
candidate.ai_analysis.match_score >= 90 ? 'bg-emerald-50 text-emerald-700 border border-emerald-200' :
|
||||
candidate.ai_analysis.match_score >= 80 ? '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>
|
||||
<div v-else class="text-center text-[10px] text-slate-300 italic">--</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center font-medium text-slate-600">
|
||||
{{ candidate.cv_score ?? '--' }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center font-medium text-slate-600">
|
||||
{{ candidate.motivation_score ?? '--' }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center font-medium text-slate-600">
|
||||
{{ (() => {
|
||||
const bestAttempt = (candidate.attempts || []).filter(a => a.finished_at).map(a => a.max_score > 0 ? (a.score / a.max_score) * 20 : 0);
|
||||
return bestAttempt.length ? Math.max(...bestAttempt).toFixed(1) : '--';
|
||||
})() }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center font-medium text-slate-600">
|
||||
{{ candidate.interview_score ?? '--' }}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template #footer>
|
||||
<tr v-if="filteredCandidates.length === 0">
|
||||
<td colspan="10" class="px-6 py-12 text-center text-slate-500 italic">
|
||||
Aucun candidat sélectionné ne correspond.
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</draggable>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Document Preview Modal -->
|
||||
<Modal :show="!!selectedDocument" @close="selectedDocument = null" max-width="4xl">
|
||||
<div class="p-6 h-[80vh] flex flex-col">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-xl font-bold">Aperçu : {{ selectedDocument.original_name }}</h3>
|
||||
<SecondaryButton @click="selectedDocument = null">Fermer</SecondaryButton>
|
||||
</div>
|
||||
<div class="flex-1 bg-slate-100 dark:bg-slate-900 rounded-lg overflow-hidden border border-slate-200 dark:border-slate-700">
|
||||
<iframe
|
||||
v-if="selectedDocument"
|
||||
:src="route('admin.documents.show', selectedDocument.id)"
|
||||
class="w-full h-full"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
@@ -30,6 +30,7 @@ const detailsForm = useForm({
|
||||
email: props.candidate.user.email,
|
||||
phone: props.candidate.phone || '',
|
||||
linkedin_url: props.candidate.linkedin_url || '',
|
||||
city: props.candidate.city || '',
|
||||
});
|
||||
const updateDetails = () => {
|
||||
detailsForm.put(route('admin.candidates.update', props.candidate.id), {
|
||||
@@ -294,6 +295,10 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
|
||||
<svg class="w-3.5 h-3.5 text-ink/30 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 16.92z"/></svg>
|
||||
<span>{{ candidate.phone }}</span>
|
||||
</div>
|
||||
<div v-if="candidate.city" class="flex items-center gap-2.5 text-xs text-ink/55 font-semibold">
|
||||
<svg class="w-3.5 h-3.5 text-ink/30 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg>
|
||||
<span>{{ candidate.city }}</span>
|
||||
</div>
|
||||
<a v-if="candidate.linkedin_url" :href="candidate.linkedin_url" target="_blank" class="flex items-center gap-2.5 text-xs text-primary font-bold hover:underline">
|
||||
<svg class="w-3.5 h-3.5 shrink-0" fill="currentColor" viewBox="0 0 24 24"><path d="M16 8a6 6 0 016 6v7h-4v-7a2 2 0 00-2-2 2 2 0 00-2 2v7h-4v-7a6 6 0 016-6zM2 9h4v12H2z"/><circle cx="4" cy="4" r="2"/></svg>
|
||||
LinkedIn
|
||||
@@ -356,8 +361,8 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
|
||||
<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',
|
||||
aiAnalysis.match_score >= 80 ? 'bg-success/12 text-success' :
|
||||
aiAnalysis.match_score >= 60 ? 'bg-highlight/15 text-highlight-on' : 'bg-accent/10 text-accent']">
|
||||
aiAnalysis.match_score >= 90 ? 'bg-success/12 text-success' :
|
||||
aiAnalysis.match_score >= 80 ? 'bg-highlight/15 text-highlight-on' : 'bg-accent/10 text-accent']">
|
||||
{{ aiAnalysis.match_score }}%
|
||||
</div>
|
||||
</div>
|
||||
@@ -497,8 +502,8 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
|
||||
<!-- Match score + synthèse -->
|
||||
<div class="flex items-start gap-5 p-5 rounded-xl border border-ink/[0.07] bg-neutral">
|
||||
<div :class="['w-14 h-14 rounded-full flex items-center justify-center text-lg font-black shrink-0',
|
||||
aiAnalysis.match_score >= 80 ? 'bg-success/12 text-success' :
|
||||
aiAnalysis.match_score >= 60 ? 'bg-highlight/15 text-highlight' : 'bg-accent/10 text-accent']">
|
||||
aiAnalysis.match_score >= 90 ? 'bg-success/12 text-success' :
|
||||
aiAnalysis.match_score >= 80 ? 'bg-highlight/15 text-highlight' : 'bg-accent/10 text-accent']">
|
||||
{{ aiAnalysis.match_score }}%
|
||||
</div>
|
||||
<div>
|
||||
@@ -777,7 +782,7 @@ const barColor = (pct) => pct >= 80 ? 'bg-success' : pct >= 60 ? 'bg-highlight'
|
||||
<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', linkedin_url:'LinkedIn URL' }" :key="key">
|
||||
<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" />
|
||||
|
||||
Reference in New Issue
Block a user