{/* Formulaire de candidature */}
{openForm === job.id && (
setOpenForm(null)}
/>
)}
))}
)
}
ADAPTATION : Dans le projet SAR, la page utilise next-intl pour les traductions.
Si votre projet est multilingue, remplacez les textes en dur par des appels à
useTranslations("recruitment").
────────────────────────────────────────────────────────────
5.4 TRADUCTIONS (next-intl — optionnel si multilingue)
────────────────────────────────────────────────────────────
Dans messages/fr.json, ajouter une section "recruitment" :
"recruitment": {
"title": "Rejoignez notre équipe",
"subtitle": "Découvrez nos offres d'emploi et postulez directement en ligne.",
"noPositions": {
"title": "Offres d'emploi",
"message": "Aucune offre publiée pour le moment.",
"subtitle": "Revenez prochainement."
},
"applyButton": "Postuler",
"form": {
"fullName": "Nom complet",
"email": "Adresse email",
"phone": "Téléphone",
"cvLabel": "CV (PDF, DOC — max 10 Mo)",
"coverLetter": "Lettre de motivation (optionnel)",
"submit": "Envoyer ma candidature",
"submitting": "Envoi en cours...",
"cancel": "Annuler",
"success": "Candidature envoyée avec succès !",
"errorRequired": "Ce champ est requis",
"errorEmail": "Email invalide",
"errorCvSize": "Le CV ne doit pas dépasser 10 Mo"
}
}
Dans messages/en.json (version anglaise) :
"recruitment": {
"title": "Join our team",
"subtitle": "Browse our job openings and apply directly online.",
"noPositions": {
"title": "Job openings",
"message": "No open positions at the moment.",
"subtitle": "Check back soon."
},
"applyButton": "Apply",
"form": {
"fullName": "Full name",
"email": "Email address",
"phone": "Phone number",
"cvLabel": "Resume (PDF, DOC — max 10 MB)",
"coverLetter": "Cover letter (optional)",
"submit": "Send my application",
"submitting": "Sending...",
"cancel": "Cancel",
"success": "Application sent successfully!",
"errorRequired": "This field is required",
"errorEmail": "Invalid email",
"errorCvSize": "Resume must not exceed 10 MB"
}
}
────────────────────────────────────────────────────────────
5.5 LIEN DANS LA NAVBAR
────────────────────────────────────────────────────────────
Dans votre composant navbar, ajouter un lien vers /emploi :
// Exemple minimal (adapter à votre navbar)
Emploi
Si vous utilisez next-intl, ajouter la clé dans les traductions navbar :
// messages/fr.json — dans la section "navbar" ou équivalent
"jobOffers": "Emploi"
// messages/en.json
"jobOffers": "Jobs"
────────────────────────────────────────────────────────────
5.6 INTERNATIONALISATION (next-intl)
────────────────────────────────────────────────────────────
Si votre projet n'utilise pas encore next-intl mais que vous souhaitez l'ajouter :
npm install next-intl
Créer i18n.ts :
import { getRequestConfig } from "next-intl/server"
export default getRequestConfig(async ({ locale }) => ({
messages: (await import(`./messages/${locale}.json`)).default,
}))
Dans next.config.mjs :
import createNextIntlPlugin from "next-intl/plugin"
const withNextIntl = createNextIntlPlugin("./i18n.ts")
export default withNextIntl({ /* vos options */ })
Structure des routes avec locale :
app/
[locale]/
emploi/
page.tsx ← votre page /fr/emploi et /en/emploi
Si vous ne souhaitez pas le multilingue, une route simple suffit :
app/
emploi/
page.tsx ← votre page /emploi
================================================================================
6. TEST DE L'INTÉGRATION
================================================================================
Étape 1 : Tester la connexion Odoo depuis le backend
curl http://10.113.245.26:8000/api/jobs/debug
→ Doit retourner un JSON avec les offres directement depuis Odoo
Étape 2 : Synchroniser le cache
php artisan odoo:sync-jobs
→ Doit afficher "X offre(s) trouvée(s) sur Odoo. Synchronisation terminée."
Étape 3 : Tester l'API de liste
curl http://10.113.245.26:8000/api/jobs
→ Doit retourner un tableau JSON des offres depuis le cache MySQL
Étape 4 : Tester la page frontend
Ouvrir http://10.113.245.26:3000/fr/emploi
→ Les offres doivent s'afficher
→ Cliquer "Postuler" pour tester le formulaire
Étape 5 : Tester une candidature
Remplir le formulaire, soumettre
→ Dans Odoo > Recrutement > Candidatures, vérifier l'apparition de la candidature
================================================================================
7. SYNCHRONISATION MANUELLE DEPUIS L'ADMIN
================================================================================
Si vous voulez un bouton "Synchroniser" dans votre interface admin :
Backend — ajouter une route dans routes/web.php (ou api.php) :
Route::post('/admin/sync-jobs', function () {
Artisan::call('odoo:sync-jobs');
return response()->json(['success' => true, 'output' => Artisan::output()]);
});
// ATTENTION : protéger cette route avec un middleware d'auth en production !
Frontend — bouton dans la page admin :
async function syncJobs() {
const res = await fetch(`${API_URL}/admin/sync-jobs`, { method: "POST" })
const data = await res.json()
alert(data.success ? "Synchronisation réussie" : "Erreur de synchronisation")
}
================================================================================
8. RÉCAPITULATIF DES FLUX DE DONNÉES
================================================================================
AFFICHAGE DES OFFRES (lecture depuis cache) :
Browser → GET /api/jobs → Laravel → MySQL (cached_jobs) → JSON → Browser
CANDIDATURE (envoi direct à Odoo) :
Browser → POST /api/jobs/{id}/apply → Laravel → OdooClient
→ hr.candidate (create)
→ ir.attachment (create, avec CV base64)
→ hr.applicant (create)
← applicant_id ← Laravel ← JSON ← Browser
SYNCHRONISATION (cron) :
php artisan odoo:sync-jobs → OdooClient
→ hr.job search_read (website_published = true)
← jobs[] → MySQL upsert (cached_jobs) ← CachedJob model
================================================================================
9. ERREURS COURANTES ET SOLUTIONS
================================================================================
ERREUR : "Odoo auth failed"
→ Vérifiez ODOO_URL, ODOO_DB, ODOO_USERNAME, ODOO_PASSWORD dans .env
→ Testez en vous connectant manuellement à l'instance Odoo avec ces identifiants
→ Assurez-vous que l'utilisateur a accès au module Recrutement
ERREUR : "Call to undefined method ... getPublishedJobs"
→ Assurez-vous que OdooClient.php est dans app/Services/ et que le namespace
est correct : namespace App\Services;
ERREUR : Aucune offre retournée (tableau vide)
→ Vérifiez que des offres existent dans Odoo > Recrutement > Postes
→ Vérifiez que ces postes sont cochés "Publiés sur le site" (website_published)
→ Testez avec la route /api/jobs/debug (retourne sans filtre website_published)
ERREUR CORS : "Access-Control-Allow-Origin"
→ Vérifiez CORS_ALLOWED_ORIGINS dans .env du backend
→ Relancez : php artisan config:clear && php artisan cache:clear
ERREUR 422 sur /apply : "The cv field must be a file"
→ Le frontend envoie un FormData — vérifiez que vous n'ajoutez pas
Content-Type manuellement dans les headers (laissez le browser le faire)
ERREUR : Le CV ne s'attache pas dans Odoo
→ Vérifiez que le fichier est bien lu avec file_get_contents($file->getRealPath())
→ Vérifiez que la taille du fichier ne dépasse pas upload_max_filesize PHP
ERREUR : "Table cached_jobs doesn't exist"
→ Exécutez : php artisan migrate
ERREUR : Les offres ne se mettent pas à jour
→ Lancez manuellement : php artisan odoo:sync-jobs
→ Vérifiez que le cron est bien configuré sur le serveur
================================================================================
10. CHECKLIST FINALE
================================================================================
BACKEND :
[ ] config/odoo.php créé
[ ] Variables ODOO_* dans .env renseignées
[ ] app/Services/OdooClient.php créé
[ ] Migration cached_jobs créée et migrée (php artisan migrate)
[ ] app/Models/CachedJob.php créé
[ ] app/Http/Controllers/Api/JobController.php créé
[ ] Routes ajoutées dans routes/api.php (dans le bon ordre)
[ ] app/Console/Commands/SyncOdooJobsCommand.php créé
[ ] Commande planifiée dans app/Console/Kernel.php
[ ] CORS configuré (config/cors.php et .env)
[ ] php artisan odoo:sync-jobs testé avec succès
[ ] php artisan config:clear exécuté
FRONTEND :
[ ] NEXT_PUBLIC_API_URL dans .env.local
[ ] Interface OdooJob définie
[ ] Page app/[locale]/emploi/page.tsx créée
[ ] Traductions ajoutées (fr.json, en.json) si multilingue
[ ] Lien "Emploi" ajouté dans la navbar
[ ] Page /emploi testée dans le navigateur
[ ] Formulaire de candidature testé (envoi complet)
[ ] Candidature vérifiée dans Odoo > Recrutement > Candidatures
PRODUCTION :
[ ] CORS_ALLOWED_ORIGINS mis à jour avec le vrai domaine
[ ] SSL activé sur le backend (CURLOPT_SSL_VERIFYPEER = true dans OdooClient)
[ ] Route /api/jobs/debug désactivée ou protégée
[ ] Route /admin/sync-jobs protégée par middleware d'auth
[ ] Cron configuré sur le serveur Linux (* * * * * php artisan schedule:run)
================================================================================
FIN DU GUIDE
================================================================================