✨ feat(crm/admin): Améliore gestion des administrateurs et sécurité
Ajoute formulaires identité et mot de passe, rôles dynamiques.
Gère statuts, journal d'audit, connexions.
Améliore les notifications.
```
434 lines
35 KiB
Twig
434 lines
35 KiB
Twig
{% extends 'dashboard/base.twig' %}
|
|
|
|
{% block title %}Administrateur : {{ admin.firstName }} {{ admin.name }}{% endblock %}
|
|
|
|
{% block actions %}
|
|
<a href="{{ path('app_crm_administrateur') }}" class="flex items-center space-x-2 px-4 py-2 text-slate-500 hover:text-slate-800 dark:hover:text-white transition-colors">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/></svg>
|
|
<span class="text-sm font-bold tracking-tight">Retour à la liste</span>
|
|
</a>
|
|
{% endblock %}
|
|
|
|
{% block body %}
|
|
<div class="max-w-full pb-12 px-6">
|
|
{# --- HEADER GÉNÉRAL --- #}
|
|
{# --- HEADER GÉNÉRAL : DESIGN ADMINISTRATEUR PRINCIPAL --- #}
|
|
<div class="mb-10 flex flex-col md:flex-row md:items-center justify-between gap-6 pb-8 border-b border-slate-800">
|
|
<div class="flex items-center space-x-6">
|
|
{# Avatar stylisé #}
|
|
<div class="h-20 w-20 rounded-3xl bg-gradient-to-br from-indigo-600 to-indigo-800 flex items-center justify-center text-white text-3xl font-black shadow-2xl shadow-indigo-500/20 border border-white/10">
|
|
{{ admin.firstName|first|upper }}{{ admin.name|first|upper }}
|
|
</div>
|
|
|
|
<div>
|
|
<div class="flex flex-col mb-1">
|
|
{# Badge Administrateur Principal #}
|
|
{% if 'ROLE_CLIENT_MAIN' in admin.roles %}
|
|
<div class="flex items-center space-x-2 mb-2">
|
|
<span class="px-3 py-1 bg-indigo-500 text-white text-[9px] font-black uppercase rounded-lg tracking-[0.2em] shadow-lg shadow-indigo-500/40">
|
|
Administrateur Principal
|
|
</span>
|
|
<div class="h-1.5 w-1.5 rounded-full bg-indigo-500 animate-pulse"></div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<h1 class="text-4xl font-black text-white tracking-tight">
|
|
{{ admin.firstName }} {{ admin.name }}
|
|
</h1>
|
|
</div>
|
|
|
|
<div class="flex items-center space-x-4">
|
|
<div class="flex items-center space-x-2 text-slate-400">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207" /></svg>
|
|
<span class="text-sm font-bold text-slate-300">{{ admin.email }}</span>
|
|
</div>
|
|
<div class="h-1 w-1 bg-slate-700 rounded-full"></div>
|
|
<span class="text-[10px] font-black text-slate-500 uppercase tracking-widest">System ID: #{{ admin.id }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center">
|
|
{# --- MODULE DE STATUT INTÉGRÉ --- #}
|
|
{% if not admin.actif %}
|
|
<div class="flex items-center bg-slate-900 border border-amber-500/30 rounded-2xl overflow-hidden shadow-2xl">
|
|
<div class="flex items-center px-5 py-3 space-x-3 bg-amber-500/5">
|
|
<div class="w-2 h-2 rounded-full bg-amber-500 shadow-[0_0_8px_rgba(245,158,11,0.8)]"></div>
|
|
<span class="text-[10px] font-black text-amber-500 uppercase tracking-widest">Compte non activé</span>
|
|
</div>
|
|
<a href="{{ path('app_crm_administrateur_view', {id: admin.id, act: 'updateStatut', status: 'true'}) }}"
|
|
class="px-6 py-3 bg-emerald-600 hover:bg-emerald-500 text-white text-[10px] font-black uppercase tracking-widest transition-all active:scale-95 border-l border-white/5">
|
|
Activer
|
|
</a>
|
|
</div>
|
|
{% else %}
|
|
<div class="flex items-center bg-slate-900 border border-emerald-500/30 rounded-2xl overflow-hidden shadow-2xl">
|
|
<div class="flex items-center px-5 py-3 space-x-3 bg-emerald-500/5">
|
|
<div class="w-2 h-2 rounded-full bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.8)]"></div>
|
|
<span class="text-[10px] font-black text-emerald-500 uppercase tracking-widest">Accès en ligne</span>
|
|
</div>
|
|
<a href="{{ path('app_crm_administrateur_view', {id: admin.id, act: 'updateStatut', status: 'false'}) }}"
|
|
onclick="return confirm('Voulez-vous vraiment désactiver cet accès ?')"
|
|
class="px-6 py-3 bg-slate-800 hover:bg-red-600 text-slate-400 hover:text-white text-[10px] font-black uppercase tracking-widest transition-all active:scale-95 border-l border-white/5">
|
|
Désactiver
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
|
|
|
{# --- COLONNE GAUCHE : FORMULAIRE IDENTITÉ (SANS LE TOGGLE ACTIF) --- #}
|
|
<div class="lg:col-span-4">
|
|
{{ form_start(form) }}
|
|
<div class="bg-white dark:bg-[#1e293b] rounded-3xl p-8 border border-slate-200 dark:border-slate-800 shadow-sm sticky top-6">
|
|
<div class="flex items-center space-x-4 mb-8">
|
|
<div class="h-16 w-16 rounded-2xl bg-indigo-600 flex items-center justify-center text-white text-2xl font-black shadow-lg shadow-indigo-500/20">
|
|
{{ admin.firstName|first|upper }}{{ admin.name|first|upper }}
|
|
</div>
|
|
<div>
|
|
<h2 class="text-xl font-black text-slate-800 dark:text-white uppercase tracking-tight">Identité</h2>
|
|
<p class="text-[10px] text-slate-400 font-black uppercase tracking-widest italic">Données personnelles</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-5">
|
|
<div class="space-y-2">
|
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1 text-indigo-500">Prénom</label>
|
|
{{ form_widget(form.firstName, { 'attr': {'class': 'w-full px-4 py-3 bg-slate-50 dark:bg-slate-900 border-none rounded-xl focus:ring-2 focus:ring-indigo-500 transition-all text-sm font-medium dark:text-white'} }) }}
|
|
</div>
|
|
<div class="space-y-2">
|
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1 text-indigo-500">Nom</label>
|
|
{{ form_widget(form.name, { 'attr': {'class': 'w-full px-4 py-3 bg-slate-50 dark:bg-slate-900 border-none rounded-xl focus:ring-2 focus:ring-indigo-500 transition-all text-sm font-medium dark:text-white'} }) }}
|
|
</div>
|
|
|
|
<hr class="border-slate-100 dark:border-slate-800 my-4">
|
|
|
|
<div class="space-y-2">
|
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1 text-amber-500">Username</label>
|
|
{{ form_widget(form.username, { 'attr': {'class': 'w-full px-4 py-3 bg-slate-50 dark:bg-slate-900 border-none rounded-xl focus:ring-2 focus:ring-amber-500 transition-all text-sm font-mono dark:text-white'} }) }}
|
|
</div>
|
|
<div class="space-y-2">
|
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1 text-amber-500">Email professionnel</label>
|
|
{{ form_widget(form.email, { 'attr': {'class': 'w-full px-4 py-3 bg-slate-50 dark:bg-slate-900 border-none rounded-xl focus:ring-2 focus:ring-amber-500 transition-all text-sm dark:text-white'} }) }}
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<button type="submit" class="w-full mt-10 flex items-center justify-center space-x-2 px-6 py-4 bg-slate-900 dark:bg-indigo-600 hover:bg-indigo-700 text-white rounded-2xl font-bold shadow-xl transition-all active:scale-95">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
|
<span>Enregistrer l'identité</span>
|
|
</button>
|
|
</div>
|
|
{{ form_end(form) }}
|
|
</div>
|
|
|
|
{# --- COLONNE DROITE : DROITS & HISTORIQUE --- #}
|
|
<div class="lg:col-span-8 space-y-8">
|
|
{# --- CARTE SÉCURITÉ & MOT DE PASSE --- #}
|
|
<div class="bg-white dark:bg-[#1e293b] rounded-3xl p-8 border border-slate-200 dark:border-slate-800 shadow-sm">
|
|
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8">
|
|
<h3 class="text-[10px] font-black text-indigo-500 uppercase tracking-[0.2em] flex items-center">
|
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
|
|
Sécurité et Mot de passe
|
|
</h3>
|
|
<a href="{{ path('app_crm_administrateur_view', {id: admin.id, act: 'sendResetLink'}) }}"
|
|
onclick="return confirm('Envoyer l\'email de réinitialisation ?')"
|
|
class="px-4 py-2 bg-slate-100 dark:bg-slate-800 hover:bg-indigo-500 hover:text-white text-slate-600 dark:text-slate-300 text-[10px] font-black uppercase rounded-xl transition-all border border-slate-200 dark:border-slate-700">
|
|
Envoyer un lien de réinitialisation
|
|
</a>
|
|
</div>
|
|
|
|
{{ form_start(passwordForm) }}
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div class="space-y-2">
|
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Définir un nouveau mot de passe</label>
|
|
{{ form_widget(passwordForm.password.first, { 'attr': {'class': 'w-full px-4 py-3 bg-slate-50 dark:bg-slate-900 border-none rounded-xl focus:ring-2 focus:ring-indigo-500 transition-all text-sm dark:text-white'} }) }}
|
|
</div>
|
|
<div class="space-y-2">
|
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Confirmer le mot de passe</label>
|
|
{{ form_widget(passwordForm.password.second, { 'attr': {'class': 'w-full px-4 py-3 bg-slate-50 dark:bg-slate-900 border-none rounded-xl focus:ring-2 focus:ring-indigo-500 transition-all text-sm dark:text-white'} }) }}
|
|
</div>
|
|
</div>
|
|
<div class="mt-6 flex justify-end">
|
|
<button type="submit" class="px-6 py-3 bg-slate-800 text-white text-[10px] font-black uppercase rounded-xl hover:bg-slate-700 transition-all active:scale-95">
|
|
Mettre à jour le mot de passe
|
|
</button>
|
|
</div>
|
|
{{ form_end(passwordForm) }}
|
|
</div>
|
|
{# HABILITATIONS #}
|
|
{% if 'ROLE_CLIENT_MAIN' not in admin.roles %}
|
|
<form action="{{ path('app_crm_administrateur_view', {id: admin.id, act: 'updateRoles'}) }}" method="POST">
|
|
<div class="bg-white dark:bg-[#1e293b] rounded-3xl p-8 border border-slate-200 dark:border-slate-800 shadow-sm">
|
|
<div class="flex items-center justify-between mb-8">
|
|
<h3 class="text-[10px] font-black text-emerald-500 uppercase tracking-[0.2em] flex items-center">
|
|
<span class="w-8 h-8 bg-emerald-100 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400 rounded-lg flex items-center justify-center mr-3">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" /></svg>
|
|
</span>
|
|
Habilitations Opérationnelles
|
|
</h3>
|
|
<button type="submit" class="px-5 py-2 bg-emerald-500 hover:bg-emerald-600 text-white text-[10px] font-black uppercase rounded-xl transition-all shadow-lg shadow-emerald-500/20">
|
|
Mettre à jour les droits
|
|
</button>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 gap-4">
|
|
{% set availableRoles = [
|
|
{'role': 'ROLE_ADMIN_EDIT', 'label': 'Gestion des Administrateurs', 'icon': 'M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z'},
|
|
{'role': 'ROLE_ADMIN_PRODUCT', 'label': 'Catalogue Produits', 'icon': 'M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4'},
|
|
{'role': 'ROLE_ADMIN_CONTRAT', 'label': 'Gestion Contrats', 'icon': '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'}
|
|
] %}
|
|
|
|
{% for item in availableRoles %}
|
|
<div class="p-4 rounded-2xl bg-slate-50 dark:bg-slate-900/50 border border-slate-100 dark:border-slate-800 flex items-center justify-between group">
|
|
<div class="flex items-center space-x-4">
|
|
<div class="w-10 h-10 rounded-xl bg-white dark:bg-slate-900 flex items-center justify-center text-slate-400 group-hover:text-emerald-500 transition-colors shadow-sm">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ item.icon }}" /></svg>
|
|
</div>
|
|
<div>
|
|
<span class="block text-sm font-bold text-slate-700 dark:text-slate-200">{{ item.label }}</span>
|
|
<span class="block text-[10px] font-medium text-slate-400 uppercase tracking-widest mt-0.5">{{ item.role }}</span>
|
|
</div>
|
|
</div>
|
|
<label class="relative inline-flex items-center cursor-pointer">
|
|
<input type="checkbox" name="roles[]" value="{{ item.role }}" class="sr-only peer" {% if item.role in admin.roles %}checked{% endif %}>
|
|
<div class="w-11 h-6 bg-slate-300 dark:bg-slate-700 rounded-full peer peer-checked:bg-emerald-500 transition-all after:content-[''] after:absolute after:top-[4px] after:left-[4px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:after:translate-x-5"></div>
|
|
</label>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</form>
|
|
{% else %}
|
|
<div class="bg-emerald-500/5 dark:bg-emerald-500/10 rounded-3xl p-8 border border-emerald-500/20 shadow-sm flex items-center space-x-6 border-l-4 border-l-emerald-500">
|
|
<div class="w-14 h-14 bg-emerald-500 text-white rounded-2xl flex items-center justify-center shadow-lg shadow-emerald-500/20">
|
|
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>
|
|
</div>
|
|
<div>
|
|
<h4 class="text-emerald-600 dark:text-emerald-400 font-black text-sm uppercase tracking-wider">Habilitation Totale Active</h4>
|
|
<p class="text-emerald-600/70 dark:text-emerald-400/70 text-xs mt-1 leading-relaxed">Le rang Client Principal accorde nativement toutes les permissions système.</p>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# PRIVILÈGE DE STRUCTURE (ROOT) #}
|
|
{% if is_granted('ROLE_ROOT') %}
|
|
<form action="{{ path('app_crm_administrateur_view', {id: admin.id, act: 'updateRoles'}) }}" method="POST">
|
|
<div class="bg-white dark:bg-[#1e293b] rounded-3xl p-8 border border-slate-200 dark:border-slate-800 shadow-sm border-l-4 border-l-indigo-500">
|
|
<div class="flex items-center justify-between mb-8">
|
|
<h3 class="text-[10px] font-black text-indigo-500 uppercase tracking-[0.2em] flex items-center">
|
|
<span class="w-8 h-8 bg-indigo-100 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 rounded-lg flex items-center justify-center mr-3">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /></svg>
|
|
</span>
|
|
Privilège de Structure
|
|
</h3>
|
|
<button type="submit" class="px-5 py-2 bg-indigo-600 hover:bg-indigo-700 text-white text-[10px] font-black uppercase rounded-xl transition-all shadow-lg shadow-indigo-500/20">
|
|
Confirmer le rang
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-6 rounded-2xl bg-[#111827] border border-slate-800 shadow-inner group transition-all hover:border-indigo-500/30">
|
|
<label class="flex items-center justify-between cursor-pointer">
|
|
<div class="flex items-center space-x-5">
|
|
<div class="relative">
|
|
<input type="checkbox" name="roles[]" value="ROLE_CLIENT_MAIN" class="sr-only peer" {% if 'ROLE_CLIENT_MAIN' in admin.roles %}checked{% endif %}>
|
|
<div class="w-14 h-7 bg-slate-700 rounded-full peer peer-checked:bg-indigo-600 transition-all duration-300"></div>
|
|
<div class="absolute top-1 left-1 w-5 h-5 bg-white rounded-full transition-all duration-300 peer-checked:translate-x-7 shadow-lg"></div>
|
|
</div>
|
|
<div>
|
|
<span class="block text-sm font-bold text-white tracking-wide group-hover:text-indigo-400 transition-colors uppercase">Administrateur Client Principal</span>
|
|
<p class="text-[10px] text-slate-500 font-medium mt-1 uppercase italic">Contrôle total de l'organisation</p>
|
|
</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{# --- SECTIONS JOURNAUX (LOGS) --- #}
|
|
<div class="mt-12 space-y-12">
|
|
|
|
{# --- SECTION : JOURNAL D'AUDIT DU COMPTE --- #}
|
|
<div class="w-full bg-white dark:bg-[#1e293b] rounded-3xl border border-slate-200 dark:border-slate-800 shadow-sm overflow-hidden mt-12">
|
|
|
|
{# HEADER DU JOURNAL #}
|
|
<div class="px-8 py-6 border-b border-slate-100 dark:border-slate-800 flex items-center justify-between bg-slate-50/30 dark:bg-slate-800/30">
|
|
<div>
|
|
<h3 class="text-[10px] font-black text-indigo-500 uppercase tracking-[0.3em]">Journal d'Audit Personnel</h3>
|
|
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1">Historique des actions effectuées par cet utilisateur</p>
|
|
</div>
|
|
<div class="flex items-center space-x-3">
|
|
<span class="px-4 py-1.5 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 text-[10px] font-black uppercase rounded-lg border border-blue-100 dark:border-blue-800/50">
|
|
{{ auditLogs.getTotalItemCount }} ÉVÈNEMENTS
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto custom-scrollbar">
|
|
<table class="w-full text-left border-collapse">
|
|
<thead>
|
|
<tr class="bg-slate-50/50 dark:bg-slate-900/40 border-b border-slate-100 dark:border-slate-800">
|
|
<th class="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em]">Horodatage</th>
|
|
<th class="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em]">Appareil / Source</th>
|
|
<th class="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em] text-center">Action</th>
|
|
<th class="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em] text-center">Intégrité</th>
|
|
<th class="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em]">Détails</th>
|
|
{% if is_granted('ROLE_ROOT') %}
|
|
<th class="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em] text-right">Actions</th>
|
|
{% endif %}
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100 dark:divide-slate-800 text-sm">
|
|
{% for log in auditLogs %}
|
|
<tr class="hover:bg-slate-50/50 dark:hover:bg-slate-800/30 transition-colors duration-150">
|
|
|
|
{# 1. DATE #}
|
|
<td class="px-8 py-4 whitespace-nowrap align-top">
|
|
<div class="text-sm font-bold text-slate-700 dark:text-slate-200">{{ log.actionAt|date('d/m/Y') }}</div>
|
|
<div class="text-[10px] text-slate-400 font-mono mt-0.5 tracking-wider">{{ log.actionAt|date('H:i:s') }}</div>
|
|
</td>
|
|
|
|
{# 2. UA (ADMIN & APPAREIL) #}
|
|
<td class="px-8 py-4 whitespace-nowrap align-top">
|
|
{% if log.userAgent %}
|
|
{% set ua = log.userAgent|lower %}
|
|
<div class="flex items-center text-[10px] text-slate-500 bg-slate-100/50 dark:bg-slate-900/50 px-3 py-2 rounded-xl border border-slate-200/50 max-w-[240px]">
|
|
<span class="mr-2">
|
|
{% if 'firefox' in ua %}
|
|
<svg class="w-3.5 h-3.5 text-orange-500" fill="currentColor" viewBox="0 0 24 24"><path d="M23.9 12c0 6.6-5.4 12-11.9 12C5.4 24 0 18.6 0 12S5.4 0 12 0c6.5 0 11.9 5.4 11.9 12zM10.8 4.7c-.5.1-1.3.4-1.3.4s.8-.2 1.3-.3c1.5-.4 3.1-.2 4.4.6 1.3.7 2.2 2 2.5 3.4.1.7.1 1.5-.1 2.2-.2 1-1.2 2.2-1.2 2.2s.8-.9 1-1.8c.3-1.4-.1-2.9-1.2-4-1.1-1.1-2.6-1.6-4.2-1.4-1.4.1-2.1.8-2.6 1.4-.5.6-.7 1.4-.6 2.1.1.7.5 1.4 1.1 1.8.6.4 1.3.5 2 .4.7-.1 1.3-.5 1.7-1.1.4-.6.5-1.3.3-2-.1-.7-.5-1.3-1-1.7s-1.2-.5-1.9-.4c-.7.1-1.3.5-1.7 1.1-.3.5-.4 1.1-.3 1.7.1.5.3.9.7 1.2.4.3.8.4 1.3.3.5-.1.9-.3 1.2-.7.2-.4.3-.8.2-1.3-.1-.4-.3-.7-.6-.9-.3-.2-.6-.3-1-.2-.3 0-.6.1-.8.4-.2.2-.3.5-.2.8.1.3.3.5.6.6.3.1.6 0 .8-.2.2-.2.3-.4.2-.7 0-.3-.2-.5-.5-.6-.2-.1-.5 0-.7.2s-.3.4-.2.7z"/></svg>
|
|
{% elseif 'chrome' in ua %}
|
|
<svg class="w-3.5 h-3.5 text-blue-400" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0C8.21 0 4.83 1.75 2.64 4.5l3.96 6.84A6.004 6.004 0 0 1 12 6h10.36A12.012 12.012 0 0 0 12 0zm-1.04 13.5l-5.12-8.88A11.936 11.936 0 0 0 0 12c0 6.07 4.51 11.08 10.36 11.92l3.96-6.84a6.012 6.012 0 0 1-3.36-3.58zm12.4-7.5H12a6.002 6.002 0 0 1 3.36 9.42l-5.12 8.88C10.74 23.9 11.36 24 12 24c6.63 0 12-5.37 12-12 0-2.12-.55-4.12-1.52-5.88z"/></svg>
|
|
{% else %}
|
|
<svg class="w-3.5 h-3.5 text-slate-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/></svg>
|
|
{% endif %}
|
|
</span>
|
|
<span class="truncate opacity-80" title="{{ log.userAgent }}">{{ log.userAgent }}</span>
|
|
</div>
|
|
{% else %}
|
|
<span class="text-slate-400 italic text-[10px]">Source inconnue</span>
|
|
{% endif %}
|
|
</td>
|
|
|
|
{# 3. TYPE D'ACTION #}
|
|
<td class="px-8 py-4 whitespace-nowrap text-center align-top">
|
|
{% set typeMapping = {
|
|
'CREATE': { 'label': 'CRÉATION', 'style': 'bg-emerald-500/10 text-emerald-600 border-emerald-500/20' },
|
|
'DELETE': { 'label': 'SUPPRESSION', 'style': 'bg-rose-500/10 text-rose-600 border-rose-500/20' },
|
|
'UPDATE': { 'label': 'MODIFICATION', 'style': 'bg-blue-500/10 text-blue-600 border-blue-500/20' },
|
|
'AUTH': { 'label': 'CONNEXION', 'style': 'bg-amber-500/10 text-amber-600 border-amber-500/20' },
|
|
'VIEW': { 'label': 'CONSULTATION', 'style': 'bg-sky-500/10 text-sky-600 border-sky-500/20' },
|
|
'SECURITY_ALERT': { 'label': 'ALERTE SÉCURITÉ', 'style': 'bg-orange-500/10 text-orange-600 border-orange-500/20' },
|
|
'SECURITY_CRITICAL': { 'label': 'CRITIQUE', 'style': 'bg-red-600 text-white border-red-700 animate-pulse font-black' }
|
|
} %}
|
|
|
|
{% set config = typeMapping[log.type] ?? { 'label': log.type, 'style': 'bg-slate-500/10 text-slate-500 border-slate-500/20' } %}
|
|
|
|
<span class="px-3 py-1.5 rounded-lg text-[10px] font-black border uppercase tracking-widest {{ config.style }}">
|
|
{{ config.label }}
|
|
</span>
|
|
</td>
|
|
|
|
{# 4. INTÉGRITÉ #}
|
|
<td class="px-8 py-4 whitespace-nowrap align-top text-center">
|
|
{% if log.hashCode == log.generateSignature %}
|
|
<div class="inline-flex flex-col items-center group cursor-help" title="Signature valide">
|
|
<div class="h-8 w-8 rounded-lg bg-emerald-500/10 flex items-center justify-center text-emerald-600 border border-emerald-500/20">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg>
|
|
</div>
|
|
<span class="text-[8px] font-black uppercase text-emerald-600 mt-1">Valide</span>
|
|
</div>
|
|
{% else %}
|
|
<div class="inline-flex flex-col items-center animate-pulse cursor-help" title="Données altérées !">
|
|
<div class="h-8 w-8 rounded-lg bg-rose-500/10 flex items-center justify-center text-rose-600 border border-rose-500/20 shadow-lg shadow-rose-500/10">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
|
</div>
|
|
<span class="text-[8px] font-black uppercase text-rose-600 mt-1">Altéré</span>
|
|
</div>
|
|
{% endif %}
|
|
</td>
|
|
|
|
{# 5. DÉTAILS #}
|
|
<td class="px-8 py-4 align-top">
|
|
<div class="text-sm text-slate-600 dark:text-slate-300 font-medium leading-relaxed max-w-xl">{{ log.message }}</div>
|
|
<div class="mt-2 flex items-center">
|
|
<code class="text-[10px] bg-slate-100 dark:bg-slate-900 px-2 py-0.5 rounded text-blue-500 font-mono italic">{{ log.path|default('/') }}</code>
|
|
</div>
|
|
</td>
|
|
|
|
{# 6. ACTIONS ROOT #}
|
|
{% if is_granted('ROLE_ROOT') %}
|
|
<td class="px-8 py-4 whitespace-nowrap text-right align-top">
|
|
<form action="{{ path('app_crm_audit_logs_delete', {id: log.id}) }}" method="POST" class="inline-block" onsubmit="return confirm('Supprimer ce log précis ?');">
|
|
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ log.id) }}">
|
|
<button type="submit" class="p-2 text-slate-400 hover:text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-900/10 rounded-lg transition-all">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
|
</button>
|
|
</form>
|
|
</td>
|
|
{% endif %}
|
|
</tr>
|
|
{% else %}
|
|
<tr>
|
|
<td colspan="{{ is_granted('ROLE_ROOT') ? 6 : 5 }}" class="px-8 py-12 text-center italic text-slate-400">
|
|
Aucun enregistrement d'audit pour ce compte.
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{# PAGINATION #}
|
|
{% if auditLogs.getTotalItemCount > 0 %}
|
|
<div class="px-8 py-6 bg-slate-50/50 dark:bg-slate-900/40 border-t border-slate-100 dark:border-slate-800">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-xs font-semibold text-slate-400 uppercase tracking-widest">
|
|
Page {{ auditLogs.getCurrentPageNumber }} — {{ auditLogs|length }} logs affichés
|
|
</span>
|
|
<div class="navigation">
|
|
{{ knp_pagination_render(auditLogs) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{# CONNEXIONS #}
|
|
<div class="bg-white dark:bg-[#1e293b] rounded-3xl border border-slate-200 dark:border-slate-800 shadow-sm overflow-hidden">
|
|
<div class="p-8 border-b border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/10">
|
|
<h3 class="text-[10px] font-black text-emerald-500 uppercase tracking-[0.3em]">Dernières Connexions</h3>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-left font-medium">
|
|
<thead>
|
|
<tr class="bg-slate-50/50 dark:bg-slate-900/50 border-b border-slate-100 dark:border-slate-800">
|
|
<th class="px-8 py-4 text-[10px] font-black uppercase text-slate-400 tracking-widest">Date & Heure</th>
|
|
<th class="px-8 py-4 text-[10px] font-black uppercase text-slate-400 tracking-widest">IP</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100 dark:divide-slate-800 text-sm">
|
|
{% for login in loginRegisters %}
|
|
<tr class="hover:bg-slate-50/50 dark:hover:bg-slate-800/30 transition-colors">
|
|
<td class="px-8 py-5 text-slate-700 dark:text-slate-200">{{ login.loginAt|date('d/m/Y H:i') }}</td>
|
|
<td class="px-8 py-5">
|
|
<span class="px-3 py-1 bg-slate-100 dark:bg-slate-800 rounded-lg text-xs font-mono text-slate-500 border border-slate-200 dark:border-slate-700">
|
|
{{ login.ip }}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr><td colspan="2" class="px-8 py-12 text-center text-slate-400 text-sm italic">Aucune connexion enregistrée.</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% if loginRegisters.getTotalItemCount > 0 %}<div class="p-6 border-t border-slate-100 dark:border-slate-800">{{ knp_pagination_render(loginRegisters) }}</div>{% endif %}
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|