<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>
{% block title %}
Administration
{% endblock %}
</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<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>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<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">
<link rel="stylesheet" href="{{ asset('build/assets/fontawesome/css/all.min.css') }}">
{% block stylesheets %}
{{ encore_entry_link_tags('app') }}
{% endblock %}
</head>
<body>
<!-- === DÉBUT DE LA SECTION POUR LES TOASTS === -->
<!-- Conteneur pour positionner les toasts en haut à droite de l'écran -->
<div class="toast-container position-fixed top-0 end-0 p-3">
{% for type, messages in app.flashes %}
<!-- Boucle sur les messages de chaque type -->
{% for message in messages %}
{% set bg_class = 'bg-primary' %}
<!-- Couleur par défaut pour 'info' ou autre -->
{% if type == 'success' %}
{% set bg_class = 'bg-success' %}
{% endif %}
{% if type == 'error' or type == 'danger' %}
{% set bg_class = 'bg-danger' %}
{% endif %}
{% if type == 'warning' %}
{% set bg_class = 'bg-warning' %}
{% endif %}
<!-- Génération du HTML pour un toast -->
<div class="toast align-items-center text-white {{ bg_class }} border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
{{ message|raw }}
<!-- On utilise 'raw' si vos messages peuvent contenir du HTML simple comme <strong> -->
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
{% endfor %}
{% endfor %}
</div>
<!-- === FIN DE LA SECTION POUR LES TOASTS === -->
<!-- === TOAST SPÉCIFIQUE POUR LES EXPORTS === -->
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1100;">
<div id="exportToast" class="toast align-items-center border-0" role="alert" style="display: none;">
<div class="d-flex">
<div class="toast-body">
<div class="d-flex align-items-center">
<div id="exportToastIcon" class="me-2"></div>
<div>
<strong id="exportToastTitle"></strong>
<br>
<small id="exportToastMessage"></small>
</div>
</div>
</div>
<button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
</div>
</div>
{% set actual_client = client_context.getCurrentClient() %}
{% if mobile_detect.isMobile() %}
{% include 'common/mobile-navi.html.twig' with { 'current_route': app.request.attributes.get('_route') } %}
{% else %}
<div class="d-flex min-vh-100">
<div class="col-md-2 p-2 bg-white px-0" id="left-panel-container">
{% include 'common/left-panel.html.twig' %}
</div>
<div class="col-md-10 bg-light">
{% if app.user.identityUid %}
<div class="position-absolute top-0 end-0 mt-4 me-5 ">
<a href="{{ path('user.account') }}" class="text-decoration-none text-black">
<div class="d-flex align-items-center rounded-pill bg-white border">
<div class="flex-shrink-0 ps-2 py-1">
<img src="{{ app.user.photo }}" class="border rounded-circle" style="width: 50px; height: 50px;">
</div>
<div class="flex-grow-1 ms-3 pe-4">
{% if app.user.firstname is not empty %}
<span class="ubutun-bold d-block">
{{ app.user.firstname ~ ' ' ~ app.user.lastname|slice(0, 1)|upper }}
.
</span>
{% endif %}
<span class="text-decoration-underline fs-12">
Mon compte
</span>
</div>
</div>
</a>
</div>
{% endif %}
{% endif %}
{% block body %}{% endblock %}
</div>
</div>
{% if app.user %}
{% include 'components/_modal_support.html.twig' %}
{% endif %}
{% if is_granted('ROLE_ADMIN') or (app.user and app.user.client and app.user.client.id == 1) %}
{% include 'feedback/_widget.html.twig' %}
{% include 'feedback/_modal.html.twig' %}
{% endif %}
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
{% if is_granted('ROLE_ADMIN') or (app.user and app.user.client and app.user.client.id == 1) %}
{{ encore_entry_script_tags('feedback') }}
{% endif %}
{% if app.request.attributes.get('_route') == 'job.index' %}
{{ encore_entry_script_tags('jobForm') }}
{% endif %}
{% if app.request.attributes.get('_route') == 'suggestion.index' %}
<script>
const USER_HAS_SEEN_PREVIEW_MODAL_INITIALLY = {{ userHasSeenPreviewModal ? 'true' : 'false' }};
</script>
{% if mobile_detect.isMobile() %}
{{ encore_entry_script_tags('suggestionIndexMobile') }}
{{ encore_entry_script_tags('suggestionTourMobile') }}
{% else %}
{{ encore_entry_script_tags('suggestionIndex') }}
{{ encore_entry_script_tags('suggestionTour') }}
{% endif %}
{% endif %}
<!-- JavaScript global pour les exports asynchrones -->
<script>
/**
* Envoie une requête d'export asynchrone et affiche un toast.
*
* @param {string} type - Type d'export (candidates, user_stats, job_stats)
* @param {string} typeName - Nom lisible du type d'export
*/
function requestExport(type, typeName) {
const button = event.target.closest('button');
const originalHTML = button.innerHTML;
// Désactiver le bouton et afficher un spinner
button.disabled = true;
button.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>' + originalHTML.replace(/<i[^>]*>.*?<\/i>/g, '');
// Envoyer la requête AJAX
fetch(`/admin/export/${type}`, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
})
.then(response => {
if (!response.ok) {
throw new Error(`Erreur ${response.status}`);
}
return response.json();
})
.then(data => {
// Afficher le toast de succès
showExportToast(
'success',
`Export "${typeName}" lancé !`,
'Vous recevrez un email avec le fichier Excel dans quelques minutes.'
);
// Réactiver le bouton
button.disabled = false;
button.innerHTML = originalHTML;
})
.catch(error => {
// Afficher le toast d'erreur
showExportToast(
'error',
'Erreur',
`Impossible de lancer l'export "${typeName}". ${error.message}`
);
// Réactiver le bouton
button.disabled = false;
button.innerHTML = originalHTML;
});
}
/**
* Affiche un toast pour les exports.
*
* @param {string} type - Type de toast (success, error)
* @param {string} title - Titre du toast
* @param {string} message - Message du toast
*/
function showExportToast(type, title, message) {
const toastEl = document.getElementById('exportToast');
const iconEl = document.getElementById('exportToastIcon');
const titleEl = document.getElementById('exportToastTitle');
const messageEl = document.getElementById('exportToastMessage');
const closeBtn = toastEl.querySelector('.btn-close');
toastEl.style.display = 'block';
// Configurer le contenu
titleEl.textContent = title;
messageEl.textContent = message;
// Configurer l'apparence selon le type
if (type === 'success') {
toastEl.className = 'toast align-items-center text-white bg-success border-0';
iconEl.innerHTML = '<div class="spinner-grow spinner-grow-sm" role="status"></div>';
closeBtn.className = 'btn-close btn-close-white me-2 m-auto';
} else {
toastEl.className = 'toast align-items-center text-white bg-danger border-0';
iconEl.innerHTML = '<i class="fas fa-exclamation-triangle"></i>';
closeBtn.className = 'btn-close btn-close-white me-2 m-auto';
}
// Afficher le toast
const toast = new bootstrap.Toast(toastEl, {
autohide: true,
delay: type === 'success' ? 5000 : 7000
});
toast.show();
}
</script>
{% endblock %}
</body>
</html>