templates/candidature/form.html.twig line 1

  1. {% extends 'base.html.twig' %}
  2. {% block title %}Nouvelle candidature | DAIP{% endblock %}
  3. {# Définition de la classe CSS réutilisable #}
  4. {% set input_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' %}
  5. {% set is_edit_page = app.request.attributes.get('_route') == 'app_candidature_edit' %}
  6. {% block body %}
  7. {# Flash messages #}
  8. {% for label, messages in app.flashes %}
  9.     {% for message in messages %}
  10.         <div class="mb-4 p-4 rounded-lg border
  11.             {% if label == 'error' %}
  12.                 bg-red-50 border-red-200 text-red-800
  13.             {% elseif label == 'success' %}
  14.                 bg-green-50 border-green-200 text-green-800
  15.             {% else %}
  16.                 bg-blue-50 border-blue-200 text-blue-800
  17.             {% endif %}
  18.         ">
  19.             {{ message|raw }}
  20.         </div>
  21.     {% endfor %}
  22. {% endfor %}
  23. <div class="bg-gradient-to-r from-indigo-600 to-purple-600 p-8 mb-8 text-white">
  24.     <div class="flex items-center gap-4 mb-4">
  25.         <div class="w-16 h-16 bg-white/20 rounded-full flex items-center justify-center">
  26.             <i class="fas fa-file-alt text-3xl"></i>
  27.         </div>
  28.         <div>
  29.             <h1 class="text-2xl font-bold">
  30.                 {% if is_edit_page %}Modification de candidature{% else %}Nouvelle candidature{% endif %}
  31.             </h1>
  32.             <p class="text-white/80">
  33.                 {% if is_edit_page %}Mettez a jour les informations de la candidature de facon claire et complete.{% else %}Vous etes sur le point de postuler a une formation{% endif %}
  34.             </p>
  35.         </div>
  36.     </div>
  37.     
  38.     <!-- Informations du candidat -->
  39.     {% if app.user %}
  40.         {% if is_granted('ROLE_ENT') and is_edit_page and candidature is defined %}
  41.             <div class="mt-4 grid grid-cols-1 lg:grid-cols-2 gap-4">
  42.                 <div class="bg-white/10 backdrop-blur-sm rounded-xl p-4 border border-white/20">
  43.                     <p class="text-sm font-semibold text-white/80 mb-3 flex items-center">
  44.                         <i class="fas fa-user-graduate mr-2"></i>
  45.                         Informations du candidat
  46.                     </p>
  47.                     <div class="space-y-3">
  48.                         <div class="bg-white/10 rounded-lg px-3 py-2">
  49.                             <p class="text-xs uppercase tracking-wide text-white/70">Nom du candidat</p>
  50.                             <p class="text-sm font-semibold text-white mt-1">{{ candidature.user ? (candidature.user.nom ~ ' ' ~ candidature.user.prenoms)|trim : 'Non renseigne' }}</p>
  51.                         </div>
  52.                         <div class="bg-white/10 rounded-lg px-3 py-2">
  53.                             <p class="text-xs uppercase tracking-wide text-white/70">Numero de candidature</p>
  54.                             <p class="text-sm font-semibold text-white mt-1">{{ candidature.numero|default('Non attribue') }}</p>
  55.                         </div>
  56.                     </div>
  57.                 </div>
  58.                 <div class="bg-white/10 backdrop-blur-sm rounded-xl p-4 border border-white/20">
  59.                     <p class="text-sm font-semibold text-white/80 mb-3 flex items-center">
  60.                         <i class="fas fa-file-signature mr-2"></i>
  61.                         Informations de la candidature
  62.                     </p>
  63.                     <div class="space-y-3">
  64.                         <div class="bg-white/10 rounded-lg px-3 py-2">
  65.                             <p class="text-xs uppercase tracking-wide text-white/70">Etablissement choisi</p>
  66.                             <p class="text-sm font-semibold text-white mt-1">{{ candidature.etablissement ? candidature.etablissement.nom : 'Non renseigne' }}</p>
  67.                         </div>
  68.                         <div class="bg-white/10 rounded-lg px-3 py-2">
  69.                             <p class="text-xs uppercase tracking-wide text-white/70">Metier choisi</p>
  70.                             <p class="text-sm font-semibold text-white mt-1">{{ candidature.metier ? candidature.metier.nom : 'Non renseigne' }}</p>
  71.                         </div>
  72.                     </div>
  73.                 </div>
  74.             </div>
  75.         {% else %}
  76.             <div class="bg-white/10 backdrop-blur-sm rounded-xl p-4 mt-4">
  77.                 <div class="flex items-center gap-3">
  78.                     <div class="w-10 h-10 bg-white/20 rounded-full flex items-center justify-center">
  79.                         <i class="fas fa-user"></i>
  80.                     </div>
  81.                     <div>
  82.                         <p class="text-sm text-white/80">Candidat</p>
  83.                         <p class="font-semibold">{{ app.user.nom }} {{ app.user.prenoms }}</p>
  84.                     </div>
  85.                     {# Calcul et affichage de l'âge #}
  86.                     {% if app.user.datenaissance %}
  87.                         {% set age = date().diff(date(app.user.datenaissance)).y %}
  88.                         <div class="ml-4 px-3 py-1 bg-white/20 rounded-full">
  89.                             <span class="text-sm">
  90.                                 <i class="fas fa-birthday-cake mr-1"></i>
  91.                                 {{ age }} ans
  92.                             </span>
  93.                         </div>
  94.                     {% endif %}
  95.                     <div class="ml-auto">
  96.                         <span class="px-3 py-1 bg-white/20 rounded-full text-sm">{{ app.user.email }}</span>
  97.                         <span class="ml-2 px-3 py-1 bg-indigo-500 rounded-full text-xs">
  98.                             {% if is_granted('ROLE_ADMIN') %}ADMIN
  99.                             {% elseif is_granted('ROLE_ENT') %}ENT
  100.                             {% elseif is_granted('ROLE_JURY') %}JURY
  101.                             {% elseif is_granted('ROLE_CANDIDAT') %}CANDIDAT
  102.                             {% endif %}
  103.                         </span>
  104.                     </div>
  105.                 </div>
  106.             </div>
  107.         {% endif %}
  108.     {% endif %}
  109.     
  110.     {# Vérification d'âge #}
  111.     {% if app.user and app.user.datenaissance %}
  112.         {% set age = date().diff(date(app.user.datenaissance)).y %}
  113.         {% if age < 16 or age > 40 %}
  114.             <div class="mt-4 bg-red-500/20 backdrop-blur-sm border border-red-500/30 rounded-xl p-4">
  115.                 <div class="flex items-center gap-3">
  116.                     <div class="w-10 h-10 bg-red-500/30 rounded-full flex items-center justify-center">
  117.                         <i class="fas fa-exclamation-triangle text-red-300"></i>
  118.                     </div>
  119.                     <div>
  120.                         <p class="font-semibold text-white">Âge non conforme</p>
  121.                         <p class="text-sm text-white/80">
  122.                             {% if age < 16 %}
  123.                                 Vous avez {{ age }} ans. L'âge minimum requis pour candidater est de 16 ans.
  124.                             {% elseif age > 40 %}
  125.                                 Vous avez {{ age }} ans. L'âge maximum autorisé pour candidater est de 40 ans.
  126.                             {% endif %}
  127.                         </p>
  128.                     </div>
  129.                 </div>
  130.             </div>
  131.         {% endif %}
  132.     {% endif %}
  133. </div>
  134. <div class="max-w-7xl mx-auto px-4 pb-16 {{ is_edit_page ? 'pt-2' : '' }}">
  135.     {% include 'partials/_flash_messages.html.twig' %}
  136.     {% if is_edit_page and candidature is defined and candidature.id %}
  137.         {# ===== PARTIE 1 : MISE À JOUR DES INFORMATIONS ===== #}
  138.         <div class="bg-white rounded-2xl shadow-xl border border-slate-200 overflow-hidden mb-8">
  139.             <div class="p-6 md:p-8">
  140.                 <div class="rounded-xl border border-indigo-200 bg-indigo-50 p-4 md:p-5 mb-8">
  141.                     <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2">
  142.                         <div>
  143.                             <p class="text-sm font-semibold text-indigo-800 flex items-center">
  144.                                 <i class="fas fa-edit mr-2"></i> Partie 1 — Mise à jour des informations
  145.                             </p>
  146.                             <p class="text-sm text-indigo-700 mt-1">Modifiez les informations du candidat, le métier et les documents.</p>
  147.                         </div>
  148.                         <span class="inline-flex items-center px-3 py-1 rounded-full bg-white text-indigo-700 text-sm font-semibold border border-indigo-200">
  149.                             N° {{ candidature.numero|default('Non attribué') }}
  150.                         </span>
  151.                     </div>
  152.                 </div>
  153.                 <form id="form-update-info" action="{{ path('app_candidature_update_info', {id: candidature.id}) }}" method="POST" enctype="multipart/form-data" class="space-y-10">
  154.                     <input type="hidden" name="_token" value="{{ csrf_token('candidature_update_info_' ~ candidature.id) }}">
  155.                     {# Identité du candidat (ENT/ADMIN) #}
  156.                     {% if is_granted('ROLE_ENT') or is_granted('ROLE_ADMIN') %}
  157.                         <div class="rounded-xl border border-orange-200 bg-orange-50 p-5">
  158.                             <h3 class="text-base font-semibold text-orange-800 mb-4 flex items-center">
  159.                                 <div class="w-7 h-7 bg-orange-200 rounded-lg flex items-center justify-center mr-2">
  160.                                     <i class="fas fa-user-edit text-orange-700 text-sm"></i>
  161.                                 </div>
  162.                                 Identité du candidat
  163.                             </h3>
  164.                             <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
  165.                                 <div>
  166.                                     <label class="block text-sm font-medium text-slate-700 mb-1">Nom <span class="text-red-500">*</span></label>
  167.                                     <input type="text" name="nom" value="{{ candidature.user ? candidature.user.nom : '' }}" class="{{ input_class }}" placeholder="Nom de famille">
  168.                                 </div>
  169.                                 <div>
  170.                                     <label class="block text-sm font-medium text-slate-700 mb-1">Prénom(s) <span class="text-red-500">*</span></label>
  171.                                     <input type="text" name="prenoms" value="{{ candidature.user ? candidature.user.prenoms : '' }}" class="{{ input_class }}" placeholder="Prénom(s)">
  172.                                 </div>
  173.                                 <div class="md:col-span-2">
  174.                                     <label class="block text-sm font-medium text-slate-700 mb-1">Contact (téléphone)</label>
  175.                                     <input type="text" name="contact" value="{{ candidature.user ? candidature.user.contact : '' }}" class="{{ input_class }}" placeholder="Ex: 0102030405">
  176.                                 </div>
  177.                             </div>
  178.                         </div>
  179.                     {% endif %}
  180.                     {# Formation souhaitée #}
  181.                     <h3 class="text-lg font-semibold text-indigo-800 flex items-center border-b pb-3 mb-5">
  182.                         <div class="w-8 h-8 bg-indigo-200 rounded-lg flex items-center justify-center mr-3">
  183.                             <i class="fas fa-graduation-cap text-indigo-700"></i>
  184.                         </div>
  185.                         Formation souhaitée
  186.                     </h3>
  187.                     <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
  188.                         <div>
  189.                             <label class="block text-sm font-medium text-slate-700 mb-1">Établissement <span class="text-red-500">*</span></label>
  190.                             <div class="{{ input_class }} bg-slate-100 text-slate-600 cursor-default flex items-center gap-2">
  191.                                 <i class="fas fa-lock text-slate-400 text-xs"></i>
  192.                                 {{ candidature.etablissement ? candidature.etablissement.nom : 'Non renseigné' }}
  193.                             </div>
  194.                             <input type="hidden" id="candidature_etablissement" value="{{ candidature.etablissement ? candidature.etablissement.id : '' }}">
  195.                         </div>
  196.                         <div>
  197.                             <label class="block text-sm font-medium text-slate-700 mb-1">Métier <span class="text-red-500">*</span></label>
  198.                             <select name="metier_id" id="candidature_metier" required class="{{ input_class }}">
  199.                                 {% if candidature.metier %}
  200.                                     <option value="{{ candidature.metier.id }}" selected>{{ candidature.metier.nom }}</option>
  201.                                 {% else %}
  202.                                     <option value="">-- Sélectionnez un métier --</option>
  203.                                 {% endif %}
  204.                             </select>
  205.                             <div id="metier-loading" class="hidden text-sm text-indigo-600 mt-1">
  206.                                 <i class="fas fa-spinner fa-spin mr-1"></i> Chargement...
  207.                             </div>
  208.                         </div>
  209.                     </div>
  210.                     {# Informations détaillées du métier #}
  211.                     <div id="metier-info" class="hidden mt-6">
  212.                         <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
  213.                             <div class="flex items-center gap-3 p-2 bg-indigo-100 rounded-lg">
  214.                                 <i class="fas fa-building text-indigo-600"></i>
  215.                                 <div>
  216.                                     <p class="text-xs text-indigo-600">Secteur</p>
  217.                                     <p id="metier-secteur" class="text-sm font-medium">-</p>
  218.                                 </div>
  219.                             </div>
  220.                             <div class="flex items-center gap-3 p-2 bg-purple-100 rounded-lg">
  221.                                 <i class="fas fa-users text-purple-600"></i>
  222.                                 <div>
  223.                                     <p class="text-xs text-purple-600">Places</p>
  224.                                     <p id="metier-nbrplace" class="text-sm font-medium">-</p>
  225.                                 </div>
  226.                             </div>
  227.                             <div class="flex items-center gap-3 p-2 bg-green-100 rounded-lg">
  228.                                 <i class="fas fa-graduation-cap text-green-600"></i>
  229.                                 <div>
  230.                                     <p class="text-xs text-green-600">Niveau</p>
  231.                                     <p id="metier-niveau" class="text-sm font-medium">-</p>
  232.                                 </div>
  233.                             </div>
  234.                             <div class="flex items-center gap-3 p-2 bg-amber-100 rounded-lg">
  235.                                 <i class="fas fa-clock text-amber-600"></i>
  236.                                 <div>
  237.                                     <p class="text-xs text-amber-600">Durée</p>
  238.                                     <p id="metier-duree" class="text-sm font-medium">-</p>
  239.                                 </div>
  240.                             </div>
  241.                         </div>
  242.                     </div>
  243.                     {# Documents #}
  244.                     <h3 class="text-lg font-semibold text-purple-800 mb-5 flex items-center border-b pb-3">
  245.                         <div class="w-8 h-8 bg-purple-200 rounded-lg flex items-center justify-center mr-3">
  246.                             <i class="fas fa-cloud-upload-alt text-purple-700"></i>
  247.                         </div>
  248.                         Documents à télécharger <span class="text-red-500"> *</span>
  249.                     </h3>
  250.                     {% include 'partials/_document_upload_plain.html.twig' with {
  251.                         'candidature': candidature,
  252.                         'document_labels': document_labels,
  253.                         'input_class': input_class
  254.                     } %}
  255.                     {# Bouton Mise à jour #}
  256.                     <div class="flex flex-col md:flex-row justify-between items-center gap-4 pt-6 border-t border-slate-200">
  257.                         <div class="text-sm text-slate-500">
  258.                             <i class="fas fa-info-circle mr-1"></i>
  259.                             Les champs marqués d'une <span class="text-red-500">*</span> sont obligatoires
  260.                         </div>
  261.                         <button type="submit" class="px-8 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 text-white font-semibold rounded-lg hover:from-indigo-700 hover:to-purple-700 transition transform hover:scale-105 shadow-lg flex items-center">
  262.                             <i class="fas fa-save mr-2"></i>
  263.                             Mise à jour
  264.                         </button>
  265.                     </div>
  266.                 </form>
  267.             </div>
  268.         </div>
  269.         {# ===== PARTIE 2 : ANALYSE DU DOSSIER ===== #}
  270.         <div class="bg-white rounded-2xl shadow-xl border border-slate-200 overflow-hidden">
  271.             <div class="p-6 md:p-8">
  272.                 {{ form_start(form, {'attr': {'class': 'space-y-10', 'id': 'candidature-form'}}) }}
  273.                     <div class="rounded-xl border border-amber-200 bg-amber-50 p-4 md:p-5 mb-4">
  274.                         <p class="text-sm font-semibold text-amber-800 flex items-center">
  275.                             <i class="fas fa-clipboard-list mr-2"></i>
  276.                             Partie 2 — Analyse du dossier
  277.                         </p>
  278.                         <p class="text-sm text-amber-700 mt-1">Étudiez le dossier, évaluez le candidat et enregistrez le résultat.</p>
  279.                     </div>
  280.                     {% include 'partials/_candidature_evaluation_sections.html.twig' %}
  281.                     <div class="flex flex-col md:flex-row justify-between items-center gap-4 pt-6 border-t border-slate-200">
  282.                         <div class="text-sm text-slate-500">
  283.                             <i class="fas fa-info-circle mr-1"></i>
  284.                             Les champs marqués d'une <span class="text-red-500">*</span> sont obligatoires
  285.                         </div>
  286.                         <button type="submit" class="px-8 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 text-white font-semibold rounded-lg hover:from-indigo-700 hover:to-purple-700 transition transform hover:scale-105 shadow-lg flex items-center">
  287.                             <i class="fas fa-paper-plane mr-2"></i>
  288.                             Enregistrer l'évaluation
  289.                         </button>
  290.                     </div>
  291.                 {{ form_end(form) }}
  292.             </div>
  293.         </div>
  294.     {% else %}
  295.     <div class="bg-white rounded-2xl shadow-xl border border-slate-200 overflow-hidden">
  296.         <div class="p-6 md:p-8">
  297.             {{ form_start(form, {'attr': {'class': 'space-y-10', 'enctype': 'multipart/form-data', 'id': 'candidature-form'}}) }}
  298.             {% if is_edit_page and candidature is defined %}
  299.                 <div class="rounded-xl border border-indigo-200 bg-indigo-50 p-4 md:p-5">
  300.                     <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2">
  301.                         <div>
  302.                             <p class="text-sm font-semibold text-indigo-800">Edition de candidature</p>
  303.                             <p class="text-sm text-indigo-700 mt-1">Verifiez les informations puis mettez a jour les documents si necessaire.</p>
  304.                         </div>
  305.                         <span class="inline-flex items-center px-3 py-1 rounded-full bg-white text-indigo-700 text-sm font-semibold border border-indigo-200">
  306.                             N° {{ candidature.numero|default('Non attribue') }}
  307.                         </span>
  308.                     </div>
  309.                 </div>
  310.             {% endif %}
  311.             {# Section : Identité du candidat (visible ENT / ADMIN en mode édition) #}
  312.             {% if is_edit_page and (is_granted('ROLE_ENT') or is_granted('ROLE_ADMIN')) and form.nom is defined %}
  313.                 <div class="rounded-xl border border-orange-200 bg-orange-50 p-5">
  314.                     <h3 class="text-base font-semibold text-orange-800 mb-4 flex items-center">
  315.                         <div class="w-7 h-7 bg-orange-200 rounded-lg flex items-center justify-center mr-2">
  316.                             <i class="fas fa-user-edit text-orange-700 text-sm"></i>
  317.                         </div>
  318.                         Identité du candidat
  319.                     </h3>
  320.                     <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
  321.                         <div>
  322.                             <label class="block text-sm font-medium text-slate-700 mb-1">
  323.                                 Nom <span class="text-red-500">*</span>
  324.                             </label>
  325.                             {{ form_widget(form.nom, {'attr': {'class': input_class, 'placeholder': 'Nom de famille'}}) }}
  326.                         </div>
  327.                         <div>
  328.                             <label class="block text-sm font-medium text-slate-700 mb-1">
  329.                                 Prénom(s) <span class="text-red-500">*</span>
  330.                             </label>
  331.                             {{ form_widget(form.prenoms, {'attr': {'class': input_class, 'placeholder': 'Prénom(s)'}}) }}
  332.                         </div>
  333.                     </div>
  334.                 </div>
  335.             {% endif %}
  336.             <!-- SECTION 1: TOUS LES UTILISATEURS - Formation souhaitée -->
  337.             <h3 class="text-lg font-semibold text-indigo-800 flex items-center border-b pb-3 mb-5">
  338.                 <div class="w-8 h-8 bg-indigo-200 rounded-lg flex items-center justify-center mr-3">
  339.                     <i class="fas fa-graduation-cap text-indigo-700"></i>
  340.                 </div>
  341.                 Formation souhaitée
  342.             </h3>
  343.             
  344.             <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
  345.                 <div>
  346.                     <label class="block text-sm font-medium text-slate-700 mb-1">
  347.                         Établissement <span class="text-red-500">*</span>
  348.                     </label>
  349.                     {% if is_edit_page and candidature is defined and candidature.etablissement %}
  350.                         {# En mode édition : établissement fixe (lecture seule) #}
  351.                         <div class="{{ input_class }} bg-slate-100 text-slate-600 cursor-default flex items-center gap-2">
  352.                             <i class="fas fa-lock text-slate-400 text-xs"></i>
  353.                             {{ candidature.etablissement.nom }}
  354.                         </div>
  355.                         {# Champ caché pour maintenir la valeur lors de la soumission #}
  356.                         {{ form_widget(form.etablissement, {'attr': {'class': 'hidden', 'id': 'candidature_etablissement'}}) }}
  357.                     {% else %}
  358.                         {{ form_widget(form.etablissement, {'attr': {'class': input_class, 'id': 'candidature_etablissement'}}) }}
  359.                     {% endif %}
  360.                     <div id="etablissement-loading" class="hidden text-sm text-indigo-600 mt-1">
  361.                         <i class="fas fa-spinner fa-spin mr-1"></i> Chargement...
  362.                     </div>
  363.                 </div>
  364.                 
  365.                 <div>
  366.                     <label class="block text-sm font-medium text-slate-700 mb-1">
  367.                         Métier <span class="text-red-500">*</span>
  368.                     </label>
  369.                     {{ form_widget(form.metier, {'attr': {'class': input_class, 'id': 'candidature_metier'}}) }}
  370.                     <div id="metier-loading" class="hidden text-sm text-indigo-600 mt-1">
  371.                         <i class="fas fa-spinner fa-spin mr-1"></i> Chargement...
  372.                     </div>
  373.                 </div>
  374.             </div>
  375.             <!-- Informations détaillées du métier -->
  376.             <div id="metier-info" class="hidden mt-6">
  377.                 <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
  378.                     <div class="flex items-center gap-3 p-2 bg-indigo-100 rounded-lg">
  379.                         <i class="fas fa-building text-indigo-600"></i>
  380.                         <div>
  381.                             <p class="text-xs text-indigo-600">Secteur</p>
  382.                             <p id="metier-secteur" class="text-sm font-medium">-</p>
  383.                         </div>
  384.                     </div>
  385.                     <div class="flex items-center gap-3 p-2 bg-purple-100 rounded-lg">
  386.                         <i class="fas fa-users text-purple-600"></i>
  387.                         <div>
  388.                             <p class="text-xs text-purple-600">Places</p>
  389.                             <p id="metier-nbrplace" class="text-sm font-medium">-</p>
  390.                         </div>
  391.                     </div>
  392.                     <div class="flex items-center gap-3 p-2 bg-green-100 rounded-lg">
  393.                         <i class="fas fa-graduation-cap text-green-600"></i>
  394.                         <div>
  395.                             <p class="text-xs text-green-600">Niveau</p>
  396.                             <p id="metier-niveau" class="text-sm font-medium">-</p>
  397.                         </div>
  398.                     </div>
  399.                     <div class="flex items-center gap-3 p-2 bg-amber-100 rounded-lg">
  400.                         <i class="fas fa-clock text-amber-600"></i>
  401.                         <div>
  402.                             <p class="text-xs text-amber-600">Durée</p>
  403.                             <p id="metier-duree" class="text-sm font-medium">-</p>
  404.                         </div>
  405.                     </div>
  406.                 </div>
  407.             </div>
  408.             <!-- SECTION 2: DOCUMENTS -->
  409.             <h3 class="text-lg font-semibold text-purple-800 mb-5 flex items-center border-b pb-3">
  410.                 <div class="w-8 h-8 bg-purple-200 rounded-lg flex items-center justify-center mr-3">
  411.                     <i class="fas fa-cloud-upload-alt text-purple-700"></i>
  412.                 </div>
  413.                 Documents à télécharger <span class="text-red-500"> *</span>
  414.             </h3>
  415.             
  416.             {% include 'partials/_document_upload.html.twig' with {
  417.                 'form': form,
  418.                 'document_labels': document_labels,
  419.                 'input_class': input_class
  420.             } %}
  421.             <!-- SECTION 3: POUR ENT ET ADMIN - Étude de dossier -->
  422.             {% if is_granted('ROLE_ENT') or is_granted('ROLE_ADMIN') %}
  423.                 <h3 class="text-lg font-semibold text-amber-800 mb-5 flex items-center border-b pb-3">
  424.                     <div class="w-8 h-8 bg-amber-200 rounded-lg flex items-center justify-center mr-3">
  425.                         <i class="fas fa-clipboard-check text-amber-700"></i>
  426.                     </div>
  427.                     Étude de dossier (ENT)
  428.                 </h3>
  429.                 
  430.                 <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
  431.                     <div>
  432.                         <label class="block text-sm font-medium text-slate-700 mb-1">Statut dossier</label>
  433.                         {{ form_widget(form.etustatut, {'attr': {'class': input_class}}) }}
  434.                     </div>
  435.                     <div>
  436.                         <label class="block text-sm font-medium text-slate-700 mb-1">Commentaire</label>
  437.                         {{ form_widget(form.etucom, {'attr': {'class': input_class, 'placeholder': 'Commentaire sur le dossier...'}}) }}
  438.                     </div>
  439.                 </div>
  440.             {% endif %}
  441.             <!-- SECTION 4: POUR JURY ET ADMIN - Évaluation entretien -->
  442.             {% if is_granted('ROLE_JURY') or is_granted('ROLE_ADMIN') %}
  443.                 <h3 class="text-lg font-semibold text-blue-800 mb-5 flex items-center border-b pb-3">
  444.                     <div class="w-8 h-8 bg-blue-200 rounded-lg flex items-center justify-center mr-3">
  445.                         <i class="fas fa-microphone-alt text-blue-700"></i>
  446.                     </div>
  447.                     Évaluation entretien (Jury)
  448.                 </h3>
  449.                 
  450.                 <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
  451.                     <div>
  452.                         <label class="block text-sm font-medium text-slate-700 mb-1">Date entretien</label>
  453.                         {{ form_widget(form.entdate, {'attr': {'class': input_class}}) }}
  454.                     </div>
  455.                     <div>
  456.                         <label class="block text-sm font-medium text-slate-700 mb-1">Lieu entretien</label>
  457.                         {{ form_widget(form.entlieu, {'attr': {'class': input_class, 'placeholder': 'Ex: Salle 101'}}) }}
  458.                     </div>
  459.                 </div>
  460.                 <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
  461.                     <div>
  462.                         <label class="block text-sm font-medium text-slate-700 mb-1">Jury n°</label>
  463.                         {{ form_widget(form.jury, {'attr': {'class': input_class, 'min': 1, 'max': 20}}) }}
  464.                     </div>
  465.                     <div>
  466.                         <label class="block text-sm font-medium text-slate-700 mb-1">Vague</label>
  467.                         {{ form_widget(form.vague, {'attr': {'class': input_class, 'min': 1}}) }}
  468.                     </div>
  469.                     <div>
  470.                         <label class="block text-sm font-medium text-slate-700 mb-1">Décision jury</label>
  471.                         {{ form_widget(form.entstatut, {'attr': {'class': input_class}}) }}
  472.                     </div>
  473.                 </div>
  474.                 <div class="mt-4">
  475.                     <label class="block text-sm font-medium text-slate-700 mb-1">Commentaire entretien</label>
  476.                     {{ form_widget(form.entcom, {'attr': {'class': input_class, 'rows': 3, 'placeholder': "Observations sur l'entretien..."}}) }}
  477.                 </div>
  478.             {% endif %}
  479.             <!-- SECTION 4bis: GRILLE D'ÉVALUATION DYNAMIQUE -->
  480.             {% if (is_granted('ROLE_ENT') or is_granted('ROLE_ADMIN') or is_granted('ROLE_JURY')) and candidature is defined and candidature.id %}
  481.                 <div id="evaluation-section">
  482.                     <h3 class="text-lg font-semibold text-indigo-800 mb-5 flex items-center border-b pb-3">
  483.                         <div class="w-8 h-8 bg-indigo-200 rounded-lg flex items-center justify-center mr-3">
  484.                             <i class="fas fa-clipboard-check text-indigo-700"></i>
  485.                         </div>
  486.                         Évaluation de l'entretien
  487.                     </h3>
  488.                     {% if is_granted('ROLE_ENT') %}
  489.                         <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
  490.                             <div>
  491.                                 <label class="block text-sm font-medium text-slate-700 mb-1">Date entretien</label>
  492.                                 {{ form_widget(form.entdate, {'attr': {'class': input_class}}) }}
  493.                             </div>
  494.                             <div>
  495.                                 <label class="block text-sm font-medium text-slate-700 mb-1">Lieu entretien</label>
  496.                                 {{ form_widget(form.entlieu, {'attr': {'class': input_class, 'placeholder': 'Ex: Salle 101'}}) }}
  497.                             </div>
  498.                         </div>
  499.                         <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
  500.                             <div>
  501.                                 <label class="block text-sm font-medium text-slate-700 mb-1">Numéro du jury</label>
  502.                                 {{ form_widget(form.jury, {'attr': {'class': input_class, 'min': 1, 'max': 20}}) }}
  503.                             </div>
  504.                             <div>
  505.                                 <label class="block text-sm font-medium text-slate-700 mb-1">Vague</label>
  506.                                 {{ form_widget(form.vague, {'attr': {'class': input_class, 'min': 1}}) }}
  507.                             </div>
  508.                             <div>
  509.                                 <label class="block text-sm font-medium text-slate-700 mb-1">Décision du jury</label>
  510.                                 {{ form_widget(form.entstatut, {'attr': {'class': input_class}}) }}
  511.                             </div>
  512.                         </div>
  513.                         <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4">
  514.                             <div>
  515.                                 <label class="block text-sm font-medium text-slate-700 mb-1">Note 1</label>
  516.                                 {{ form_widget(form.note1, {'attr': {'class': input_class, 'min': 0, 'max': 20, 'step': 0.5}}) }}
  517.                             </div>
  518.                             <div>
  519.                                 <label class="block text-sm font-medium text-slate-700 mb-1">Note 2</label>
  520.                                 {{ form_widget(form.note2, {'attr': {'class': input_class, 'min': 0, 'max': 20, 'step': 0.5}}) }}
  521.                             </div>
  522.                             <div>
  523.                                 <label class="block text-sm font-medium text-slate-700 mb-1">Note 3</label>
  524.                                 {{ form_widget(form.note3, {'attr': {'class': input_class, 'min': 0, 'max': 20, 'step': 0.5}}) }}
  525.                             </div>
  526.                             <div>
  527.                                 <label class="block text-sm font-medium text-slate-700 mb-1">Note 4</label>
  528.                                 {{ form_widget(form.note4, {'attr': {'class': input_class, 'min': 0, 'max': 20, 'step': 0.5}}) }}
  529.                             </div>
  530.                             <div>
  531.                                 <label class="block text-sm font-medium text-slate-700 mb-1">Note 5</label>
  532.                                 {{ form_widget(form.note5, {'attr': {'class': input_class, 'min': 0, 'max': 20, 'step': 0.5}}) }}
  533.                             </div>
  534.                             <div>
  535.                                 <label class="block text-sm font-medium text-slate-700 mb-1">Note 6</label>
  536.                                 {{ form_widget(form.note6, {'attr': {'class': input_class, 'min': 0, 'max': 20, 'step': 0.5}}) }}
  537.                             </div>
  538.                             <div>
  539.                                 <label class="block text-sm font-medium text-slate-700 mb-1">Note 7</label>
  540.                                 {{ form_widget(form.note7, {'attr': {'class': input_class, 'min': 0, 'max': 20, 'step': 0.5}}) }}
  541.                             </div>
  542.                             <div>
  543.                                 <label class="block text-sm font-medium text-slate-700 mb-1">Note 8</label>
  544.                                 {{ form_widget(form.note8, {'attr': {'class': input_class, 'min': 0, 'max': 20, 'step': 0.5}}) }}
  545.                             </div>
  546.                             <div>
  547.                                 <label class="block text-sm font-medium text-slate-700 mb-1">Note 9</label>
  548.                                 {{ form_widget(form.note9, {'attr': {'class': input_class, 'min': 0, 'max': 20, 'step': 0.5}}) }}
  549.                             </div>
  550.                         </div>
  551.                         <div class="mt-4 mb-6">
  552.                             <label class="block text-sm font-medium text-slate-700 mb-1">Commentaire entretien</label>
  553.                             {{ form_widget(form.entcom, {'attr': {'class': input_class, 'rows': 3, 'placeholder': "Observations sur l'entretien..."}}) }}
  554.                         </div>
  555.                     {% endif %}
  556.                     <!-- Évaluations existantes -->
  557.                     {% if evaluations is defined and evaluations|length > 0 %}
  558.                     <div class="mb-6 space-y-3" id="existing-evaluations">
  559.                         <p class="text-sm font-medium text-slate-600 flex items-center mb-2">
  560.                             <i class="fas fa-history text-indigo-500 mr-2"></i>
  561.                             Évaluations précédentes
  562.                         </p>
  563.                         {% for eval in evaluations %}
  564.                         <div class="bg-slate-50 rounded-xl p-4 border border-slate-200">
  565.                             <div class="flex items-center justify-between mb-2">
  566.                                 <div class="flex items-center gap-3">
  567.                                     <span class="font-medium text-slate-800">{{ eval.grille.nom }}</span>
  568.                                     <span class="text-xs text-slate-500">{{ eval.createdAt|date('d/m/Y H:i') }}</span>
  569.                                 </div>
  570.                                 <div class="flex items-center gap-2">
  571.                                     {% set percentage = eval.noteMaxTotale > 0 ? (eval.scoreTotal / eval.noteMaxTotale * 100) : 0 %}
  572.                                     <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-bold
  573.                                         {% if percentage >= 70 %}bg-green-100 text-green-700
  574.                                         {% elseif percentage >= 40 %}bg-yellow-100 text-yellow-700
  575.                                         {% else %}bg-red-100 text-red-700{% endif %}">
  576.                                         {{ eval.scoreTotal }} / {{ eval.noteMaxTotale }}
  577.                                     </span>
  578.                                 </div>
  579.                             </div>
  580.                             <!-- Progress bar -->
  581.                             <div class="w-full bg-slate-200 rounded-full h-2.5 mb-2">
  582.                                 <div class="h-2.5 rounded-full transition-all duration-500
  583.                                     {% if percentage >= 70 %}bg-green-500
  584.                                     {% elseif percentage >= 40 %}bg-yellow-500
  585.                                     {% else %}bg-red-500{% endif %}" 
  586.                                     style="width: {{ percentage }}%"></div>
  587.                             </div>
  588.                             {% if eval.commentaire %}
  589.                             <p class="text-xs text-slate-500 mt-1"><i class="fas fa-comment mr-1"></i> {{ eval.commentaire }}</p>
  590.                             {% endif %}
  591.                         </div>
  592.                         {% endfor %}
  593.                     </div>
  594.                     {% endif %}
  595.                     {#
  596.                     <!-- Sélection de la grille -->
  597.                     <div class="bg-white rounded-xl border border-slate-200 p-5 mb-4">
  598.                         <label class="block text-sm font-medium text-slate-700 mb-2">
  599.                             <i class="fas fa-th-list text-indigo-500 mr-1"></i>
  600.                             Sélectionner une grille d'évaluation
  601.                         </label>
  602.                         <select id="grille-select" class="{{ input_class }}">
  603.                             <option value="">-- Choisir une grille --</option>
  604.                             {% if grilles is defined %}
  605.                                 {% for grille in grilles %}
  606.                                     <option value="{{ grille.id }}" data-note-max="{{ grille.noteMaxTotale }}" data-nb-criteres="{{ grille.criteres|length }}">
  607.                                         {{ grille.nom }} ({{ grille.criteres|length }} critères — max {{ grille.noteMaxTotale }})
  608.                                     </option>
  609.                                 {% endfor %}
  610.                             {% endif %}
  611.                         </select>
  612.                     </div>
  613.                     #}
  614.                     <!-- Zone dynamique des critères -->
  615.                     <div id="criteres-container" class="hidden">
  616.                         <div class="bg-white rounded-xl border border-slate-200 overflow-hidden mb-4">
  617.                             <div class="px-5 py-3 bg-slate-50 border-b border-slate-200 flex items-center justify-between">
  618.                                 <span class="text-sm font-semibold text-slate-700">
  619.                                     <i class="fas fa-star text-yellow-500 mr-1"></i>
  620.                                     Critères d'évaluation
  621.                                 </span>
  622.                                 <span id="grille-name" class="text-xs text-indigo-600 font-medium"></span>
  623.                             </div>
  624.                             <div class="overflow-x-auto">
  625.                                 <table class="w-full">
  626.                                     <thead class="bg-slate-50/50">
  627.                                         <tr>
  628.                                             <th class="px-5 py-3 text-left text-xs font-semibold text-slate-600 uppercase tracking-wider">Critère</th>
  629.                                             <th class="px-5 py-3 text-center text-xs font-semibold text-slate-600 uppercase tracking-wider w-28">Note max</th>
  630.                                             <th class="px-5 py-3 text-center text-xs font-semibold text-slate-600 uppercase tracking-wider w-36">Note attribuée</th>
  631.                                         </tr>
  632.                                     </thead>
  633.                                     <tbody id="criteres-body" class="divide-y divide-slate-100">
  634.                                         <!-- Rempli dynamiquement par JS -->
  635.                                     </tbody>
  636.                                 </table>
  637.                             </div>
  638.                         </div>
  639.                         <!-- Score total + progress bar -->
  640.                         <div class="bg-white rounded-xl border border-slate-200 p-5 mb-4">
  641.                             <div class="flex items-center justify-between mb-3">
  642.                                 <span class="text-sm font-semibold text-slate-700">
  643.                                     <i class="fas fa-calculator text-indigo-500 mr-1"></i>
  644.                                     Score total
  645.                                 </span>
  646.                                 <span id="score-display" class="text-lg font-bold text-slate-800">0 / 0</span>
  647.                             </div>
  648.                             <div class="w-full bg-slate-200 rounded-full h-3">
  649.                                 <div id="score-bar" class="h-3 rounded-full transition-all duration-300 bg-slate-400" style="width: 0%"></div>
  650.                             </div>
  651.                             <p id="score-label" class="text-xs text-slate-500 mt-2 text-center"></p>
  652.                         </div>
  653.                         <!-- Commentaire -->
  654.                         <div class="bg-white rounded-xl border border-slate-200 p-5 mb-4">
  655.                             <label class="block text-sm font-medium text-slate-700 mb-2">
  656.                                 <i class="fas fa-comment-alt text-indigo-500 mr-1"></i>
  657.                                 Observation du jury
  658.                             </label>
  659.                             <textarea id="evaluation-commentaire" rows="3" 
  660.                                 class="{{ input_class }}" 
  661.                                 placeholder="Saisissez les observations du jury..."></textarea>
  662.                         </div>
  663.                         <!-- Bouton enregistrer -->
  664.                         <div class="flex justify-end">
  665.                             <button type="button" id="save-evaluation-btn" 
  666.                                 class="px-6 py-2.5 bg-gradient-to-r from-indigo-600 to-purple-600 text-white font-semibold rounded-lg hover:from-indigo-700 hover:to-purple-700 transition transform hover:scale-105 shadow-lg inline-flex items-center">
  667.                                 <i class="fas fa-save mr-2"></i>
  668.                                 Enregistrer l'évaluation
  669.                             </button>
  670.                         </div>
  671.                         <!-- Message de succès -->
  672.                         <div id="evaluation-success" class="hidden mt-4 p-4 rounded-xl bg-green-50 text-green-800 border border-green-200">
  673.                             <div class="flex items-center">
  674.                                 <i class="fas fa-check-circle text-green-500 mr-3 text-lg"></i>
  675.                                 <span id="evaluation-success-msg">Évaluation enregistrée avec succès.</span>
  676.                             </div>
  677.                         </div>
  678.                         <!-- Message d'erreur -->
  679.                         <div id="evaluation-error" class="hidden mt-4 p-4 rounded-xl bg-red-50 text-red-800 border border-red-200">
  680.                             <div class="flex items-center">
  681.                                 <i class="fas fa-exclamation-circle text-red-500 mr-3 text-lg"></i>
  682.                                 <span id="evaluation-error-msg">Erreur lors de l'enregistrement.</span>
  683.                             </div>
  684.                         </div>
  685.                     </div>
  686.                 </div>
  687.             {% endif %}
  688.             <!-- SECTION 5: Visite médicale et résultat final -->
  689.             {% if is_granted('ROLE_ENT') or is_granted('ROLE_JURY') or is_granted('ROLE_ADMIN') %}
  690.                 <div class="bg-emerald-50 rounded-2xl border border-emerald-200 p-6">
  691.                     <h3 class="text-lg font-semibold text-emerald-800 mb-5 flex items-center border-b border-emerald-200 pb-3">
  692.                         <div class="w-8 h-8 bg-emerald-200 rounded-lg flex items-center justify-center mr-3">
  693.                             <i class="fas fa-hospital text-emerald-700"></i>
  694.                         </div>
  695.                         VISITE MÉDICALE & Résultat final
  696.                     </h3>
  697.                     
  698.                     <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
  699.                         <div>
  700.                             <label class="block text-sm font-medium text-slate-700 mb-1">Date visite</label>
  701.                             {{ form_widget(form.visdate, {'attr': {'class': input_class}}) }}
  702.                         </div>
  703.                         <div>
  704.                             <label class="block text-sm font-medium text-slate-700 mb-1">Lieu visite</label>
  705.                             {{ form_widget(form.vislieu, {'attr': {'class': input_class, 'placeholder': 'Ex: Centre médical'}}) }}
  706.                         </div>
  707.                     </div>
  708.                     <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
  709.                         <div>
  710.                             <label class="block text-sm font-medium text-slate-700 mb-1">Statut visite</label>
  711.                             {{ form_widget(form.visstatut, {'attr': {'class': input_class}}) }}
  712.                         </div>
  713.                         <div>
  714.                             <label class="block text-sm font-medium text-slate-700 mb-1">Résultat final</label>
  715.                             {{ form_widget(form.resultat, {'attr': {'class': input_class}}) }}
  716.                         </div>
  717.                         <div>
  718.                             <label class="block text-sm font-medium text-slate-700 mb-1">Commentaire</label>
  719.                             {{ form_widget(form.viscom, {'attr': {'class': input_class, 'placeholder': 'Commentaire visite...'}}) }}
  720.                         </div>
  721.                     </div>
  722.                 </div>
  723.             {% endif %}
  724.             <!-- Boutons d'action -->
  725.             <div class="flex flex-col md:flex-row justify-between items-center gap-4 pt-6 border-t border-slate-200">
  726.                 <div class="text-sm text-slate-500">
  727.                     <i class="fas fa-info-circle mr-1"></i>
  728.                     Les champs marqués d'une <span class="text-red-500">*</span> sont obligatoires
  729.                 </div>
  730.                 
  731.                 <div class="flex gap-3">
  732.                     {% if app.user %}
  733.                         {# Vérification de l'âge pour afficher/masquer le bouton de soumission #}
  734.                         {% set age = app.user.datenaissance ? date().diff(date(app.user.datenaissance)).y : null %}
  735.                         
  736.                         {% if is_granted('ROLE_ADMIN') or is_granted('ROLE_ENT') or is_granted('ROLE_JURY') %}
  737.                             {# Les rôles admin/ent/jury peuvent toujours soumettre #}
  738.                             <button type="submit" class="px-8 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 text-white font-semibold rounded-lg hover:from-indigo-700 hover:to-purple-700 transition transform hover:scale-105 shadow-lg flex items-center">
  739.                                 <i class="fas fa-paper-plane mr-2"></i>
  740.                                 Enregistrer l'évaluation
  741.                             </button>
  742.                         {% elseif age >= 16 and age <= 40 %}
  743.                             {# Candidat avec âge valide #}
  744.                             <button type="submit" class="px-8 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 text-white font-semibold rounded-lg hover:from-indigo-700 hover:to-purple-700 transition transform hover:scale-105 shadow-lg flex items-center">
  745.                                 <i class="fas fa-paper-plane mr-2"></i>
  746.                                 Soumettre ma candidature
  747.                             </button>
  748.                         {% else %}
  749.                             {# Candidat avec âge invalide - bouton désactivé #}
  750.                             <button type="button" disabled class="px-8 py-2 bg-gray-400 text-white font-semibold rounded-lg cursor-not-allowed opacity-50 flex items-center" title="Vous ne remplissez pas les conditions d'âge">
  751.                                 <i class="fas fa-ban mr-2"></i>
  752.                                 Soumission impossible
  753.                             </button>
  754.                         {% endif %}
  755.                     {% else %}
  756.                         {% set redirectUrl = path('app_candidature_new', {
  757.                             'etablissement': app.request.get('etablissement'),
  758.                             'metier': app.request.get('metier')
  759.                         }) %}
  760.                         
  761.                         <a href="{{ path('app_login', {'redirect': redirectUrl}) }}" 
  762.                            class="px-8 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 text-white font-semibold rounded-lg hover:from-indigo-700 hover:to-purple-700 transition transform hover:scale-105 shadow-lg flex items-center">
  763.                             <i class="fas fa-sign-in-alt mr-2"></i>
  764.                             Se connecter pour postuler
  765.                         </a>
  766.                     {% endif %}
  767.                 </div>
  768.             </div>
  769.             {{ form_end(form) }}
  770.         </div>
  771.     </div>
  772.     {% endif %}
  773. </div>
  774. <!-- JavaScript pour la gestion dynamique établissement/métier -->
  775. <script>
  776. document.addEventListener('DOMContentLoaded', function() {
  777.     const etablissementSelect = document.getElementById('candidature_etablissement');
  778.     const metierSelect = document.getElementById('candidature_metier');
  779.     const metierInfo = document.getElementById('metier-info');
  780.     const metierSecteur = document.getElementById('metier-secteur');
  781.     const metierNiveau = document.getElementById('metier-niveau');
  782.     const metierNbrPlace = document.getElementById('metier-nbrplace');
  783.     const metierDuree = document.getElementById('metier-duree');
  784.     const etablissementLoading = document.getElementById('etablissement-loading');
  785.     const metierLoading = document.getElementById('metier-loading');
  786.     
  787.     const currentMetierValue = metierSelect.value;
  788.     function loadMetiers(etablissementId, callback) {
  789.         if (!etablissementId) {
  790.             metierSelect.innerHTML = '<option value="">-- Choisissez d\'abord un établissement --</option>';
  791.             metierSelect.disabled = true;
  792.             metierInfo.classList.add('hidden');
  793.             if (callback) callback();
  794.             return;
  795.         }
  796.         metierLoading.classList.remove('hidden');
  797.         metierSelect.disabled = true;
  798.         fetch('/candidature/etablissement/' + etablissementId + '/metiers')
  799.             .then(response => response.json())
  800.             .then(data => {
  801.                 metierSelect.innerHTML = '<option value="">-- Sélectionnez un métier --</option>';
  802.                 
  803.                 const grouped = {};
  804.                 data.forEach(metier => {
  805.                     const secteur = metier.secteur_nom || 'Autres';
  806.                     if (!grouped[secteur]) {
  807.                         grouped[secteur] = [];
  808.                     }
  809.                     grouped[secteur].push(metier);
  810.                 });
  811.                 
  812.                 for (const [secteur, metiers] of Object.entries(grouped)) {
  813.                     const optgroup = document.createElement('optgroup');
  814.                     optgroup.label = secteur;
  815.                     
  816.                     metiers.sort((a, b) => a.nom.localeCompare(b.nom));
  817.                     metiers.forEach(metier => {
  818.                         const option = document.createElement('option');
  819.                         option.value = metier.id;
  820.                         option.dataset.secteur = metier.secteur_nom || '';
  821.                         option.dataset.niveau = metier.niveau || '';
  822.                         option.dataset.duree = metier.duree || '';
  823.                         option.dataset.nbrplace = metier.nbrplace || '';
  824.                         let label = metier.nom;
  825.                         if (metier.nbrplace) {
  826.                             label += ' (' + metier.nbrplace + ' places)';
  827.                         }
  828.                         option.textContent = label;
  829.                         optgroup.appendChild(option);
  830.                     });
  831.                     
  832.                     metierSelect.appendChild(optgroup);
  833.                 }
  834.                 
  835.                 const metierToSelect = currentMetierValue || '{{ app.request.get('metier')|default('') }}';
  836.                 
  837.                 if (metierToSelect) {
  838.                     const options = metierSelect.options;
  839.                     for (let i = 0; i < options.length; i++) {
  840.                         if (options[i].value == metierToSelect) {
  841.                             options[i].selected = true;
  842.                             updateMetierInfo(options[i]);
  843.                             break;
  844.                         }
  845.                     }
  846.                 }
  847.                 
  848.                 metierSelect.disabled = false;
  849.                 metierLoading.classList.add('hidden');
  850.                 
  851.                 if (callback) callback();
  852.             })
  853.             .catch(error => {
  854.                 console.error('Erreur:', error);
  855.                 metierSelect.disabled = false;
  856.                 metierLoading.classList.add('hidden');
  857.                 if (callback) callback();
  858.             });
  859.     }
  860.     function updateMetierInfo(selectedOption) {
  861.         if (!selectedOption || !selectedOption.value) {
  862.             metierInfo.classList.add('hidden');
  863.             return;
  864.         }
  865.         
  866.         metierSecteur.textContent = selectedOption.dataset.secteur || 'Non spécifié';
  867.         metierNiveau.textContent = selectedOption.dataset.niveau || 'Non spécifié';
  868.         metierDuree.textContent = selectedOption.dataset.duree || 'Non spécifié';
  869.         metierNbrPlace.textContent = selectedOption.dataset.nbrplace || 'Non spécifié';
  870.         metierInfo.classList.remove('hidden');
  871.     }
  872.     {% if app.request.get('etablissement') %}
  873.         const etablissementId = '{{ app.request.get('etablissement') }}';
  874.         if (etablissementId) {
  875.             setTimeout(function() {
  876.                 const options = etablissementSelect.options;
  877.                 for (let i = 0; i < options.length; i++) {
  878.                     if (options[i].value == etablissementId) {
  879.                         options[i].selected = true;
  880.                         break;
  881.                     }
  882.                 }
  883.                 loadMetiers(etablissementId);
  884.             }, 100);
  885.         }
  886.     {% else %}
  887.         if (etablissementSelect.value) {
  888.             loadMetiers(etablissementSelect.value);
  889.         } else {
  890.             metierSelect.innerHTML = '<option value="">-- Choisissez d\'abord un établissement --</option>';
  891.             metierSelect.disabled = true;
  892.         }
  893.     {% endif %}
  894.     etablissementSelect.addEventListener('change', function() {
  895.         loadMetiers(this.value);
  896.         metierInfo.classList.add('hidden');
  897.     });
  898.     metierSelect.addEventListener('change', function() {
  899.         const selectedOption = this.options[this.selectedIndex];
  900.         updateMetierInfo(selectedOption);
  901.     });
  902.     const form = document.getElementById('candidature-form');
  903.     form.addEventListener('submit', function(e) {
  904.         {% if not app.user %}
  905.         e.preventDefault();
  906.         alert('Vous devez être connecté pour soumettre une candidature.');
  907.         const redirectUrl = '/candidature/new?etablissement=' + etablissementSelect.value + '&metier=' + metierSelect.value;
  908.         window.location.href = '/login?redirect=' + encodeURIComponent(redirectUrl);
  909.         return false;
  910.         {% endif %}
  911.         {# Vérification supplémentaire en JavaScript #}
  912.         {% if app.user and app.user.datenaissance %}
  913.             {% set age = date().diff(date(app.user.datenaissance)).y %}
  914.             {% if age < 16 or age > 40 %}
  915.                 e.preventDefault();
  916.                 alert('Vous ne remplissez pas les conditions d\'âge pour candidater (16 à 40 ans).');
  917.                 return false;
  918.             {% endif %}
  919.         {% endif %}
  920.         if (!etablissementSelect.value) {
  921.             e.preventDefault();
  922.             alert('Veuillez sélectionner un établissement.');
  923.             return false;
  924.         }
  925.         
  926.         if (!metierSelect.value) {
  927.             e.preventDefault();
  928.             alert('Veuillez sélectionner un métier.');
  929.             return false;
  930.         }
  931.     });
  932. });
  933. </script>
  934. <!-- JavaScript pour la grille d'évaluation dynamique -->
  935. {% if (is_granted('ROLE_ENT') or is_granted('ROLE_ADMIN') or is_granted('ROLE_JURY')) and candidature is defined and candidature.id %}
  936. <script>
  937. document.addEventListener('DOMContentLoaded', function() {
  938.     const grilleSelect = document.getElementById('grille-select');
  939.     const criteresContainer = document.getElementById('criteres-container');
  940.     const criteresBody = document.getElementById('criteres-body');
  941.     const grilleName = document.getElementById('grille-name');
  942.     const scoreDisplay = document.getElementById('score-display');
  943.     const scoreBar = document.getElementById('score-bar');
  944.     const scoreLabel = document.getElementById('score-label');
  945.     const saveBtn = document.getElementById('save-evaluation-btn');
  946.     const commentaireField = document.getElementById('evaluation-commentaire');
  947.     const successDiv = document.getElementById('evaluation-success');
  948.     const errorDiv = document.getElementById('evaluation-error');
  949.     const successMsg = document.getElementById('evaluation-success-msg');
  950.     const errorMsg = document.getElementById('evaluation-error-msg');
  951.     if (!grilleSelect) return;
  952.     const candidatureId = {{ candidature.id }};
  953.     let currentGrilleId = null;
  954.     let noteMaxTotale = 0;
  955.     // Charger les critères quand on sélectionne une grille
  956.     grilleSelect.addEventListener('change', function() {
  957.         const grilleId = this.value;
  958.         if (!grilleId) {
  959.             criteresContainer.classList.add('hidden');
  960.             currentGrilleId = null;
  961.             return;
  962.         }
  963.         currentGrilleId = grilleId;
  964.         loadCriteres(grilleId);
  965.     });
  966.     function loadCriteres(grilleId) {
  967.         fetch('/evaluation/grille/' + grilleId + '/criteres')
  968.             .then(r => r.json())
  969.             .then(data => {
  970.                 criteresBody.innerHTML = '';
  971.                 noteMaxTotale = data.grille.noteMaxTotale;
  972.                 grilleName.textContent = data.grille.nom;
  973.                 data.criteres.forEach(critere => {
  974.                     const tr = document.createElement('tr');
  975.                     tr.className = 'hover:bg-slate-50/50 transition';
  976.                     tr.innerHTML = `
  977.                         <td class="px-5 py-3">
  978.                             <div>
  979.                                 <span class="font-medium text-slate-800 text-sm">${critere.nom}</span>
  980.                                 ${critere.description ? '<p class="text-xs text-slate-500 mt-0.5">' + critere.description + '</p>' : ''}
  981.                             </div>
  982.                         </td>
  983.                         <td class="px-5 py-3 text-center">
  984.                             <span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold bg-purple-100 text-purple-800">${critere.noteMax}</span>
  985.                         </td>
  986.                         <td class="px-5 py-3 text-center">
  987.                             <input type="number" 
  988.                                 class="w-24 mx-auto px-3 py-2 rounded-lg border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition text-center text-sm font-medium note-input"
  989.                                 data-critere-id="${critere.id}" 
  990.                                 data-note-max="${critere.noteMax}"
  991.                                 min="0" 
  992.                                 max="${critere.noteMax}" 
  993.                                 step="0.5" 
  994.                                 placeholder="..."
  995.                             >
  996.                         </td>
  997.                     `;
  998.                     criteresBody.appendChild(tr);
  999.                 });
  1000.                 criteresContainer.classList.remove('hidden');
  1001.                 updateScore();
  1002.                 // Charger les notes existantes
  1003.                 loadExistingEvaluation(grilleId);
  1004.                 // Ajouter les écouteurs sur les champs de notes
  1005.                 document.querySelectorAll('.note-input').forEach(input => {
  1006.                     input.addEventListener('input', function() {
  1007.                         const max = parseFloat(this.dataset.noteMax);
  1008.                         let val = parseFloat(this.value);
  1009.                         if (val > max) { this.value = max; }
  1010.                         if (val < 0) { this.value = 0; }
  1011.                         updateScore();
  1012.                     });
  1013.                 });
  1014.             })
  1015.             .catch(err => {
  1016.                 console.error('Erreur chargement critères:', err);
  1017.             });
  1018.     }
  1019.     function loadExistingEvaluation(grilleId) {
  1020.         fetch('/evaluation/candidature/' + candidatureId + '/evaluation')
  1021.             .then(r => r.json())
  1022.             .then(evaluations => {
  1023.                 const existing = evaluations.find(e => e.grilleId == grilleId);
  1024.                 if (existing) {
  1025.                     existing.notations.forEach(n => {
  1026.                         const input = document.querySelector(`.note-input[data-critere-id="${n.critereId}"]`);
  1027.                         if (input && n.note !== null) {
  1028.                             input.value = n.note;
  1029.                         }
  1030.                     });
  1031.                     if (existing.commentaire) {
  1032.                         commentaireField.value = existing.commentaire;
  1033.                     }
  1034.                     updateScore();
  1035.                 }
  1036.             })
  1037.             .catch(err => console.error('Erreur chargement évaluation:', err));
  1038.     }
  1039.     function updateScore() {
  1040.         let total = 0;
  1041.         document.querySelectorAll('.note-input').forEach(input => {
  1042.             const val = parseFloat(input.value);
  1043.             if (!isNaN(val)) total += val;
  1044.         });
  1045.         scoreDisplay.textContent = total + ' / ' + noteMaxTotale;
  1046.         const percentage = noteMaxTotale > 0 ? (total / noteMaxTotale * 100) : 0;
  1047.         scoreBar.style.width = percentage + '%';
  1048.         // Couleur de la barre de progression
  1049.         scoreBar.classList.remove('bg-green-500', 'bg-yellow-500', 'bg-red-500', 'bg-slate-400');
  1050.         if (percentage >= 70) {
  1051.             scoreBar.classList.add('bg-green-500');
  1052.             scoreLabel.textContent = 'Excellent';
  1053.             scoreLabel.className = 'text-xs mt-2 text-center text-green-600 font-medium';
  1054.         } else if (percentage >= 40) {
  1055.             scoreBar.classList.add('bg-yellow-500');
  1056.             scoreLabel.textContent = 'Moyen';
  1057.             scoreLabel.className = 'text-xs mt-2 text-center text-yellow-600 font-medium';
  1058.         } else if (total > 0) {
  1059.             scoreBar.classList.add('bg-red-500');
  1060.             scoreLabel.textContent = 'Faible';
  1061.             scoreLabel.className = 'text-xs mt-2 text-center text-red-600 font-medium';
  1062.         } else {
  1063.             scoreBar.classList.add('bg-slate-400');
  1064.             scoreLabel.textContent = '';
  1065.         }
  1066.     }
  1067.     // Enregistrer l'évaluation
  1068.     saveBtn.addEventListener('click', function() {
  1069.         if (!currentGrilleId) return;
  1070.         successDiv.classList.add('hidden');
  1071.         errorDiv.classList.add('hidden');
  1072.         const notations = [];
  1073.         document.querySelectorAll('.note-input').forEach(input => {
  1074.             notations.push({
  1075.                 critereId: parseInt(input.dataset.critereId),
  1076.                 note: input.value !== '' ? parseFloat(input.value) : null,
  1077.             });
  1078.         });
  1079.         const payload = {
  1080.             grilleId: parseInt(currentGrilleId),
  1081.             notations: notations,
  1082.             commentaire: commentaireField.value || null,
  1083.         };
  1084.         saveBtn.disabled = true;
  1085.         saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Enregistrement...';
  1086.         fetch('/evaluation/candidature/' + candidatureId + '/evaluer', {
  1087.             method: 'POST',
  1088.             headers: { 'Content-Type': 'application/json' },
  1089.             body: JSON.stringify(payload),
  1090.         })
  1091.         .then(r => r.json())
  1092.         .then(data => {
  1093.             if (data.success) {
  1094.                 successMsg.textContent = data.message;
  1095.                 successDiv.classList.remove('hidden');
  1096.                 errorDiv.classList.add('hidden');
  1097.                 // Mettre à jour l'affichage du score
  1098.                 scoreDisplay.textContent = data.scoreTotal + ' / ' + data.noteMaxTotale;
  1099.             } else {
  1100.                 errorMsg.textContent = data.error || 'Erreur inconnue.';
  1101.                 errorDiv.classList.remove('hidden');
  1102.                 successDiv.classList.add('hidden');
  1103.             }
  1104.         })
  1105.         .catch(err => {
  1106.             console.error('Erreur:', err);
  1107.             errorMsg.textContent = 'Erreur réseau lors de l\'enregistrement.';
  1108.             errorDiv.classList.remove('hidden');
  1109.             successDiv.classList.add('hidden');
  1110.         })
  1111.         .finally(() => {
  1112.             saveBtn.disabled = false;
  1113.             saveBtn.innerHTML = '<i class="fas fa-save mr-2"></i> Enregistrer l\'évaluation';
  1114.         });
  1115.     });
  1116. });
  1117. </script>
  1118. {% endif %}
  1119. {% endblock %}