installation du module RH
This commit is contained in:
78
app/Http/Controllers/Admin/JobPositionAiHelperController.php
Normal file
78
app/Http/Controllers/Admin/JobPositionAiHelperController.php
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class JobPositionAiHelperController extends Controller
|
||||||
|
{
|
||||||
|
public function generate(Request $request)
|
||||||
|
{
|
||||||
|
if (!auth()->user()->isAdmin()) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'title' => 'required|string',
|
||||||
|
'description' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$prompt = "Tu es un expert en ingénierie logicielle spécialisé dans la Fonction Publique Territoriale (FPT) et le Code Général de la Fonction Publique (CGFP).
|
||||||
|
Mon application permet de créer des offres d'emploi. Je n'ai pas encore de backend RH métier. Ton rôle est de transformer une saisie utilisateur (souvent incomplète) en un objet JSON structuré respectant strictement les obligations réglementaires françaises (DVE, publicité légale).
|
||||||
|
|
||||||
|
### INPUT UTILISATEUR :
|
||||||
|
Titre : {$request->title}
|
||||||
|
Description : {$request->description}
|
||||||
|
|
||||||
|
### TES MISSIONS :
|
||||||
|
1. ANALYSE STATUTAIRE : Identifie automatiquement la catégorie (A, B, C), le cadre d'emplois et les grades possibles selon l'intitulé du poste.
|
||||||
|
2. COMPLÉTION RÉGLEMENTAIRE : Génère les mentions obligatoires manquantes (fondements juridiques pour les contractuels, références au CGFP).
|
||||||
|
3. STRUCTURATION DES DONNÉES : Retourne uniquement un objet JSON contenant :
|
||||||
|
- \"infos_poste\" : {intitule, categorie, cadre_emplois, grade_mini, grade_maxi}
|
||||||
|
- \"conformite\" : {fondement_juridique_recrutement, mentions_legales_obligatoires: []}
|
||||||
|
- \"publication\" : {support_obligatoire: \"Choisir le service public / Emploi Territorial\", delai_affichage_minimal: \"30 jours\"}
|
||||||
|
- \"fiche_synthese\" : Un texte complet de l'annonce, richement formaté en Markdown (avec des titres ###, des puces -, et du texte en **gras** pour optimiser l'attractivité et le SEO), tout en restant conforme au droit.
|
||||||
|
|
||||||
|
### CONTRAINTES :
|
||||||
|
- Ne propose que des cadres d'emplois existants dans la FPT (ex: Adjoint technique, Rédacteur, Attaché).
|
||||||
|
- Si le poste semble ouvert aux contractuels, précise l'article L332-8 ou L332-14 du CGFP approprié.
|
||||||
|
- L'ensemble de ta réponse doit être un objet JSON brut. Ne mets PAS de blocs de code markdown (comme ```json) autour de la réponse, retourne juste le JSON. Le contenu de la clé `fiche_synthese` DOIT cependant contenir du formatage Markdown interne.";
|
||||||
|
|
||||||
|
$apiKey = env('GEMINI_API_KEY');
|
||||||
|
if (!$apiKey) {
|
||||||
|
return response()->json(['error' => 'API Key non configurée'], 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = Http::timeout(60)->post("https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-flash-lite-preview:generateContent?key={$apiKey}", [
|
||||||
|
'generationConfig' => [
|
||||||
|
'temperature' => 0.2,
|
||||||
|
'responseMimeType' => 'application/json'
|
||||||
|
],
|
||||||
|
'contents' => [
|
||||||
|
['role' => 'user', 'parts' => [['text' => $prompt]]]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($response->successful()) {
|
||||||
|
$text = $response->json('candidates.0.content.parts.0.text');
|
||||||
|
// Extract JSON if it contains markdown formatting
|
||||||
|
preg_match('/\{.*\}/s', $text, $matches);
|
||||||
|
$json = $matches[0] ?? $text;
|
||||||
|
|
||||||
|
return response()->json(json_decode($json, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::error("Gemini API Error: " . $response->body());
|
||||||
|
return response()->json(['error' => 'Erreur de génération IA'], 500);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error("Gemini Connection Failed: " . $e->getMessage());
|
||||||
|
return response()->json(['error' => 'Erreur de connexion à l\'IA'], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ class JobPositionController extends Controller
|
|||||||
'requirements' => 'nullable|array',
|
'requirements' => 'nullable|array',
|
||||||
'ai_prompt' => 'nullable|string',
|
'ai_prompt' => 'nullable|string',
|
||||||
'ai_bypass_base_prompt' => 'boolean',
|
'ai_bypass_base_prompt' => 'boolean',
|
||||||
|
'fpt_metadata' => 'nullable|array',
|
||||||
'tenant_id' => 'nullable|exists:tenants,id',
|
'tenant_id' => 'nullable|exists:tenants,id',
|
||||||
'quiz_ids' => 'nullable|array',
|
'quiz_ids' => 'nullable|array',
|
||||||
'quiz_ids.*' => 'exists:quizzes,id',
|
'quiz_ids.*' => 'exists:quizzes,id',
|
||||||
@@ -40,6 +41,7 @@ class JobPositionController extends Controller
|
|||||||
'requirements' => $request->requirements,
|
'requirements' => $request->requirements,
|
||||||
'ai_prompt' => $request->ai_prompt,
|
'ai_prompt' => $request->ai_prompt,
|
||||||
'ai_bypass_base_prompt' => $request->boolean('ai_bypass_base_prompt'),
|
'ai_bypass_base_prompt' => $request->boolean('ai_bypass_base_prompt'),
|
||||||
|
'fpt_metadata' => $request->fpt_metadata,
|
||||||
'tenant_id' => auth()->user()->isSuperAdmin() ? $request->tenant_id : auth()->user()->tenant_id,
|
'tenant_id' => auth()->user()->isSuperAdmin() ? $request->tenant_id : auth()->user()->tenant_id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -58,6 +60,7 @@ class JobPositionController extends Controller
|
|||||||
'requirements' => 'nullable|array',
|
'requirements' => 'nullable|array',
|
||||||
'ai_prompt' => 'nullable|string',
|
'ai_prompt' => 'nullable|string',
|
||||||
'ai_bypass_base_prompt' => 'boolean',
|
'ai_bypass_base_prompt' => 'boolean',
|
||||||
|
'fpt_metadata' => 'nullable|array',
|
||||||
'tenant_id' => 'nullable|exists:tenants,id',
|
'tenant_id' => 'nullable|exists:tenants,id',
|
||||||
'quiz_ids' => 'nullable|array',
|
'quiz_ids' => 'nullable|array',
|
||||||
'quiz_ids.*' => 'exists:quizzes,id',
|
'quiz_ids.*' => 'exists:quizzes,id',
|
||||||
@@ -69,6 +72,7 @@ class JobPositionController extends Controller
|
|||||||
'requirements' => $request->requirements,
|
'requirements' => $request->requirements,
|
||||||
'ai_prompt' => $request->ai_prompt,
|
'ai_prompt' => $request->ai_prompt,
|
||||||
'ai_bypass_base_prompt' => $request->boolean('ai_bypass_base_prompt'),
|
'ai_bypass_base_prompt' => $request->boolean('ai_bypass_base_prompt'),
|
||||||
|
'fpt_metadata' => $request->fpt_metadata,
|
||||||
'tenant_id' => auth()->user()->isSuperAdmin() ? $request->tenant_id : auth()->user()->tenant_id,
|
'tenant_id' => auth()->user()->isSuperAdmin() ? $request->tenant_id : auth()->user()->tenant_id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,11 @@ class PublicJobApplicationController extends Controller
|
|||||||
{
|
{
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$jobs = JobPosition::with('tenant')->orderBy('created_at', 'desc')->get();
|
$jobs = JobPosition::with('tenant')->orderBy('created_at', 'desc')->get()->map(function($job) {
|
||||||
|
$job->description = strip_tags(\Illuminate\Support\Str::markdown($job->description));
|
||||||
|
return $job;
|
||||||
|
});
|
||||||
|
|
||||||
return Inertia::render('Public/Jobs/Index', [
|
return Inertia::render('Public/Jobs/Index', [
|
||||||
'jobs' => $jobs
|
'jobs' => $jobs
|
||||||
]);
|
]);
|
||||||
@@ -24,8 +28,11 @@ class PublicJobApplicationController extends Controller
|
|||||||
|
|
||||||
public function show(JobPosition $jobPosition)
|
public function show(JobPosition $jobPosition)
|
||||||
{
|
{
|
||||||
|
$data = $jobPosition->toArray();
|
||||||
|
$data['description_html'] = \Illuminate\Support\Str::markdown($jobPosition->description);
|
||||||
|
|
||||||
return Inertia::render('Public/Jobs/Show', [
|
return Inertia::render('Public/Jobs/Show', [
|
||||||
'jobPosition' => $jobPosition
|
'jobPosition' => $data
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||||||
|
|
||||||
use App\Traits\BelongsToTenant;
|
use App\Traits\BelongsToTenant;
|
||||||
|
|
||||||
#[Fillable(['title', 'description', 'requirements', 'ai_prompt', 'ai_bypass_base_prompt', 'gemini_cache_id', 'gemini_cache_expires_at', 'tenant_id'])]
|
#[Fillable(['title', 'description', 'requirements', 'ai_prompt', 'ai_bypass_base_prompt', 'gemini_cache_id', 'gemini_cache_expires_at', 'tenant_id', 'fpt_metadata'])]
|
||||||
class JobPosition extends Model
|
class JobPosition extends Model
|
||||||
{
|
{
|
||||||
use HasFactory, BelongsToTenant;
|
use HasFactory, BelongsToTenant;
|
||||||
@@ -18,6 +18,7 @@ class JobPosition extends Model
|
|||||||
'requirements' => 'array',
|
'requirements' => 'array',
|
||||||
'ai_bypass_base_prompt' => 'boolean',
|
'ai_bypass_base_prompt' => 'boolean',
|
||||||
'gemini_cache_expires_at' => 'datetime',
|
'gemini_cache_expires_at' => 'datetime',
|
||||||
|
'fpt_metadata' => 'array',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function candidates(): HasMany
|
public function candidates(): HasMany
|
||||||
|
|||||||
@@ -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('job_positions', function (Blueprint $table) {
|
||||||
|
$table->json('fpt_metadata')->nullable()->after('ai_bypass_base_prompt');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('job_positions', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('fpt_metadata');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -25,8 +25,11 @@ const form = useForm({
|
|||||||
ai_bypass_base_prompt: false,
|
ai_bypass_base_prompt: false,
|
||||||
tenant_id: '',
|
tenant_id: '',
|
||||||
quiz_ids: [],
|
quiz_ids: [],
|
||||||
|
fpt_metadata: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isGeneratingFpt = ref(false);
|
||||||
|
|
||||||
const openModal = (position = null) => {
|
const openModal = (position = null) => {
|
||||||
editingPosition.value = position;
|
editingPosition.value = position;
|
||||||
if (position) {
|
if (position) {
|
||||||
@@ -37,12 +40,36 @@ const openModal = (position = null) => {
|
|||||||
form.ai_bypass_base_prompt = !!position.ai_bypass_base_prompt;
|
form.ai_bypass_base_prompt = !!position.ai_bypass_base_prompt;
|
||||||
form.tenant_id = position.tenant_id || '';
|
form.tenant_id = position.tenant_id || '';
|
||||||
form.quiz_ids = position.quizzes ? position.quizzes.map(q => q.id) : [];
|
form.quiz_ids = position.quizzes ? position.quizzes.map(q => q.id) : [];
|
||||||
|
form.fpt_metadata = position.fpt_metadata || null;
|
||||||
} else {
|
} else {
|
||||||
form.reset();
|
form.reset();
|
||||||
}
|
}
|
||||||
showingModal.value = true;
|
showingModal.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const generateFpt = async () => {
|
||||||
|
if (!form.title || !form.description) {
|
||||||
|
alert("Veuillez remplir le titre et la description avant de générer.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isGeneratingFpt.value = true;
|
||||||
|
try {
|
||||||
|
const response = await axios.post(route('admin.job-positions.ai-fpt'), {
|
||||||
|
title: form.title,
|
||||||
|
description: form.description
|
||||||
|
});
|
||||||
|
form.fpt_metadata = response.data;
|
||||||
|
if (response.data.fiche_synthese) {
|
||||||
|
form.description = response.data.fiche_synthese;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
alert("Une erreur est survenue lors de la génération IA.");
|
||||||
|
} finally {
|
||||||
|
isGeneratingFpt.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
showingModal.value = false;
|
showingModal.value = false;
|
||||||
form.reset();
|
form.reset();
|
||||||
@@ -215,6 +242,59 @@ const copyLink = (position) => {
|
|||||||
<InputError :message="form.errors.description" />
|
<InputError :message="form.errors.description" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center bg-indigo-50 dark:bg-indigo-900/10 p-4 rounded-2xl border border-indigo-100 dark:border-indigo-800/50">
|
||||||
|
<div>
|
||||||
|
<h4 class="text-xs font-black text-indigo-700 dark:text-indigo-400 uppercase tracking-widest mb-1">Assistant RH FPT (IA)</h4>
|
||||||
|
<p class="text-[10px] text-indigo-500 font-bold">Génère automatiquement les mentions réglementaires et catégorise le poste (CGFP).</p>
|
||||||
|
</div>
|
||||||
|
<PrimaryButton type="button" @click="generateFpt" :disabled="isGeneratingFpt || !form.title || !form.description" class="whitespace-nowrap text-xs py-2 px-4 bg-indigo-600 hover:bg-indigo-700">
|
||||||
|
<svg v-if="isGeneratingFpt" class="animate-spin -ml-1 mr-2 h-4 w-4 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>
|
||||||
|
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" 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>
|
||||||
|
{{ isGeneratingFpt ? 'Génération...' : 'Structurer l\'offre' }}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="form.fpt_metadata" class="bg-slate-50 dark:bg-slate-800/50 rounded-2xl p-4 border border-slate-200 dark:border-slate-700 space-y-4">
|
||||||
|
<div>
|
||||||
|
<h5 class="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-2">Informations Statutaires</h5>
|
||||||
|
<div class="grid grid-cols-2 gap-2 text-xs font-bold">
|
||||||
|
<div class="bg-white dark:bg-slate-900 p-2 rounded-xl">
|
||||||
|
<span class="text-slate-400 block text-[9px] uppercase">Catégorie</span>
|
||||||
|
{{ form.fpt_metadata.infos_poste?.categorie }}
|
||||||
|
</div>
|
||||||
|
<div class="bg-white dark:bg-slate-900 p-2 rounded-xl">
|
||||||
|
<span class="text-slate-400 block text-[9px] uppercase">Cadre d'emplois</span>
|
||||||
|
{{ form.fpt_metadata.infos_poste?.cadre_emplois }}
|
||||||
|
</div>
|
||||||
|
<div class="bg-white dark:bg-slate-900 p-2 rounded-xl">
|
||||||
|
<span class="text-slate-400 block text-[9px] uppercase">Grade Mini</span>
|
||||||
|
{{ form.fpt_metadata.infos_poste?.grade_mini }}
|
||||||
|
</div>
|
||||||
|
<div class="bg-white dark:bg-slate-900 p-2 rounded-xl">
|
||||||
|
<span class="text-slate-400 block text-[9px] uppercase">Grade Maxi</span>
|
||||||
|
{{ form.fpt_metadata.infos_poste?.grade_maxi }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h5 class="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-2">Conformité CGFP</h5>
|
||||||
|
<div class="bg-white dark:bg-slate-900 p-3 rounded-xl text-xs font-bold text-slate-600 dark:text-slate-300">
|
||||||
|
<p class="mb-2"><span class="text-indigo-500">Fondement :</span> {{ form.fpt_metadata.conformite?.fondement_juridique_recrutement }}</p>
|
||||||
|
<ul class="list-disc list-inside space-y-1">
|
||||||
|
<li v-for="(mention, i) in form.fpt_metadata.conformite?.mentions_legales_obligatoires" :key="i">
|
||||||
|
{{ mention }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="bg-indigo-50/50 dark:bg-indigo-900/10 p-6 rounded-3xl border border-indigo-100 dark:border-indigo-800/50">
|
<div class="bg-indigo-50/50 dark:bg-indigo-900/10 p-6 rounded-3xl border border-indigo-100 dark:border-indigo-800/50">
|
||||||
<label class="block text-xs font-black uppercase tracking-widest text-indigo-600 dark:text-indigo-400 mb-2">IA Context & Prompt Personnalisé</label>
|
<label class="block text-xs font-black uppercase tracking-widest text-indigo-600 dark:text-indigo-400 mb-2">IA Context & Prompt Personnalisé</label>
|
||||||
<p class="text-[10px] text-indigo-400 mb-4 font-bold uppercase tracking-tight">Utilisez cette zone pour donner des instructions spécifiques à l'IA (priorités, contexte entreprise, ton de l'analyse...)</p>
|
<p class="text-[10px] text-indigo-400 mb-4 font-bold uppercase tracking-tight">Utilisez cette zone pour donner des instructions spécifiques à l'IA (priorités, contexte entreprise, ton de l'analyse...)</p>
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ defineProps({
|
|||||||
<span v-if="job.tenant" class="px-3 py-1 bg-highlight/20 text-[#3a2800] rounded-full text-xs font-bold uppercase tracking-wider">
|
<span v-if="job.tenant" class="px-3 py-1 bg-highlight/20 text-[#3a2800] rounded-full text-xs font-bold uppercase tracking-wider">
|
||||||
{{ job.tenant.name }}
|
{{ job.tenant.name }}
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="job.fpt_metadata?.infos_poste?.categorie" class="text-xs font-bold text-anthracite/50 uppercase tracking-widest bg-neutral px-2 py-1 rounded">
|
||||||
|
Catégorie {{ job.fpt_metadata.infos_poste.categorie }}
|
||||||
|
</span>
|
||||||
<span class="text-xs font-bold text-anthracite/50 uppercase tracking-widest">Temps plein</span>
|
<span class="text-xs font-bold text-anthracite/50 uppercase tracking-widest">Temps plein</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-2xl font-bold font-serif text-primary group-hover:text-highlight transition-colors mb-4">
|
<h2 class="text-2xl font-bold font-serif text-primary group-hover:text-highlight transition-colors mb-4">
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ const submit = () => {
|
|||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>
|
||||||
Offre d'emploi
|
Offre d'emploi
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="jobPosition.fpt_metadata?.infos_poste?.categorie" class="px-3 py-1 bg-white/50 rounded-full font-bold uppercase tracking-widest text-[10px]">
|
||||||
|
Catégorie {{ jobPosition.fpt_metadata.infos_poste.categorie }} - {{ jobPosition.fpt_metadata.infos_poste.cadre_emplois }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -69,7 +72,8 @@ const submit = () => {
|
|||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold font-subtitle text-anthracite mb-3 border-b pb-2">Description du poste</h2>
|
<h2 class="text-xl font-bold font-subtitle text-anthracite mb-3 border-b pb-2">Description du poste</h2>
|
||||||
<div class="prose prose-sm prose-neutral text-anthracite/80 whitespace-pre-line">{{ jobPosition.description }}</div>
|
<div v-if="jobPosition.description_html" class="prose prose-sm prose-indigo text-anthracite/80 max-w-none" v-html="jobPosition.description_html"></div>
|
||||||
|
<div v-else class="prose prose-sm prose-neutral text-anthracite/80 whitespace-pre-line">{{ jobPosition.description }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="jobPosition.requirements && jobPosition.requirements.length > 0">
|
<div v-if="jobPosition.requirements && jobPosition.requirements.length > 0">
|
||||||
@@ -78,6 +82,26 @@ const submit = () => {
|
|||||||
<li v-for="(req, i) in jobPosition.requirements" :key="i">{{ req }}</li>
|
<li v-for="(req, i) in jobPosition.requirements" :key="i">{{ req }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="jobPosition.fpt_metadata" class="bg-neutral/30 rounded-xl p-5 border border-anthracite/10 mt-8">
|
||||||
|
<h2 class="text-sm font-bold font-subtitle uppercase tracking-widest text-primary mb-4 flex items-center gap-2">
|
||||||
|
<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="M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3" />
|
||||||
|
</svg>
|
||||||
|
Informations Statutaires (FPT)
|
||||||
|
</h2>
|
||||||
|
<div class="space-y-3 text-sm text-anthracite/80">
|
||||||
|
<p><strong class="text-anthracite">Grade(s) recherché(s) :</strong> {{ jobPosition.fpt_metadata.infos_poste?.grade_mini }} à {{ jobPosition.fpt_metadata.infos_poste?.grade_maxi }}</p>
|
||||||
|
<p><strong class="text-anthracite">Fondement :</strong> {{ jobPosition.fpt_metadata.conformite?.fondement_juridique_recrutement }}</p>
|
||||||
|
|
||||||
|
<div class="pt-3 border-t border-anthracite/10">
|
||||||
|
<strong class="text-anthracite block mb-2 text-xs uppercase tracking-wider">Mentions Légales :</strong>
|
||||||
|
<ul class="list-disc list-inside space-y-1 text-xs">
|
||||||
|
<li v-for="(mention, idx) in jobPosition.fpt_metadata.conformite?.mentions_legales_obligatoires" :key="idx">{{ mention }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Application Form -->
|
<!-- Application Form -->
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ use Illuminate\Support\Facades\Route;
|
|||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
|
||||||
Route::get('/', function () {
|
Route::get('/', function () {
|
||||||
$latestJobs = \App\Models\JobPosition::with('tenant')->orderBy('created_at', 'desc')->take(3)->get();
|
$latestJobs = \App\Models\JobPosition::with('tenant')->orderBy('created_at', 'desc')->take(3)->get()->map(function($job) {
|
||||||
|
$job->description = strip_tags(\Illuminate\Support\Str::markdown($job->description));
|
||||||
|
return $job;
|
||||||
|
});
|
||||||
return Inertia::render('Welcome', [
|
return Inertia::render('Welcome', [
|
||||||
'canLogin' => Route::has('login'),
|
'canLogin' => Route::has('login'),
|
||||||
'latestJobs' => $latestJobs,
|
'latestJobs' => $latestJobs,
|
||||||
@@ -103,6 +106,7 @@ Route::middleware('auth')->group(function () {
|
|||||||
|
|
||||||
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('job-positions', \App\Http\Controllers\JobPositionController::class)->only(['index', 'store', 'update', 'destroy']);
|
||||||
|
Route::post('/job-positions/ai-fpt', [\App\Http\Controllers\Admin\JobPositionAiHelperController::class, 'generate'])->name('job-positions.ai-fpt');
|
||||||
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::resource('tenants', \App\Http\Controllers\TenantController::class)->only(['index', 'store', 'update', 'destroy']);
|
Route::resource('tenants', \App\Http\Controllers\TenantController::class)->only(['index', 'store', 'update', 'destroy']);
|
||||||
Route::resource('users', \App\Http\Controllers\UserController::class)->except(['show', 'create', 'edit']);
|
Route::resource('users', \App\Http\Controllers\UserController::class)->except(['show', 'create', 'edit']);
|
||||||
|
|||||||
Reference in New Issue
Block a user