1436 lines
99 KiB
Vue
1436 lines
99 KiB
Vue
<script setup>
|
|
import AdminLayout from '@/Layouts/AdminLayout.vue';
|
|
import axios from 'axios';
|
|
import { Head, Link, router, useForm, usePage } from '@inertiajs/vue3';
|
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
|
|
import { marked } from 'marked';
|
|
import Modal from '@/Components/Modal.vue';
|
|
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
|
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
|
import DangerButton from '@/Components/DangerButton.vue';
|
|
import InputError from '@/Components/InputError.vue';
|
|
import Chart from 'chart.js/auto';
|
|
|
|
const props = defineProps({
|
|
candidate: Object,
|
|
jobPositions: Array,
|
|
tenants: Array,
|
|
ai_config: Object
|
|
});
|
|
|
|
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,
|
|
});
|
|
};
|
|
|
|
const tenantForm = useForm({
|
|
tenant_id: props.candidate.tenant_id || ''
|
|
});
|
|
|
|
const updateTenant = () => {
|
|
tenantForm.patch(route('admin.candidates.update-tenant', props.candidate.id), {
|
|
preserveScroll: true,
|
|
});
|
|
};
|
|
|
|
const toggleSelection = () => {
|
|
router.patch(route('admin.candidates.toggle-selection', props.candidate.id), {}, { preserveScroll: true });
|
|
};
|
|
|
|
const selectedDocument = ref(null);
|
|
|
|
const docForm = useForm({
|
|
cv: null,
|
|
cover_letter: null,
|
|
_method: 'PUT' // For file upload via PUT in Laravel
|
|
});
|
|
|
|
const rawInterviewDetails = props.candidate.interview_details || {};
|
|
const notesForm = useForm({
|
|
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({
|
|
cv_score: props.candidate.cv_score || 0,
|
|
motivation_score: props.candidate.motivation_score || 0,
|
|
interview_score: props.candidate.interview_score || 0,
|
|
});
|
|
|
|
const isPreview = ref(true);
|
|
const renderedNotes = computed(() => marked.parse(notesForm.notes || ''));
|
|
|
|
const openAttempts = ref([]);
|
|
|
|
const toggleAttempt = (id) => {
|
|
if (openAttempts.value.includes(id)) {
|
|
openAttempts.value = openAttempts.value.filter(item => item !== id);
|
|
} else {
|
|
openAttempts.value.push(id);
|
|
}
|
|
};
|
|
|
|
const formatDateTime = (date) => {
|
|
return new Date(date).toLocaleString('fr-FR', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
};
|
|
|
|
const deleteCandidate = () => {
|
|
if (confirm('Voulez-vous vraiment supprimer ce candidat ? Toutes ses données seront DEFINITIVEMENT perdues.')) {
|
|
router.delete(route('admin.candidates.destroy', props.candidate.id));
|
|
}
|
|
};
|
|
|
|
const deleteAttempt = (id) => {
|
|
if (confirm('Voulez-vous vraiment supprimer cette tentative de test ? Cette action sera enregistrée dans les logs.')) {
|
|
router.delete(route('admin.attempts.destroy', id), {
|
|
preserveScroll: true
|
|
});
|
|
}
|
|
};
|
|
|
|
const resetPassword = () => {
|
|
if (confirm('Générer un nouveau mot de passe pour ce candidat ?')) {
|
|
router.post(route('admin.candidates.reset-password', props.candidate.id));
|
|
}
|
|
};
|
|
|
|
const updateDocuments = () => {
|
|
docForm.post(route('admin.candidates.update', props.candidate.id), {
|
|
onSuccess: () => {
|
|
docForm.reset();
|
|
},
|
|
});
|
|
};
|
|
|
|
|
|
const saveScores = () => {
|
|
scoreForm.patch(route('admin.candidates.update-scores', props.candidate.id), {
|
|
preserveScroll: true,
|
|
});
|
|
};
|
|
|
|
const openPreview = (doc) => {
|
|
selectedDocument.value = doc;
|
|
};
|
|
|
|
const updateAnswerScore = (answerId, score) => {
|
|
router.patch(route('admin.answers.update-score', answerId), {
|
|
score: score
|
|
}, {
|
|
preserveScroll: true,
|
|
});
|
|
};
|
|
|
|
// ─── Radar Chart ──────────────────────────────────────────────────────────────
|
|
const radarCanvasRef = ref(null);
|
|
let radarChartInstance = null;
|
|
|
|
// Calcul du score test (meilleure tentative ramenée sur 20)
|
|
const bestTestScore = computed(() => {
|
|
if (!props.candidate.attempts || props.candidate.attempts.length === 0) return 0;
|
|
const finished = props.candidate.attempts.filter(a => a.finished_at && a.max_score > 0);
|
|
if (finished.length === 0) return 0;
|
|
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 = () => {
|
|
if (!radarCanvasRef.value) return;
|
|
if (radarChartInstance) {
|
|
radarChartInstance.destroy();
|
|
radarChartInstance = null;
|
|
}
|
|
|
|
const isDark = false; // Désactivation forcée du mode sombre
|
|
const gridColor = isDark ? 'rgba(148,163,184,0.15)' : 'rgba(100,116,139,0.15)';
|
|
const labelColor = isDark ? '#94a3b8' : '#64748b';
|
|
|
|
radarChartInstance = new Chart(radarCanvasRef.value, {
|
|
type: 'radar',
|
|
data: {
|
|
labels: ['Analyse CV', 'Lettre Motiv.', 'Entretien', 'Test Technique', 'Soft Skills'],
|
|
datasets: [{
|
|
label: 'Profil Candidat (%)',
|
|
data: radarData.value,
|
|
backgroundColor: 'rgba(99,102,241,0.15)',
|
|
borderColor: 'rgba(99,102,241,0.9)',
|
|
borderWidth: 2.5,
|
|
pointBackgroundColor: 'rgba(99,102,241,1)',
|
|
pointBorderColor: '#fff',
|
|
pointBorderWidth: 2,
|
|
pointRadius: 5,
|
|
pointHoverRadius: 7,
|
|
pointHoverBackgroundColor: 'rgba(139,92,246,1)',
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: true,
|
|
animation: { duration: 700, easing: 'easeInOutQuart' },
|
|
plugins: {
|
|
legend: { display: false },
|
|
tooltip: {
|
|
callbacks: {
|
|
label: (ctx) => ` ${ctx.raw}%`,
|
|
},
|
|
backgroundColor: isDark ? '#1e293b' : '#fff',
|
|
titleColor: isDark ? '#e2e8f0' : '#0f172a',
|
|
bodyColor: isDark ? '#94a3b8' : '#475569',
|
|
borderColor: isDark ? '#334155' : '#e2e8f0',
|
|
borderWidth: 1,
|
|
padding: 10,
|
|
cornerRadius: 10,
|
|
}
|
|
},
|
|
scales: {
|
|
r: {
|
|
min: 0,
|
|
max: 100,
|
|
ticks: {
|
|
stepSize: 25,
|
|
color: labelColor,
|
|
backdropColor: 'transparent',
|
|
font: { size: 9, weight: 'bold' },
|
|
callback: (v) => v + '%',
|
|
},
|
|
grid: { color: gridColor },
|
|
angleLines: { color: gridColor },
|
|
pointLabels: {
|
|
color: labelColor,
|
|
font: { size: 11, weight: 'bold' },
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
onMounted(() => {
|
|
nextTick(() => buildRadarChart());
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
if (radarChartInstance) radarChartInstance.destroy();
|
|
});
|
|
|
|
// Mise à jour du radar quand les scores changent
|
|
watch(
|
|
() => [scoreForm.cv_score, scoreForm.motivation_score, scoreForm.interview_score, bestTestScore.value, softSkillsScore.value],
|
|
() => {
|
|
if (radarChartInstance) {
|
|
radarChartInstance.data.datasets[0].data = radarData.value;
|
|
radarChartInstance.update();
|
|
}
|
|
}
|
|
);
|
|
|
|
// 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);
|
|
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("");
|
|
|
|
const runAI = async () => {
|
|
if (!props.candidate.job_position_id) {
|
|
modalErrorMessage.value = "Veuillez d'abord associer une fiche de poste à ce candidat.";
|
|
showErrorModal.value = true;
|
|
return;
|
|
}
|
|
|
|
isAnalyzing.value = true;
|
|
try {
|
|
const response = await axios.post(route('admin.candidates.analyze', props.candidate.id), {
|
|
provider: selectedProvider.value,
|
|
force: forceAnalysis.value
|
|
});
|
|
aiAnalysis.value = response.data;
|
|
} catch (error) {
|
|
console.error('AI Analysis Error:', error);
|
|
modalErrorMessage.value = error.response?.data?.error || "Une erreur est survenue lors de l'analyse.";
|
|
showErrorModal.value = true;
|
|
} finally {
|
|
isAnalyzing.value = false;
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<Head :title="'Candidat - ' + candidate.user.name" />
|
|
|
|
<AdminLayout>
|
|
<template #header>
|
|
<div class="flex items-center gap-4">
|
|
<Link :href="route('admin.candidates.index')" class="p-2 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors">
|
|
<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="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
|
</svg>
|
|
</Link>
|
|
<span>Détails Candidat</span>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- 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">
|
|
<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 class="flex-1">
|
|
<p class="font-bold text-emerald-800 dark:text-emerald-400">Action réussie !</p>
|
|
<p class="text-emerald-700 dark:text-emerald-500 text-sm">{{ flashSuccess }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<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-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-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>
|
|
{{ 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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div> <!-- Fin Hero Header (387) -->
|
|
|
|
<!-- 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
|
|
@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'"
|
|
>
|
|
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>
|
|
</div>
|
|
|
|
<!-- 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">
|
|
<div class="flex items-center gap-6">
|
|
<div class="relative flex items-center justify-center">
|
|
<svg class="w-24 h-24 transform -rotate-90">
|
|
<circle cx="48" cy="48" r="40" stroke="currentColor" stroke-width="8" fill="transparent" class="text-slate-700" />
|
|
<circle cx="48" cy="48" r="40" stroke="currentColor" stroke-width="8" fill="transparent" class="text-indigo-500"
|
|
:stroke-dasharray="251.2"
|
|
:stroke-dashoffset="251.2 - (candidate.weighted_score / 20) * 251.2"
|
|
stroke-linecap="round"
|
|
/>
|
|
</svg>
|
|
<span class="absolute text-2xl font-black">{{ candidate.weighted_score }}</span>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-2xl font-black tracking-tight mb-1">Score Global</h3>
|
|
<p class="text-slate-400 text-sm font-medium">Note pondérée sur 20</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-4">
|
|
<PrimaryButton @click="saveScores" v-if="scoreForm.isDirty" class="!bg-indigo-500 hover:!bg-indigo-400 !border-none animate-bounce">
|
|
Enregistrer les modifications
|
|
</PrimaryButton>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="p-8 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
<!-- CV Score -->
|
|
<div class="group">
|
|
<label class="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-3 block">Analyse CV /20</label>
|
|
<div class="relative">
|
|
<input type="number" v-model="scoreForm.cv_score" min="0" max="20" step="0.5"
|
|
class="w-full bg-slate-50 dark:bg-slate-900 border-none rounded-2xl p-4 font-black text-xl text-indigo-600 focus:ring-2 focus:ring-indigo-500/20 transition-all" />
|
|
<div class="absolute right-4 top-1/2 -translate-y-1/2 text-slate-300 font-bold">/ 20</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Letter Score -->
|
|
<div class="group">
|
|
<label class="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-3 block">Lettre Motiv. /10</label>
|
|
<div class="relative">
|
|
<input type="number" v-model="scoreForm.motivation_score" min="0" max="10" step="0.5"
|
|
class="w-full bg-slate-50 dark:bg-slate-900 border-none rounded-2xl p-4 font-black text-xl text-emerald-600 focus:ring-2 focus:ring-emerald-500/20 transition-all" />
|
|
<div class="absolute right-4 top-1/2 -translate-y-1/2 text-slate-300 font-bold">/ 10</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Interview Score -->
|
|
<div class="group">
|
|
<label class="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-3 block">Entretien /30</label>
|
|
<div class="relative">
|
|
<input type="number" v-model="scoreForm.interview_score" min="0" max="30" step="0.5"
|
|
class="w-full bg-slate-50 dark:bg-slate-900 border-none rounded-2xl p-4 font-black text-xl text-purple-600 focus:ring-2 focus:ring-purple-500/20 transition-all" />
|
|
<div class="absolute right-4 top-1/2 -translate-y-1/2 text-slate-300 font-bold">/ 30</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Test Score (Read Only) -->
|
|
<div class="group">
|
|
<label class="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-3 block">Test Technique /20</label>
|
|
<div class="w-full bg-slate-100 dark:bg-slate-900/50 rounded-2xl p-4 flex items-center justify-between border-2 border-dashed border-slate-200 dark:border-slate-700">
|
|
<span class="font-black text-xl text-slate-500">
|
|
{{ candidate.attempts.length > 0 ? (Math.max(...candidate.attempts.map(a => a.max_score > 0 ? a.score/a.max_score : 0)) * 20).toFixed(2) : '0.00' }}
|
|
</span>
|
|
<div class="p-2 bg-white dark:bg-slate-800 rounded-lg shadow-sm font-bold text-[10px] text-slate-400">
|
|
/ 20
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Radar Chart Section -->
|
|
<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-3">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z" />
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z" />
|
|
</svg>
|
|
Profil de Compétences
|
|
</h4>
|
|
<div class="flex flex-col items-end gap-1">
|
|
<span class="text-[10px] font-black uppercase tracking-widest text-slate-400">Score global</span>
|
|
<span class="text-2xl font-black text-indigo-600">{{ candidate.weighted_score }}<span class="text-sm text-slate-300 font-normal ml-1">/20</span></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
|
|
<!-- Radar Canvas -->
|
|
<div class="relative flex items-center justify-center">
|
|
<canvas ref="radarCanvasRef" class="max-h-72"></canvas>
|
|
</div>
|
|
|
|
<!-- Score Breakdown Legend -->
|
|
<div class="space-y-4">
|
|
<!-- CV -->
|
|
<div class="group">
|
|
<div class="flex justify-between items-center mb-1.5">
|
|
<span class="text-xs font-black uppercase tracking-widest text-slate-500">Analyse CV</span>
|
|
<span class="text-sm font-black text-indigo-600">{{ scoreForm.cv_score }} <span class="text-slate-300 font-normal">/ 20</span></span>
|
|
</div>
|
|
<div class="h-2 bg-slate-100 dark:bg-slate-700 rounded-full overflow-hidden">
|
|
<div
|
|
class="h-full bg-gradient-to-r from-indigo-400 to-indigo-600 rounded-full transition-all duration-700"
|
|
:style="{ width: (scoreForm.cv_score / 20 * 100) + '%' }"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Motivation -->
|
|
<div class="group">
|
|
<div class="flex justify-between items-center mb-1.5">
|
|
<span class="text-xs font-black uppercase tracking-widest text-slate-500">Lettre de Motivation</span>
|
|
<span class="text-sm font-black text-emerald-600">{{ scoreForm.motivation_score }} <span class="text-slate-300 font-normal">/ 10</span></span>
|
|
</div>
|
|
<div class="h-2 bg-slate-100 dark:bg-slate-700 rounded-full overflow-hidden">
|
|
<div
|
|
class="h-full bg-gradient-to-r from-emerald-400 to-emerald-600 rounded-full transition-all duration-700"
|
|
:style="{ width: (scoreForm.motivation_score / 10 * 100) + '%' }"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Entretien -->
|
|
<div class="group">
|
|
<div class="flex justify-between items-center mb-1.5">
|
|
<span class="text-xs font-black uppercase tracking-widest text-slate-500">Entretien</span>
|
|
<span class="text-sm font-black text-purple-600">{{ scoreForm.interview_score }} <span class="text-slate-300 font-normal">/ 30</span></span>
|
|
</div>
|
|
<div class="h-2 bg-slate-100 dark:bg-slate-700 rounded-full overflow-hidden">
|
|
<div
|
|
class="h-full bg-gradient-to-r from-purple-400 to-purple-600 rounded-full transition-all duration-700"
|
|
:style="{ width: (scoreForm.interview_score / 30 * 100) + '%' }"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Test Technique -->
|
|
<div class="group">
|
|
<div class="flex justify-between items-center mb-1.5">
|
|
<span class="text-xs font-black uppercase tracking-widest text-slate-500">Test Technique</span>
|
|
<span class="text-sm font-black text-amber-600">{{ bestTestScore.toFixed(2) }} <span class="text-slate-300 font-normal">/ 20</span></span>
|
|
</div>
|
|
<div class="h-2 bg-slate-100 dark:bg-slate-700 rounded-full overflow-hidden">
|
|
<div
|
|
class="h-full bg-gradient-to-r from-amber-400 to-amber-600 rounded-full transition-all duration-700"
|
|
:style="{ width: (bestTestScore / 20 * 100) + '%' }"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</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 -->
|
|
|
|
<!-- 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>
|
|
<h3 class="font-black text-3xl mb-4 flex items-center gap-4 flex-wrap">
|
|
<div class="p-3 bg-indigo-500 text-white rounded-2xl shadow-indigo-200 shadow-lg">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9l-.707.707M12 21v-1m4.243-4.243l-.707-.707m2.828-9.9l-.707.707" />
|
|
</svg>
|
|
</div>
|
|
<span>Analyse IA de la Candidature</span>
|
|
<div class="flex items-center gap-3">
|
|
<span v-if="aiAnalysis?.provider" class="text-sm px-4 py-1 rounded-full bg-slate-100 dark:bg-slate-900 text-slate-500 uppercase font-black border border-slate-200 dark:border-slate-700">
|
|
{{ aiAnalysis.provider }}
|
|
</span>
|
|
<span v-if="aiAnalysis?.analyzed_at" class="text-xs text-slate-400 italic font-medium">
|
|
Effectuée le {{ new Date(aiAnalysis.analyzed_at).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' }) }}
|
|
</span>
|
|
</div>
|
|
</h3>
|
|
<p class="text-sm text-slate-400 mt-2 uppercase font-black tracking-[0.2em]">Moteur décisionnel assisté par Intelligence Artificielle</p>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap items-center gap-6">
|
|
<!-- Provider Selector -->
|
|
<div v-if="props.ai_config?.enabled_providers" class="flex items-center bg-slate-100 dark:bg-slate-900/80 p-2 rounded-2xl border border-slate-200 dark:border-slate-800">
|
|
<button
|
|
v-for="provider in Object.keys(props.ai_config.enabled_providers)"
|
|
:key="provider"
|
|
@click="selectedProvider = provider"
|
|
class="px-6 py-2.5 text-xs font-black uppercase tracking-widest rounded-xl transition-all"
|
|
:class="selectedProvider === provider ? 'bg-white dark:bg-slate-800 shadow-xl text-indigo-600' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-300'"
|
|
>
|
|
{{ provider }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Force option for Super Admin -->
|
|
<div v-if="$page.props.auth.user.role === 'super_admin'" class="flex items-center gap-3 px-5 py-3 bg-red-50 dark:bg-red-900/10 border border-red-100 dark:border-red-900/30 rounded-2xl">
|
|
<input
|
|
type="checkbox"
|
|
id="forceAnalysis"
|
|
v-model="forceAnalysis"
|
|
class="rounded-lg border-red-300 text-red-600 focus:ring-red-500/20 w-5 h-5 cursor-pointer"
|
|
/>
|
|
<label for="forceAnalysis" class="text-xs font-black uppercase tracking-widest text-red-600 cursor-pointer select-none">
|
|
Forcer (Bypass 7j)
|
|
</label>
|
|
</div>
|
|
|
|
<PrimaryButton
|
|
@click="runAI"
|
|
:disabled="isAnalyzing"
|
|
class="!bg-indigo-600 hover:!bg-indigo-500 !border-none !rounded-2xl !py-4 !px-8 group shadow-xl shadow-indigo-200 dark:shadow-none"
|
|
>
|
|
<span v-if="isAnalyzing" class="flex items-center gap-3">
|
|
<svg class="animate-spin h-5 w-5 text-white" 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>
|
|
<span class="font-black uppercase tracking-widest text-xs">Analyse en cours...</span>
|
|
</span>
|
|
<span v-else class="flex items-center gap-3">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 group-hover:scale-110 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<span class="font-black uppercase tracking-widest text-xs">Lancer l'analyse complète</span>
|
|
</span>
|
|
</PrimaryButton>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- AI Results -->
|
|
<div v-if="aiAnalysis" class="space-y-12 animate-in fade-in slide-in-from-bottom-6 duration-1000">
|
|
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
|
<div class="lg:col-span-1 bg-slate-50 dark:bg-slate-900/50 p-8 rounded-[2.5rem] border border-slate-100 dark:border-slate-800 text-center flex flex-col items-center justify-center shadow-inner">
|
|
<div class="text-xs font-black uppercase tracking-[0.2em] text-slate-400 mb-6">Score Global</div>
|
|
<div class="text-7xl font-black text-indigo-600 mb-6 tracking-tighter">{{ aiAnalysis.match_score }}%</div>
|
|
<div
|
|
class="px-6 py-2 rounded-2xl text-xs font-black uppercase tracking-widest shadow-sm"
|
|
:class="[
|
|
aiAnalysis.match_score >= 80 ? 'bg-emerald-100 text-emerald-700' :
|
|
aiAnalysis.match_score >= 60 ? 'bg-amber-100 text-amber-700' :
|
|
'bg-red-100 text-red-700'
|
|
]"
|
|
>
|
|
{{ aiAnalysis.verdict }}
|
|
</div>
|
|
</div>
|
|
<div class="lg:col-span-3 bg-indigo-50/30 dark:bg-indigo-900/10 p-10 rounded-[2.5rem] border border-indigo-100/50 dark:border-indigo-900/30">
|
|
<div class="text-xs font-black uppercase tracking-[0.2em] text-indigo-400 mb-6">Synthèse du Profil & Potentiel</div>
|
|
<p class="text-2xl leading-relaxed text-slate-700 dark:text-slate-300 font-medium italic">
|
|
" {{ aiAnalysis.summary }} "
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-10">
|
|
<!-- Strengths -->
|
|
<div class="space-y-6">
|
|
<h5 class="flex items-center gap-3 text-sm font-black uppercase tracking-widest text-emerald-500">
|
|
<div class="w-8 h-8 rounded-xl bg-emerald-100 dark:bg-emerald-900/20 flex items-center justify-center">
|
|
<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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
Points Forts Identifiés
|
|
</h5>
|
|
<div class="grid grid-cols-1 gap-4">
|
|
<div v-for="(strength, idx) in aiAnalysis.strengths" :key="idx" class="flex items-start gap-4 p-5 bg-emerald-50/30 dark:bg-emerald-900/10 rounded-3xl border border-emerald-100/50 dark:border-emerald-800/30 transition-hover hover:border-emerald-300">
|
|
<div class="mt-1 flex-shrink-0 w-5 h-5 bg-emerald-500 rounded-full flex items-center justify-center text-white text-[10px] font-bold">✓</div>
|
|
<span class="text-lg text-slate-700 dark:text-slate-300">{{ strength }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Gaps -->
|
|
<div class="space-y-6">
|
|
<h5 class="flex items-center gap-3 text-sm font-black uppercase tracking-widest text-amber-500">
|
|
<div class="w-8 h-8 rounded-xl bg-amber-100 dark:bg-amber-900/20 flex items-center justify-center">
|
|
<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 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>
|
|
Points de Vigilance / Gaps
|
|
</h5>
|
|
<div class="grid grid-cols-1 gap-4">
|
|
<div v-for="(gap, idx) in aiAnalysis.gaps" :key="idx" class="flex items-start gap-4 p-5 bg-amber-50/30 dark:bg-amber-900/10 rounded-3xl border border-amber-100/50 dark:border-amber-800/30 hover:border-amber-300 transition-colors">
|
|
<div class="mt-1 flex-shrink-0 w-5 h-5 bg-amber-500 rounded-full flex items-center justify-center text-white text-[10px] font-bold">!</div>
|
|
<span class="text-lg text-slate-700 dark:text-slate-300">{{ gap }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Detailed Scores (if available) -->
|
|
<div v-if="aiAnalysis.scores_detailles" class="space-y-8">
|
|
<h5 class="text-sm font-black uppercase tracking-widest text-indigo-500 border-l-4 border-indigo-500 pl-4">Détail des scores par dimension</h5>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
|
<div v-for="(details, key) in aiAnalysis.scores_detailles" :key="key" class="bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 p-8 rounded-[2rem] shadow-sm hover:shadow-xl transition-all duration-300 group">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<span class="text-xs font-black uppercase tracking-[0.1em] text-slate-400 group-hover:text-indigo-500 transition-colors">
|
|
{{ key.replace(/_/g, ' ') }}
|
|
</span>
|
|
<span class="text-2xl font-black text-indigo-600 bg-indigo-50 dark:bg-indigo-900/30 px-4 py-1 rounded-xl">{{ details.score }}%</span>
|
|
</div>
|
|
<div class="h-3 bg-slate-100 dark:bg-slate-800 rounded-full mb-6 overflow-hidden">
|
|
<div class="h-full bg-gradient-to-r from-indigo-400 to-indigo-600 rounded-full transition-all duration-1000 group-hover:from-indigo-500 group-hover:to-purple-600" :style="{ width: details.score + '%' }"></div>
|
|
</div>
|
|
<p class="text-sm text-slate-500 dark:text-slate-400 leading-relaxed font-medium">{{ details.justification }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Blocking Elements -->
|
|
<div v-if="aiAnalysis.elements_bloquants?.length > 0 && aiAnalysis.elements_bloquants[0] !== ''" class="p-10 bg-red-50/50 dark:bg-red-900/10 border-2 border-dashed border-red-200 dark:border-red-900/30 rounded-[3rem]">
|
|
<h5 class="flex items-center gap-4 text-sm font-black uppercase tracking-widest text-red-600 mb-8">
|
|
<div class="w-10 h-10 rounded-2xl bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
|
|
<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="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>
|
|
Signaux Critiques / Éléments Bloquants
|
|
</h5>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div v-for="(item, idx) in aiAnalysis.elements_bloquants" :key="idx" class="flex items-center gap-4 p-6 bg-white dark:bg-slate-900 rounded-[2rem] border border-red-100 dark:border-red-900/20 text-lg text-red-700 dark:text-red-400 font-black shadow-sm">
|
|
<span class="w-3 h-3 bg-red-500 rounded-full animate-pulse"></span>
|
|
{{ item }}
|
|
</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">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-slate-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
</svg>
|
|
</div>
|
|
<p class="text-xl text-slate-400 font-bold uppercase tracking-widest">En attente d'analyse IA</p>
|
|
<p class="text-slate-400 mt-2">Cliquez sur le bouton ci-dessus pour lancer le moteur décisionnel.</p>
|
|
</div>
|
|
|
|
<div v-if="isAnalyzing" class="absolute inset-0 bg-white/80 dark:bg-slate-800/80 backdrop-blur-md z-20 flex flex-col items-center justify-center gap-6">
|
|
<div class="flex gap-2 animate-bounce">
|
|
<div class="w-4 h-4 bg-indigo-500 rounded-full"></div>
|
|
<div class="w-4 h-4 bg-purple-500 rounded-full"></div>
|
|
<div class="w-4 h-4 bg-indigo-500 rounded-full"></div>
|
|
</div>
|
|
<div class="text-center">
|
|
<p class="text-2xl font-black text-indigo-600 uppercase tracking-[0.2em] mb-2">Traitement IA en cours</p>
|
|
<p class="text-slate-500 font-medium">Analyse sémantique et pondération des scores...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-amber-500" 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>
|
|
Notes d'entretien & Préparation
|
|
</h4>
|
|
<div class="flex items-center gap-4">
|
|
<div class="flex items-center bg-slate-100 dark:bg-slate-900 p-1 rounded-xl">
|
|
<button
|
|
@click="isPreview = false"
|
|
class="px-4 py-1.5 text-[10px] font-black uppercase tracking-widest rounded-lg transition-all"
|
|
:class="!isPreview ? 'bg-white dark:bg-slate-800 shadow-sm text-indigo-600' : 'text-slate-500'"
|
|
>
|
|
Édition
|
|
</button>
|
|
<button
|
|
@click="isPreview = true"
|
|
class="px-4 py-1.5 text-[10px] font-black uppercase tracking-widest rounded-lg transition-all"
|
|
:class="isPreview ? 'bg-white dark:bg-slate-800 shadow-sm text-indigo-600' : 'text-slate-500'"
|
|
>
|
|
Aperçu
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="notesForm.isDirty" class="flex items-center gap-2 animate-pulse">
|
|
<PrimaryButton @click="saveNotes" class="!px-4 !py-1 text-[10px]" :disabled="notesForm.processing">
|
|
Enregistrer
|
|
</PrimaryButton>
|
|
</div>
|
|
<div v-else class="flex items-center gap-2 text-emerald-500">
|
|
<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 13l4 4L19 7" />
|
|
</svg>
|
|
<span class="text-[10px] font-black uppercase tracking-widest">Enregistré</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Markdown Help Guide -->
|
|
<div v-if="!isPreview" class="mb-6 grid grid-cols-2 md:grid-cols-4 gap-2">
|
|
<div class="p-2 border border-slate-100 dark:border-slate-700/50 rounded-lg text-[10px] text-slate-500 dark:text-slate-400">
|
|
<code class="text-indigo-500 font-bold"># Titre</code>, <code class="text-indigo-500 font-bold">## Sous-titre</code>
|
|
</div>
|
|
<div class="p-2 border border-slate-100 dark:border-slate-700/50 rounded-lg text-[10px] text-slate-500 dark:text-slate-400">
|
|
<code class="text-indigo-500 font-bold">**Gras**</code>, <code class="text-indigo-500 font-bold">*Italique*</code>
|
|
</div>
|
|
<div class="p-2 border border-slate-100 dark:border-slate-700/50 rounded-lg text-[10px] text-slate-500 dark:text-slate-400">
|
|
<code class="text-indigo-500 font-bold">* Liste</code>, <code class="text-indigo-500 font-bold">1. Liste num.</code>
|
|
</div>
|
|
<div class="p-2 border border-slate-100 dark:border-slate-700/50 rounded-lg text-[10px] text-slate-500 dark:text-slate-400">
|
|
<code class="text-indigo-500 font-bold">> Citation</code>, <code class="text-indigo-500 font-bold">--- (Ligne)</code>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="relative group">
|
|
<div v-if="isPreview"
|
|
class="prose dark:prose-invert prose-slate max-w-none w-full bg-slate-50 dark:bg-slate-900 rounded-2xl p-8 min-h-[300px] text-sm leading-relaxed"
|
|
v-html="renderedNotes">
|
|
</div>
|
|
<textarea
|
|
v-else
|
|
v-model="notesForm.notes"
|
|
rows="12"
|
|
class="w-full bg-slate-50 dark:bg-slate-900 border-none rounded-2xl p-6 text-sm selection:bg-indigo-100 dark:selection:bg-indigo-900 focus:ring-2 focus:ring-indigo-500/20 transition-all placeholder:text-slate-300 dark:placeholder:text-slate-600 leading-relaxed font-mono"
|
|
placeholder="Rédigez ici vos questions (utilisez # pour les titres, * pour les listes...)"
|
|
></textarea>
|
|
|
|
<div v-if="!isPreview" class="absolute bottom-4 right-4 text-[9px] font-bold text-slate-400 opacity-60 flex items-center gap-1">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
|
</svg>
|
|
Markdown Supporté
|
|
</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">
|
|
Historique des Tests
|
|
<span class="text-sm font-normal text-slate-400">{{ candidate.attempts.length }} tentative(s)</span>
|
|
</h3>
|
|
|
|
<div v-if="candidate.attempts.length > 0" class="space-y-6">
|
|
<div v-for="attempt in candidate.attempts" :key="attempt.id" class="bg-slate-50 dark:bg-slate-900 rounded-3xl border border-slate-200 dark:border-slate-800 overflow-hidden group">
|
|
<!-- Attempt Header -->
|
|
<div
|
|
@click="toggleAttempt(attempt.id)"
|
|
class="p-6 flex flex-col md:flex-row md:items-center justify-between gap-6 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800/50 transition-colors"
|
|
>
|
|
<div class="flex items-center gap-6">
|
|
<div class="px-5 py-3 bg-white dark:bg-slate-800 rounded-2xl border border-slate-200 dark:border-slate-700 shadow-sm text-center min-w-[100px]">
|
|
<div class="text-[10px] uppercase font-black tracking-widest text-slate-400">Score</div>
|
|
<div class="text-xl font-black text-indigo-600 dark:text-indigo-400">
|
|
{{ attempt.score }}<span class="text-slate-300 dark:text-slate-600 mx-1">/</span>{{ attempt.max_score || '?' }}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h4 class="text-xl font-black uppercase tracking-tight">{{ attempt.quiz?.title ?? 'Quiz supprimé' }}</h4>
|
|
<div class="text-xs text-slate-400 mt-1 uppercase font-bold tracking-widest">
|
|
Fini le {{ formatDateTime(attempt.finished_at) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-4 self-end md:self-auto">
|
|
<button
|
|
@click.stop="deleteAttempt(attempt.id)"
|
|
class="p-3 text-slate-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-xl transition-all"
|
|
title="Supprimer ce test"
|
|
>
|
|
<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 class="w-10 h-10 rounded-full bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 flex items-center justify-center text-slate-400 group-hover:text-indigo-600 group-hover:border-indigo-500 transition-all shadow-sm">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 transition-transform duration-300" :class="{ 'rotate-180': openAttempts.includes(attempt.id) }" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Accordion Content -->
|
|
<div v-show="openAttempts.includes(attempt.id)" class="border-t border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-800/50 divide-y divide-slate-100 dark:divide-slate-800 animate-in slide-in-from-top-2 duration-300">
|
|
<div v-for="(answer, aIdx) in attempt.answers" :key="answer.id" class="p-8">
|
|
<div class="flex items-start gap-6">
|
|
<div class="w-10 h-10 rounded-2xl bg-slate-50 dark:bg-slate-900 border border-slate-100 dark:border-slate-800 flex items-center justify-center font-black text-xs shrink-0 text-slate-400">
|
|
{{ String(aIdx + 1).padStart(2, '0') }}
|
|
</div>
|
|
<div class="flex-1">
|
|
<h5 class="text-sm font-bold text-slate-700 dark:text-slate-300 mb-4 leading-relaxed">{{ answer.question.label }}</h5>
|
|
|
|
<!-- QCM Answer -->
|
|
<div v-if="answer.question.type === 'qcm'" class="flex items-center">
|
|
<div
|
|
class="px-5 py-3 rounded-2xl text-[13px] font-bold border flex items-center gap-3 shadow-sm"
|
|
:class="[
|
|
answer.option.is_correct
|
|
? 'bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-900/20 dark:border-emerald-800 dark:text-emerald-400'
|
|
: 'bg-red-50 text-red-700 border-red-200 dark:bg-red-900/20 dark:border-red-800 dark:text-red-400'
|
|
]"
|
|
>
|
|
<div class="p-1 rounded-full bg-white/50 dark:bg-black/20">
|
|
<svg v-if="answer.option.is_correct" 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 13l4 4L19 7" />
|
|
</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="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</div>
|
|
{{ answer.option.option_text }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Open Answer -->
|
|
<div v-else class="space-y-4">
|
|
<div class="p-6 bg-slate-50 dark:bg-slate-900 border border-slate-100 dark:border-slate-800 rounded-3xl text-[13px] text-slate-600 dark:text-slate-400 leading-relaxed italic shadow-inner">
|
|
" {{ answer.text_content || 'Aucune réponse fournie.' }} "
|
|
</div>
|
|
|
|
<!-- Score Input for Open Question -->
|
|
<div class="flex items-center gap-4 bg-slate-50 dark:bg-slate-900/50 p-4 rounded-2xl border border-dashed border-slate-200 dark:border-slate-700">
|
|
<div class="text-[10px] font-black uppercase tracking-widest text-slate-400">Note Attribuée :</div>
|
|
<div class="flex items-center gap-2">
|
|
<input
|
|
type="number"
|
|
v-model="answer.score"
|
|
min="0"
|
|
:max="answer.question.points"
|
|
step="0.5"
|
|
@change="updateAnswerScore(answer.id, answer.score)"
|
|
class="w-20 bg-white dark:bg-slate-800 border-none rounded-xl py-1 px-3 font-black text-indigo-600 focus:ring-2 focus:ring-indigo-500/20 transition-all text-center"
|
|
/>
|
|
<span class="text-xs font-bold text-slate-300">/ {{ answer.question.points }}</span>
|
|
</div>
|
|
<div class="text-[9px] text-slate-400 italic">Enregistré au clic/changement</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else class="py-12 text-center text-slate-400 italic">
|
|
Ce candidat n'a pas encore terminé de test.
|
|
</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">
|
|
<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>
|
|
|
|
<!-- 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">
|
|
<div class="flex items-center gap-4 mb-4 text-red-600">
|
|
<div class="flex-shrink-0 w-12 h-12 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center">
|
|
<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="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>
|
|
<h2 class="text-xl font-bold">Attention</h2>
|
|
</div>
|
|
|
|
<p class="text-slate-600 dark:text-slate-400 mb-6">
|
|
{{ modalErrorMessage }}
|
|
</p>
|
|
|
|
<div class="flex justify-end">
|
|
<PrimaryButton @click="showErrorModal = false" class="!bg-red-600 hover:!bg-red-500 !border-none">
|
|
Fermer
|
|
</PrimaryButton>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
</template>
|