Initial commit
This commit is contained in:
250
resources/js/Pages/Admin/Candidates/Index.vue
Normal file
250
resources/js/Pages/Admin/Candidates/Index.vue
Normal file
@@ -0,0 +1,250 @@
|
||||
<script setup>
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue';
|
||||
import { Head, useForm, Link, usePage, router } from '@inertiajs/vue3';
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
const page = usePage();
|
||||
const flashSuccess = computed(() => page.props.flash?.success);
|
||||
import Modal from '@/Components/Modal.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
|
||||
const props = defineProps({
|
||||
candidates: Array
|
||||
});
|
||||
|
||||
const isModalOpen = ref(false);
|
||||
const selectedDocument = ref(null);
|
||||
|
||||
const form = useForm({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
linkedin_url: '',
|
||||
cv: null,
|
||||
cover_letter: null,
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('admin.candidates.store'), {
|
||||
onSuccess: () => {
|
||||
isModalOpen.value = false;
|
||||
form.reset();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deleteCandidate = (id) => {
|
||||
if (confirm('Voulez-vous vraiment supprimer ce candidat ?')) {
|
||||
router.delete(route('admin.candidates.destroy', id));
|
||||
}
|
||||
};
|
||||
|
||||
const openPreview = (doc) => {
|
||||
selectedDocument.value = doc;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Gestion des Candidats" />
|
||||
|
||||
<AdminLayout>
|
||||
<template #header>
|
||||
Gestion des Candidats
|
||||
</template>
|
||||
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<h3 class="text-2xl font-bold">Liste des Candidats</h3>
|
||||
<PrimaryButton @click="isModalOpen = true">
|
||||
Ajouter un Candidat
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
<div v-if="flashSuccess" class="mb-8 p-6 bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-800 rounded-2xl flex items-center gap-4 animate-in fade-in slide-in-from-top-4 duration-500">
|
||||
<div class="p-2 bg-emerald-500 rounded-lg text-white">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-bold text-emerald-800 dark:text-emerald-400">Succès !</p>
|
||||
<p class="text-emerald-700 dark:text-emerald-500 text-sm">{{ flashSuccess }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Candidates Table -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
<table class="w-full text-left">
|
||||
<thead class="bg-slate-50 dark:bg-slate-700/50 border-b border-slate-200 dark:border-slate-700">
|
||||
<tr>
|
||||
<th class="px-6 py-4 font-semibold text-slate-700 dark:text-slate-300">Nom</th>
|
||||
<th class="px-6 py-4 font-semibold text-slate-700 dark:text-slate-300">Email</th>
|
||||
<th class="px-6 py-4 font-semibold text-slate-700 dark:text-slate-300">Statut</th>
|
||||
<th class="px-6 py-4 font-semibold text-slate-700 dark:text-slate-300">Documents</th>
|
||||
<th class="px-6 py-4 font-semibold text-slate-700 dark:text-slate-300 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-200 dark:divide-slate-700">
|
||||
<tr v-for="candidate in candidates" :key="candidate.id" class="hover:bg-slate-50/50 dark:hover:bg-slate-700/30 transition-colors">
|
||||
<td class="px-6 py-4">
|
||||
<div class="font-medium">{{ candidate.user.name }}</div>
|
||||
<div class="text-xs text-slate-500">{{ candidate.phone }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-slate-600 dark:text-slate-400">
|
||||
{{ candidate.user.email }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span
|
||||
class="px-2 py-1 rounded-full text-xs font-semibold"
|
||||
:class="{
|
||||
'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400': candidate.status === 'en_attente',
|
||||
'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400': candidate.status === 'en_cours',
|
||||
'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400': candidate.status === 'termine'
|
||||
}"
|
||||
>
|
||||
{{ candidate.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
v-for="doc in candidate.documents"
|
||||
:key="doc.id"
|
||||
@click="openPreview(doc)"
|
||||
class="p-2 bg-slate-100 dark:bg-slate-700 rounded-lg hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors"
|
||||
:title="doc.type.toUpperCase()"
|
||||
>
|
||||
<svg v-if="doc.type === 'cv'" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-indigo-600 dark:text-indigo-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-emerald-600 dark:text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<div class="flex items-center justify-end gap-3">
|
||||
<Link :href="route('admin.candidates.show', candidate.id)" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 font-medium">Détails</Link>
|
||||
<button @click="deleteCandidate(candidate.id)" class="p-2 text-slate-400 hover:text-red-600 transition-colors" title="Supprimer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="candidates.length === 0">
|
||||
<td colspan="5" class="px-6 py-12 text-center text-slate-500 italic">
|
||||
Aucun candidat trouvé.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Add Candidate Modal -->
|
||||
<Modal :show="isModalOpen" @close="isModalOpen = false">
|
||||
<div class="p-6">
|
||||
<h3 class="text-xl font-bold mb-6">Ajouter un nouveau candidat</h3>
|
||||
|
||||
<form @submit.prevent="submit" class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<InputLabel for="name" value="Nom Complet" />
|
||||
<TextInput id="name" type="text" class="mt-1 block w-full" v-model="form.name" required />
|
||||
<InputError class="mt-2" :message="form.errors.name" />
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel for="email" value="Email" />
|
||||
<TextInput id="email" type="email" class="mt-1 block w-full" v-model="form.email" required />
|
||||
<InputError class="mt-2" :message="form.errors.email" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<InputLabel for="phone" value="Téléphone" />
|
||||
<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="linkedin_url" value="LinkedIn URL" />
|
||||
<TextInput id="linkedin_url" type="url" class="mt-1 block w-full" v-model="form.linkedin_url" />
|
||||
<InputError class="mt-2" :message="form.errors.linkedin_url" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<InputLabel value="CV (PDF)" />
|
||||
<div class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-slate-300 dark:border-slate-600 border-dashed rounded-lg">
|
||||
<div class="space-y-1 text-center">
|
||||
<svg class="mx-auto h-10 w-10 text-slate-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
|
||||
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<div class="flex text-sm text-slate-600 dark:text-slate-400">
|
||||
<label class="relative cursor-pointer bg-white dark:bg-slate-800 rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
|
||||
<span>Téléverser</span>
|
||||
<input type="file" class="sr-only" @input="form.cv = $event.target.files[0]" accept="application/pdf" />
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500">{{ form.cv ? form.cv.name : 'PDF uniquement' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<InputError class="mt-2" :message="form.errors.cv" />
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel value="Lettre de motivation (PDF)" />
|
||||
<div class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-slate-300 dark:border-slate-600 border-dashed rounded-lg">
|
||||
<div class="space-y-1 text-center">
|
||||
<svg class="mx-auto h-10 w-10 text-slate-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
|
||||
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<div class="flex text-sm text-slate-600 dark:text-slate-400">
|
||||
<label class="relative cursor-pointer bg-white dark:bg-slate-800 rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
|
||||
<span>Téléverser</span>
|
||||
<input type="file" class="sr-only" @input="form.cover_letter = $event.target.files[0]" accept="application/pdf" />
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500">{{ form.cover_letter ? form.cover_letter.name : 'PDF uniquement' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<InputError class="mt-2" :message="form.errors.cover_letter" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 mt-8">
|
||||
<SecondaryButton @click="isModalOpen = false" :disabled="form.processing">
|
||||
Annuler
|
||||
</SecondaryButton>
|
||||
<PrimaryButton :disabled="form.processing">
|
||||
Enregistrer Candidat
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<!-- 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>
|
||||
321
resources/js/Pages/Admin/Candidates/Show.vue
Normal file
321
resources/js/Pages/Admin/Candidates/Show.vue
Normal file
@@ -0,0 +1,321 @@
|
||||
<script setup>
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue';
|
||||
import { Head, Link, router, useForm, usePage } from '@inertiajs/vue3';
|
||||
import { ref, computed } from 'vue';
|
||||
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';
|
||||
|
||||
const props = defineProps({
|
||||
candidate: Object
|
||||
});
|
||||
|
||||
const page = usePage();
|
||||
const flashSuccess = computed(() => page.props.flash?.success);
|
||||
|
||||
const selectedDocument = ref(null);
|
||||
|
||||
const docForm = useForm({
|
||||
cv: null,
|
||||
cover_letter: null,
|
||||
_method: 'PUT' // For file upload via PUT in Laravel
|
||||
});
|
||||
|
||||
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 openPreview = (doc) => {
|
||||
selectedDocument.value = doc;
|
||||
};
|
||||
</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="grid grid-cols-1 xl:grid-cols-3 gap-8">
|
||||
<!-- Sidebar: Profile & Docs -->
|
||||
<div class="space-y-8">
|
||||
<!-- Profile Card -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
<div class="h-24 bg-gradient-to-r from-indigo-500 to-purple-600"></div>
|
||||
<div class="px-6 pb-6 text-center -mt-12">
|
||||
<div class="w-24 h-24 bg-white dark:bg-slate-900 rounded-2xl shadow-xl border-4 border-white dark:border-slate-800 flex items-center justify-center text-4xl font-black text-indigo-600 mx-auto mb-4">
|
||||
{{ candidate.user.name.charAt(0) }}
|
||||
</div>
|
||||
<h3 class="text-xl font-bold">{{ candidate.user.name }}</h3>
|
||||
<p class="text-slate-500 text-sm mb-6">{{ candidate.user.email }}</p>
|
||||
|
||||
<div class="flex flex-col gap-3 text-left">
|
||||
<div class="flex items-center gap-3 p-3 bg-slate-50 dark:bg-slate-900 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
</svg>
|
||||
<span class="text-sm font-medium">{{ candidate.phone || 'Non renseigné' }}</span>
|
||||
</div>
|
||||
<a v-if="candidate.linkedin_url" :href="candidate.linkedin_url" target="_blank" class="flex items-center gap-3 p-3 bg-slate-50 dark:bg-slate-900 rounded-xl hover:text-indigo-600 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-slate-400" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/>
|
||||
</svg>
|
||||
<span class="text-sm font-medium">LinkedIn Profile</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-6 py-4 bg-slate-50 dark:bg-slate-900 border-t border-slate-200 dark:border-slate-700 flex justify-between items-center gap-2">
|
||||
<SecondaryButton @click="resetPassword" class="!px-3 !py-1 text-[10px] uppercase font-bold tracking-widest">Réinitialiser MDP</SecondaryButton>
|
||||
<DangerButton @click="deleteCandidate" class="!px-3 !py-1 text-[10px] uppercase font-bold tracking-widest">Supprimer Compte</DangerButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Documents Card -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700 p-6">
|
||||
<h4 class="font-bold mb-4 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
|
||||
</svg>
|
||||
Documents joints
|
||||
</h4>
|
||||
<div class="space-y-3">
|
||||
<button
|
||||
v-for="doc in candidate.documents"
|
||||
:key="doc.id"
|
||||
@click="openPreview(doc)"
|
||||
class="w-full flex items-center justify-between p-4 bg-slate-100 dark:bg-slate-900 rounded-xl hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors group"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 bg-white dark:bg-slate-800 rounded-lg group-hover:bg-indigo-500 group-hover:text-white transition-colors">
|
||||
<svg v-if="doc.type === 'cv'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<div class="text-sm font-bold uppercase tracking-tight">{{ doc.type }}</div>
|
||||
<div class="text-[10px] text-slate-500 truncate max-w-[150px]">{{ doc.original_name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-slate-400 group-hover:translate-x-1 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="pt-6 mt-6 border-t border-slate-100 dark:border-slate-700">
|
||||
<h5 class="text-xs font-black uppercase text-slate-400 tracking-widest mb-4">Ajouter / Remplacer</h5>
|
||||
<form @submit.prevent="updateDocuments" class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="relative group/file">
|
||||
<label class="flex flex-col items-center justify-center p-3 bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl cursor-pointer hover:border-indigo-500 transition-colors">
|
||||
<span class="text-[10px] font-bold uppercase tracking-tight text-slate-500">CV (PDF)</span>
|
||||
<span class="text-[9px] text-slate-400 truncate w-full text-center mt-1">{{ docForm.cv ? docForm.cv.name : 'Choisir...' }}</span>
|
||||
<input type="file" class="hidden" @input="docForm.cv = $event.target.files[0]" accept="application/pdf" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="relative group/file">
|
||||
<label class="flex flex-col items-center justify-center p-3 bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl cursor-pointer hover:border-emerald-500 transition-colors">
|
||||
<span class="text-[10px] font-bold uppercase tracking-tight text-slate-500">Lettre (PDF)</span>
|
||||
<span class="text-[9px] text-slate-400 truncate w-full text-center mt-1">{{ docForm.cover_letter ? docForm.cover_letter.name : 'Choisir...' }}</span>
|
||||
<input type="file" class="hidden" @input="docForm.cover_letter = $event.target.files[0]" accept="application/pdf" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<InputError :message="docForm.errors.cv" />
|
||||
<InputError :message="docForm.errors.cover_letter" />
|
||||
<PrimaryButton class="w-full !justify-center !py-2 text-xs" :disabled="docForm.processing || (!docForm.cv && !docForm.cover_letter)">
|
||||
Mettre à jour les fichiers
|
||||
</PrimaryButton>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main: Attempts -->
|
||||
<div class="xl:col-span-2 space-y-8">
|
||||
<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 }}</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>
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
Reference in New Issue
Block a user