src/Form/CandidatureType.php line 113

  1. <?php
  2. namespace App\Form;
  3. use App\Entity\Candidature;
  4. use App\Entity\Metier;
  5. use App\Entity\Etablissement;
  6. use App\Service\Constant;
  7. use Doctrine\ORM\EntityRepository;
  8. use Doctrine\ORM\EntityManagerInterface;
  9. use Symfony\Bridge\Doctrine\Form\Type\EntityType;
  10. use Symfony\Component\Form\AbstractType;
  11. use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
  12. use Symfony\Component\Form\Extension\Core\Type\DateType;
  13. use Symfony\Component\Form\Extension\Core\Type\IntegerType;
  14. use Symfony\Component\Form\Extension\Core\Type\NumberType;
  15. use Symfony\Component\Form\Extension\Core\Type\TextType;
  16. use Symfony\Component\Form\Extension\Core\Type\FileType;
  17. use Symfony\Component\Form\Extension\Core\Type\HiddenType;
  18. use Symfony\Component\Form\FormBuilderInterface;
  19. use Symfony\Component\Form\FormInterface;
  20. use Symfony\Component\OptionsResolver\OptionsResolver;
  21. use Symfony\Component\Validator\Constraints\File;
  22. use Symfony\Component\Form\FormEvents;
  23. use Symfony\Component\Form\FormEvent;
  24. use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
  25. class CandidatureType extends AbstractType
  26. {
  27.     private $authorization;
  28.     private $constant;
  29.     private $entityManager;
  30.     public function __construct(
  31.         AuthorizationCheckerInterface $authorization,
  32.         Constant $constant,
  33.         EntityManagerInterface $entityManager
  34.     ) {
  35.         $this->authorization $authorization;
  36.         $this->constant $constant;
  37.         $this->entityManager $entityManager;
  38.     }
  39.     public function buildForm(FormBuilderInterface $builder, array $options): void
  40.     {
  41.         $evaluationOnly $options['evaluation_only'];
  42.         // Écouter l'événement PRE_SET_DATA pour configurer le formulaire avec les données existantes
  43.         $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($evaluationOnly) {
  44.             $form $event->getForm();
  45.             /** @var Candidature|null $data */
  46.             $data $event->getData();
  47.             $etablissement = (!$evaluationOnly && $data) ? $data->getEtablissement() : null;
  48.             $this->addFormFields($form$etablissement$data$evaluationOnly);
  49.         });
  50.         // Écouter l'événement PRE_SUBMIT pour reconfigurer le formulaire avec les données soumises
  51.         $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($evaluationOnly) {
  52.             $form $event->getForm();
  53.             $data $event->getData();
  54.             $candidature $form->getData();
  55.             // Récupérer l'ID de l'établissement depuis les données soumises
  56.             $etablissementId = (!$evaluationOnly && isset($data['etablissement'])) ? $data['etablissement'] : null;
  57.             $etablissement $etablissementId $this->entityManager->getRepository(Etablissement::class)->find($etablissementId) : null;
  58.             $this->addFormFields($form$etablissement$candidature instanceof Candidature $candidature null$evaluationOnly);
  59.         });
  60.     }
  61.     private function addFormFields(FormInterface $form, ?Etablissement $etablissement, ?Candidature $candidature nullbool $evaluationOnly false): void
  62.     {
  63.         if (!$evaluationOnly) {
  64.             // Établissement (toujours disponible)
  65.             $form->add('etablissement'EntityType::class, [
  66.                 'class' => Etablissement::class,
  67.                 'query_builder' => function (EntityRepository $er) {
  68.                     return $er->createQueryBuilder('e')
  69.                         ->leftJoin('e.localite''l')
  70.                         ->leftJoin('l.directionRegionale''d')
  71.                         ->where('e.id IN (116) OR e.nom IN (:noms)')
  72.                         ->setParameter('noms'\App\Controller\HomeController::ETABLISSEMENTS_ACTIFS_NOMS)
  73.                         ->orderBy('e.nom''ASC');
  74.                 },
  75.                 'choice_label' => 'nom',
  76.                 'choice_value' => 'id',
  77.                 'attr' => [
  78.                     'data-metiers-target' => 'etablissement-select',
  79.                     'class' => 'w-full px-4 py-2 rounded-lg border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition'
  80.                 ],
  81.                 'label' => 'Établissement',
  82.                 'placeholder' => '-- Choisissez un établissement --',
  83.                 'required' => true
  84.             ]);
  85.             // Configuration du métier (dynamique selon l'établissement)
  86.             $metierOptions = [
  87.                 'class' => Metier::class,
  88.                 'choice_label' => 'nom',
  89.                 'choice_value' => 'id',
  90.                 'group_by' => 'secteur.nom',
  91.                 'label' => 'Métier',
  92.                 'required' => true,
  93.                 'attr' => [
  94.                     'class' => 'w-full px-4 py-2 rounded-lg border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition'
  95.                 ]
  96.             ];
  97.             if ($etablissement) {
  98.                 // Si un établissement est sélectionné, charger les métiers associés
  99.                 $metierOptions['query_builder'] = function (EntityRepository $er) use ($etablissement) {
  100.                     return $er->createQueryBuilder('m')
  101.                         ->innerJoin('m.etablissementMetiers''em')
  102.                         ->innerJoin('m.secteur''s')
  103.                         ->where('em.etablissement = :etablissement')
  104.                         ->setParameter('etablissement'$etablissement)
  105.                         ->orderBy('s.nom''ASC')
  106.                         ->addOrderBy('m.nom''ASC');
  107.                 };
  108.                 $metierOptions['placeholder'] = '-- Sélectionnez un métier --';
  109.                 $metierOptions['attr']['disabled'] = false;
  110.             } else {
  111.                 // Sans établissement, liste vide et champ désactivé
  112.                 $metierOptions['choices'] = [];
  113.                 $metierOptions['placeholder'] = '-- Choisissez d\'abord un établissement --';
  114.                 $metierOptions['attr']['disabled'] = true;
  115.             }
  116.             $form->add('metier'EntityType::class, $metierOptions);
  117.             // Champ caché pour stocker la valeur du niveau
  118.             $form->add('niveau'HiddenType::class, [
  119.                 'data' => $this->getNiveauRequis($etablissement)
  120.             ]);
  121.             // Documents à télécharger
  122.             foreach ($this->constant->document_labels as $field => $config) {
  123.                 $form->add($fieldFileType::class, [
  124.                     'label' => $config['text'],
  125.                     'mapped' => false,
  126.                     'required' => false,
  127.                     'attr' => [
  128.                         'data-field' => $field,
  129.                         'accept' => $config['accept'],
  130.                         'class' => 'hidden file-input'
  131.                     ],
  132.                     'constraints' => [
  133.                         new File([
  134.                             'maxSize' => '2M',
  135.                             'mimeTypes' => explode(','str_replace(' '''$config['accept'])),
  136.                             'mimeTypesMessage' => 'Format ' $config['formats'] . ' requis'
  137.                         ])
  138.                     ]
  139.                 ]);
  140.             }
  141.         } // fin if (!$evaluationOnly)
  142.         // Champs pour les évaluateurs (ENT, ADMIN)
  143.         if ($this->authorization->isGranted('ROLE_ENT') || $this->authorization->isGranted('ROLE_ADMIN')) {
  144.             $form
  145.                 ->add('etustatut'ChoiceType::class, [
  146.                     'choices' => [
  147.                         'En attente' => null,
  148.                         'Accepté' => 2,
  149.                         'Rejeté' => 1
  150.                     ],
  151.                     'label' => 'Étude de dossier',
  152.                     'required' => false,
  153.                     'attr' => ['class' => 'w-full px-4 py-2 rounded-lg border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition']
  154.                 ])
  155.                 ->add('etucom'TextType::class, [
  156.                     'label' => 'Commentaire étude',
  157.                     'required' => false,
  158.                     'attr' => ['class' => 'w-full px-4 py-2 rounded-lg border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition']
  159.                 ])
  160.                 ->add('entdate'DateType::class, [
  161.                     'widget' => 'single_text',
  162.                     'label' => 'Date entretien',
  163.                     'required' => false,
  164.                     'attr' => ['class' => 'w-full px-4 py-2 rounded-lg border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition']
  165.                 ])
  166.                 ->add('entlieu'TextType::class, [
  167.                     'label' => 'Lieu entretien',
  168.                     'required' => false,
  169.                     'attr' => ['class' => 'w-full px-4 py-2 rounded-lg border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition']
  170.                 ])
  171.                 ->add('jury'IntegerType::class, [
  172.                     'attr' => [
  173.                         'min' => 1,
  174.                         'max' => 20,
  175.                         'class' => 'w-full px-4 py-2 rounded-lg border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition'
  176.                     ],
  177.                     'label' => 'Numéro du jury',
  178.                     'required' => false
  179.                 ])
  180.                 ->add('vague'IntegerType::class, [
  181.                     'attr' => [
  182.                         'min' => 1,
  183.                         'class' => 'w-full px-4 py-2 rounded-lg border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition'
  184.                     ],
  185.                     'label' => 'Vague',
  186.                     'required' => false
  187.                 ]);
  188.             // Notes
  189.             for ($i 1$i <= 9$i++) {
  190.                 $noteOptions = [
  191.                     'attr' => [
  192.                         'min' => 0,
  193.                         'max' => 20,
  194.                         'step' => 0.5,
  195.                         'class' => 'w-full px-4 py-2 rounded-lg border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition'
  196.                     ],
  197.                     'label' => 'Note ' $i,
  198.                     'required' => false
  199.                 ];
  200.                 $form->add('note' $iNumberType::class, $noteOptions + ['scale' => 1]);
  201.             }
  202.             $form->add('entcom'TextType::class, [
  203.                 'label' => 'Commentaire entretien',
  204.                 'required' => false,
  205.                 'attr' => ['class' => 'w-full px-4 py-2 rounded-lg border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition']
  206.             ]);
  207.         }
  208.         // Champs Nom/Prénom du candidat (édition uniquement pour ENT/ADMIN, hors mode évaluation)
  209.         if (
  210.             !$evaluationOnly &&
  211.             ($this->authorization->isGranted('ROLE_ENT') || $this->authorization->isGranted('ROLE_ADMIN')) &&
  212.             $candidature !== null && $candidature->getId() !== null
  213.         ) {
  214.             $inputCls 'w-full px-4 py-2 rounded-lg border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition';
  215.             $user $candidature->getUser();
  216.             $form->add('nom'TextType::class, [
  217.                 'mapped' => false,
  218.                 'required' => false,
  219.                 'label' => 'Nom',
  220.                 'data' => $user ? ($user->getNom() ?? '') : '',
  221.                 'attr' => ['class' => $inputCls]
  222.             ]);
  223.             $form->add('prenoms'TextType::class, [
  224.                 'mapped' => false,
  225.                 'required' => false,
  226.                 'label' => 'Prénom(s)',
  227.                 'data' => $user ? ($user->getPrenoms() ?? '') : '',
  228.                 'attr' => ['class' => $inputCls]
  229.             ]);
  230.         }
  231.         // Champs pour le jury
  232.         if (
  233.             $this->authorization->isGranted('ROLE_JURY') ||
  234.             $this->authorization->isGranted('ROLE_ADMIN') ||
  235.             $this->authorization->isGranted('ROLE_ENT')
  236.         ) {
  237.             $form
  238.                 ->add('entstatut'ChoiceType::class, [
  239.                     'choices' => [
  240.                         'En attente' => null,
  241.                         'Admissible' => 2,
  242.                         'Non admissible' => 1
  243.                     ],
  244.                     'label' => 'Décision du jury',
  245.                     'required' => false,
  246.                     'attr' => ['class' => 'w-full px-4 py-2 rounded-lg border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition']
  247.                 ])
  248.                 ->add('visdate'DateType::class, [
  249.                     'widget' => 'single_text',
  250.                     'label' => 'Date visite',
  251.                     'required' => false,
  252.                     'attr' => ['class' => 'w-full px-4 py-2 rounded-lg border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition']
  253.                 ])
  254.                 ->add('vislieu'TextType::class, [
  255.                     'label' => 'Lieu visite',
  256.                     'required' => false,
  257.                     'attr' => ['class' => 'w-full px-4 py-2 rounded-lg border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition']
  258.                 ])
  259.                 ->add('visstatut'ChoiceType::class, [
  260.                     'choices' => [
  261.                         'En attente' => null,
  262.                         'Effectué' => 2,
  263.                         'Non effectué' => 1
  264.                     ],
  265.                     'label' => 'Statut visite',
  266.                     'required' => false,
  267.                     'attr' => ['class' => 'w-full px-4 py-2 rounded-lg border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition']
  268.                 ])
  269.                 ->add('viscom'TextType::class, [
  270.                     'label' => 'Commentaire visite',
  271.                     'required' => false,
  272.                     'attr' => ['class' => 'w-full px-4 py-2 rounded-lg border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition']
  273.                 ])
  274.                 ->add('resultat'ChoiceType::class, [
  275.                     'choices' => [
  276.                         'En attente' => null,
  277.                         'Admis' => 2,
  278.                         'Non admis' => 1
  279.                     ],
  280.                     'label' => 'Résultat final',
  281.                     'required' => false,
  282.                     'attr' => ['class' => 'w-full px-4 py-2 rounded-lg border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition']
  283.                 ]);
  284.         }
  285.     }
  286.     private function getNiveauRequis(?Etablissement $etablissement): string
  287.     {
  288.         if (!$etablissement) {
  289.             return 'Sélectionnez d\'abord un établissement';
  290.         }
  291.         // Par défaut, on ne peut pas déterminer le niveau car il dépend du métier
  292.         // Ce sera mis à jour dynamiquement via JavaScript
  293.         return 'Sélectionnez un métier';
  294.     }
  295.     public function configureOptions(OptionsResolver $resolver): void
  296.     {
  297.         $resolver->setDefaults([
  298.             'data_class' => Candidature::class,
  299.             'csrf_protection' => true,
  300.             'csrf_field_name' => '_token',
  301.             'csrf_token_id'   => 'candidature_item',
  302.             'evaluation_only' => false,
  303.         ]);
  304.     }
  305. }