src/Form/CandidatureType.php line 28

  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.                         ->orderBy('e.nom''ASC');
  72.                 },
  73.                 'choice_label' => 'nom',
  74.                 'choice_value' => 'id',
  75.                 'attr' => [
  76.                     'data-metiers-target' => 'etablissement-select',
  77.                     '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'
  78.                 ],
  79.                 'label' => 'Établissement',
  80.                 'placeholder' => '-- Choisissez un établissement --',
  81.                 'required' => true
  82.             ]);
  83.             // Configuration du métier (dynamique selon l'établissement)
  84.             $metierOptions = [
  85.                 'class' => Metier::class,
  86.                 'choice_label' => 'nom',
  87.                 'choice_value' => 'id',
  88.                 'group_by' => 'secteur.nom',
  89.                 'label' => 'Métier',
  90.                 'required' => true,
  91.                 'attr' => [
  92.                     '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'
  93.                 ]
  94.             ];
  95.             if ($etablissement) {
  96.                 // Si un établissement est sélectionné, charger les métiers associés
  97.                 $metierOptions['query_builder'] = function (EntityRepository $er) use ($etablissement) {
  98.                     return $er->createQueryBuilder('m')
  99.                         ->innerJoin('m.etablissementMetiers''em')
  100.                         ->innerJoin('m.secteur''s')
  101.                         ->where('em.etablissement = :etablissement')
  102.                         ->setParameter('etablissement'$etablissement)
  103.                         ->orderBy('s.nom''ASC')
  104.                         ->addOrderBy('m.nom''ASC');
  105.                 };
  106.                 $metierOptions['placeholder'] = '-- Sélectionnez un métier --';
  107.                 $metierOptions['attr']['disabled'] = false;
  108.             } else {
  109.                 // Sans établissement, liste vide et champ désactivé
  110.                 $metierOptions['choices'] = [];
  111.                 $metierOptions['placeholder'] = '-- Choisissez d\'abord un établissement --';
  112.                 $metierOptions['attr']['disabled'] = true;
  113.             }
  114.             $form->add('metier'EntityType::class, $metierOptions);
  115.             // Champ caché pour stocker la valeur du niveau
  116.             $form->add('niveau'HiddenType::class, [
  117.                 'data' => $this->getNiveauRequis($etablissement)
  118.             ]);
  119.             // Documents à télécharger
  120.             foreach ($this->constant->document_labels as $field => $config) {
  121.                 $form->add($fieldFileType::class, [
  122.                     'label' => $config['text'],
  123.                     'mapped' => false,
  124.                     'required' => false,
  125.                     'attr' => [
  126.                         'data-field' => $field,
  127.                         'accept' => $config['accept'],
  128.                         'class' => 'hidden file-input'
  129.                     ],
  130.                     'constraints' => [
  131.                         new File([
  132.                             'maxSize' => '2M',
  133.                             'mimeTypes' => explode(','str_replace(' '''$config['accept'])),
  134.                             'mimeTypesMessage' => 'Format ' $config['formats'] . ' requis'
  135.                         ])
  136.                     ]
  137.                 ]);
  138.             }
  139.         } // fin if (!$evaluationOnly)
  140.         // Champs pour les évaluateurs (ENT, ADMIN)
  141.         if ($this->authorization->isGranted('ROLE_ENT') || $this->authorization->isGranted('ROLE_ADMIN')) {
  142.             $form
  143.                 ->add('etustatut'ChoiceType::class, [
  144.                     'choices' => [
  145.                         'En attente' => null,
  146.                         'Accepté' => 2,
  147.                         'Rejeté' => 1
  148.                     ],
  149.                     'label' => 'Étude de dossier',
  150.                     'required' => false,
  151.                     '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']
  152.                 ])
  153.                 ->add('etucom'TextType::class, [
  154.                     'label' => 'Commentaire étude',
  155.                     'required' => false,
  156.                     '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']
  157.                 ])
  158.                 ->add('entdate'DateType::class, [
  159.                     'widget' => 'single_text',
  160.                     'label' => 'Date entretien',
  161.                     'required' => false,
  162.                     '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']
  163.                 ])
  164.                 ->add('entlieu'TextType::class, [
  165.                     'label' => 'Lieu entretien',
  166.                     'required' => false,
  167.                     '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']
  168.                 ])
  169.                 ->add('jury'IntegerType::class, [
  170.                     'attr' => [
  171.                         'min' => 1,
  172.                         'max' => 20,
  173.                         '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'
  174.                     ],
  175.                     'label' => 'Numéro du jury',
  176.                     'required' => false
  177.                 ])
  178.                 ->add('vague'IntegerType::class, [
  179.                     'attr' => [
  180.                         'min' => 1,
  181.                         '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'
  182.                     ],
  183.                     'label' => 'Vague',
  184.                     'required' => false
  185.                 ]);
  186.             // Notes
  187.             for ($i 1$i <= 9$i++) {
  188.                 $noteOptions = [
  189.                     'attr' => [
  190.                         'min' => 0,
  191.                         'max' => 20,
  192.                         'step' => 0.5,
  193.                         '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'
  194.                     ],
  195.                     'label' => 'Note ' $i,
  196.                     'required' => false
  197.                 ];
  198.                 $form->add('note' $iNumberType::class, $noteOptions + ['scale' => 1]);
  199.             }
  200.             $form->add('entcom'TextType::class, [
  201.                 'label' => 'Commentaire entretien',
  202.                 'required' => false,
  203.                 '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']
  204.             ]);
  205.         }
  206.         // Champs Nom/Prénom du candidat (édition uniquement pour ENT/ADMIN, hors mode évaluation)
  207.         if (
  208.             !$evaluationOnly &&
  209.             ($this->authorization->isGranted('ROLE_ENT') || $this->authorization->isGranted('ROLE_ADMIN')) &&
  210.             $candidature !== null && $candidature->getId() !== null
  211.         ) {
  212.             $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';
  213.             $user $candidature->getUser();
  214.             $form->add('nom'TextType::class, [
  215.                 'mapped' => false,
  216.                 'required' => false,
  217.                 'label' => 'Nom',
  218.                 'data' => $user ? ($user->getNom() ?? '') : '',
  219.                 'attr' => ['class' => $inputCls]
  220.             ]);
  221.             $form->add('prenoms'TextType::class, [
  222.                 'mapped' => false,
  223.                 'required' => false,
  224.                 'label' => 'Prénom(s)',
  225.                 'data' => $user ? ($user->getPrenoms() ?? '') : '',
  226.                 'attr' => ['class' => $inputCls]
  227.             ]);
  228.         }
  229.         // Champs pour le jury
  230.         if (
  231.             $this->authorization->isGranted('ROLE_JURY') ||
  232.             $this->authorization->isGranted('ROLE_ADMIN') ||
  233.             $this->authorization->isGranted('ROLE_ENT')
  234.         ) {
  235.             $form
  236.                 ->add('entstatut'ChoiceType::class, [
  237.                     'choices' => [
  238.                         'En attente' => null,
  239.                         'Admissible' => 2,
  240.                         'Non admissible' => 1
  241.                     ],
  242.                     'label' => 'Décision du jury',
  243.                     'required' => false,
  244.                     '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']
  245.                 ])
  246.                 ->add('visdate'DateType::class, [
  247.                     'widget' => 'single_text',
  248.                     'label' => 'Date visite',
  249.                     'required' => false,
  250.                     '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']
  251.                 ])
  252.                 ->add('vislieu'TextType::class, [
  253.                     'label' => 'Lieu visite',
  254.                     'required' => false,
  255.                     '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']
  256.                 ])
  257.                 ->add('visstatut'ChoiceType::class, [
  258.                     'choices' => [
  259.                         'En attente' => null,
  260.                         'Effectué' => 2,
  261.                         'Non effectué' => 1
  262.                     ],
  263.                     'label' => 'Statut visite',
  264.                     'required' => false,
  265.                     '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']
  266.                 ])
  267.                 ->add('viscom'TextType::class, [
  268.                     'label' => 'Commentaire visite',
  269.                     'required' => false,
  270.                     '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']
  271.                 ])
  272.                 ->add('resultat'ChoiceType::class, [
  273.                     'choices' => [
  274.                         'En attente' => null,
  275.                         'Admis' => 2,
  276.                         'Non admis' => 1
  277.                     ],
  278.                     'label' => 'Résultat final',
  279.                     'required' => false,
  280.                     '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']
  281.                 ]);
  282.         }
  283.     }
  284.     private function getNiveauRequis(?Etablissement $etablissement): string
  285.     {
  286.         if (!$etablissement) {
  287.             return 'Sélectionnez d\'abord un établissement';
  288.         }
  289.         // Par défaut, on ne peut pas déterminer le niveau car il dépend du métier
  290.         // Ce sera mis à jour dynamiquement via JavaScript
  291.         return 'Sélectionnez un métier';
  292.     }
  293.     public function configureOptions(OptionsResolver $resolver): void
  294.     {
  295.         $resolver->setDefaults([
  296.             'data_class' => Candidature::class,
  297.             'csrf_protection' => true,
  298.             'csrf_field_name' => '_token',
  299.             'csrf_token_id'   => 'candidature_item',
  300.             'evaluation_only' => false,
  301.         ]);
  302.     }
  303. }