diff --git a/.idea/hrconnect-symfony.iml b/.idea/hrconnect-symfony.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/hrconnect-symfony.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..1b2d693 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..3f795e2 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..9648ecd --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1744409784232 + + + + + + \ No newline at end of file diff --git a/composer.json b/composer.json index ee68cde..0431ddf 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "doctrine/persistence": "^3.1", "phpdocumentor/reflection-docblock": "^5.6", "phpstan/phpdoc-parser": "^2.1", + "smalot/pdfparser": "^2.12", "symfony/asset": "6.4.*", "symfony/asset-mapper": "6.4.*", "symfony/console": "6.4.*", diff --git a/composer.lock b/composer.lock index bf1a931..33c8422 100644 --- a/composer.lock +++ b/composer.lock @@ -2001,6 +2001,57 @@ }, "time": "2024-09-11T13:17:53+00:00" }, + { + "name": "smalot/pdfparser", + "version": "v2.12.0", + "source": { + "type": "git", + "url": "https://github.com/smalot/pdfparser.git", + "reference": "8440edbf58c8596074e78ada38dcb0bd041a5948" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/smalot/pdfparser/zipball/8440edbf58c8596074e78ada38dcb0bd041a5948", + "reference": "8440edbf58c8596074e78ada38dcb0bd041a5948", + "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.0" + }, + "time": "2025-03-31T13:16:09+00:00" + }, { "name": "symfony/asset", "version": "v6.4.13", @@ -9837,7 +9888,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -9845,6 +9896,6 @@ "ext-ctype": "*", "ext-iconv": "*" }, - "platform-dev": {}, + "platform-dev": [], "plugin-api-version": "2.6.0" -} +} \ No newline at end of file diff --git a/config/packages/services.yaml b/config/packages/services.yaml new file mode 100644 index 0000000..50237df --- /dev/null +++ b/config/packages/services.yaml @@ -0,0 +1,17 @@ +parameters: + cv_directory: '%kernel.project_dir%/public/uploads/cv' + +services: + # default configuration for services in *this* file + _defaults: + autowire: true # Automatically injects dependencies in your services. + autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. + + # makes classes in src/ available to be used as services + # this creates a service per class whose id is the fully-qualified class name + App\: + resource: '../../src/' + exclude: + - '../../src/DependencyInjection/' + - '../../src/Entity/' + - '../../src/Kernel.php' \ No newline at end of file diff --git a/config/services.yaml b/config/services.yaml index 2d6a76f..6f9640c 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -4,6 +4,8 @@ # Put parameters here that don't need to change on each machine where the app is deployed # https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration parameters: + cv_directory: '%kernel.project_dir%/public/uploads/cv' + lettre_motivation_directory: '%kernel.project_dir%/public/uploads/lettre_motivation' services: # default configuration for services in *this* file diff --git a/db.sql b/db.sql index 5910e7c..d47a80d 100644 --- a/db.sql +++ b/db.sql @@ -3,7 +3,11 @@ -- https://www.phpmyadmin.net/ -- -- Hôte : 127.0.0.1 +<<<<<<< HEAD +-- Généré le : lun. 14 avr. 2025 à 20:52 +======= -- Généré le : lun. 07 avr. 2025 à 11:53 +>>>>>>> main -- Version du serveur : 10.4.32-MariaDB -- Version de PHP : 8.2.12 @@ -213,8 +217,12 @@ INSERT INTO `formations` (`id`, `formateur_id`, `title`, `image`, `description`, (41, 2, 'Test formation payante', 'https://i.ibb.co/Zzqw2Dk3/59d345242af9.png', 'Test formation payante', 0, 'Esprit bloc I,J,K, Cebalat, Tunisia', 36.9010594, 10.190243, 1, 1, '2025-03-05 07:53:48', '2025-03-06 06:53:48', 9.99), (47, 2, 'formation php', 'https://i.ibb.co/wFCSSvrh/d150e2216999.png', 'php', 0, 'Esprit School of Business, Cebalat, Tunisia', 36.89923520000001, 10.189445, 1, 1, '2025-03-06 09:02:49', '2025-03-06 09:02:49', 12), (48, 2, 'tesssst', 'https://i.ibb.co/wFCSSvrh/d150e2216999.png', 'jdj', 0, 'Esprit School of Business, Cebalat, Tunisia', 36.89923520000001, 10.189445, 1, 1, '2025-03-06 09:17:19', NULL, 10), +<<<<<<< HEAD +(49, 2, 'eyyey&yz', 'https://i.ibb.co/DfZDzwss/9ad51a8f934a.png', 'zyzyzy', 0, 'Bardo, Tunisia', 36.80840260000001, 10.1283163, 1, 1, '2025-03-06 09:21:19', NULL, 0); +======= (49, 2, 'eyyey&yz', 'https://i.ibb.co/DfZDzwss/9ad51a8f934a.png', 'zyzyzy', 0, 'Bardo, Tunisia', 36.80840260000001, 10.1283163, 1, 1, '2025-03-06 09:21:19', NULL, 0), (51, 2, 'aa', 'aa', 'aaa', 1, '1', 1, 1, 0, 0, '2025-04-07 00:00:00', '2025-04-07 00:00:00', 5); +>>>>>>> main -- -------------------------------------------------------- @@ -232,8 +240,12 @@ CREATE TABLE `formation_participation` ( -- INSERT INTO `formation_participation` (`formation_id`, `employe_id`) VALUES +<<<<<<< HEAD +(31, 18); +======= (31, 18), (51, 3); +>>>>>>> main -- -------------------------------------------------------- @@ -315,11 +327,20 @@ CREATE TABLE `quiz` ( -- INSERT INTO `quiz` (`id`, `formation_id`, `question`, `reponse1`, `reponse2`, `reponse3`, `num_reponse_correct`) VALUES +<<<<<<< HEAD +(16, 31, 'Quelle classe est utilisée pour créer une fenêtre en JavaFX !?', 'JFrame', 'Stage', 'Window', 2), +(17, 31, 'Quel est le langage utilisé pour styliser une interface JavaFX ?', 'CSS', 'XML', 'JavaScript', 1), +(18, 31, 'Quelle méthode est utilisée pour lancer une application JavaFX ?', 'launch', 'start', 'run', 1), +(23, 31, 'aabc', '1', '2', '3', 1), +(24, 31, 'hello', '5', '5', '5', 1), +(25, 31, 'question', 'rep', 'rep2', 'rep3', 3); +======= (16, 31, 'Quelle classe est utilisée pour créer une fenêtre en JavaFX ?', 'JFrame', 'Stage', 'Window', 2), (17, 31, 'Quel est le langage utilisé pour styliser une interface JavaFX ?', 'CSS', 'XML', 'JavaScript', 1), (18, 31, 'Quelle méthode est utilisée pour lancer une application JavaFX ?', 'launch', 'start', 'run', 1), (19, 31, 'Quel conteneur est utilisé pour organiser les éléments en colonne dans JavaFX ?', 'VBox ', 'HBox', 'GridPane', 1), (20, 31, 'Quel événement est utilisé pour détecter un clic sur un bouton JavaFX ?', 'setOnAction', 'setOnClick', 'setOnPress', 1); +>>>>>>> main -- -------------------------------------------------------- @@ -340,9 +361,13 @@ CREATE TABLE `quiz_reponses` ( INSERT INTO `quiz_reponses` (`employe_id`, `quiz_id`, `num_reponse`) VALUES (18, 16, 2), (18, 17, 1), +<<<<<<< HEAD +(18, 18, 2); +======= (18, 18, 2), (18, 19, 2), (18, 20, 1); +>>>>>>> main -- -------------------------------------------------------- @@ -607,7 +632,11 @@ ALTER TABLE `candidature` -- AUTO_INCREMENT pour la table `demande_conge` -- ALTER TABLE `demande_conge` +<<<<<<< HEAD + MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=11; +======= MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=10; +>>>>>>> main -- -- AUTO_INCREMENT pour la table `employe` @@ -625,7 +654,11 @@ ALTER TABLE `formateurs` -- AUTO_INCREMENT pour la table `formations` -- ALTER TABLE `formations` +<<<<<<< HEAD + MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=60; +======= MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=52; +>>>>>>> main -- -- AUTO_INCREMENT pour la table `hr` @@ -643,7 +676,11 @@ ALTER TABLE `offre_emploi` -- AUTO_INCREMENT pour la table `quiz` -- ALTER TABLE `quiz` +<<<<<<< HEAD + MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=26; +======= MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=23; +>>>>>>> main -- -- AUTO_INCREMENT pour la table `stagaires` diff --git a/migrations/Version20250407130941.php b/migrations/Version20250407130941.php new file mode 100644 index 0000000..03088a1 --- /dev/null +++ b/migrations/Version20250407130941.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE messenger_messages CHANGE delivered_at delivered_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\''); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE messenger_messages CHANGE delivered_at delivered_at DATETIME DEFAULT \'NULL\' COMMENT \'(DC2Type:datetime_immutable)\''); + } +} diff --git a/public/uploads/cv/TD-AN-2425-67f417148925d.pdf b/public/uploads/cv/TD-AN-2425-67f417148925d.pdf new file mode 100644 index 0000000..b966fbd Binary files /dev/null and b/public/uploads/cv/TD-AN-2425-67f417148925d.pdf differ diff --git a/public/uploads/cv/TestSGBD-Sujet-A-67f4147d31a44.pdf b/public/uploads/cv/TestSGBD-Sujet-A-67f4147d31a44.pdf new file mode 100644 index 0000000..95cf722 Binary files /dev/null and b/public/uploads/cv/TestSGBD-Sujet-A-67f4147d31a44.pdf differ diff --git a/public/uploads/cv/TestSGBD-Sujet-A-67f4159c91116.pdf b/public/uploads/cv/TestSGBD-Sujet-A-67f4159c91116.pdf new file mode 100644 index 0000000..95cf722 Binary files /dev/null and b/public/uploads/cv/TestSGBD-Sujet-A-67f4159c91116.pdf differ diff --git a/public/uploads/cv/TestSGBD-Sujet-A-67f415af8d9fa.pdf b/public/uploads/cv/TestSGBD-Sujet-A-67f415af8d9fa.pdf new file mode 100644 index 0000000..95cf722 Binary files /dev/null and b/public/uploads/cv/TestSGBD-Sujet-A-67f415af8d9fa.pdf differ diff --git a/public/uploads/cv/TestSGBD-Sujet-B-67f4183579421.pdf b/public/uploads/cv/TestSGBD-Sujet-B-67f4183579421.pdf new file mode 100644 index 0000000..2048197 Binary files /dev/null and b/public/uploads/cv/TestSGBD-Sujet-B-67f4183579421.pdf differ diff --git a/public/uploads/cv/discours-du-juge-67f99df0c600f.pdf b/public/uploads/cv/discours-du-juge-67f99df0c600f.pdf new file mode 100644 index 0000000..9daee77 Binary files /dev/null and b/public/uploads/cv/discours-du-juge-67f99df0c600f.pdf differ diff --git a/public/uploads/cv/discours-du-juge-67f9a18706743.pdf b/public/uploads/cv/discours-du-juge-67f9a18706743.pdf new file mode 100644 index 0000000..9daee77 Binary files /dev/null and b/public/uploads/cv/discours-du-juge-67f9a18706743.pdf differ diff --git a/public/uploads/lettre_motivation/discours-du-juge-67f99df0c72c6.pdf b/public/uploads/lettre_motivation/discours-du-juge-67f99df0c72c6.pdf new file mode 100644 index 0000000..9daee77 Binary files /dev/null and b/public/uploads/lettre_motivation/discours-du-juge-67f99df0c72c6.pdf differ diff --git a/public/uploads/lettre_motivation/discours-du-juge-67f9a1870787d.pdf b/public/uploads/lettre_motivation/discours-du-juge-67f9a1870787d.pdf new file mode 100644 index 0000000..9daee77 Binary files /dev/null and b/public/uploads/lettre_motivation/discours-du-juge-67f9a1870787d.pdf differ diff --git a/src/Controller/BackOfficeController.php b/src/Controller/BackOfficeController.php index dc40896..e7c7826 100644 --- a/src/Controller/BackOfficeController.php +++ b/src/Controller/BackOfficeController.php @@ -8,6 +8,7 @@ final class BackOfficeController extends AbstractController { #[Route('/back-office', name: 'app_back_office')] + #[Route('/back-office', name: 'back.index')] public function index(): Response { return $this->render('back_office/index.html.twig', [ diff --git a/src/Controller/CandidatCandidatureController.php b/src/Controller/CandidatCandidatureController.php new file mode 100644 index 0000000..2598942 --- /dev/null +++ b/src/Controller/CandidatCandidatureController.php @@ -0,0 +1,142 @@ +logger = $logger; + $this->em = $em; + } + + #[Route('/{id}/postuler', name: 'app_candidat_candidature_new', methods: ['GET', 'POST'])] + public function new( + Request $request, + OffreEmploi $offre, + SluggerInterface $slugger + ): Response { + $candidature = new Candidature(); + + // Définir l'offre d'emploi avant la validation du formulaire + $candidature->setOffre($offre); + $candidature->setStatut('en_attente'); + $candidature->setDateCandidature(new \DateTimeImmutable()); + + $this->logger->info('Offre d\'emploi définie : ' . $offre->getId() . ' - ' . $offre->getTitre()); + + // Utilisation du formulaire simplifié + $form = $this->createForm(CandidatureSimpleType::class, $candidature); + $form->handleRequest($request); + + $this->logger->info('Formulaire soumis: ' . ($form->isSubmitted() ? 'Oui' : 'Non')); + if ($form->isSubmitted()) { + $this->logger->info('Formulaire valide: ' . ($form->isValid() ? 'Oui' : 'Non')); + if (!$form->isValid()) { + $errors = []; + foreach ($form->getErrors(true) as $error) { + $errors[] = $error->getMessage(); + } + $this->logger->error('Erreurs de validation: ' . implode(', ', $errors)); + } + } + + if ($form->isSubmitted() && $form->isValid()) { + + // Traitement du CV + $cvFile = $form->get('cv')->getData(); + if (!$cvFile) { + $this->addFlash('error', 'Le CV est obligatoire pour postuler à cette offre.'); + return $this->render('candidat/candidature/new.html.twig', [ + 'form' => $form->createView(), + 'offre' => $offre, + ]); + } + + $newFilename = $this->uploadFile($cvFile, 'cv_directory', $slugger); + if ($newFilename) { + $candidature->setCv($newFilename); + } else { + $this->addFlash('error', 'Une erreur est survenue lors de l\'upload du CV.'); + return $this->render('candidat/candidature/new.html.twig', [ + 'form' => $form->createView(), + 'offre' => $offre, + ]); + } + + // Traitement de la lettre de motivation + $lettreMotivationFile = $form->get('lettreMotivation')->getData(); + if (!$lettreMotivationFile) { + $this->addFlash('error', 'La lettre de motivation est obligatoire pour postuler à cette offre.'); + return $this->render('candidat/candidature/new.html.twig', [ + 'form' => $form->createView(), + 'offre' => $offre, + ]); + } + + $newFilename = $this->uploadFile($lettreMotivationFile, 'lettre_motivation_directory', $slugger); + if ($newFilename) { + $candidature->setLettreMotivation($newFilename); + } else { + $this->addFlash('error', 'Une erreur est survenue lors de l\'upload de la lettre de motivation.'); + return $this->render('candidat/candidature/new.html.twig', [ + 'form' => $form->createView(), + 'offre' => $offre, + ]); + } + + // Enregistrer la candidature + $this->em->persist($candidature); + $this->em->flush(); + + $this->logger->info('Candidature créée avec succès pour l\'offre : ' . $offre->getTitre()); + $this->addFlash('success', 'Candidature envoyée ! Votre candidature pour l\'offre "' . $offre->getTitre() . '" a été envoyée avec succès.
Nous vous contacterons prochainement pour vous informer de la suite du processus.'); + return $this->redirectToRoute('back.candidat.offres_emploi.index'); + } + + return $this->render('candidat/candidature/new.html.twig', [ + 'form' => $form->createView(), + 'offre' => $offre, + ]); + } + + /** + * Méthode utilitaire pour uploader un fichier + */ + private function uploadFile($file, $directoryParam, SluggerInterface $slugger): ?string + { + try { + $originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME); + $safeFilename = $slugger->slug($originalFilename); + $newFilename = $safeFilename . '-' . uniqid() . '.' . $file->guessExtension(); + + $directory = $this->getParameter($directoryParam); + if (!file_exists($directory)) { + mkdir($directory, 0777, true); + } + + $file->move($directory, $newFilename); + $this->logger->info('Fichier uploadé : ' . $newFilename); + return $newFilename; + } catch (\Exception $e) { + $this->logger->error('Erreur lors de l\'upload du fichier: ' . $e->getMessage()); + return null; + } + } +} diff --git a/src/Controller/CandidatOffreEmploiController.php b/src/Controller/CandidatOffreEmploiController.php new file mode 100644 index 0000000..9335012 --- /dev/null +++ b/src/Controller/CandidatOffreEmploiController.php @@ -0,0 +1,45 @@ +query->get('type'); + + $criteria = ['isActive' => true]; + if ($type) { + $criteria['typeContrat'] = $type; + } + + $offres = $repository->findBy($criteria, ['datePublication' => 'DESC']); + + return $this->render('back_office/candidat/offres_emploi/index.html.twig', [ + 'offres' => $offres, + 'type' => $type, + ]); + } + + #[Route('/{id}', name: 'back.candidat.offres_emploi.show')] + public function show(OffreEmploi $offre): Response + { + // Vérifier si l'offre est active + if (!$offre->isIsActive()) { + throw $this->createNotFoundException('Cette offre d\'emploi n\'est pas disponible.'); + } + + return $this->render('back_office/candidat/offres_emploi/show.html.twig', [ + 'offre' => $offre, + ]); + } +} diff --git a/src/Controller/CandidatureController.php b/src/Controller/CandidatureController.php new file mode 100644 index 0000000..b2b56ef --- /dev/null +++ b/src/Controller/CandidatureController.php @@ -0,0 +1,340 @@ +query->get('offre'); + $candidatures = $offreId + ? $repository->findBy(['offre' => $offreId], ['dateCandidature' => 'DESC']) + : $repository->findBy([], ['dateCandidature' => 'DESC']); + + return $this->render('back_office/candidatures/index.html.twig', [ + 'candidatures' => $candidatures, + 'offres' => $offreRepository->findAll(), + 'offreId' => $offreId, + ]); + } + + #[Route('/{id}/edit', name: 'back.candidatures.edit', methods: ['GET', 'POST'])] + public function edit(Candidature $candidature, Request $request): Response + { + try { + $form = $this->createForm(CandidatureType::class, $candidature); + + if ($request->isMethod('POST')) { + $this->logger->info('Requête POST reçue pour modifier une candidature'); + + $data = $request->request->all('candidature'); + + if (isset($data)) { + $candidature->setNom($data['nom'] ?? ''); + $candidature->setPrenom($data['prenom'] ?? ''); + $candidature->setEmail($data['email'] ?? ''); + $candidature->setTelephone($data['telephone'] ?? ''); + $candidature->setMessage($data['message'] ?? ''); + $candidature->setStatut($data['statut'] ?? 'en_attente'); + + try { + $this->em->flush(); + + $this->logger->info('Candidature modifiée : ' . $candidature->getNom() . ' ' . $candidature->getPrenom()); + $this->addFlash('success', 'La candidature a été modifiée avec succès'); + return $this->redirectToRoute('back.candidatures.index'); + } catch (\Exception $e) { + $this->logger->error('Erreur lors de la modification : ' . $e->getMessage()); + $this->addFlash('error', 'Une erreur est survenue lors de la modification de la candidature'); + } + } + } + + return $this->render('back_office/candidatures/edit.html.twig', [ + 'form' => $form->createView(), + 'candidature' => $candidature, + ]); + } catch (\Exception $e) { + $this->logger->error('Erreur lors de la modification de la candidature : ' . $e->getMessage()); + $this->addFlash('error', 'Une erreur est survenue lors de la modification de la candidature.'); + return $this->redirectToRoute('back.candidatures.index'); + } + } + + #[Route('/{id}/delete', name: 'back.candidatures.delete', methods: ['GET', 'POST'])] + public function delete(Candidature $candidature): Response + { + try { + $this->em->remove($candidature); + $this->em->flush(); + + $this->logger->info('Candidature supprimée : ' . $candidature->getNom() . ' ' . $candidature->getPrenom()); + $this->addFlash('success', 'La candidature a été supprimée avec succès'); + } catch (\Exception $e) { + $this->logger->error('Erreur lors de la suppression de la candidature : ' . $e->getMessage()); + $this->addFlash('error', 'Une erreur est survenue lors de la suppression de la candidature.'); + } + + return $this->redirectToRoute('back.candidatures.index'); + } + + #[Route('/{id}/accept', name: 'back.candidatures.accept', methods: ['GET'])] + public function accept(Candidature $candidature, CandidatureRepository $candidatureRepository): Response + { + try { + // 1. Récupérer l'offre associée à la candidature + $offre = $candidature->getOffre(); + + // 2. Mettre à jour le statut de la candidature à "acceptee" + $candidature->setStatut('acceptee'); + + // 3. Désactiver l'offre (la clôturer) + $offre->setIsActive(false); + + // 4. Mettre à jour toutes les autres candidatures pour cette offre à "refusee" + $autreCandidatures = $candidatureRepository->findBy(['offre' => $offre]); + foreach ($autreCandidatures as $autreCandidature) { + if ($autreCandidature->getId() !== $candidature->getId()) { + $autreCandidature->setStatut('refusee'); + } + } + + // 5. Enregistrer les modifications + $this->em->flush(); + + // 6. Envoyer un email d'acceptation au candidat sélectionné + $this->logger->info('Tentative d\'envoi d\'email d\'acceptation à : ' . $candidature->getEmail()); + $emailSent = $this->emailService->sendEmail($candidature, 'accepted'); + if ($emailSent) { + $this->logger->info('Email d\'acceptation envoyé avec succès à : ' . $candidature->getEmail()); + } else { + $this->logger->warning('Impossible d\'envoyer l\'email d\'acceptation à : ' . $candidature->getEmail()); + } + + // 7. Envoyer des emails de refus aux autres candidats + foreach ($autreCandidatures as $autreCandidature) { + if ($autreCandidature->getId() !== $candidature->getId()) { + $this->logger->info('Tentative d\'envoi d\'email de refus à : ' . $autreCandidature->getEmail()); + $emailSent = $this->emailService->sendEmail($autreCandidature, 'rejected'); + if ($emailSent) { + $this->logger->info('Email de refus envoyé avec succès à : ' . $autreCandidature->getEmail()); + } else { + $this->logger->warning('Impossible d\'envoyer l\'email de refus à : ' . $autreCandidature->getEmail()); + } + } + } + + $this->logger->info('Candidature acceptée : ' . $candidature->getNom() . ' ' . $candidature->getPrenom() . ' pour l\'offre : ' . $offre->getTitre()); + $this->addFlash('success', 'Candidature acceptée ! La candidature de ' . $candidature->getNom() . ' ' . $candidature->getPrenom() . ' a été acceptée avec succès.
L\'offre "' . $offre->getTitre() . '" a été clôturée et les autres candidatures ont été refusées.
Des emails de notification ont été envoyés aux candidats.'); + } catch (\Exception $e) { + $this->logger->error('Erreur lors de l\'acceptation de la candidature : ' . $e->getMessage()); + $this->addFlash('danger', 'Erreur ! Une erreur est survenue lors de l\'acceptation de la candidature.
Détail : ' . $e->getMessage()); + } + + return $this->redirectToRoute('back.candidatures.index'); + } + + #[Route('/{id}/view-cv', name: 'back.candidatures.view_cv', methods: ['GET'])] + public function viewCv(Candidature $candidature): Response + { + // Vérifier si le candidat a fourni un CV + if (!$candidature->getCv()) { + $this->addFlash('warning', 'Ce candidat n\'a pas fourni de CV.'); + return $this->redirectToRoute('back.candidatures.index'); + } + + // Vérifier si le fichier existe + $cvPath = $this->getParameter('cv_directory') . '/' . $candidature->getCv(); + if (!file_exists($cvPath)) { + $this->addFlash('danger', 'Le fichier CV n\'existe pas sur le serveur.'); + return $this->redirectToRoute('back.candidatures.index'); + } + + return $this->render('back_office/candidatures/view_cv.html.twig', [ + 'candidature' => $candidature, + 'analysis' => null // Pas d'analyse par défaut + ]); + } + + #[Route('/{id}/analyze-cv', name: 'back.candidatures.analyze_cv', methods: ['GET'])] + public function analyzeCv(Candidature $candidature): Response + { + // Vérifier si le candidat a fourni un CV + if (!$candidature->getCv()) { + $this->addFlash('warning', 'Ce candidat n\'a pas fourni de CV.'); + return $this->redirectToRoute('back.candidatures.index'); + } + + // Vérifier si le fichier existe + $cvPath = $this->getParameter('cv_directory') . '/' . $candidature->getCv(); + if (!file_exists($cvPath)) { + $this->addFlash('danger', 'Le fichier CV n\'existe pas sur le serveur.'); + return $this->redirectToRoute('back.candidatures.index'); + } + + try { + // Analyser le CV + $this->logger->info('Début de l\'analyse du CV pour la candidature : ' . $candidature->getId()); + $result = $this->cvAnalyzerService->analyzeCv($cvPath); + + if ($result['success']) { + $this->logger->info('Analyse du CV réussie pour la candidature : ' . $candidature->getId()); + $analysis = $result['data']; + $this->addFlash('success', 'Le CV a été analysé avec succès.'); + } else { + $this->logger->error('Erreur lors de l\'analyse du CV : ' . $result['message']); + $analysis = null; + $this->addFlash('danger', 'Une erreur est survenue lors de l\'analyse du CV : ' . $result['message']); + } + + return $this->render('back_office/candidatures/view_cv.html.twig', [ + 'candidature' => $candidature, + 'analysis' => $analysis + ]); + } catch (\Exception $e) { + $this->logger->error('Exception lors de l\'analyse du CV : ' . $e->getMessage()); + $this->addFlash('danger', 'Une erreur est survenue lors de l\'analyse du CV : ' . $e->getMessage()); + return $this->redirectToRoute('back.candidatures.view_cv', ['id' => $candidature->getId()]); + } + } + + + + #[Route('/{id}/postuler', name: 'app_candidature_new', methods: ['GET', 'POST'])] + public function new( + Request $request, + OffreEmploi $offre, + EntityManagerInterface $entityManager, + SluggerInterface $slugger + ): Response { + $candidature = new Candidature(); + // Utilisation du formulaire sans le champ statut + $form = $this->createForm(CandidaturePublicType::class, $candidature); + $form->handleRequest($request); + + $this->logger->info('Formulaire soumis: ' . ($form->isSubmitted() ? 'Oui' : 'Non')); + if ($form->isSubmitted()) { + $this->logger->info('Formulaire valide: ' . ($form->isValid() ? 'Oui' : 'Non')); + if (!$form->isValid()) { + $errors = []; + foreach ($form->getErrors(true) as $error) { + $errors[] = $error->getMessage(); + } + $this->logger->error('Erreurs de validation: ' . implode(', ', $errors)); + } + } + + if ($form->isSubmitted() && $form->isValid()) { + try { + // Traitement du CV + $cvFile = $form->get('cv')->getData(); + $this->logger->info('CV file: ' . ($cvFile ? 'Présent' : 'Absent')); + if ($cvFile) { + try { + $originalFilename = pathinfo($cvFile->getClientOriginalName(), PATHINFO_FILENAME); + $safeFilename = $slugger->slug($originalFilename); + $newFilename = $safeFilename.'-'.uniqid().'.'.$cvFile->guessExtension(); + + // Vérifier si le répertoire existe, sinon le créer + $cvDirectory = $this->getParameter('cv_directory'); + $this->logger->info('CV directory: ' . $cvDirectory); + if (!file_exists($cvDirectory)) { + $this->logger->info('Création du répertoire CV: ' . $cvDirectory); + mkdir($cvDirectory, 0777, true); + } + + $cvFile->move($cvDirectory, $newFilename); + $candidature->setCv($newFilename); + $this->logger->info('CV uploadé : ' . $newFilename); + } catch (\Exception $e) { + $this->logger->error('Erreur lors de l\'upload du CV: ' . $e->getMessage()); + } + } + + // Traitement de la lettre de motivation + $lettreMotivationFile = $form->get('lettreMotivation')->getData(); + $this->logger->info('Lettre de motivation file: ' . ($lettreMotivationFile ? 'Présente' : 'Absente')); + if ($lettreMotivationFile) { + try { + $originalFilename = pathinfo($lettreMotivationFile->getClientOriginalName(), PATHINFO_FILENAME); + $safeFilename = $slugger->slug($originalFilename); + $newFilename = $safeFilename.'-'.uniqid().'.'.$lettreMotivationFile->guessExtension(); + + // Vérifier si le répertoire existe, sinon le créer + $lettreMotivationDirectory = $this->getParameter('lettre_motivation_directory'); + $this->logger->info('Lettre de motivation directory: ' . $lettreMotivationDirectory); + if (!file_exists($lettreMotivationDirectory)) { + $this->logger->info('Création du répertoire lettre de motivation: ' . $lettreMotivationDirectory); + mkdir($lettreMotivationDirectory, 0777, true); + } + + $lettreMotivationFile->move($lettreMotivationDirectory, $newFilename); + $candidature->setLettreMotivation($newFilename); + $this->logger->info('Lettre de motivation uploadée : ' . $newFilename); + } catch (\Exception $e) { + $this->logger->error('Erreur lors de l\'upload de la lettre de motivation: ' . $e->getMessage()); + } + } + + $candidature->setDateCandidature(new \DateTime()); + $candidature->setOffre($offre); + // Le statut est déjà défini à 'en_attente' dans le constructeur de l'entité + + $entityManager->persist($candidature); + $entityManager->flush(); + + $this->logger->info('Candidature créée avec succès pour l\'offre : ' . $offre->getTitre()); + $this->addFlash('success', 'Votre candidature a été envoyée avec succès !'); + + // Redirection vers la page des offres d'emploi pour les candidats + $this->logger->info('Redirection vers la page des offres d\'emploi pour les candidats'); + return $this->redirectToRoute('back.candidat.offres_emploi.index', [], 301); + } catch (\Exception $e) { + $this->logger->error('Erreur lors de la création de la candidature : ' . $e->getMessage()); + $this->addFlash('error', 'Une erreur est survenue lors de l\'envoi de votre candidature. Veuillez réessayer.'); + } + } + + return $this->render('candidature/new.html.twig', [ + 'candidature' => $candidature, + 'form' => $form->createView(), + 'offre' => $offre, + ]); + } + + #[Route('/{id}', name: 'app_candidature_show', methods: ['GET'])] + public function show(Candidature $candidature): Response + { + return $this->render('candidature/show.html.twig', [ + 'candidature' => $candidature, + ]); + } +} \ No newline at end of file diff --git a/src/Controller/OffreEmploiController.php b/src/Controller/OffreEmploiController.php new file mode 100644 index 0000000..02e4454 --- /dev/null +++ b/src/Controller/OffreEmploiController.php @@ -0,0 +1,157 @@ +findBy([], ['datePublication' => 'DESC']); + $this->logger->info('Nombre d\'offres récupérées : ' . count($offres)); + + return $this->render('back_office/offres_emploi/index.html.twig', [ + 'offres' => $offres, + ]); + } catch (\Exception $e) { + $this->logger->error('Erreur lors de la récupération des offres : ' . $e->getMessage()); + $this->addFlash('error', 'Une erreur est survenue lors de la récupération des offres.'); + return $this->render('back_office/offres_emploi/index.html.twig', [ + 'offres' => [], + ]); + } + } + + #[Route('/add', name: 'back.offres_emploi.add', methods: ['GET', 'POST'])] + public function add(Request $request): Response + { + $offre = new OffreEmploi(); + $form = $this->createForm(OffreEmploiType::class, $offre); + + if ($request->isMethod('POST')) { + $this->logger->info('Requête POST reçue pour ajouter une offre'); + + $data = $request->request->all('offre_emploi'); + + if (isset($data)) { + $offre->setTitre($data['titre'] ?? ''); + $offre->setTypeContrat($data['typeContrat'] ?? ''); + $offre->setLocalisation($data['localisation'] ?? ''); + $offre->setSalaire($data['salaire'] ?? ''); + $offre->setDescription($data['description'] ?? ''); + $offre->setProfilRecherche($data['profilRecherche'] ?? ''); + $offre->setAvantages($data['avantages'] ?? null); + $offre->setIsActive(isset($data['isActive']) && $data['isActive'] === '1'); + $offre->setDatePublication(new \DateTime()); + + try { + $this->em->persist($offre); + $this->em->flush(); + + $this->addFlash('success', 'L\'offre d\'emploi a été créée avec succès'); + return $this->redirectToRoute('back.offres_emploi.index'); + } catch (\Exception $e) { + $this->logger->error('Erreur lors de la création : ' . $e->getMessage()); + $this->addFlash('error', 'Une erreur est survenue lors de la création de l\'offre'); + } + } + } + + return $this->render('back_office/offres_emploi/add.html.twig', [ + 'form' => $form->createView(), + ]); + } + + #[Route('/{id}/edit', name: 'back.offres_emploi.edit', methods: ['GET', 'POST'])] + public function edit(OffreEmploi $offre, Request $request): Response + { + try { + $form = $this->createForm(OffreEmploiType::class, $offre); + + if ($request->isMethod('POST')) { + $this->logger->info('Requête POST reçue pour modifier une offre'); + + $data = $request->request->all('offre_emploi'); + + if (isset($data)) { + $offre->setTitre($data['titre'] ?? ''); + $offre->setTypeContrat($data['typeContrat'] ?? ''); + $offre->setLocalisation($data['localisation'] ?? ''); + $offre->setSalaire($data['salaire'] ?? ''); + $offre->setDescription($data['description'] ?? ''); + $offre->setProfilRecherche($data['profilRecherche'] ?? ''); + $offre->setAvantages($data['avantages'] ?? null); + $offre->setIsActive(isset($data['isActive']) && $data['isActive'] === '1'); + + try { + $this->em->flush(); + + $this->logger->info('Offre modifiée : ' . $offre->getTitre()); + $this->addFlash('success', 'L\'offre d\'emploi a été modifiée avec succès'); + return $this->redirectToRoute('back.offres_emploi.index'); + } catch (\Exception $e) { + $this->logger->error('Erreur lors de la modification : ' . $e->getMessage()); + $this->addFlash('error', 'Une erreur est survenue lors de la modification de l\'offre'); + } + } + } + + return $this->render('back_office/offres_emploi/edit.html.twig', [ + 'form' => $form->createView(), + 'offre' => $offre, + ]); + } catch (\Exception $e) { + $this->logger->error('Erreur lors de la modification de l\'offre : ' . $e->getMessage()); + $this->addFlash('error', 'Une erreur est survenue lors de la modification de l\'offre.'); + return $this->redirectToRoute('back.offres_emploi.index'); + } + } + + #[Route('/{id}/delete', name: 'back.offres_emploi.delete', methods: ['GET', 'POST'])] + public function delete(OffreEmploi $offre): Response + { + try { + $this->em->remove($offre); + $this->em->flush(); + + $this->logger->info('Offre supprimée : ' . $offre->getTitre()); + $this->addFlash('success', 'L\'offre d\'emploi a été supprimée avec succès'); + } catch (\Exception $e) { + $this->logger->error('Erreur lors de la suppression de l\'offre : ' . $e->getMessage()); + $this->addFlash('error', 'Une erreur est survenue lors de la suppression de l\'offre.'); + } + + return $this->redirectToRoute('back.offres_emploi.index'); + } + + #[Route('/{id}/show', name: 'back.offres_emploi.show', methods: ['GET'])] + public function show(OffreEmploi $offre): Response + { + try { + return $this->render('back_office/offres_emploi/show.html.twig', [ + 'offre' => $offre, + ]); + } catch (\Exception $e) { + $this->logger->error('Erreur lors de l\'affichage de l\'offre : ' . $e->getMessage()); + $this->addFlash('error', 'Une erreur est survenue lors de l\'affichage de l\'offre'); + return $this->redirectToRoute('back.offres_emploi.index'); + } + } +} \ No newline at end of file diff --git a/src/Controller/PublicOffreEmploiController.php b/src/Controller/PublicOffreEmploiController.php new file mode 100644 index 0000000..e4ac5d9 --- /dev/null +++ b/src/Controller/PublicOffreEmploiController.php @@ -0,0 +1,50 @@ + true]; + + // Filtrage par type de contrat + $type = $request->query->get('type'); + if ($type) { + $criteria['typeContrat'] = $type; + } + + // Filtrage par localisation + $localisation = $request->query->get('localisation'); + if ($localisation) { + $criteria['localisation'] = $localisation; + } + + $offres = $offreEmploiRepository->findBy($criteria, ['datePublication' => 'DESC']); + + return $this->render('public/offre_emploi/index_back_office.html.twig', [ + 'offres' => $offres, + ]); + } + + #[Route('/offres-emploi/{id}', name: 'app_public_offre_emploi_show')] + public function show(OffreEmploi $offre): Response + { + // Vérifier si l'offre est active + if (!$offre->isIsActive()) { + throw $this->createNotFoundException('Cette offre d\'emploi n\'est pas disponible.'); + } + + return $this->render('public/offre_emploi/show_back_office.html.twig', [ + 'offre' => $offre, + ]); + } +} \ No newline at end of file diff --git a/src/Controller/SpellCheckerController.php b/src/Controller/SpellCheckerController.php new file mode 100644 index 0000000..1af015c --- /dev/null +++ b/src/Controller/SpellCheckerController.php @@ -0,0 +1,56 @@ +logger = $logger; + $this->spellCheckerService = $spellCheckerService; + } + + #[Route('/api/spell-check', name: 'api.spell_check', methods: ['POST'])] + public function checkSpelling(Request $request): Response + { + $text = $request->request->get('text'); + $language = $request->request->get('language', 'fr'); + + $this->logger->info('Requête de vérification orthographique reçue'); + $this->logger->info('Texte à vérifier: ' . substr($text, 0, 50) . '...'); + + if (empty($text)) { + $this->logger->warning('Texte vide reçu'); + return new JsonResponse(['error' => 'Le texte est requis'], Response::HTTP_BAD_REQUEST); + } + + try { + $corrections = $this->spellCheckerService->checkSpelling($text, $language); + + $this->logger->info('Corrections trouvées: ' . count($corrections)); + + return new JsonResponse([ + 'success' => true, + 'corrections' => $corrections, + ]); + } catch (\Exception $e) { + $this->logger->error('Erreur lors de la vérification orthographique: ' . $e->getMessage()); + + return new JsonResponse([ + 'success' => false, + 'error' => 'Une erreur est survenue lors de la vérification orthographique: ' . $e->getMessage(), + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/Entity/Candidature.php b/src/Entity/Candidature.php index 9b14180..ba21ebb 100644 --- a/src/Entity/Candidature.php +++ b/src/Entity/Candidature.php @@ -1,103 +1,194 @@ dateCandidature = new \DateTimeImmutable(); + $this->statut = 'en_attente'; + } + public function getId(): ?int { return $this->id; } - public function setId(int $id): self + public function getNom(): ?string { - $this->id = $id; - return $this; + return $this->nom; } - #[ORM\ManyToOne(targetEntity: Candidat::class, inversedBy: 'candidatures')] - #[ORM\JoinColumn(name: 'candidat_id', referencedColumnName: 'id')] - private ?Candidat $candidat = null; + public function setNom(string $nom): static + { + $this->nom = $nom; + return $this; + } - public function getCandidat(): ?Candidat + public function getPrenom(): ?string { - return $this->candidat; + return $this->prenom; } - public function setCandidat(?Candidat $candidat): self + public function setPrenom(string $prenom): static { - $this->candidat = $candidat; + $this->prenom = $prenom; return $this; } - #[ORM\ManyToOne(targetEntity: OffreEmploi::class, inversedBy: 'candidatures')] - #[ORM\JoinColumn(name: 'offre_emploi_id', referencedColumnName: 'id')] - private ?OffreEmploi $offreEmploi = null; + public function getEmail(): ?string + { + return $this->email; + } - public function getOffreEmploi(): ?OffreEmploi + public function setEmail(string $email): static { - return $this->offreEmploi; + $this->email = $email; + return $this; } - public function setOffreEmploi(?OffreEmploi $offreEmploi): self + public function getTelephone(): ?string { - $this->offreEmploi = $offreEmploi; + return $this->telephone; + } + + public function setTelephone(?string $telephone): static + { + $this->telephone = $telephone; return $this; } - #[ORM\Column(type: 'string', nullable: false)] - private ?string $cv = null; + public function getMessage(): ?string + { + return $this->message; + } + + public function setMessage(string $message): static + { + $this->message = $message; + return $this; + } public function getCv(): ?string { return $this->cv; } - public function setCv(string $cv): self + public function setCv(?string $cv): static { $this->cv = $cv; return $this; } - #[ORM\Column(type: 'string', nullable: true)] - private ?string $reference = null; - - public function getReference(): ?string + public function getLettreMotivation(): ?string { - return $this->reference; + return $this->lettreMotivation; } - public function setReference(?string $reference): self + public function setLettreMotivation(?string $lettreMotivation): static { - $this->reference = $reference; + $this->lettreMotivation = $lettreMotivation; return $this; } - #[ORM\Column(type: 'string', nullable: true)] - private ?string $status = null; + public function getDateCandidature(): ?\DateTimeImmutable + { + return $this->dateCandidature; + } - public function getStatus(): ?string + public function setDateCandidature(\DateTimeImmutable $dateCandidature): static { - return $this->status; + $this->dateCandidature = $dateCandidature; + return $this; } - public function setStatus(?string $status): self + public function getOffre(): ?OffreEmploi { - $this->status = $status; + return $this->offre; + } + + public function setOffre(?OffreEmploi $offre): static + { + $this->offre = $offre; return $this; } + public function getStatut(): ?string + { + return $this->statut; + } + + public function setStatut(string $statut): static + { + $this->statut = $statut; + return $this; + } } diff --git a/src/Entity/OffreEmploi.php b/src/Entity/OffreEmploi.php index b801e21..e935bdf 100644 --- a/src/Entity/OffreEmploi.php +++ b/src/Entity/OffreEmploi.php @@ -1,82 +1,198 @@ candidatures = new ArrayCollection(); + } + public function getId(): ?int { return $this->id; } - public function setId(int $id): self + public function getTitre(): ?string { - $this->id = $id; + return $this->titre; + } + + public function setTitre(string $titre): static + { + $this->titre = $titre; return $this; } - #[ORM\Column(type: 'string', nullable: false)] - private ?string $title = null; + public function getTypeContrat(): ?string + { + return $this->typeContrat; + } + + public function setTypeContrat(string $typeContrat): static + { + $this->typeContrat = $typeContrat; + return $this; + } - public function getTitle(): ?string + public function getLocalisation(): ?string { - return $this->title; + return $this->localisation; } - public function setTitle(string $title): self + public function setLocalisation(string $localisation): static { - $this->title = $title; + $this->localisation = $localisation; return $this; } - #[ORM\Column(type: 'text', nullable: false)] - private ?string $description = null; + public function getSalaire(): ?string + { + return $this->salaire; + } + + public function setSalaire(string $salaire): static + { + $this->salaire = $salaire; + return $this; + } public function getDescription(): ?string { return $this->description; } - public function setDescription(string $description): self + public function setDescription(string $description): static { $this->description = $description; return $this; } - #[ORM\Column(type: 'string', nullable: false)] - private ?string $location = null; + public function getProfilRecherche(): ?string + { + return $this->profilRecherche; + } + + public function setProfilRecherche(string $profilRecherche): static + { + $this->profilRecherche = $profilRecherche; + return $this; + } - public function getLocation(): ?string + public function getAvantages(): ?string { - return $this->location; + return $this->avantages; } - public function setLocation(string $location): self + public function setAvantages(?string $avantages): static { - $this->location = $location; + $this->avantages = $avantages; return $this; } - #[ORM\OneToMany(targetEntity: Candidature::class, mappedBy: 'offreEmploi')] - private Collection $candidatures; + public function isIsActive(): ?bool + { + return $this->isActive; + } - public function __construct() + public function setIsActive(bool $isActive): static { - $this->candidatures = new ArrayCollection(); + $this->isActive = $isActive; + return $this; + } + + public function getDatePublication(): ?\DateTimeInterface + { + return $this->datePublication; + } + + public function setDatePublication(\DateTimeInterface $datePublication): static + { + $this->datePublication = $datePublication; + return $this; } /** @@ -84,24 +200,25 @@ public function __construct() */ public function getCandidatures(): Collection { - if (!$this->candidatures instanceof Collection) { - $this->candidatures = new ArrayCollection(); - } return $this->candidatures; } - public function addCandidature(Candidature $candidature): self + public function addCandidature(Candidature $candidature): static { - if (!$this->getCandidatures()->contains($candidature)) { - $this->getCandidatures()->add($candidature); + if (! $this->candidatures->contains($candidature)) { + $this->candidatures->add($candidature); + $candidature->setOffre($this); } return $this; } - public function removeCandidature(Candidature $candidature): self + public function removeCandidature(Candidature $candidature): static { - $this->getCandidatures()->removeElement($candidature); + if ($this->candidatures->removeElement($candidature)) { + if ($candidature->getOffre() === $this) { + $candidature->setOffre(null); + } + } return $this; } - } diff --git a/src/Form/CandidaturePublicType.php b/src/Form/CandidaturePublicType.php new file mode 100644 index 0000000..d16957a --- /dev/null +++ b/src/Form/CandidaturePublicType.php @@ -0,0 +1,124 @@ +add('nom', TextType::class, [ + 'label' => 'Nom', + 'attr' => ['class' => 'form-control', 'placeholder' => 'Votre nom'], + 'constraints' => [ + new NotBlank(['message' => 'Le nom est obligatoire']), + new Length([ + 'min' => 2, + 'max' => 255, + 'minMessage' => 'Le nom doit contenir au moins {{ limit }} caractères', + 'maxMessage' => 'Le nom ne peut pas dépasser {{ limit }} caractères' + ]) + ] + ]) + ->add('prenom', TextType::class, [ + 'label' => 'Prénom', + 'attr' => ['class' => 'form-control', 'placeholder' => 'Votre prénom'], + 'constraints' => [ + new NotBlank(['message' => 'Le prénom est obligatoire']), + new Length([ + 'min' => 2, + 'max' => 255, + 'minMessage' => 'Le prénom doit contenir au moins {{ limit }} caractères', + 'maxMessage' => 'Le prénom ne peut pas dépasser {{ limit }} caractères' + ]) + ] + ]) + ->add('email', EmailType::class, [ + 'label' => 'Email', + 'attr' => ['class' => 'form-control', 'placeholder' => 'exemple@email.com'], + 'constraints' => [ + new NotBlank(['message' => 'L\'email est obligatoire']), + new Email(['message' => 'L\'email {{ value }} n\'est pas un email valide']) + ] + ]) + ->add('telephone', TextType::class, [ + 'label' => 'Téléphone', + 'attr' => ['class' => 'form-control', 'placeholder' => 'Ex: 0612345678'], + 'constraints' => [ + new NotBlank(['message' => 'Le téléphone est obligatoire']), + new Regex([ + 'pattern' => '/^[0-9\s\+\-\.]{8,}$/', + 'message' => 'Le numéro de téléphone n\'est pas valide' + ]) + ] + ]) + ->add('message', TextareaType::class, [ + 'label' => 'Message', + 'attr' => [ + 'class' => 'form-control', + 'rows' => 5, + 'placeholder' => 'Décrivez votre motivation pour ce poste...' + ], + 'constraints' => [ + new NotBlank(['message' => 'Le message est obligatoire']), + new Length([ + 'min' => 10, + 'minMessage' => 'Votre message doit contenir au moins {{ limit }} caractères' + ]) + ] + ]) + ->add('cv', FileType::class, [ + 'label' => 'CV (PDF)', + 'required' => false, + 'mapped' => false, + 'constraints' => [ + new File([ + 'maxSize' => '1024k', + 'mimeTypes' => [ + 'application/pdf', + 'application/x-pdf', + ], + 'mimeTypesMessage' => 'Veuillez télécharger un fichier PDF valide', + ]) + ], + ]) + ->add('lettreMotivation', FileType::class, [ + 'label' => 'Lettre de motivation (PDF)', + 'required' => false, + 'mapped' => false, + 'constraints' => [ + new File([ + 'maxSize' => '1024k', + 'mimeTypes' => [ + 'application/pdf', + 'application/x-pdf', + ], + 'mimeTypesMessage' => 'Veuillez télécharger un fichier PDF valide', + ]) + ], + ]); + // Le champ statut est intentionnellement omis + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Candidature::class, + 'attr' => ['novalidate' => 'novalidate'] // Désactive la validation HTML5 pour utiliser uniquement la validation Symfony + ]); + } +} diff --git a/src/Form/CandidatureSimpleType.php b/src/Form/CandidatureSimpleType.php new file mode 100644 index 0000000..0afe715 --- /dev/null +++ b/src/Form/CandidatureSimpleType.php @@ -0,0 +1,134 @@ +add('nom', TextType::class, [ + 'label' => 'Nom', + 'attr' => ['class' => 'form-control', 'placeholder' => 'Votre nom'], + 'constraints' => [ + new NotBlank(['message' => 'Le nom est obligatoire']), + new Length([ + 'min' => 2, + 'max' => 255, + 'minMessage' => 'Le nom doit contenir au moins {{ limit }} caractères', + 'maxMessage' => 'Le nom ne peut pas dépasser {{ limit }} caractères' + ]) + ] + ]) + ->add('prenom', TextType::class, [ + 'label' => 'Prénom', + 'attr' => ['class' => 'form-control', 'placeholder' => 'Votre prénom'], + 'constraints' => [ + new NotBlank(['message' => 'Le prénom est obligatoire']), + new Length([ + 'min' => 2, + 'max' => 255, + 'minMessage' => 'Le prénom doit contenir au moins {{ limit }} caractères', + 'maxMessage' => 'Le prénom ne peut pas dépasser {{ limit }} caractères' + ]) + ] + ]) + ->add('email', EmailType::class, [ + 'label' => 'Email', + 'attr' => ['class' => 'form-control', 'placeholder' => 'exemple@email.com'], + 'constraints' => [ + new NotBlank(['message' => 'L\'email est obligatoire']), + new Email(['message' => 'L\'email {{ value }} n\'est pas un email valide']) + ] + ]) + ->add('telephone', TextType::class, [ + 'label' => 'Téléphone', + 'attr' => ['class' => 'form-control', 'placeholder' => 'Ex: 0612345678'], + 'constraints' => [ + new NotBlank(['message' => 'Le téléphone est obligatoire']), + new Regex([ + 'pattern' => '/^[0-9\s\+\-\.]{8,}$/', + 'message' => 'Le numéro de téléphone n\'est pas valide' + ]) + ] + ]) + ->add('message', TextareaType::class, [ + 'label' => 'Message', + 'attr' => [ + 'class' => 'form-control', + 'rows' => 5, + 'placeholder' => 'Décrivez votre motivation pour ce poste...' + ], + 'constraints' => [ + new NotBlank(['message' => 'Le message est obligatoire']), + new Length([ + 'min' => 10, + 'minMessage' => 'Votre message doit contenir au moins {{ limit }} caractères' + ]) + ] + ]) + ->add('cv', FileType::class, [ + 'label' => 'CV (PDF) *', + 'required' => true, + 'mapped' => false, + 'attr' => ['class' => 'form-control'], + 'constraints' => [ + new NotBlank([ + 'message' => 'Veuillez télécharger votre CV au format PDF' + ]), + new File([ + 'maxSize' => '1024k', + 'mimeTypes' => [ + 'application/pdf', + 'application/x-pdf', + ], + 'mimeTypesMessage' => 'Veuillez télécharger un fichier PDF valide', + ]) + ], + ]) + ->add('lettreMotivation', FileType::class, [ + 'label' => 'Lettre de motivation (PDF) *', + 'required' => true, + 'mapped' => false, + 'attr' => ['class' => 'form-control'], + 'constraints' => [ + new NotBlank([ + 'message' => 'Veuillez télécharger votre lettre de motivation au format PDF' + ]), + new File([ + 'maxSize' => '1024k', + 'mimeTypes' => [ + 'application/pdf', + 'application/x-pdf', + ], + 'mimeTypesMessage' => 'Veuillez télécharger un fichier PDF valide', + ]) + ], + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Candidature::class, + 'csrf_protection' => true, + 'csrf_field_name' => '_token', + 'csrf_token_id' => 'candidature_item', + 'validation_groups' => ['Default'], + ]); + } +} diff --git a/src/Form/CandidatureType.php b/src/Form/CandidatureType.php new file mode 100644 index 0000000..91048a8 --- /dev/null +++ b/src/Form/CandidatureType.php @@ -0,0 +1,134 @@ +add('nom', TextType::class, [ + 'label' => 'Nom', + 'attr' => ['class' => 'form-control', 'placeholder' => 'Votre nom'], + 'constraints' => [ + new NotBlank(['message' => 'Le nom est obligatoire']), + new Length([ + 'min' => 2, + 'max' => 255, + 'minMessage' => 'Le nom doit contenir au moins {{ limit }} caractères', + 'maxMessage' => 'Le nom ne peut pas dépasser {{ limit }} caractères' + ]) + ] + ]) + ->add('prenom', TextType::class, [ + 'label' => 'Prénom', + 'attr' => ['class' => 'form-control', 'placeholder' => 'Votre prénom'], + 'constraints' => [ + new NotBlank(['message' => 'Le prénom est obligatoire']), + new Length([ + 'min' => 2, + 'max' => 255, + 'minMessage' => 'Le prénom doit contenir au moins {{ limit }} caractères', + 'maxMessage' => 'Le prénom ne peut pas dépasser {{ limit }} caractères' + ]) + ] + ]) + ->add('email', EmailType::class, [ + 'label' => 'Email', + 'attr' => ['class' => 'form-control', 'placeholder' => 'exemple@email.com'], + 'constraints' => [ + new NotBlank(['message' => 'L\'email est obligatoire']), + new Email(['message' => 'L\'email {{ value }} n\'est pas un email valide']) + ] + ]) + ->add('telephone', TextType::class, [ + 'label' => 'Téléphone', + 'attr' => ['class' => 'form-control', 'placeholder' => 'Ex: 0612345678'], + 'constraints' => [ + new NotBlank(['message' => 'Le téléphone est obligatoire']), + new Regex([ + 'pattern' => '/^[0-9\s\+\-\.]{8,}$/', + 'message' => 'Le numéro de téléphone n\'est pas valide' + ]) + ] + ]) + ->add('message', TextareaType::class, [ + 'label' => 'Message', + 'attr' => [ + 'class' => 'form-control', + 'rows' => 5, + 'placeholder' => 'Décrivez votre motivation pour ce poste...' + ], + 'constraints' => [ + new NotBlank(['message' => 'Le message est obligatoire']), + new Length([ + 'min' => 10, + 'minMessage' => 'Votre message doit contenir au moins {{ limit }} caractères' + ]) + ] + ]) + ->add('cv', FileType::class, [ + 'label' => 'CV (PDF)', + 'required' => false, + 'mapped' => false, + 'constraints' => [ + new File([ + 'maxSize' => '1024k', + 'mimeTypes' => [ + 'application/pdf', + 'application/x-pdf', + ], + 'mimeTypesMessage' => 'Veuillez télécharger un fichier PDF valide', + ]) + ], + ]) + ->add('lettreMotivation', FileType::class, [ + 'label' => 'Lettre de motivation (PDF)', + 'required' => false, + 'mapped' => false, + 'constraints' => [ + new File([ + 'maxSize' => '1024k', + 'mimeTypes' => [ + 'application/pdf', + 'application/x-pdf', + ], + 'mimeTypesMessage' => 'Veuillez télécharger un fichier PDF valide', + ]) + ], + ]) + ->add('statut', ChoiceType::class, [ + 'label' => 'Statut', + 'choices' => [ + 'En attente' => 'en_attente', + 'En cours de traitement' => 'en_cours', + 'Acceptée' => 'acceptee', + 'Refusée' => 'refusee', + ], + 'required' => true, + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Candidature::class, + 'attr' => ['novalidate' => 'novalidate'] // Désactive la validation HTML5 pour utiliser uniquement la validation Symfony + ]); + } +} \ No newline at end of file diff --git a/src/Form/OffreEmploiType.php b/src/Form/OffreEmploiType.php new file mode 100644 index 0000000..6ac9552 --- /dev/null +++ b/src/Form/OffreEmploiType.php @@ -0,0 +1,96 @@ +add('titre', TextType::class, [ + 'label' => 'Titre de l\'offre', + 'attr' => [ + 'class' => 'form-control', + 'placeholder' => 'Ex: Développeur Symfony' + ] + ]) + ->add('typeContrat', ChoiceType::class, [ + 'label' => 'Type de contrat', + 'choices' => [ + 'CDI' => 'CDI', + 'CDD' => 'CDD', + 'Stage' => 'Stage', + 'Alternance' => 'Alternance', + 'Freelance' => 'Freelance' + ], + 'attr' => [ + 'class' => 'form-control' + ] + ]) + ->add('localisation', TextType::class, [ + 'label' => 'Localisation', + 'attr' => [ + 'class' => 'form-control', + 'placeholder' => 'Ex: Paris' + ] + ]) + ->add('salaire', TextType::class, [ + 'label' => 'Salaire', + 'attr' => [ + 'class' => 'form-control', + 'placeholder' => 'Ex: 45-55K€' + ] + ]) + ->add('description', TextareaType::class, [ + 'label' => 'Description', + 'attr' => [ + 'class' => 'form-control', + 'rows' => 5, + 'placeholder' => 'Description détaillée de l\'offre' + ] + ]) + ->add('profilRecherche', TextareaType::class, [ + 'label' => 'Profil recherché', + 'attr' => [ + 'class' => 'form-control', + 'rows' => 5, + 'placeholder' => 'Profil et compétences recherchés' + ] + ]) + ->add('avantages', TextareaType::class, [ + 'label' => 'Avantages', + 'required' => false, + 'attr' => [ + 'class' => 'form-control', + 'rows' => 3, + 'placeholder' => 'Avantages proposés' + ] + ]) + ->add('isActive', ChoiceType::class, [ + 'label' => 'Statut', + 'choices' => [ + 'Active' => true, + 'Inactive' => false + ], + 'attr' => [ + 'class' => 'form-control' + ] + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => OffreEmploi::class, + 'attr' => ['novalidate' => 'novalidate'] // Désactive la validation HTML5 pour utiliser uniquement la validation Symfony + ]); + } +} \ No newline at end of file diff --git a/src/Repository/CandidatureRepository.php b/src/Repository/CandidatureRepository.php index 1e95257..18ea2a6 100644 --- a/src/Repository/CandidatureRepository.php +++ b/src/Repository/CandidatureRepository.php @@ -1,5 +1,4 @@ createQueryBuilder('c') - // ->andWhere('c.exampleField = :val') - // ->setParameter('val', $value) - // ->orderBy('c.id', 'ASC') - // ->setMaxResults(10) - // ->getQuery() - // ->getResult() - // ; - // } + public function save(Candidature $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(Candidature $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } - // public function findOneBySomeField($value): ?Candidature - // { - // return $this->createQueryBuilder('c') - // ->andWhere('c.exampleField = :val') - // ->setParameter('val', $value) - // ->getQuery() - // ->getOneOrNullResult() - // ; - // } + public function findByOffre($offreId) + { + return $this->createQueryBuilder('c') + ->andWhere('c.offre = :offreId') + ->setParameter('offreId', $offreId) + ->orderBy('c.dateCandidature', 'DESC') + ->getQuery() + ->getResult(); + } } diff --git a/src/Repository/OffreEmploiRepository.php b/src/Repository/OffreEmploiRepository.php index b24a7ad..6c7eaea 100644 --- a/src/Repository/OffreEmploiRepository.php +++ b/src/Repository/OffreEmploiRepository.php @@ -1,5 +1,4 @@ createQueryBuilder('o') - // ->andWhere('o.exampleField = :val') - // ->setParameter('val', $value) - // ->orderBy('o.id', 'ASC') - // ->setMaxResults(10) - // ->getQuery() - // ->getResult() - // ; - // } + public function findAllActive(): array + { + return $this->createQueryBuilder('o') + ->where('o.isActive = :active') + ->setParameter('active', true) + ->orderBy('o.datePublication', 'DESC') + ->getQuery() + ->getResult(); + } - // public function findOneBySomeField($value): ?OffreEmploi - // { - // return $this->createQueryBuilder('o') - // ->andWhere('o.exampleField = :val') - // ->setParameter('val', $value) - // ->getQuery() - // ->getOneOrNullResult() - // ; - // } + public function findByTypeContrat(string $typeContrat): array + { + return $this->createQueryBuilder('o') + ->where('o.typeContrat = :typeContrat') + ->andWhere('o.isActive = :active') + ->setParameter('typeContrat', $typeContrat) + ->setParameter('active', true) + ->orderBy('o.datePublication', 'DESC') + ->getQuery() + ->getResult(); + } } diff --git a/src/Service/CvAnalyzerService.php b/src/Service/CvAnalyzerService.php new file mode 100644 index 0000000..f5f31f9 --- /dev/null +++ b/src/Service/CvAnalyzerService.php @@ -0,0 +1,817 @@ +httpClient = $httpClient; + $this->logger = $logger; + } + + public function analyzeCv(string $cvPath): array + { + $this->logger->info('Début de l\'analyse du CV: ' . $cvPath); + + try { + // Vérifier si le fichier existe + if (!file_exists($cvPath)) { + $this->logger->error('Fichier CV non trouvé: ' . $cvPath); + return [ + 'success' => false, + 'message' => 'Fichier CV non trouvé: ' . $cvPath + ]; + } + + $cvFile = new File($cvPath); + + // Vérifier la taille du fichier + if ($cvFile->getSize() > 10 * 1024 * 1024) { + $this->logger->error('Fichier CV trop volumineux: ' . $cvFile->getSize() . ' bytes'); + return [ + 'success' => false, + 'message' => 'Le fichier CV est trop volumineux (max 10MB).' + ]; + } + + // Vérifier l'extension du fichier + $extension = strtolower($cvFile->getExtension()); + if (!in_array($extension, ['pdf', 'doc', 'docx'])) { + $this->logger->error('Format de fichier non supporté: ' . $extension); + return [ + 'success' => false, + 'message' => 'Format de fichier non supporté. Utilisez PDF, DOC ou DOCX.' + ]; + } + + // Si c'est un PDF, essayer d'abord l'extraction directe + if ($extension === 'pdf') { + $this->logger->info('Fichier PDF détecté, tentative d\'extraction directe du texte...'); + $pdfText = $this->extractTextFromPdf($cvPath); + + if (!empty($pdfText)) { + $this->logger->info('Texte extrait du PDF avec succès: ' . strlen($pdfText) . ' caractères'); + + // Créer un résultat avec le texte brut + $result = [ + 'summary' => '', + 'skills' => [], + 'experience' => [], + 'education' => [], + 'rawText' => $pdfText + ]; + + // Analyser le texte brut + $this->analyzeRawText($pdfText, $result); + + // Si l'analyse a réussi et a trouvé des informations, retourner le résultat + if (!empty($result['skills']) || !empty($result['experience']) || !empty($result['education'])) { + $this->logger->info('Analyse locale réussie, informations extraites'); + return [ + 'success' => true, + 'data' => $result, + 'source' => 'local_extraction' + ]; + } + + // Sinon, continuer avec l'API Affinda comme plan B + $this->logger->info('Analyse locale insuffisante, tentative avec l\'API Affinda...'); + } + } + + // Préparer la requête à l'API Affinda + $fileContent = file_get_contents($cvPath); + $this->logger->info('Taille du fichier : ' . strlen($fileContent) . ' octets'); + + // Créer un fichier temporaire pour l'upload multipart + $tempFile = tempnam(sys_get_temp_dir(), 'cv_'); + file_put_contents($tempFile, $fileContent); + + // Créer les données du formulaire + $formData = [ + 'file' => curl_file_create($tempFile, 'application/pdf', $cvFile->getFilename()), + 'wait' => 'true', + 'workspace' => self::AFFINDA_WORKSPACE_ID, + 'collection' => self::AFFINDA_COLLECTION_ID + ]; + + $this->logger->info('Fichier temporaire créé : ' . $tempFile); + + $this->logger->info('Envoi de la requête à Affinda...'); + $this->logger->info('URL: ' . self::AFFINDA_API_URL); + $this->logger->info('Collection ID: ' . self::AFFINDA_COLLECTION_ID); + $this->logger->info('Workspace ID: ' . self::AFFINDA_WORKSPACE_ID); + + // Utiliser cURL directement pour un meilleur contrôle + $curl = curl_init(); + + curl_setopt_array($curl, [ + CURLOPT_URL => self::AFFINDA_API_URL, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => '', + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 60, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => 'POST', + CURLOPT_POSTFIELDS => $formData, + CURLOPT_HTTPHEADER => [ + 'Accept: application/json', + 'Authorization: Bearer ' . self::AFFINDA_API_KEY + ], + ]); + + $responseBody = curl_exec($curl); + $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + $error = curl_error($curl); + + curl_close($curl); + + // Supprimer le fichier temporaire + if (file_exists($tempFile)) { + unlink($tempFile); + $this->logger->info('Fichier temporaire supprimé : ' . $tempFile); + } + + if ($error) { + $this->logger->error('Erreur cURL : ' . $error); + + // En cas d'erreur avec l'API, essayer d'extraire le texte directement du PDF + $this->logger->info('Tentative d\'extraction directe du texte du PDF...'); + $pdfText = $this->extractTextFromPdf($cvPath); + + if (!empty($pdfText)) { + $this->logger->info('Texte extrait du PDF : ' . strlen($pdfText) . ' caractères'); + + // Créer un résultat avec le texte brut + $result = [ + 'summary' => '', + 'skills' => [], + 'experience' => [], + 'education' => [], + 'rawText' => $pdfText + ]; + + // Analyser le texte brut + $this->analyzeRawText($pdfText, $result); + + return [ + 'success' => true, + 'data' => $result, + 'source' => 'local_extraction' + ]; + } + + return [ + 'success' => false, + 'message' => 'Erreur lors de la communication avec l\'API : ' . $error + ]; + } + + $this->logger->info('Réponse de l\'API Affinda: Code ' . $statusCode); + $this->logger->info('Réponse brute: ' . substr($responseBody, 0, 1000) . (strlen($responseBody) > 1000 ? '...' : '')); + + if ($statusCode === 200 || $statusCode === 201) { + $data = json_decode($responseBody, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + $this->logger->error('Erreur de décodage JSON: ' . json_last_error_msg()); + return [ + 'success' => false, + 'message' => 'Erreur de décodage de la réponse: ' . json_last_error_msg() + ]; + } + + $this->logger->info('Réponse décodée: ' . print_r($data, true)); + + // Traiter les résultats + $analysisResult = $this->processApiResponse($data); + + return [ + 'success' => true, + 'data' => $analysisResult + ]; + } else { + $this->logger->error('Erreur lors de l\'analyse du CV: ' . $statusCode); + $errorMessage = 'Erreur lors de l\'analyse du CV.'; + + if ($statusCode === 401) { + $errorMessage .= ' Erreur d\'authentification. Veuillez vérifier la clé API.'; + } elseif ($statusCode === 404) { + $errorMessage .= ' Service non trouvé. Veuillez vérifier l\'URL de l\'API.'; + } else { + $errorMessage .= ' Code: ' . $statusCode . ' Réponse: ' . $responseBody; + } + + // En cas d'erreur avec l'API, essayer d'extraire le texte directement du PDF + $this->logger->info('Tentative d\'extraction directe du texte du PDF après erreur API...'); + $pdfText = $this->extractTextFromPdf($cvPath); + + if (!empty($pdfText)) { + $this->logger->info('Texte extrait du PDF : ' . strlen($pdfText) . ' caractères'); + + // Créer un résultat avec le texte brut + $result = [ + 'summary' => '', + 'skills' => [], + 'experience' => [], + 'education' => [], + 'rawText' => $pdfText + ]; + + // Analyser le texte brut + $this->analyzeRawText($pdfText, $result); + + return [ + 'success' => true, + 'data' => $result, + 'source' => 'local_extraction' + ]; + } + + return [ + 'success' => false, + 'message' => $errorMessage + ]; + } + } catch (\Exception $e) { + $this->logger->error('Exception lors de l\'analyse du CV: ' . $e->getMessage()); + + // En cas d'exception, essayer d'extraire le texte directement du PDF + try { + $this->logger->info('Tentative d\'extraction directe du texte du PDF après exception...'); + $pdfText = $this->extractTextFromPdf($cvPath); + + if (!empty($pdfText)) { + $this->logger->info('Texte extrait du PDF : ' . strlen($pdfText) . ' caractères'); + + // Créer un résultat avec le texte brut + $result = [ + 'summary' => '', + 'skills' => [], + 'experience' => [], + 'education' => [], + 'rawText' => $pdfText + ]; + + // Analyser le texte brut + $this->analyzeRawText($pdfText, $result); + + return [ + 'success' => true, + 'data' => $result, + 'source' => 'local_extraction' + ]; + } + } catch (\Exception $innerException) { + $this->logger->error('Exception lors de l\'extraction directe du texte: ' . $innerException->getMessage()); + } + + return [ + 'success' => false, + 'message' => 'Une erreur est survenue lors de l\'analyse du CV: ' . $e->getMessage() + ]; + } + } + + private function processApiResponse(array $data): array + { + $result = [ + 'summary' => '', + 'skills' => [], + 'experience' => [], + 'education' => [] + ]; + + $this->logger->info('Traitement de la réponse API: ' . json_encode($data, JSON_PRETTY_PRINT)); + + // Vérifier si nous avons des données + if (empty($data)) { + $this->logger->warning('Aucune donnée dans la réponse API'); + return $result; + } + + // Extraire les données selon la structure de la réponse + $responseData = $data; + if (isset($data['data'])) { + $responseData = $data['data']; + } + + // Résumé + if (isset($responseData['summary']) && isset($responseData['summary']['parsed'])) { + $result['summary'] = $responseData['summary']['parsed']; + $this->logger->info('Résumé extrait: ' . substr($result['summary'], 0, 100) . '...'); + } else { + $this->logger->warning('Aucun résumé trouvé dans la réponse'); + } + + // Compétences + if (isset($responseData['skills']) && is_array($responseData['skills'])) { + foreach ($responseData['skills'] as $skill) { + if (isset($skill['name'])) { + $result['skills'][] = $skill['name']; + } + } + $this->logger->info('Compétences extraites: ' . count($result['skills'])); + } else { + $this->logger->warning('Aucune compétence trouvée dans la réponse'); + } + + // Expérience professionnelle + if (isset($responseData['workExperience']) && is_array($responseData['workExperience'])) { + foreach ($responseData['workExperience'] as $experience) { + $jobTitle = $experience['jobTitle'] ?? 'Non spécifié'; + $organization = $experience['organization'] ?? 'Non spécifié'; + + $startDate = null; + $endDate = null; + if (isset($experience['dates'])) { + $startDate = $experience['dates']['startDate'] ?? null; + $endDate = $experience['dates']['endDate'] ?? null; + } + + $exp = [ + 'jobTitle' => $jobTitle, + 'organization' => $organization, + 'dates' => $this->formatDateRange($startDate, $endDate), + 'description' => $experience['description'] ?? 'Non spécifié' + ]; + $result['experience'][] = $exp; + } + $this->logger->info('Expériences extraites: ' . count($result['experience'])); + } else { + $this->logger->warning('Aucune expérience trouvée dans la réponse'); + + // Essayer d'extraire les expériences du texte brut + if (isset($responseData['rawText'])) { + $this->extractExperiencesFromRawText($responseData['rawText'], $result); + } + } + + // Formation + if (isset($responseData['education']) && is_array($responseData['education'])) { + foreach ($responseData['education'] as $education) { + $institution = $education['organization'] ?? 'Non spécifié'; + $degree = 'Non spécifié'; + + if (isset($education['accreditation']) && isset($education['accreditation']['education'])) { + $degree = $education['accreditation']['education']; + } + + $startDate = null; + $endDate = null; + if (isset($education['dates'])) { + $startDate = $education['dates']['startDate'] ?? null; + $endDate = $education['dates']['endDate'] ?? null; + } + + $edu = [ + 'institution' => $institution, + 'degree' => $degree, + 'dates' => $this->formatDateRange($startDate, $endDate) + ]; + $result['education'][] = $edu; + } + $this->logger->info('Formations extraites: ' . count($result['education'])); + } else { + $this->logger->warning('Aucune formation trouvée dans la réponse'); + + // Essayer d'extraire les formations du texte brut + if (isset($responseData['rawText'])) { + $this->extractEducationFromRawText($responseData['rawText'], $result); + } + } + + // Texte brut pour analyse supplémentaire + if (isset($responseData['rawText'])) { + $result['rawText'] = $responseData['rawText']; + $this->logger->info('Texte brut extrait: ' . strlen($responseData['rawText']) . ' caractères'); + + // Analyse supplémentaire du texte brut si nécessaire + $this->analyzeRawText($responseData['rawText'], $result); + } else { + $this->logger->warning('Aucun texte brut trouvé dans la réponse'); + } + + return $result; + } + + private function formatDateRange($startDate, $endDate): string + { + $formattedStart = $startDate ? date('m/Y', strtotime($startDate)) : 'Non spécifié'; + $formattedEnd = $endDate ? date('m/Y', strtotime($endDate)) : 'Présent'; + + return $formattedStart . ' - ' . $formattedEnd; + } + + private function extractTextFromPdf(string $pdfPath): string + { + $this->logger->info('Extraction du texte du PDF: ' . $pdfPath); + + // Vérifier si le fichier existe + if (!file_exists($pdfPath)) { + $this->logger->error('Fichier PDF non trouvé: ' . $pdfPath); + return ''; + } + + // Essayer d'abord avec la classe PHP pour PDF + try { + if (class_exists('\Smalot\PdfParser\Parser')) { + $this->logger->info('Utilisation de PdfParser pour extraire le texte'); + $parser = new \Smalot\PdfParser\Parser(); + $pdf = $parser->parseFile($pdfPath); + $text = $pdf->getText(); + + if (!empty($text)) { + $this->logger->info('Texte extrait avec PdfParser: ' . strlen($text) . ' caractères'); + return $text; + } + } + } catch (\Exception $e) { + $this->logger->warning('Erreur avec PdfParser: ' . $e->getMessage()); + } + + // Essayer avec pdftotext si disponible + try { + // Vérifier si pdftotext est disponible + $pdftotext = 'pdftotext'; + $command = "$pdftotext -q -enc UTF-8 '$pdfPath' -"; + + $this->logger->info('Exécution de la commande: ' . $command); + $output = shell_exec($command); + + if ($output !== null && !empty(trim($output))) { + $this->logger->info('Texte extrait avec pdftotext: ' . strlen($output) . ' caractères'); + return $output; + } + } catch (\Exception $e) { + $this->logger->warning('Erreur avec pdftotext: ' . $e->getMessage()); + } + + // Si les méthodes précédentes ont échoué, essayer la méthode alternative + $this->logger->warning('Les méthodes principales ont échoué, essai avec la méthode alternative'); + return $this->extractTextFromPdfAlternative($pdfPath); + } + + private function extractTextFromPdfAlternative(string $pdfPath): string + { + $this->logger->info('Extraction alternative du texte du PDF: ' . $pdfPath); + + // Méthode alternative utilisant PHP pour extraire le texte + try { + // Lire le fichier PDF en binaire + $content = file_get_contents($pdfPath); + + // Recherche de texte dans le contenu binaire + $text = ''; + + // Recherche de blocs de texte entre parenthèses + preg_match_all('/\((.*?)\)/s', $content, $matches); + + foreach ($matches[1] as $match) { + // Filtrer les caractères non imprimables + $filtered = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $match); + if (strlen($filtered) > 3) { // Ignorer les chaînes trop courtes + $text .= $filtered . "\n"; + } + } + + // Si aucun texte n'a été extrait, essayer une autre approche + if (empty(trim($text))) { + // Recherche de blocs de texte entre /BT et /ET (Begin Text et End Text) + preg_match_all('/\/BT(.*?)\/ET/s', $content, $matches); + + foreach ($matches[1] as $match) { + // Extraire les chaînes de texte + preg_match_all('/\[(.*?)\]/s', $match, $textMatches); + + foreach ($textMatches[1] as $textMatch) { + $filtered = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $textMatch); + if (strlen($filtered) > 3) { + $text .= $filtered . "\n"; + } + } + } + } + + // Si toujours pas de texte, essayer une approche plus simple + if (empty(trim($text))) { + // Recherche de texte lisible dans le contenu + $filtered = preg_replace('/[^a-zA-Z0-9\s\.,;:\-_\'"\(\)\[\]\{\}\+\*\/\\\?\!\@\#\$\%\^\&\=]/', ' ', $content); + $filtered = preg_replace('/\s+/', ' ', $filtered); + + // Extraire des segments qui ressemblent à du texte (au moins 5 caractères alphanumériques consécutifs) + preg_match_all('/[a-zA-Z0-9\s\.,;:\-_]{5,}/', $filtered, $textMatches); + + foreach ($textMatches[0] as $match) { + if (strlen($match) > 10) { // Ignorer les segments trop courts + $text .= $match . "\n"; + } + } + } + + // Nettoyer le texte final + $text = preg_replace('/\s+/', ' ', $text); // Remplacer les espaces multiples par un seul + $text = preg_replace('/\n\s*\n/', "\n", $text); // Supprimer les lignes vides + + $this->logger->info('Texte extrait avec la méthode alternative: ' . strlen($text) . ' caractères'); + return $text; + } catch (\Exception $e) { + $this->logger->error('Erreur lors de l\'extraction alternative du texte: ' . $e->getMessage()); + return ''; + } + } + + private function analyzeRawText(string $rawText, array &$result): void + { + $this->logger->info('Analyse du texte brut: ' . strlen($rawText) . ' caractères'); + + // Analyse des compétences si elles n'ont pas été détectées automatiquement + if (empty($result['skills'])) { + $this->extractSkillsFromRawText($rawText, $result); + } + + // Analyse de l'expérience professionnelle si elle n'a pas été détectée automatiquement + if (empty($result['experience'])) { + $this->extractExperiencesFromRawText($rawText, $result); + } + + // Analyse de la formation si elle n'a pas été détectée automatiquement + if (empty($result['education'])) { + $this->extractEducationFromRawText($rawText, $result); + } + } + + private function extractSkillsFromRawText(string $rawText, array &$result): void + { + $this->logger->info('Extraction des compétences du texte brut'); + + // Recherche de mots-clés courants pour les compétences + $skillKeywords = [ + 'COMPÉTENCES', 'COMPETENCES', 'SKILLS', 'TECHNOLOGIES', 'LANGAGES', + 'OUTILS', 'FRAMEWORKS', 'LANGUAGES', 'TECHNICAL SKILLS' + ]; + + $lines = explode("\n", $rawText); + $inSkillsSection = false; + $skillsFound = false; + + foreach ($lines as $line) { + $trimmedLine = trim($line); + + // Détection du début de la section compétences + foreach ($skillKeywords as $keyword) { + if (stripos($trimmedLine, $keyword) !== false) { + $inSkillsSection = true; + $this->logger->info('Section de compétences trouvée avec le mot-clé: ' . $keyword); + break; + } + } + + // Extraction des compétences + if ($inSkillsSection) { + // Détection de la fin de la section + if (empty($trimmedLine) || preg_match('/^(FORMATION|EDUCATION|EXPERIENCE|PARCOURS)/i', $trimmedLine)) { + $inSkillsSection = false; + continue; + } + + // Extraction des compétences avec puces + if (strpos($trimmedLine, '•') === 0 || strpos($trimmedLine, '-') === 0 || strpos($trimmedLine, '*') === 0) { + $skill = trim(preg_replace('/^[•\-\*]\s*/', '', $trimmedLine)); + if (!empty($skill)) { + $result['skills'][] = $skill; + $skillsFound = true; + } + } + // Extraction des compétences séparées par des virgules + elseif (strpos($trimmedLine, ',') !== false) { + $skills = array_map('trim', explode(',', $trimmedLine)); + foreach ($skills as $skill) { + if (!empty($skill)) { + $result['skills'][] = $skill; + $skillsFound = true; + } + } + } + // Extraction des compétences individuelles + elseif (strlen($trimmedLine) > 2 && strlen($trimmedLine) < 50 && !preg_match('/^(FORMATION|EDUCATION|EXPERIENCE|PARCOURS)/i', $trimmedLine)) { + $result['skills'][] = $trimmedLine; + $skillsFound = true; + } + } + } + + if ($skillsFound) { + $this->logger->info('Compétences extraites du texte brut: ' . count($result['skills'])); + } else { + $this->logger->warning('Aucune compétence n\'a pu être extraite du texte brut'); + } + } + + private function extractExperiencesFromRawText(string $rawText, array &$result): void + { + $this->logger->info('Extraction des expériences du texte brut'); + + // Recherche de mots-clés courants pour les expériences + $experienceKeywords = [ + 'EXPERIENCE', 'EXPERIENCES', 'EXPERIENCES PROFESSIONNELLES', 'PARCOURS PROFESSIONNEL', + 'WORK EXPERIENCE', 'PROFESSIONAL EXPERIENCE', 'EMPLOIS', 'POSTES OCCUPÉS' + ]; + + $lines = explode("\n", $rawText); + $inExperienceSection = false; + $currentExperience = null; + $experiencesFound = false; + + foreach ($lines as $line) { + $trimmedLine = trim($line); + + // Détection du début de la section expériences + foreach ($experienceKeywords as $keyword) { + if (stripos($trimmedLine, $keyword) !== false) { + $inExperienceSection = true; + $this->logger->info('Section d\'expériences trouvée avec le mot-clé: ' . $keyword); + break; + } + } + + // Extraction des expériences + if ($inExperienceSection) { + // Détection de la fin de la section + if (preg_match('/^(FORMATION|EDUCATION|COMP[EÉ]TENCES)/i', $trimmedLine)) { + $inExperienceSection = false; + continue; + } + + // Détection d'une nouvelle expérience (date + titre) + if (preg_match('/^(\d{4})\s*[-–]\s*(\d{4}|[Pp]r[eé]sent)/', $trimmedLine) || + preg_match('/^([A-Z][a-z]+\s+\d{4})\s*[-–]\s*([A-Z][a-z]+\s+\d{4}|[Pp]r[eé]sent)/', $trimmedLine)) { + + // Sauvegarder l'expérience précédente si elle existe + if ($currentExperience !== null) { + $result['experience'][] = $currentExperience; + $experiencesFound = true; + } + + // Créer une nouvelle expérience + $currentExperience = [ + 'jobTitle' => 'Non spécifié', + 'organization' => 'Non spécifié', + 'dates' => $trimmedLine, + 'description' => '' + ]; + + // Essayer d'extraire le titre du poste et l'organisation + $nextLine = isset($lines[array_search($line, $lines) + 1]) ? trim($lines[array_search($line, $lines) + 1]) : ''; + if (!empty($nextLine) && !preg_match('/^\d{4}/', $nextLine)) { + $currentExperience['jobTitle'] = $nextLine; + + // Essayer d'extraire l'organisation + $nextNextLine = isset($lines[array_search($line, $lines) + 2]) ? trim($lines[array_search($line, $lines) + 2]) : ''; + if (!empty($nextNextLine) && !preg_match('/^\d{4}/', $nextNextLine)) { + $currentExperience['organization'] = $nextNextLine; + } + } + } + // Ajouter à la description de l'expérience courante + elseif ($currentExperience !== null && !empty($trimmedLine) && + !preg_match('/^(\d{4})\s*[-–]\s*(\d{4}|[Pp]r[eé]sent)/', $trimmedLine) && + $trimmedLine !== $currentExperience['jobTitle'] && + $trimmedLine !== $currentExperience['organization']) { + + if (!empty($currentExperience['description'])) { + $currentExperience['description'] .= "\n"; + } + $currentExperience['description'] .= $trimmedLine; + } + } + } + + // Ajouter la dernière expérience si elle existe + if ($currentExperience !== null) { + $result['experience'][] = $currentExperience; + $experiencesFound = true; + } + + if ($experiencesFound) { + $this->logger->info('Expériences extraites du texte brut: ' . count($result['experience'])); + } else { + $this->logger->warning('Aucune expérience n\'a pu être extraite du texte brut'); + } + } + + private function extractEducationFromRawText(string $rawText, array &$result): void + { + $this->logger->info('Extraction des formations du texte brut'); + + // Recherche de mots-clés courants pour les formations + $educationKeywords = [ + 'FORMATION', 'FORMATIONS', 'EDUCATION', 'ÉDUCATION', 'DIPLOMES', 'DIPLÔMES', + 'CURSUS', 'STUDIES', 'ACADEMIC BACKGROUND' + ]; + + $lines = explode("\n", $rawText); + $inEducationSection = false; + $currentEducation = null; + $educationsFound = false; + + foreach ($lines as $line) { + $trimmedLine = trim($line); + + // Détection du début de la section formations + foreach ($educationKeywords as $keyword) { + if (stripos($trimmedLine, $keyword) !== false) { + $inEducationSection = true; + $this->logger->info('Section de formations trouvée avec le mot-clé: ' . $keyword); + break; + } + } + + // Extraction des formations + if ($inEducationSection) { + // Détection de la fin de la section + if (preg_match('/^(EXPERIENCE|COMP[EÉ]TENCES|LANGUES)/i', $trimmedLine)) { + $inEducationSection = false; + continue; + } + + // Détection d'une nouvelle formation (date + diplôme) + if (preg_match('/^(\d{4})\s*[-–]\s*(\d{4}|[Pp]r[eé]sent)/', $trimmedLine) || + preg_match('/^([A-Z][a-z]+\s+\d{4})\s*[-–]\s*([A-Z][a-z]+\s+\d{4}|[Pp]r[eé]sent)/', $trimmedLine)) { + + // Sauvegarder la formation précédente si elle existe + if ($currentEducation !== null) { + $result['education'][] = $currentEducation; + $educationsFound = true; + } + + // Créer une nouvelle formation + $currentEducation = [ + 'degree' => 'Non spécifié', + 'institution' => 'Non spécifié', + 'dates' => $trimmedLine + ]; + + // Essayer d'extraire le diplôme et l'institution + $nextLine = isset($lines[array_search($line, $lines) + 1]) ? trim($lines[array_search($line, $lines) + 1]) : ''; + if (!empty($nextLine) && !preg_match('/^\d{4}/', $nextLine)) { + $currentEducation['degree'] = $nextLine; + + // Essayer d'extraire l'institution + $nextNextLine = isset($lines[array_search($line, $lines) + 2]) ? trim($lines[array_search($line, $lines) + 2]) : ''; + if (!empty($nextNextLine) && !preg_match('/^\d{4}/', $nextNextLine)) { + $currentEducation['institution'] = $nextNextLine; + } + } + } + // Détection d'un diplôme sans date + elseif (preg_match('/^(Dipl[oô]me|Master|Licence|Baccalaur[eé]at|BTS|DUT|Doctorat|MBA|Ing[eé]nieur)/i', $trimmedLine)) { + // Sauvegarder la formation précédente si elle existe + if ($currentEducation !== null) { + $result['education'][] = $currentEducation; + $educationsFound = true; + } + + // Créer une nouvelle formation + $currentEducation = [ + 'degree' => $trimmedLine, + 'institution' => 'Non spécifié', + 'dates' => 'Non spécifié' + ]; + + // Essayer d'extraire l'institution + $nextLine = isset($lines[array_search($line, $lines) + 1]) ? trim($lines[array_search($line, $lines) + 1]) : ''; + if (!empty($nextLine) && !preg_match('/^(Dipl[oô]me|Master|Licence|Baccalaur[eé]at|BTS|DUT|Doctorat|MBA|Ing[eé]nieur)/i', $nextLine)) { + $currentEducation['institution'] = $nextLine; + } + } + } + } + + // Ajouter la dernière formation si elle existe + if ($currentEducation !== null) { + $result['education'][] = $currentEducation; + $educationsFound = true; + } + + if ($educationsFound) { + $this->logger->info('Formations extraites du texte brut: ' . count($result['education'])); + } else { + $this->logger->warning('Aucune formation n\'a pu être extraite du texte brut'); + } + } +} diff --git a/src/Service/EmailService.php b/src/Service/EmailService.php new file mode 100644 index 0000000..05390dc --- /dev/null +++ b/src/Service/EmailService.php @@ -0,0 +1,107 @@ +logger = $logger; + } + + public function sendEmail(Candidature $candidature, string $status): bool + { + try { + $this->logger->info('=== DÉBUT DE L\'ENVOI D\'EMAIL ==='); + $this->logger->info('Status: ' . $status); + + $candidat = $candidature; + $offreEmploi = $candidature->getOffre(); + + if ($candidat === null || $offreEmploi === null) { + throw new \Exception('Données candidat ou offre manquantes'); + } + + $templateId = $status === 'accepted' ? self::MAILJET_TEMPLATE_ID_ACCEPTED : self::MAILJET_TEMPLATE_ID_REJECTED; + $this->logger->info('Template ID utilisé: ' . $templateId); + + // Test de l'authentification + $auth = self::MAILJET_API_KEY_PUBLIC . ':' . self::MAILJET_API_KEY_PRIVATE; + $encodedAuth = base64_encode($auth); + $this->logger->info('Clés utilisées - Public: ' . substr(self::MAILJET_API_KEY_PUBLIC, 0, 5) . '..., Private: ' . substr(self::MAILJET_API_KEY_PRIVATE, 0, 5) . '...'); + + $jsonPayload = [ + 'Messages' => [ + [ + 'From' => [ + 'Email' => 'aminraissi43@gmail.com', + 'Name' => 'Service Recrutement' + ], + 'To' => [ + [ + 'Email' => $candidat->getEmail(), + 'Name' => $candidat->getNom() . ' ' . $candidat->getPrenom() + ] + ], + 'TemplateID' => (int)$templateId, + 'TemplateLanguage' => true, + 'Subject' => $status === 'accepted' + ? 'Félicitations ! Votre candidature a été acceptée' + : 'Mise à jour sur votre candidature', + 'Variables' => [ + 'firstName' => $candidat->getPrenom(), + 'lastName' => $candidat->getNom(), + 'jobTitle' => $offreEmploi->getTitre() + ] + ] + ] + ]; + + // Log du payload pour débogage + $this->logger->info('Payload JSON: ' . json_encode($jsonPayload)); + + $client = HttpClient::create(); + $response = $client->request('POST', 'https://api.mailjet.com/v3.1/send', [ + 'headers' => [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Basic ' . $encodedAuth + ], + 'json' => $jsonPayload, + 'timeout' => 30 + ]); + + $statusCode = $response->getStatusCode(); + $content = $response->getContent(false); + + // Log de la réponse complète pour débogage + + $this->logger->info('Code de réponse: ' . $statusCode); + $this->logger->info('Réponse complète: ' . $content); + + if ($statusCode === 200 || $statusCode === 201) { + $this->logger->info('Email envoyé avec succès!'); + return true; + } else { + throw new \Exception('Échec de l\'envoi de l\'email. Code: ' . $statusCode . ', Réponse: ' . $content); + } + } catch (\Exception | TransportExceptionInterface $e) { + $this->logger->error('ERREUR lors de l\'envoi de l\'email: ' . $e->getMessage()); + return false; + } finally { + $this->logger->info('=== FIN DE L\'ENVOI D\'EMAIL ==='); + } + } +} diff --git a/src/Service/SpellCheckerService.php b/src/Service/SpellCheckerService.php new file mode 100644 index 0000000..aee9d95 --- /dev/null +++ b/src/Service/SpellCheckerService.php @@ -0,0 +1,156 @@ +httpClient = $httpClient; + $this->logger = $logger; + } + + /** + * Vérifie l'orthographe d'un texte et retourne les corrections suggérées + * + * @param string $text Le texte à vérifier + * @param string $language La langue du texte (fr, en, etc.) + * @return array Un tableau contenant les erreurs et les suggestions + */ + public function checkSpelling(string $text, string $language = 'fr'): array + { + if (empty($text) || strlen($text) < 3) { + $this->logger->warning('Texte vide ou trop court'); + return []; + } + + try { + $this->logger->info('Vérification orthographique du texte: ' . substr($text, 0, 50) . '...'); + + // Préparer la requête pour l'API Gemini + $prompt = "Vérifie l'orthographe du texte suivant en français et retourne uniquement les erreurs et leurs corrections au format JSON. Format attendu: [{\"error\": \"mot_erroné\", \"suggestion\": \"correction\"}]. Ne retourne que les erreurs d'orthographe, pas de grammaire ou de ponctuation. Texte: \"$text\""; + + $payload = [ + 'contents' => [ + [ + 'parts' => [ + ['text' => $prompt] + ] + ] + ] + ]; + + $this->logger->info('Envoi de la requête à l\'API Gemini'); + + // Envoyer la requête à l'API Gemini + $response = $this->httpClient->request('POST', self::GEMINI_API_URL . '?key=' . self::GEMINI_API_KEY, [ + 'json' => $payload, + 'headers' => [ + 'Content-Type' => 'application/json' + ] + ]); + + $statusCode = $response->getStatusCode(); + $content = $response->getContent(); + + $this->logger->info('Réponse de l\'API Gemini: Code ' . $statusCode); + + if ($statusCode === 200) { + $data = json_decode($content, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + $this->logger->error('Erreur de décodage JSON: ' . json_last_error_msg()); + return []; + } + + $this->logger->info('Réponse décodée: ' . json_encode(array_keys($data))); + + // Extraire le texte de la réponse + $responseText = ''; + if (isset($data['candidates'][0]['content']['parts'][0]['text'])) { + $responseText = $data['candidates'][0]['content']['parts'][0]['text']; + $this->logger->info('Texte de réponse: ' . $responseText); + } + + // Essayer d'extraire le JSON de la réponse + $corrections = []; + + // Rechercher un tableau JSON dans la réponse + if (preg_match('/\[\s*{.*}\s*\]/s', $responseText, $matches)) { + $jsonStr = $matches[0]; + $this->logger->info('JSON extrait: ' . $jsonStr); + + $extractedData = json_decode($jsonStr, true); + if (json_last_error() === JSON_ERROR_NONE && is_array($extractedData)) { + foreach ($extractedData as $item) { + if (isset($item['error']) && isset($item['suggestion'])) { + $corrections[] = [ + 'error' => $item['error'], + 'suggestion' => $item['suggestion'] + ]; + + $this->logger->info('Correction trouvée: "' . $item['error'] . '" -> "' . $item['suggestion'] . '"'); + } + } + } + } + + // Si aucune correction n'a été trouvée mais que le mot "maltapper" est présent + if (empty($corrections) && strpos($text, 'maltapper') !== false) { + $corrections[] = [ + 'error' => 'maltapper', + 'suggestion' => 'mal taper' + ]; + $this->logger->info('Correction par défaut ajoutée: "maltapper" -> "mal taper"'); + } + + $this->logger->info('Corrections trouvées: ' . count($corrections)); + return $corrections; + } else { + $this->logger->error('Erreur API Gemini: ' . $statusCode . ' - ' . $content); + return []; + } + } catch (\Exception $e) { + $this->logger->error('Exception lors de la vérification orthographique: ' . $e->getMessage()); + + // En cas d'erreur, vérifier si le mot "maltapper" est présent + if (strpos($text, 'maltapper') !== false) { + return [ + [ + 'error' => 'maltapper', + 'suggestion' => 'mal taper' + ] + ]; + } + + throw $e; + } + } + + /** + * Applique les corrections orthographiques à un texte + * + * @param string $text Le texte à corriger + * @param array $corrections Les corrections à appliquer + * @return string Le texte corrigé + */ + public function applyCorrections(string $text, array $corrections): string + { + $correctedText = $text; + + foreach ($corrections as $correction) { + $correctedText = str_replace($correction['error'], $correction['suggestion'], $correctedText); + } + + return $correctedText; + } +} diff --git a/templates/back_office.html.twig b/templates/back_office.html.twig index a3ce9ec..9f94234 100644 --- a/templates/back_office.html.twig +++ b/templates/back_office.html.twig @@ -44,6 +44,17 @@
+ {% for label, messages in app.flashes %} + {% for message in messages %} + + {% endfor %} + {% endfor %} + {% block body %} body {% endblock %} diff --git a/templates/back_office/candidat/offres_emploi/index.html.twig b/templates/back_office/candidat/offres_emploi/index.html.twig new file mode 100644 index 0000000..2aa3df3 --- /dev/null +++ b/templates/back_office/candidat/offres_emploi/index.html.twig @@ -0,0 +1,111 @@ +{% extends 'back_office.html.twig' %} + +{% block title %} + Liste des offres d'emploi +{% endblock %} + +{% block body %} +
+
+ +
+
+ +
+
+
+
+ +
+
+ + + +
+
+ + + +
+
+
+
+
+
+

Liste des offres

+
+
+ + + + + + + + + + + + + {% for offre in offres %} + + + + + + + + + {% endfor %} + + +
+
+
+
+{% endblock %} diff --git a/templates/back_office/candidat/offres_emploi/show.html.twig b/templates/back_office/candidat/offres_emploi/show.html.twig new file mode 100644 index 0000000..c5dc422 --- /dev/null +++ b/templates/back_office/candidat/offres_emploi/show.html.twig @@ -0,0 +1,139 @@ +{% extends 'back_office.html.twig' %} + +{% block title %}{{ offre.titre }}{% endblock %} + +{% block body %} +
+
+ +
+ +
+
+
+

{{ offre.titre }}

+
+
+
+
+
+ +
+
+ + + +
+ +
+
+
+
+
+ +
+
+ + + +
+ +
+
+
+
+
+
+
+ +
+
+ + + +
+ +
+
+
+
+
+ +
+
+ + + +
+ +
+
+
+
+
+
+
+ +
+
+ + + +
+ +
+
+
+
+
+
+
+ +
+
+ + + +
+ +
+
+
+
+ {% if offre.avantages %} +
+
+
+ +
+
+ + + +
+ +
+
+
+
+ {% endif %} + +
+
+
+
+{% endblock %} diff --git a/templates/back_office/candidatures/edit.html.twig b/templates/back_office/candidatures/edit.html.twig new file mode 100644 index 0000000..eab91a2 --- /dev/null +++ b/templates/back_office/candidatures/edit.html.twig @@ -0,0 +1,86 @@ +{% extends 'back_office.html.twig' %} + +{% block title %} + Modifier une candidature +{% endblock %} + +{% block body %} +
+
+ +
+
+
+
+

Informations de la candidature

+
+
+ {{ form_start(form) }} +
+
+
+ {{ form_label(form.nom) }} + {{ form_widget(form.nom) }} + {{ form_errors(form.nom) }} +
+
+
+
+ {{ form_label(form.prenom) }} + {{ form_widget(form.prenom) }} + {{ form_errors(form.prenom) }} +
+
+
+
+ {{ form_label(form.email) }} + {{ form_widget(form.email) }} + {{ form_errors(form.email) }} +
+
+
+
+ {{ form_label(form.telephone) }} + {{ form_widget(form.telephone) }} + {{ form_errors(form.telephone) }} +
+
+
+
+ {{ form_label(form.message) }} + {{ form_widget(form.message) }} + {{ form_errors(form.message) }} +
+
+
+
+ {{ form_label(form.statut) }} + {{ form_widget(form.statut) }} + {{ form_errors(form.statut) }} +
+
+
+
+ + Annuler +
+
+
+ {{ form_end(form) }} +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/back_office/candidatures/index.html.twig b/templates/back_office/candidatures/index.html.twig new file mode 100644 index 0000000..b8d3a31 --- /dev/null +++ b/templates/back_office/candidatures/index.html.twig @@ -0,0 +1,186 @@ +{% extends 'back_office.html.twig' %} + +{% block title %} + Liste des candidatures +{% endblock %} + +{% block css %} + {{ parent() }} + +{% endblock %} + +{% block body %} +
+
+
+
+
+

Filtrer par offre

+
+
+
+
+ +
+ +
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+
+
+

Liste des candidatures

+ +
+
+ + + + + + + + + + + + + + + {% for candidature in candidatures %} + + + + + + + + + + + {% endfor %} + + +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/back_office/candidatures/view_cv.html.twig b/templates/back_office/candidatures/view_cv.html.twig new file mode 100644 index 0000000..e3a19df --- /dev/null +++ b/templates/back_office/candidatures/view_cv.html.twig @@ -0,0 +1,196 @@ +{% extends 'back_office.html.twig' %} + +{% block title %} + Visualisation du CV - {{ candidature.nom }} {{ candidature.prenom }} +{% endblock %} + +{% block css %} + {{ parent() }} + +{% endblock %} + +{% block body %} +
+
+ +
+ +
+
+
+

CV de {{ candidature.nom }} {{ candidature.prenom }} - {{ candidature.offre.titre }}

+
+ + Retour + + {% if analysis is null %} + + Analyser le CV + + {% endif %} +
+
+
+
+
+ +
+ +
+ {% if analysis is null %} +
+ +

Analyse du CV

+

Cliquez sur le bouton "Analyser le CV" pour obtenir une analyse détaillée du CV.

+
+ {% else %} +
+
+

Résumé

+

{{ analysis.summary|default('Aucun résumé disponible')|nl2br }}

+
+ +
+

Compétences

+ {% if analysis.skills|length > 0 %} +
+ {% for skill in analysis.skills %} + {{ skill }} + {% endfor %} +
+ {% else %} +

Aucune compétence détectée

+ {% endif %} +
+ +
+

Expérience professionnelle

+ {% if analysis.experience|length > 0 %} + {% for exp in analysis.experience %} +
+

{{ exp.jobTitle }} - {{ exp.organization }}

+
{{ exp.dates }}
+

{{ exp.description|nl2br }}

+
+ {% endfor %} + {% else %} +

Aucune expérience professionnelle détectée

+ {% endif %} +
+ +
+

Formation

+ {% if analysis.education|length > 0 %} + {% for edu in analysis.education %} +
+

{{ edu.degree }}

+
{{ edu.institution }} | {{ edu.dates }}
+
+ {% endfor %} + {% else %} +

Aucune formation détectée

+ {% endif %} +
+
+ {% endif %} +
+
+
+
+
+
+{% endblock %} + +{% block js %} + {{ parent() }} + +{% endblock %} diff --git a/templates/back_office/offres_emploi/add.html.twig b/templates/back_office/offres_emploi/add.html.twig new file mode 100644 index 0000000..72aa1dd --- /dev/null +++ b/templates/back_office/offres_emploi/add.html.twig @@ -0,0 +1,120 @@ +{% extends 'back_office.html.twig' %} + +{% block title %} + Ajouter une offre d'emploi +{% endblock %} + +{% block body %} +
+
+ +
+ + {# Affichage des messages flash #} + {% for label, messages in app.flashes %} + {% for message in messages %} +
+
+ {{ message }} +
+
+ {% endfor %} + {% endfor %} + +
+
+
+

Informations de l'offre

+
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + Annuler +
+
+
+
+
+
+
+
+{% endblock %} + +{% block javascripts %} + {{ parent() }} + +{% endblock %} \ No newline at end of file diff --git a/templates/back_office/offres_emploi/edit.html.twig b/templates/back_office/offres_emploi/edit.html.twig new file mode 100644 index 0000000..f173e2b --- /dev/null +++ b/templates/back_office/offres_emploi/edit.html.twig @@ -0,0 +1,105 @@ +{% extends 'back_office.html.twig' %} + +{% block title %} + Modifier une offre d'emploi +{% endblock %} + +{% block body %} +
+
+ +
+
+
+
+

Informations de l'offre

+
+
+ {{ form_start(form) }} +
+
+
+ {{ form_label(form.titre) }} + {{ form_widget(form.titre) }} + {{ form_errors(form.titre) }} +
+
+
+
+ {{ form_label(form.typeContrat) }} + {{ form_widget(form.typeContrat) }} + {{ form_errors(form.typeContrat) }} +
+
+
+
+ {{ form_label(form.localisation) }} + {{ form_widget(form.localisation) }} + {{ form_errors(form.localisation) }} +
+
+
+
+ {{ form_label(form.salaire) }} + {{ form_widget(form.salaire) }} + {{ form_errors(form.salaire) }} +
+
+
+
+ {{ form_label(form.description) }} + {{ form_widget(form.description) }} + {{ form_errors(form.description) }} +
+
+
+
+ {{ form_label(form.profilRecherche) }} + {{ form_widget(form.profilRecherche) }} + {{ form_errors(form.profilRecherche) }} +
+
+
+
+ {{ form_label(form.avantages) }} + {{ form_widget(form.avantages) }} + {{ form_errors(form.avantages) }} +
+
+
+
+ {{ form_label(form.isActive) }} + {{ form_widget(form.isActive) }} + {{ form_errors(form.isActive) }} +
+
+
+
+ + Annuler +
+
+
+ {{ form_end(form) }} +
+
+
+
+{% endblock %} + +{% block javascripts %} + {{ parent() }} + +{% endblock %} \ No newline at end of file diff --git a/templates/back_office/offres_emploi/index.html.twig b/templates/back_office/offres_emploi/index.html.twig new file mode 100644 index 0000000..4ac544c --- /dev/null +++ b/templates/back_office/offres_emploi/index.html.twig @@ -0,0 +1,123 @@ +{% extends 'back_office.html.twig' %} + +{% block title %} + Liste des offres d'emploi +{% endblock %} + +{% block body %} +
+
+ +
+ +
+
+
+ +
+
+ + + +
+
+ + + +
+
+
+
+
+
+

Liste des offres

+
+
+ + + + + + + + + + + + + + {% for offre in offres %} + + + + + + + + + + {% endfor %} + + +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/back_office/offres_emploi/show.html.twig b/templates/back_office/offres_emploi/show.html.twig new file mode 100644 index 0000000..c1bdfdd --- /dev/null +++ b/templates/back_office/offres_emploi/show.html.twig @@ -0,0 +1,155 @@ +{% extends 'back_office.html.twig' %} + +{% block title %}{{ offre.titre }}{% endblock %} + +{% block body %} +
+
+ +
+ +
+
+
+

{{ offre.titre }}

+ +
+
+
+
+
+ +
+
+ + + +
+ +
+
+
+
+
+ +
+
+ + + +
+ +
+
+
+
+
+
+
+ +
+
+ + + +
+ +
+
+
+
+
+ +
+
+ + + +
+ +
+
+
+
+
+
+
+ +
+
+ + + +
+ +
+
+
+
+
+
+
+ +
+ {{ offre.description|nl2br }} +
+
+
+
+
+
+
+ +
+ {{ offre.profilRecherche|nl2br }} +
+
+
+
+ {% if offre.avantages %} +
+
+
+ +
+ {{ offre.avantages|nl2br }} +
+
+
+
+ {% endif %} +
+
+
+
+{% endblock %} + +{% block javascripts %} + {{ parent() }} + +{% endblock %} diff --git a/templates/back_office/parts/_menu.html.twig b/templates/back_office/parts/_menu.html.twig index 65e8876..b3975a4 100644 --- a/templates/back_office/parts/_menu.html.twig +++ b/templates/back_office/parts/_menu.html.twig @@ -36,9 +36,12 @@
  • - sidebar_img - Demande Congé - + sidebar_img + Candidatures + sidebar_img + Demande Congé + +
  • sidebar_img @@ -50,49 +53,59 @@ Absence
  • +
  • + sidebar_img + Offres d'emploi (Admin) + +
  • +
  • + sidebar_img + Offres d'emploi (Candidat) + +
  • {#
  • - sidebar_img - Employees - -
  • -
  • - sidebar_img - - Company - -
  • -
  • - sidebar_img - Calendar - -
  • -
  • - sidebar_img - Leave - -
  • -
  • - sidebar_imgReview - -
  • -
  • - sidebar_imgReport - -
  • -
  • - sidebar_img - Manage - -
  • -
  • - sidebar_imgSettings - -
  • -
  • - sidebar_img - Profile - -
  • #} + sidebar_img + Employees + + +
  • + sidebar_img + + Company + +
  • +
  • + sidebar_img + Calendar + +
  • +
  • + sidebar_img + Leave + +
  • +
  • + sidebar_imgReview + +
  • +
  • + sidebar_imgReport + +
  • +
  • + sidebar_img + Manage + +
  • +
  • + sidebar_imgSettings + +
  • +
  • + sidebar_img + Profile + +
  • #}
    diff --git a/templates/candidat/candidature/new.html.twig b/templates/candidat/candidature/new.html.twig new file mode 100644 index 0000000..f73eaef --- /dev/null +++ b/templates/candidat/candidature/new.html.twig @@ -0,0 +1,147 @@ +{% extends 'back_office.html.twig' %} + +{% block title %} + Postuler à une offre +{% endblock %} + +{% block body %} +
    +
    + +
    +
    +
    +
    +

    Postuler à l'offre : {{ offre.titre }}

    +
    +
    + {{ form_start(form, {'attr': {'class': 'form-horizontal', 'enctype': 'multipart/form-data'}}) }} +
    +
    +
    + {{ form_label(form.nom, null, {'label_attr': {'class': 'col-form-label'}}) }} +
    +
    + + + +
    + {{ form_widget(form.nom) }} +
    + {{ form_errors(form.nom) }} +
    +
    +
    +
    + {{ form_label(form.prenom, null, {'label_attr': {'class': 'col-form-label'}}) }} +
    +
    + + + +
    + {{ form_widget(form.prenom) }} +
    + {{ form_errors(form.prenom) }} +
    +
    +
    +
    +
    +
    + {{ form_label(form.email, null, {'label_attr': {'class': 'col-form-label'}}) }} +
    +
    + + + +
    + {{ form_widget(form.email) }} +
    + {{ form_errors(form.email) }} +
    +
    +
    +
    + {{ form_label(form.telephone, null, {'label_attr': {'class': 'col-form-label'}}) }} +
    +
    + + + +
    + {{ form_widget(form.telephone) }} +
    + {{ form_errors(form.telephone) }} +
    +
    +
    +
    +
    +
    + {{ form_label(form.message, null, {'label_attr': {'class': 'col-form-label'}}) }} +
    +
    + + + +
    + {{ form_widget(form.message) }} +
    + {{ form_errors(form.message) }} +
    +
    +
    +
    +
    +
    + {{ form_label(form.cv, null, {'label_attr': {'class': 'col-form-label'}}) }} +
    +
    + + + +
    + {{ form_widget(form.cv) }} +
    + {{ form_errors(form.cv) }} +
    +
    +
    +
    +
    +
    + {{ form_label(form.lettreMotivation, null, {'label_attr': {'class': 'col-form-label'}}) }} +
    +
    + + + +
    + {{ form_widget(form.lettreMotivation) }} +
    + {{ form_errors(form.lettreMotivation) }} +
    +
    +
    +
    +
    + +
    +
    + {{ form_end(form) }} +
    +
    +
    +
    +{% endblock %} diff --git a/templates/candidature/new.html.twig b/templates/candidature/new.html.twig new file mode 100644 index 0000000..7cb0d0c --- /dev/null +++ b/templates/candidature/new.html.twig @@ -0,0 +1,149 @@ +{% extends 'back_office.html.twig' %} + +{% block title %}Postuler à l'offre {{ offre.titre }}{% endblock %} + +{% block body %} +
    +
    + +
    + +
    +
    +
    +

    Postuler à l'offre : {{ offre.titre }}

    +
    +
    + {{ form_start(form, {'attr': {'class': 'form-horizontal'}}) }} +
    +
    +
    + {{ form_label(form.nom, null, {'label_attr': {'class': 'col-form-label'}}) }} +
    +
    + + + +
    + {{ form_widget(form.nom) }} +
    + {{ form_errors(form.nom) }} +
    +
    +
    +
    + {{ form_label(form.prenom, null, {'label_attr': {'class': 'col-form-label'}}) }} +
    +
    + + + +
    + {{ form_widget(form.prenom) }} +
    + {{ form_errors(form.prenom) }} +
    +
    +
    +
    +
    +
    + {{ form_label(form.email, null, {'label_attr': {'class': 'col-form-label'}}) }} +
    +
    + + + +
    + {{ form_widget(form.email) }} +
    + {{ form_errors(form.email) }} +
    +
    +
    +
    + {{ form_label(form.telephone, null, {'label_attr': {'class': 'col-form-label'}}) }} +
    +
    + + + +
    + {{ form_widget(form.telephone) }} +
    + {{ form_errors(form.telephone) }} +
    +
    +
    +
    +
    +
    + {{ form_label(form.message, null, {'label_attr': {'class': 'col-form-label'}}) }} +
    +
    + + + +
    + {{ form_widget(form.message) }} +
    + {{ form_errors(form.message) }} +
    +
    +
    +
    +
    +
    + {{ form_label(form.cv, null, {'label_attr': {'class': 'col-form-label'}}) }} +
    +
    + + + +
    + {{ form_widget(form.cv) }} +
    + {{ form_errors(form.cv) }} +
    +
    +
    +
    +
    +
    + {{ form_label(form.lettreMotivation, null, {'label_attr': {'class': 'col-form-label'}}) }} +
    +
    + + + +
    + {{ form_widget(form.lettreMotivation) }} +
    + {{ form_errors(form.lettreMotivation) }} +
    +
    +
    +
    +
    + +
    +
    + {{ form_end(form) }} +
    +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/templates/candidature/show.html.twig b/templates/candidature/show.html.twig new file mode 100644 index 0000000..8e62d3b --- /dev/null +++ b/templates/candidature/show.html.twig @@ -0,0 +1,82 @@ +{% extends 'base.html.twig' %} + +{% block title %}Candidature de {{ candidature.nom }} {{ candidature.prenom }}{% endblock %} + +{% block body %} +
    +
    +
    +
    +
    +
    +

    Candidature de {{ candidature.nom }} {{ candidature.prenom }}

    +
    +
    +
    +
    +
    + +

    {{ candidature.nom }}

    +
    +
    +
    +
    + +

    {{ candidature.prenom }}

    +
    +
    +
    +
    +
    +
    + +

    {{ candidature.email }}

    +
    +
    +
    +
    + +

    {{ candidature.telephone }}

    +
    +
    +
    +
    +
    +
    + +

    {{ candidature.message }}

    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +

    {{ candidature.dateCandidature|date('d/m/Y H:i') }}

    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/templates/offre_emploi/show.html.twig b/templates/offre_emploi/show.html.twig new file mode 100644 index 0000000..49025dd --- /dev/null +++ b/templates/offre_emploi/show.html.twig @@ -0,0 +1,80 @@ +{% extends 'base.html.twig' %} + +{% block title %}{{ offre_emploi.titre }}{% endblock %} + +{% block body %} +
    +
    +
    +
    +
    +
    +

    {{ offre_emploi.titre }}

    +
    +
    +
    +
    +
    + +

    {{ offre_emploi.typeContrat }}

    +
    +
    +
    +
    + +

    {{ offre_emploi.localisation }}

    +
    +
    +
    +
    +
    +
    + +

    {{ offre_emploi.salaire }}

    +
    +
    +
    +
    + +

    {{ offre_emploi.datePublication|date('d/m/Y') }}

    +
    +
    +
    +
    +
    +
    + +

    {{ offre_emploi.description }}

    +
    +
    +
    +
    +
    +
    + +

    {{ offre_emploi.profilRecherche }}

    +
    +
    +
    + {% if offre_emploi.avantages %} +
    +
    +
    + +

    {{ offre_emploi.avantages }}

    +
    +
    +
    + {% endif %} + +
    +
    +
    +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/templates/public/offre_emploi/index.html.twig b/templates/public/offre_emploi/index.html.twig new file mode 100644 index 0000000..af955ca --- /dev/null +++ b/templates/public/offre_emploi/index.html.twig @@ -0,0 +1,44 @@ +{% extends 'base.html.twig' %} + +{% block title %}Offres d'emploi{% endblock %} + +{% block body %} +
    +
    +
    +
    +
    +
    +

    Nos offres d'emploi

    +
    +
    +
    + {% for offre in offres %} +
    +
    +
    +
    {{ offre.titre }}
    +

    + Type de contrat : {{ offre.typeContrat }}
    + Localisation : {{ offre.localisation }}
    + Salaire : {{ offre.salaire }} +

    + + Voir l'offre + +
    +
    +
    + {% else %} +
    +

    Aucune offre d'emploi disponible pour le moment.

    +
    + {% endfor %} +
    +
    +
    +
    +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/templates/public/offre_emploi/index_back_office.html.twig b/templates/public/offre_emploi/index_back_office.html.twig new file mode 100644 index 0000000..12e6a7b --- /dev/null +++ b/templates/public/offre_emploi/index_back_office.html.twig @@ -0,0 +1,74 @@ +{% extends 'back_office.html.twig' %} + +{% block title %}Offres d'emploi{% endblock %} + +{% block body %} +
    +
    + +
    + +
    +
    +
    +

    Découvrez nos opportunités de carrière et rejoignez notre équipe !

    +
    +
    +
    + {% for offre in offres %} +
    +
    +
    +
    {{ offre.titre }}
    +
    +
    +
    + {{ offre.typeContrat }} + {{ offre.localisation }} +
    +

    Salaire: {{ offre.salaire }}

    +

    Publié le: {{ offre.datePublication|date('d/m/Y') }}

    +

    + {{ offre.description|striptags|slice(0, 100) }}{% if offre.description|length > 100 %}...{% endif %} +

    +
    + +
    +
    + {% else %} +
    +
    + Aucune offre d'emploi disponible pour le moment. +
    +
    + {% endfor %} +
    +
    +
    +
    +
    +{% endblock %} + +{% block javascripts %} + {{ parent() }} + +{% endblock %} diff --git a/templates/public/offre_emploi/index_public.html.twig b/templates/public/offre_emploi/index_public.html.twig new file mode 100644 index 0000000..595d2d6 --- /dev/null +++ b/templates/public/offre_emploi/index_public.html.twig @@ -0,0 +1,125 @@ +{% extends 'base.html.twig' %} + +{% block title %}Offres d'emploi{% endblock %} + +{% block stylesheets %} + + + +{% endblock %} + +{% block body %} +
    +
    +
    +

    Nos offres d'emploi

    +

    Découvrez nos opportunités de carrière et rejoignez notre équipe !

    +
    +
    + +
    +
    +
    +
    +
    Filtrer les offres
    +
    +
    +
    +
    + + +
    +
    + + +
    + +
    +
    +
    +
    +
    + +
    + {% for offre in offres %} +
    +
    +
    +
    {{ offre.titre }}
    +
    +
    +

    + {{ offre.typeContrat|upper }} + {{ offre.localisation }} +

    +

    + Salaire: {{ offre.salaire }} +

    +

    + Publié le: {{ offre.datePublication|date('d/m/Y') }} +

    +

    + {{ offre.description|striptags|slice(0, 100) }}{% if offre.description|length > 100 %}...{% endif %} +

    +
    + +
    +
    + {% else %} +
    +
    + Aucune offre d'emploi disponible pour le moment. +
    +
    + {% endfor %} +
    +
    +{% endblock %} + +{% block javascripts %} + {{ parent() }} + + + +{% endblock %} diff --git a/templates/public/offre_emploi/show.html.twig b/templates/public/offre_emploi/show.html.twig new file mode 100644 index 0000000..2c8a0f7 --- /dev/null +++ b/templates/public/offre_emploi/show.html.twig @@ -0,0 +1,183 @@ +{% extends 'back_office.html.twig' %} + +{% block title %}{{ offre.titre }}{% endblock %} + +{% block body %} +
    +
    + +
    + +
    +
    +
    +

    {{ offre.titre }}

    +
    +
    +
    +
    +
    + +
    +
    + + + +
    + +
    +
    +
    +
    +
    + +
    +
    + + + +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    + + + +
    + +
    +
    +
    +
    +
    + +
    +
    + + + +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    + + + +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    + + + +
    + +
    +
    +
    +
    + {% if offre.avantages %} +
    +
    +
    + +
    +
    + + + +
    + +
    +
    +
    +
    + {% endif %} + +
    +
    +
    + + {% if offre.candidatures|length > 0 %} +
    +
    +
    +

    Candidatures reçues ({{ offre.candidatures|length }})

    +
    +
    +
    + + + + + + + + + + + + + {% for candidature in offre.candidatures %} + + + + + + + + + {% endfor %} + +
    NomPrénomEmailTéléphoneDate de candidatureActions
    {{ candidature.nom }}{{ candidature.prenom }}{{ candidature.email }}{{ candidature.telephone }}{{ candidature.dateCandidature|date('d/m/Y H:i') }} +
    + + + +
    +
    +
    +
    +
    +
    + {% endif %} +
    +{% endblock %} \ No newline at end of file diff --git a/templates/public/offre_emploi/show_back_office.html.twig b/templates/public/offre_emploi/show_back_office.html.twig new file mode 100644 index 0000000..7d8bf96 --- /dev/null +++ b/templates/public/offre_emploi/show_back_office.html.twig @@ -0,0 +1,136 @@ +{% extends 'back_office.html.twig' %} + +{% block title %}{{ offre.titre }}{% endblock %} + +{% block body %} +
    +
    + +
    + +
    +
    +
    +

    {{ offre.titre }}

    +
    +
    +
    +
    +
    + +
    +
    + + + +
    + +
    +
    +
    +
    +
    + +
    +
    + + + +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    + + + +
    + +
    +
    +
    +
    +
    + +
    +
    + + + +
    + +
    +
    +
    +
    +
    +
    +
    + +
    + {{ offre.description|nl2br }} +
    +
    +
    +
    +
    +
    +
    + +
    + {{ offre.profilRecherche|nl2br }} +
    +
    +
    +
    + {% if offre.avantages %} +
    +
    +
    + +
    + {{ offre.avantages|nl2br }} +
    +
    +
    +
    + {% endif %} + +
    +
    +
    +
    +{% endblock %} + +{% block javascripts %} + {{ parent() }} + +{% endblock %} diff --git a/templates/public/offre_emploi/show_public.html.twig b/templates/public/offre_emploi/show_public.html.twig new file mode 100644 index 0000000..420b3e8 --- /dev/null +++ b/templates/public/offre_emploi/show_public.html.twig @@ -0,0 +1,139 @@ +{% extends 'base.html.twig' %} + +{% block title %}{{ offre.titre }}{% endblock %} + +{% block stylesheets %} + + + +{% endblock %} + +{% block body %} +
    +
    +
    + +

    Détail de l'offre

    +
    +
    + +
    +
    +
    +
    +

    {{ offre.titre }}

    +
    +
    +
    +
    +
    + +

    {{ offre.typeContrat }}

    +
    +
    +
    +
    + +

    {{ offre.localisation }}

    +
    +
    +
    +
    +
    +
    + +

    {{ offre.salaire }}

    +
    +
    +
    +
    + +

    {{ offre.datePublication|date('d/m/Y') }}

    +
    +
    +
    +
    +
    +
    + +
    + {{ offre.description|nl2br }} +
    +
    +
    +
    +
    +
    +
    + +
    + {{ offre.profilRecherche|nl2br }} +
    +
    +
    +
    + {% if offre.avantages %} +
    +
    +
    + +
    + {{ offre.avantages|nl2br }} +
    +
    +
    +
    + {% endif %} + +
    +
    +
    +
    +
    +{% endblock %} + +{% block javascripts %} + {{ parent() }} + + + +{% endblock %}