✨ feat(ansible): Ajoute des headers de sécurité et limite la taille des requêtes.
✨ feat(Security): Active l'authentification à deux facteurs (2FA). ✨ feat(Account): Ajoute une entité et un formulaire pour les administrateurs. 🐛 fix(Security): Corrige la redirection après la connexion. ✨ feat(CRM): Ajoute une page d'administration des comptes administrateurs.
This commit is contained in:
@@ -3,181 +3,110 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Tableau de Bord Administratif</title>
|
||||
<!-- Chargement du CDN de Tailwind CSS -->
|
||||
{{ vite_asset('admin.js',{}) }}
|
||||
|
||||
<style>
|
||||
/* Configuration de la police Inter (utilisée par défaut par Tailwind) */
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
/* Style pour les cartes (utilisé pour l'effet de survol) */
|
||||
.dashboard-card {
|
||||
transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
|
||||
}
|
||||
.dashboard-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Styles spécifiques pour le mode sombre pour une meilleure clarté */
|
||||
.dark .bg-gray-50 { background-color: #111827; }
|
||||
.dark .bg-white { background-color: #1f2937; }
|
||||
.dark .text-gray-800 { color: #f3f4f6; }
|
||||
.dark .text-gray-900 { color: #ffffff; }
|
||||
.dark .text-gray-500 { color: #9ca3af; }
|
||||
.dark .border-gray-200 { border-color: #374151; }
|
||||
.dark .shadow-sm { box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.5); }
|
||||
.dark .shadow-lg { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -2px rgba(0, 0, 0, 0.05); }
|
||||
</style>
|
||||
<title>{% block title %}Administration{% endblock %} - LudikEvent</title>
|
||||
{{ vite_asset('admin.js', {}) }}
|
||||
</head>
|
||||
<body class="bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 transition-colors duration-300">
|
||||
|
||||
<div class="flex h-screen">
|
||||
<!-- 1. Barre Latérale (Sidebar) -->
|
||||
<body class="bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-100 antialiased">
|
||||
|
||||
<div class="flex h-screen overflow-hidden">
|
||||
|
||||
{# Overlay pour mobile #}
|
||||
<div id="sidebar-overlay" class="fixed inset-0 z-20 bg-black/50 lg:hidden hidden"></div>
|
||||
|
||||
{# SIDEBAR #}
|
||||
<aside id="sidebar" class="fixed inset-y-0 left-0 z-30 w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 transform -translate-x-full lg:translate-x-0 transition-transform duration-300 ease-in-out">
|
||||
<div class="flex items-center justify-center h-16 border-b border-gray-200 dark:border-gray-700">
|
||||
<span class="text-2xl font-bold text-primary-500">Tableau de Bord</span>
|
||||
<div class="flex items-center justify-between px-6 h-16 border-b border-gray-200 dark:border-gray-700">
|
||||
<span class="text-xl font-bold bg-gradient-to-r from-blue-600 to-indigo-500 bg-clip-text text-transparent">LudikEvent CRM</span>
|
||||
</div>
|
||||
|
||||
<!-- Liens de Navigation -->
|
||||
<nav class="flex flex-col p-4 space-y-2">
|
||||
<a href="{{ path('app_crm') }}" class="flex items-center space-x-3 p-3 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition duration-150 ease-in-out">
|
||||
<!-- Icône SVG pour Clients (Users/People) -->
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M7.5 14a6.5 6.5 0 100-13 6.5 6.5 0 000 13z"></path></svg>
|
||||
<span>Tableau de bord</span>
|
||||
</a>
|
||||
<nav class="flex flex-col p-4 space-y-1 h-[calc(100vh-64px)] overflow-y-auto">
|
||||
{% macro nav_link(path, label, icon_svg, current_route) %}
|
||||
{% set isActive = app.request.get('_route') == current_route %}
|
||||
<a href="{{ path }}" class="flex items-center space-x-3 p-3 rounded-lg transition-all duration-200 {{ isActive ? 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400' : 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700' }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">{{ icon_svg|raw }}</svg>
|
||||
<span class="font-medium text-sm">{{ label }}</span>
|
||||
</a>
|
||||
{% endmacro %}
|
||||
|
||||
<!-- Lien: Clients -->
|
||||
<a href="#" class="flex items-center space-x-3 p-3 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition duration-150 ease-in-out">
|
||||
<!-- Icône SVG pour Clients (Users/People) -->
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M7.5 14a6.5 6.5 0 100-13 6.5 6.5 0 000 13z"></path></svg>
|
||||
<span>Clients</span>
|
||||
</a>
|
||||
{% import _self as menu %}
|
||||
|
||||
<!-- Lien: Contrats de location -->
|
||||
<a href="#" class="flex items-center space-x-3 p-3 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition duration-150 ease-in-out">
|
||||
<!-- Icône SVG pour Contrats (Documents/Paper) -->
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
|
||||
<span>Contrats de location</span>
|
||||
</a>
|
||||
{{ menu.nav_link(path('app_crm'), 'Tableau de bord', '<path d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path>', 'app_crm') }}
|
||||
{{ menu.nav_link('#', 'Clients', '<path d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>', 'app_clients') }}
|
||||
{{ menu.nav_link('#', 'Articles', '<path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"></path>', 'app_articles') }}
|
||||
|
||||
<a href="#" class="flex items-center space-x-3 p-3 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition duration-150 ease-in-out">
|
||||
<!-- Icône SVG pour Clients (Users/People) -->
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M7.5 14a6.5 6.5 0 100-13 6.5 6.5 0 000 13z"></path></svg>
|
||||
<span>Articles</span>
|
||||
</a>
|
||||
|
||||
<a href="#" class="flex items-center space-x-3 p-3 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition duration-150 ease-in-out">
|
||||
<!-- Icône SVG pour Clients (Users/People) -->
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M7.5 14a6.5 6.5 0 100-13 6.5 6.5 0 000 13z"></path></svg>
|
||||
<span>Contrat</span>
|
||||
</a>
|
||||
|
||||
<!-- Menu Paramètres (avec sous-menus) -->
|
||||
<div>
|
||||
<button id="settings-toggle" class="w-full flex items-center justify-between p-3 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition duration-150 ease-in-out focus:outline-none">
|
||||
{# Groupe Paramètres #}
|
||||
<div class="pt-2">
|
||||
{% set isAdminRoute = app.request.get('_route') matches '/^app_crm_administrateur/' %}
|
||||
<button id="settings-toggle" class="w-full flex items-center justify-between p-3 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition duration-150">
|
||||
<div class="flex items-center space-x-3">
|
||||
<!-- Icône SVG pour Paramètres -->
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37a1.724 1.724 0 002.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
||||
<span>Paramètres</span>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37a1.724 1.724 0 002.572-1.065z"></path></svg>
|
||||
<span class="font-medium text-sm">Paramètres</span>
|
||||
</div>
|
||||
<!-- Icône de Chevron (pour l'état ouvert/fermé) -->
|
||||
<svg id="settings-chevron" class="w-4 h-4 transform transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
||||
<svg id="settings-chevron" class="w-4 h-4 transition-transform duration-200 {{ isAdminRoute ? 'rotate-180' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M19 9l-7 7-7-7"></path></svg>
|
||||
</button>
|
||||
|
||||
<!-- Sous-menu -->
|
||||
<ul id="settings-submenu" class="ml-4 mt-1 space-y-1 hidden">
|
||||
<li class="pl-2">
|
||||
<a href="{{ path('app_crm_administrateur') }}" class="block p-2 rounded-lg text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 transition duration-150 ease-in-out">
|
||||
Administrateur
|
||||
<ul id="settings-submenu" class="ml-9 mt-1 space-y-1 {{ isAdminRoute ? '' : 'hidden' }}">
|
||||
<li>
|
||||
<a href="{{ path('app_crm_administrateur') }}" class="block p-2 rounded-lg text-sm {{ isAdminRoute ? 'text-blue-600 font-bold dark:text-blue-400' : 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white' }}">
|
||||
Administrateurs
|
||||
</a>
|
||||
</li>
|
||||
<li class="pl-2">
|
||||
<a href="#" class="block p-2 rounded-lg text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 transition duration-150 ease-in-out">
|
||||
Services
|
||||
</a>
|
||||
<li>
|
||||
<a href="#" class="block p-2 rounded-lg text-sm text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white">Services</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- 2. Contenu Principal -->
|
||||
<main class="flex-1 lg:ml-64 overflow-y-auto">
|
||||
<!-- 2.1 En-tête (Header) -->
|
||||
<header class="h-16 flex items-center justify-between px-6 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-20 shadow-sm dark:shadow-none">
|
||||
{# CONTENU PRINCIPAL #}
|
||||
<main class="flex-1 flex flex-col min-w-0 lg:ml-64 bg-gray-50 dark:bg-gray-900">
|
||||
|
||||
<!-- Bouton pour ouvrir la barre latérale sur mobile -->
|
||||
<button id="sidebar-toggle" class="lg:hidden text-gray-500 dark:text-gray-400 hover:text-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500 p-2 rounded-md">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
|
||||
{# Header #}
|
||||
<header class="h-16 flex items-center justify-between px-6 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-20">
|
||||
<button id="sidebar-toggle" class="lg:hidden text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700 p-2 rounded-md transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16"></path></svg>
|
||||
</button>
|
||||
|
||||
<div class="flex items-center space-x-4 ml-auto">
|
||||
<div class="text-right hidden sm:block">
|
||||
<p class="text-xs font-medium text-gray-900 dark:text-white">{{ app.user.username|default('Admin') }}</p>
|
||||
<a href="{{ path('app_logout') }}" class="text-[10px] text-red-500 hover:underline">Déconnexion</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{# Zone Flash Messages #}
|
||||
<div id="flash-container" class="fixed top-20 right-6 z-50 flex flex-col gap-3 w-80">
|
||||
{% for label, messages in app.flashes %}
|
||||
{% for message in messages %}
|
||||
<div class="flash-message transform transition-all duration-500 p-4 rounded-lg shadow-lg border flex items-center justify-between {{ label == 'success' ? 'bg-green-100 border-green-200 text-green-800 dark:bg-green-900/80 dark:text-green-300 dark:border-green-800' : 'bg-red-100 border-red-200 text-red-800 dark:bg-red-900/80 dark:text-red-300 dark:border-red-800' }}">
|
||||
<p class="text-sm font-medium">{{ message }}</p>
|
||||
<button type="button" onclick="this.parentElement.remove()" class="ml-4 opacity-50 hover:opacity-100">×</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-3xl font-semibold text-gray-900 dark:text-gray-100">
|
||||
{% block title %}{% endblock %}
|
||||
</h1>
|
||||
<div class="flex space-x-2">
|
||||
{% block actions %}{% endblock %}
|
||||
{# Zone Contenu #}
|
||||
<div class="p-6 md:p-8">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-4">
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{% block title_header %}{{ block('title') }}{% endblock %}
|
||||
</h1>
|
||||
<div class="flex items-center space-x-3">
|
||||
{% block actions %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="animate-fadeIn w-full">
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% block body %}{% endblock %}
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Script JavaScript pour la fonctionnalité de la barre latérale mobile et le menu déroulant des paramètres -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const toggleButton = document.getElementById('sidebar-toggle');
|
||||
|
||||
// Fonction pour basculer la visibilité de la barre latérale
|
||||
toggleButton.addEventListener('click', () => {
|
||||
sidebar.classList.toggle('-translate-x-full');
|
||||
});
|
||||
|
||||
// Masquer la barre latérale si on clique en dehors (sur mobile)
|
||||
document.querySelector('main').addEventListener('click', () => {
|
||||
if (!sidebar.classList.contains('-translate-x-full') && window.innerWidth < 1024) {
|
||||
sidebar.classList.add('-translate-x-full');
|
||||
}
|
||||
});
|
||||
|
||||
// Assurer que la barre latérale est visible sur les grands écrans au chargement
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth >= 1024) {
|
||||
sidebar.classList.remove('-translate-x-full');
|
||||
} else {
|
||||
// Cacher si on passe en mobile, sauf si déjà ouvert
|
||||
if (sidebar.classList.contains('lg:translate-x-0')) {
|
||||
sidebar.classList.add('-translate-x-full');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
handleResize(); // Appel initial
|
||||
|
||||
// --- Logique du Menu Paramètres ---
|
||||
const settingsToggle = document.getElementById('settings-toggle');
|
||||
const settingsSubmenu = document.getElementById('settings-submenu');
|
||||
const settingsChevron = document.getElementById('settings-chevron');
|
||||
|
||||
if (settingsToggle) {
|
||||
settingsToggle.addEventListener('click', (e) => {
|
||||
e.preventDefault(); // Empêche la navigation et permet le dépliage
|
||||
settingsSubmenu.classList.toggle('hidden');
|
||||
settingsChevron.classList.toggle('rotate-180');
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user