templates/layout.html.twig line 1

Open in your IDE?
  1. <!DOCTYPE html>
  2. <html>
  3.   <head>
  4.     <meta charset="UTF-8">
  5.     <title>
  6.       {% block title %}
  7.         Administration
  8.       {% endblock %}
  9.     </title>
  10.     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  11.     <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.js" integrity="sha512-+k1pnlgt4F1H8L7t3z95o3/KO+o78INEcXTbnoJQ/F2VqDVhWoaiVml/OEHv9HsVgxUaVW+IbiZPUJQfF/YxZw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  12.     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
  13.     <link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}" />
  14.     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
  15.     <link rel="preconnect" href="https://fonts.googleapis.com">
  16.     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  17.     <link href="https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap" rel="stylesheet">
  18.     <link rel="stylesheet" href="{{ asset('build/assets/fontawesome/css/all.min.css') }}">
  19.     {% block stylesheets %}
  20.       {{ encore_entry_link_tags('app') }}
  21.     {% endblock %}
  22.   </head>
  23.   <body>
  24.     <!-- === DÉBUT DE LA SECTION POUR LES TOASTS === -->
  25.     <!-- Conteneur pour positionner les toasts en haut à droite de l'écran -->
  26.       <div class="toast-container position-fixed top-0 end-0 p-3">
  27.       {% for type, messages in app.flashes %}
  28.         <!-- Boucle sur les messages de chaque type -->
  29.         {% for message in messages %}
  30.           {% set bg_class = 'bg-primary' %}
  31.           <!-- Couleur par défaut pour 'info' ou autre -->
  32.           {% if type == 'success' %}
  33.             {% set bg_class = 'bg-success' %}
  34.           {% endif %}
  35.           {% if type == 'error' or type == 'danger' %}
  36.             {% set bg_class = 'bg-danger' %}
  37.           {% endif %}
  38.           {% if type == 'warning' %}
  39.             {% set bg_class = 'bg-warning' %}
  40.           {% endif %}
  41.           <!-- Génération du HTML pour un toast -->
  42.           <div class="toast align-items-center text-white {{ bg_class }} border-0" role="alert" aria-live="assertive" aria-atomic="true">
  43.             <div class="d-flex">
  44.               <div class="toast-body">
  45.                 {{ message|raw }}
  46.               <!-- On utilise 'raw' si vos messages peuvent contenir du HTML simple comme <strong> -->
  47.               </div>
  48.               <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
  49.             </div>
  50.           </div>
  51.         {% endfor %}
  52.       {% endfor %}
  53.     </div>
  54.     <!-- === FIN DE LA SECTION POUR LES TOASTS === -->
  55.     <!-- === TOAST SPÉCIFIQUE POUR LES EXPORTS === -->
  56.     <div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1100;">
  57.       <div id="exportToast" class="toast align-items-center border-0" role="alert" style="display: none;">
  58.         <div class="d-flex">
  59.           <div class="toast-body">
  60.             <div class="d-flex align-items-center">
  61.               <div id="exportToastIcon" class="me-2"></div>
  62.               <div>
  63.                 <strong id="exportToastTitle"></strong>
  64.                 <br>
  65.                 <small id="exportToastMessage"></small>
  66.               </div>
  67.             </div>
  68.           </div>
  69.           <button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast"></button>
  70.         </div>
  71.       </div>
  72.     </div>
  73.     {% set actual_client = client_context.getCurrentClient() %}
  74.     {% if mobile_detect.isMobile() %}
  75.       {% include 'common/mobile-navi.html.twig' with { 'current_route': app.request.attributes.get('_route') } %}
  76.     {% else %}
  77.       <div class="d-flex min-vh-100">
  78.         <div class="col-md-2 p-2 bg-white px-0" id="left-panel-container">
  79.           {% include 'common/left-panel.html.twig' %}
  80.         </div>
  81.         <div class="col-md-10 bg-light">
  82.           {% if app.user.identityUid %}
  83.             <div class="position-absolute top-0 end-0 mt-4 me-5 ">
  84.               <a href="{{ path('user.account') }}" class="text-decoration-none text-black">
  85.                 <div class="d-flex align-items-center rounded-pill bg-white border">
  86.                   <div class="flex-shrink-0 ps-2 py-1">
  87.                     <img src="{{ app.user.photo }}" class="border rounded-circle" style="width: 50px; height: 50px;">
  88.                   </div>
  89.                   <div class="flex-grow-1 ms-3 pe-4">
  90.                     {% if app.user.firstname is not empty %}
  91.                       <span class="ubutun-bold d-block">
  92.                         {{ app.user.firstname ~ ' ' ~ app.user.lastname|slice(0, 1)|upper }}
  93.                         .
  94.                       </span>
  95.                     {% endif %}
  96.                     <span class="text-decoration-underline fs-12">
  97.                       Mon compte
  98.                     </span>
  99.                   </div>
  100.                 </div>
  101.               </a>
  102.             </div>
  103.           {% endif %}
  104.         {% endif %}
  105.         {% block body %}{% endblock %}
  106.       </div>
  107.     </div>
  108.     {% if app.user %}
  109.       {% include 'components/_modal_support.html.twig' %}
  110.     {% endif %}
  111.     {% if is_granted('ROLE_ADMIN') or (app.user and app.user.client and app.user.client.id == 1) %}
  112.       {% include 'feedback/_widget.html.twig' %}
  113.       {% include 'feedback/_modal.html.twig' %}
  114.     {% endif %}
  115.     {% block javascripts %}
  116.       {{ encore_entry_script_tags('app') }}
  117.       {% if is_granted('ROLE_ADMIN') or (app.user and app.user.client and app.user.client.id == 1) %}
  118.         {{ encore_entry_script_tags('feedback') }}
  119.       {% endif %}
  120.       {% if app.request.attributes.get('_route') == 'job.index' %}
  121.         {{ encore_entry_script_tags('jobForm') }}
  122.       {% endif %}
  123.       {% if app.request.attributes.get('_route') == 'suggestion.index' %}
  124.         <script>
  125.           const USER_HAS_SEEN_PREVIEW_MODAL_INITIALLY = {{ userHasSeenPreviewModal ? 'true' : 'false' }};
  126.         </script>
  127.         {% if mobile_detect.isMobile() %}
  128.           {{ encore_entry_script_tags('suggestionIndexMobile') }}
  129.           {{ encore_entry_script_tags('suggestionTourMobile') }}
  130.         {% else %}
  131.           {{ encore_entry_script_tags('suggestionIndex') }}
  132.           {{ encore_entry_script_tags('suggestionTour') }}
  133.         {% endif %}
  134.       {% endif %}
  135.       <!-- JavaScript global pour les exports asynchrones -->
  136.       <script>
  137.         /**
  138.          * Envoie une requête d'export asynchrone et affiche un toast.
  139.          * 
  140.          * @param {string} type - Type d'export (candidates, user_stats, job_stats)
  141.          * @param {string} typeName - Nom lisible du type d'export
  142.          */
  143.         function requestExport(type, typeName) {
  144.             const button = event.target.closest('button');
  145.             const originalHTML = button.innerHTML;
  146.             
  147.             // Désactiver le bouton et afficher un spinner
  148.             button.disabled = true;
  149.             button.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>' + originalHTML.replace(/<i[^>]*>.*?<\/i>/g, '');
  150.             // Envoyer la requête AJAX
  151.             fetch(`/admin/export/${type}`, {
  152.                 method: 'GET',
  153.                 headers: {
  154.                     'X-Requested-With': 'XMLHttpRequest',
  155.                     'Accept': 'application/json'
  156.                 }
  157.             })
  158.             .then(response => {
  159.                 if (!response.ok) {
  160.                     throw new Error(`Erreur ${response.status}`);
  161.                 }
  162.                 return response.json();
  163.             })
  164.             .then(data => {
  165.                 // Afficher le toast de succès
  166.                 showExportToast(
  167.                     'success',
  168.                     `Export "${typeName}" lancé !`,
  169.                     'Vous recevrez un email avec le fichier Excel dans quelques minutes.'
  170.                 );
  171.                 
  172.                 // Réactiver le bouton
  173.                 button.disabled = false;
  174.                 button.innerHTML = originalHTML;
  175.             })
  176.             .catch(error => {
  177.                 // Afficher le toast d'erreur
  178.                 showExportToast(
  179.                     'error',
  180.                     'Erreur',
  181.                     `Impossible de lancer l'export "${typeName}". ${error.message}`
  182.                 );
  183.                 
  184.                 // Réactiver le bouton
  185.                 button.disabled = false;
  186.                 button.innerHTML = originalHTML;
  187.             });
  188.         }
  189.         /**
  190.          * Affiche un toast pour les exports.
  191.          * 
  192.          * @param {string} type - Type de toast (success, error)
  193.          * @param {string} title - Titre du toast
  194.          * @param {string} message - Message du toast
  195.          */
  196.         function showExportToast(type, title, message) {
  197.             const toastEl = document.getElementById('exportToast');
  198.             const iconEl = document.getElementById('exportToastIcon');
  199.             const titleEl = document.getElementById('exportToastTitle');
  200.             const messageEl = document.getElementById('exportToastMessage');
  201.             const closeBtn = toastEl.querySelector('.btn-close');
  202.             toastEl.style.display = 'block';
  203.       
  204.             // Configurer le contenu
  205.             titleEl.textContent = title;
  206.             messageEl.textContent = message;
  207.             
  208.             // Configurer l'apparence selon le type
  209.             if (type === 'success') {
  210.                 toastEl.className = 'toast align-items-center text-white bg-success border-0';
  211.                 iconEl.innerHTML = '<div class="spinner-grow spinner-grow-sm" role="status"></div>';
  212.                 closeBtn.className = 'btn-close btn-close-white me-2 m-auto';
  213.             } else {
  214.                 toastEl.className = 'toast align-items-center text-white bg-danger border-0';
  215.                 iconEl.innerHTML = '<i class="fas fa-exclamation-triangle"></i>';
  216.                 closeBtn.className = 'btn-close btn-close-white me-2 m-auto';
  217.             }
  218.             
  219.             // Afficher le toast
  220.             const toast = new bootstrap.Toast(toastEl, {
  221.                 autohide: true,
  222.                 delay: type === 'success' ? 5000 : 7000
  223.             });
  224.             toast.show();
  225.         }
  226.         </script>
  227.     {% endblock %}
  228.   </body>
  229. </html>