AI Analysis: JobPosition infrastructure and candidate association
This commit is contained in:
@@ -75,11 +75,13 @@ class CandidateController extends Controller
|
|||||||
'documents',
|
'documents',
|
||||||
'attempts.quiz',
|
'attempts.quiz',
|
||||||
'attempts.answers.question',
|
'attempts.answers.question',
|
||||||
'attempts.answers.option'
|
'attempts.answers.option',
|
||||||
|
'jobPosition'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return \Inertia\Inertia::render('Admin/Candidates/Show', [
|
return \Inertia\Inertia::render('Admin/Candidates/Show', [
|
||||||
'candidate' => $candidate
|
'candidate' => $candidate,
|
||||||
|
'jobPositions' => \App\Models\JobPosition::all()
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,6 +144,19 @@ class CandidateController extends Controller
|
|||||||
return back()->with('success', 'Notes mises à jour avec succès.');
|
return back()->with('success', 'Notes mises à jour avec succès.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function updatePosition(Request $request, Candidate $candidate)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'job_position_id' => 'nullable|exists:job_positions,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$candidate->update([
|
||||||
|
'job_position_id' => $request->job_position_id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('success', 'Fiche de poste associée au candidat.');
|
||||||
|
}
|
||||||
|
|
||||||
public function resetPassword(Candidate $candidate)
|
public function resetPassword(Candidate $candidate)
|
||||||
{
|
{
|
||||||
$password = Str::random(10);
|
$password = Str::random(10);
|
||||||
|
|||||||
73
app/Http/Controllers/JobPositionController.php
Normal file
73
app/Http/Controllers/JobPositionController.php
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\JobPosition;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
class JobPositionController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$this->authorizeAdmin();
|
||||||
|
|
||||||
|
return Inertia::render('Admin/JobPositions/Index', [
|
||||||
|
'jobPositions' => JobPosition::all()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$this->authorizeAdmin();
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'title' => 'required|string|max:255',
|
||||||
|
'description' => 'required|string',
|
||||||
|
'requirements' => 'nullable|array',
|
||||||
|
]);
|
||||||
|
|
||||||
|
JobPosition::create([
|
||||||
|
'title' => $request->title,
|
||||||
|
'description' => $request->description,
|
||||||
|
'requirements' => $request->requirements,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('success', 'Fiche de poste créée avec succès.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, JobPosition $jobPosition)
|
||||||
|
{
|
||||||
|
$this->authorizeAdmin();
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'title' => 'required|string|max:255',
|
||||||
|
'description' => 'required|string',
|
||||||
|
'requirements' => 'nullable|array',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$jobPosition->update([
|
||||||
|
'title' => $request->title,
|
||||||
|
'description' => $request->description,
|
||||||
|
'requirements' => $request->requirements,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('success', 'Fiche de poste mise à jour.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(JobPosition $jobPosition)
|
||||||
|
{
|
||||||
|
$this->authorizeAdmin();
|
||||||
|
|
||||||
|
$jobPosition->delete();
|
||||||
|
|
||||||
|
return back()->with('success', 'Fiche de poste supprimée.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function authorizeAdmin()
|
||||||
|
{
|
||||||
|
if (!auth()->user()->isAdmin()) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,11 +9,16 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
||||||
#[Fillable(['user_id', 'phone', 'linkedin_url', 'status', 'notes', 'cv_score', 'motivation_score', 'interview_score'])]
|
#[Fillable(['user_id', 'job_position_id', 'phone', 'linkedin_url', 'status', 'notes', 'cv_score', 'motivation_score', 'interview_score'])]
|
||||||
class Candidate extends Model
|
class Candidate extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
public function jobPosition(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(JobPosition::class);
|
||||||
|
}
|
||||||
|
|
||||||
protected $appends = ['weighted_score'];
|
protected $appends = ['weighted_score'];
|
||||||
|
|
||||||
public function getWeightedScoreAttribute(): float
|
public function getWeightedScoreAttribute(): float
|
||||||
|
|||||||
23
app/Models/JobPosition.php
Normal file
23
app/Models/JobPosition.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
|
||||||
|
#[Fillable(['title', 'description', 'requirements'])]
|
||||||
|
class JobPosition extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'requirements' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function candidates(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Candidate::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
"laravel/framework": "^13.0",
|
"laravel/framework": "^13.0",
|
||||||
"laravel/sanctum": "^4.0",
|
"laravel/sanctum": "^4.0",
|
||||||
"laravel/tinker": "^3.0",
|
"laravel/tinker": "^3.0",
|
||||||
|
"smalot/pdfparser": "^2.12",
|
||||||
"tightenco/ziggy": "^2.0"
|
"tightenco/ziggy": "^2.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
|||||||
53
composer.lock
generated
53
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "377f950bdc38b8b812713688e6de1d7d",
|
"content-hash": "6b34d5dd0c12bcfc3d1253f72a392749",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
@@ -3433,6 +3433,57 @@
|
|||||||
},
|
},
|
||||||
"time": "2025-12-14T04:43:48+00:00"
|
"time": "2025-12-14T04:43:48+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "smalot/pdfparser",
|
||||||
|
"version": "v2.12.4",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/smalot/pdfparser.git",
|
||||||
|
"reference": "028d7cc0ceff323bc001d763caa2bbdf611866c4"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/smalot/pdfparser/zipball/028d7cc0ceff323bc001d763caa2bbdf611866c4",
|
||||||
|
"reference": "028d7cc0ceff323bc001d763caa2bbdf611866c4",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-iconv": "*",
|
||||||
|
"ext-zlib": "*",
|
||||||
|
"php": ">=7.1",
|
||||||
|
"symfony/polyfill-mbstring": "^1.18"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-0": {
|
||||||
|
"Smalot\\PdfParser\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"LGPL-3.0"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Sebastien MALOT",
|
||||||
|
"email": "sebastien@malot.fr"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Pdf parser library. Can read and extract information from pdf file.",
|
||||||
|
"homepage": "https://www.pdfparser.org",
|
||||||
|
"keywords": [
|
||||||
|
"extract",
|
||||||
|
"parse",
|
||||||
|
"parser",
|
||||||
|
"pdf",
|
||||||
|
"text"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/smalot/pdfparser/issues",
|
||||||
|
"source": "https://github.com/smalot/pdfparser/tree/v2.12.4"
|
||||||
|
},
|
||||||
|
"time": "2026-03-10T15:39:47+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/clock",
|
"name": "symfony/clock",
|
||||||
"version": "v7.4.0",
|
"version": "v7.4.0",
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('job_positions', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('title');
|
||||||
|
$table->text('description');
|
||||||
|
$table->json('requirements')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('job_positions');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('candidates', function (Blueprint $table) {
|
||||||
|
$table->foreignId('job_position_id')->nullable()->after('user_id')->constrained()->onDelete('set null');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('candidates', function (Blueprint $table) {
|
||||||
|
$table->dropConstrainedForeignId('job_position_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -56,6 +56,17 @@ const isSidebarOpen = ref(true);
|
|||||||
<span v-if="isSidebarOpen">Quiz</span>
|
<span v-if="isSidebarOpen">Quiz</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
:href="route('admin.job-positions.index')"
|
||||||
|
class="flex items-center gap-3 px-3 py-2 rounded-lg transition-colors"
|
||||||
|
:class="[route().current('admin.job-positions.*') ? 'bg-indigo-50 text-indigo-600 dark:bg-indigo-900/50 dark:text-indigo-400' : 'hover:bg-slate-50 dark:hover:bg-slate-700/50']"
|
||||||
|
>
|
||||||
|
<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 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>
|
||||||
|
<span v-if="isSidebarOpen">Fiches de Poste</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
:href="route('admin.comparative')"
|
:href="route('admin.comparative')"
|
||||||
class="flex items-center gap-3 px-3 py-2 rounded-lg transition-colors"
|
class="flex items-center gap-3 px-3 py-2 rounded-lg transition-colors"
|
||||||
|
|||||||
@@ -10,12 +10,23 @@ import DangerButton from '@/Components/DangerButton.vue';
|
|||||||
import InputError from '@/Components/InputError.vue';
|
import InputError from '@/Components/InputError.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
candidate: Object
|
candidate: Object,
|
||||||
|
jobPositions: Array
|
||||||
});
|
});
|
||||||
|
|
||||||
const page = usePage();
|
const page = usePage();
|
||||||
const flashSuccess = computed(() => page.props.flash?.success);
|
const flashSuccess = computed(() => page.props.flash?.success);
|
||||||
|
|
||||||
|
const positionForm = useForm({
|
||||||
|
job_position_id: props.candidate.job_position_id || ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatePosition = () => {
|
||||||
|
positionForm.patch(route('admin.candidates.update-position', props.candidate.id), {
|
||||||
|
preserveScroll: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const selectedDocument = ref(null);
|
const selectedDocument = ref(null);
|
||||||
|
|
||||||
const docForm = useForm({
|
const docForm = useForm({
|
||||||
@@ -149,7 +160,21 @@ const updateAnswerScore = (answerId, score) => {
|
|||||||
{{ candidate.user.name.charAt(0) }}
|
{{ candidate.user.name.charAt(0) }}
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl font-bold">{{ candidate.user.name }}</h3>
|
<h3 class="text-xl font-bold">{{ candidate.user.name }}</h3>
|
||||||
<p class="text-slate-500 text-sm mb-6">{{ candidate.user.email }}</p>
|
<p class="text-slate-500 text-sm mb-4">{{ candidate.user.email }}</p>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-2 block text-left">Poste Cible</label>
|
||||||
|
<select
|
||||||
|
v-model="positionForm.job_position_id"
|
||||||
|
@change="updatePosition"
|
||||||
|
class="w-full bg-slate-50 dark:bg-slate-900 border-none rounded-xl py-2 px-3 text-xs font-bold text-indigo-600 focus:ring-2 focus:ring-indigo-500/20 transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
<option value="">Non assigné</option>
|
||||||
|
<option v-for="pos in jobPositions" :key="pos.id" :value="pos.id">
|
||||||
|
{{ pos.title }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-3 text-left">
|
<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">
|
<div class="flex items-center gap-3 p-3 bg-slate-50 dark:bg-slate-900 rounded-xl">
|
||||||
|
|||||||
200
resources/js/Pages/Admin/JobPositions/Index.vue
Normal file
200
resources/js/Pages/Admin/JobPositions/Index.vue
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
<script setup>
|
||||||
|
import AdminLayout from '@/Layouts/AdminLayout.vue';
|
||||||
|
import { Head, useForm } from '@inertiajs/vue3';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import Modal from '@/Components/Modal.vue';
|
||||||
|
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||||
|
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
||||||
|
import DangerButton from '@/Components/DangerButton.vue';
|
||||||
|
import InputError from '@/Components/InputError.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
jobPositions: Array
|
||||||
|
});
|
||||||
|
|
||||||
|
const showingModal = ref(false);
|
||||||
|
const editingPosition = ref(null);
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
requirements: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const openModal = (position = null) => {
|
||||||
|
editingPosition.value = position;
|
||||||
|
if (position) {
|
||||||
|
form.title = position.title;
|
||||||
|
form.description = position.description;
|
||||||
|
form.requirements = position.requirements || [];
|
||||||
|
} else {
|
||||||
|
form.reset();
|
||||||
|
}
|
||||||
|
showingModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
showingModal.value = false;
|
||||||
|
form.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
if (editingPosition.value) {
|
||||||
|
form.put(route('admin.job-positions.update', editingPosition.value.id), {
|
||||||
|
onSuccess: () => closeModal(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
form.post(route('admin.job-positions.store'), {
|
||||||
|
onSuccess: () => closeModal(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deletePosition = (id) => {
|
||||||
|
if (confirm('Voulez-vous vraiment supprimer cette fiche de poste ?')) {
|
||||||
|
form.delete(route('admin.job-positions.destroy', id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addRequirement = () => {
|
||||||
|
form.requirements.push('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeRequirement = (index) => {
|
||||||
|
form.requirements.splice(index, 1);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Head title="Fiches de Poste" />
|
||||||
|
|
||||||
|
<AdminLayout>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h2 class="text-xl font-semibold leading-tight capitalize">
|
||||||
|
Fiches de Poste (Analyse IA)
|
||||||
|
</h2>
|
||||||
|
<PrimaryButton @click="openModal()">
|
||||||
|
Nouvelle Fiche
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="p-8">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
<div
|
||||||
|
v-for="position in jobPositions"
|
||||||
|
:key="position.id"
|
||||||
|
class="bg-white dark:bg-slate-800 rounded-3xl p-8 shadow-sm border border-slate-200 dark:border-slate-700 hover:shadow-2xl transition-all duration-300 group flex flex-col h-full"
|
||||||
|
>
|
||||||
|
<div class="mb-6 flex-1">
|
||||||
|
<div class="text-[10px] font-black uppercase tracking-widest text-indigo-500 mb-2">Poste / Compétences</div>
|
||||||
|
<h3 class="text-2xl font-black mb-3 group-hover:text-indigo-600 transition-colors">{{ position.title }}</h3>
|
||||||
|
<p class="text-slate-500 dark:text-slate-400 text-sm line-clamp-3 leading-relaxed">
|
||||||
|
{{ position.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 mb-6" v-if="position.requirements?.length">
|
||||||
|
<span
|
||||||
|
v-for="(req, idx) in position.requirements.slice(0, 3)"
|
||||||
|
:key="idx"
|
||||||
|
class="px-2 py-1 bg-slate-100 dark:bg-slate-900 rounded-lg text-[10px] font-bold text-slate-500"
|
||||||
|
>
|
||||||
|
{{ req }}
|
||||||
|
</span>
|
||||||
|
<span v-if="position.requirements.length > 3" class="text-[10px] text-slate-400 font-bold">
|
||||||
|
+{{ position.requirements.length - 3 }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-6 border-t border-slate-100 dark:border-slate-700 flex justify-between gap-3">
|
||||||
|
<SecondaryButton @click="openModal(position)" class="flex-1 !justify-center !py-2 text-xs">Modifier</SecondaryButton>
|
||||||
|
<button
|
||||||
|
@click="deletePosition(position.id)"
|
||||||
|
class="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-xl transition-all"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-if="jobPositions.length === 0" class="col-span-full py-32 text-center">
|
||||||
|
<div class="inline-flex p-6 bg-slate-100 dark:bg-slate-800 rounded-full mb-6">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-2xl font-black mb-2">Aucune fiche de poste</h3>
|
||||||
|
<p class="text-slate-500 mb-8">Créez votre première fiche de poste pour permettre l'analyse IA.</p>
|
||||||
|
<PrimaryButton @click="openModal()">Créer une fiche</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Create/Edit -->
|
||||||
|
<Modal :show="showingModal" @close="closeModal">
|
||||||
|
<div class="p-8">
|
||||||
|
<h3 class="text-2xl font-black mb-8">{{ editingPosition ? 'Modifier' : 'Créer' }} la Fiche de Poste</h3>
|
||||||
|
|
||||||
|
<form @submit.prevent="submit" class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-black uppercase tracking-widest text-slate-400 mb-2">Titre du Poste</label>
|
||||||
|
<input
|
||||||
|
v-model="form.title"
|
||||||
|
type="text"
|
||||||
|
class="w-full bg-slate-50 dark:bg-slate-900 border-none rounded-2xl p-4 focus:ring-2 focus:ring-indigo-500/20 transition-all font-bold"
|
||||||
|
placeholder="Ex: Développeur Fullstack Senior"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<InputError :message="form.errors.title" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-black uppercase tracking-widest text-slate-400 mb-2">Description / Fiche de Poste</label>
|
||||||
|
<textarea
|
||||||
|
v-model="form.description"
|
||||||
|
rows="8"
|
||||||
|
class="w-full bg-slate-50 dark:bg-slate-900 border-none rounded-2xl p-4 focus:ring-2 focus:ring-indigo-500/20 transition-all text-sm leading-relaxed"
|
||||||
|
placeholder="Détaillez les missions et les attentes pour ce poste..."
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
<InputError :message="form.errors.description" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<label class="text-xs font-black uppercase tracking-widest text-slate-400">Compétences clés / Pré-requis</label>
|
||||||
|
<button type="button" @click="addRequirement" class="text-[10px] font-black text-indigo-500 uppercase hover:underline">+ Ajouter</button>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div v-for="(req, index) in form.requirements" :key="index" class="flex gap-2">
|
||||||
|
<input
|
||||||
|
v-model="form.requirements[index]"
|
||||||
|
type="text"
|
||||||
|
class="flex-1 bg-slate-50 dark:bg-slate-900 border-none rounded-xl p-3 text-xs font-bold focus:ring-2 focus:ring-indigo-500/20 transition-all"
|
||||||
|
placeholder="Ex: Maitrise de Laravel / Vue.js"
|
||||||
|
>
|
||||||
|
<button type="button" @click="removeRequirement(index)" class="p-2 text-slate-400 hover:text-red-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="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-8 border-t border-slate-100 dark:border-slate-800 flex justify-end gap-3">
|
||||||
|
<SecondaryButton @click="closeModal" :disabled="form.processing">Annuler</SecondaryButton>
|
||||||
|
<PrimaryButton :disabled="form.processing">
|
||||||
|
{{ editingPosition ? 'Mettre à jour' : 'Enregistrer' }}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</AdminLayout>
|
||||||
|
</template>
|
||||||
@@ -74,10 +74,12 @@ Route::middleware('auth')->group(function () {
|
|||||||
Route::resource('candidates', \App\Http\Controllers\CandidateController::class)->only(['index', 'store', 'show', 'destroy', 'update']);
|
Route::resource('candidates', \App\Http\Controllers\CandidateController::class)->only(['index', 'store', 'show', 'destroy', 'update']);
|
||||||
Route::patch('/candidates/{candidate}/notes', [\App\Http\Controllers\CandidateController::class, 'updateNotes'])->name('candidates.update-notes');
|
Route::patch('/candidates/{candidate}/notes', [\App\Http\Controllers\CandidateController::class, 'updateNotes'])->name('candidates.update-notes');
|
||||||
Route::patch('/candidates/{candidate}/scores', [\App\Http\Controllers\CandidateController::class, 'updateScores'])->name('candidates.update-scores');
|
Route::patch('/candidates/{candidate}/scores', [\App\Http\Controllers\CandidateController::class, 'updateScores'])->name('candidates.update-scores');
|
||||||
|
Route::patch('/candidates/{candidate}/position', [\App\Http\Controllers\CandidateController::class, 'updatePosition'])->name('candidates.update-position');
|
||||||
Route::post('/candidates/{candidate}/reset-password', [\App\Http\Controllers\CandidateController::class, 'resetPassword'])->name('candidates.reset-password');
|
Route::post('/candidates/{candidate}/reset-password', [\App\Http\Controllers\CandidateController::class, 'resetPassword'])->name('candidates.reset-password');
|
||||||
Route::get('/documents/{document}', [\App\Http\Controllers\DocumentController::class, 'show'])->name('documents.show');
|
Route::get('/documents/{document}', [\App\Http\Controllers\DocumentController::class, 'show'])->name('documents.show');
|
||||||
|
|
||||||
Route::resource('quizzes', \App\Http\Controllers\QuizController::class)->only(['index', 'store', 'show', 'update', 'destroy']);
|
Route::resource('quizzes', \App\Http\Controllers\QuizController::class)->only(['index', 'store', 'show', 'update', 'destroy']);
|
||||||
|
Route::resource('job-positions', \App\Http\Controllers\JobPositionController::class)->only(['index', 'store', 'update', 'destroy']);
|
||||||
Route::resource('quizzes.questions', \App\Http\Controllers\QuestionController::class)->only(['store', 'update', 'destroy']);
|
Route::resource('quizzes.questions', \App\Http\Controllers\QuestionController::class)->only(['store', 'update', 'destroy']);
|
||||||
Route::delete('/attempts/{attempt}', [\App\Http\Controllers\AttemptController::class, 'destroy'])->name('attempts.destroy');
|
Route::delete('/attempts/{attempt}', [\App\Http\Controllers\AttemptController::class, 'destroy'])->name('attempts.destroy');
|
||||||
Route::patch('/answers/{answer}/score', [\App\Http\Controllers\AttemptController::class, 'updateAnswerScore'])->name('answers.update-score');
|
Route::patch('/answers/{answer}/score', [\App\Http\Controllers\AttemptController::class, 'updateAnswerScore'])->name('answers.update-score');
|
||||||
|
|||||||
Reference in New Issue
Block a user