{# @tailwind-safelist bg-primary/10 bg-cohorts/10 bg-warning/10 bg-error/10 bg-success/10 bg-info/10 bg-pink/10 bg-primary/[0.08] bg-cohorts/[0.08] bg-warning/[0.08] bg-error/[0.08] bg-success/[0.08] bg-info/[0.08] bg-pink/[0.08] bg-primary/[0.14] bg-cohorts/[0.14] bg-warning/[0.14] bg-error/[0.14] bg-success/[0.14] bg-info/[0.14] bg-pink/[0.14] text-primary text-cohorts text-warning text-error text-success text-info text-pink hover:bg-primary/[0.07] hover:bg-cohorts/[0.07] hover:bg-warning/[0.07] hover:bg-error/[0.07] hover:bg-success/[0.07] hover:bg-info/[0.07] hover:bg-pink/[0.07] has-[:checked]:from-primary/[0.08] has-[:checked]:from-cohorts/[0.08] has-[:checked]:from-warning/[0.08] has-[:checked]:from-error/[0.08] has-[:checked]:from-success/[0.08] has-[:checked]:from-info/[0.08] has-[:checked]:from-pink/[0.08] has-[:checked]:to-primary/[0.03] has-[:checked]:to-cohorts/[0.03] has-[:checked]:to-warning/[0.03] has-[:checked]:to-error/[0.03] has-[:checked]:to-success/[0.03] has-[:checked]:to-info/[0.03] has-[:checked]:to-pink/[0.03] group-hover/opt:border-primary group-hover/opt:border-cohorts group-hover/opt:border-warning group-hover/opt:border-error group-hover/opt:border-success group-hover/opt:border-info group-hover/opt:border-pink group-hover/opt:bg-primary/[0.14] group-hover/opt:bg-cohorts/[0.14] group-hover/opt:bg-warning/[0.14] group-hover/opt:bg-error/[0.14] group-hover/opt:bg-success/[0.14] group-hover/opt:bg-info/[0.14] group-hover/opt:bg-pink/[0.14] group-has-[:checked]/opt:bg-primary group-has-[:checked]/opt:bg-cohorts group-has-[:checked]/opt:bg-warning group-has-[:checked]/opt:bg-error group-has-[:checked]/opt:bg-success group-has-[:checked]/opt:bg-info group-has-[:checked]/opt:bg-pink group-has-[:checked]/opt:border-primary group-has-[:checked]/opt:border-cohorts group-has-[:checked]/opt:border-warning group-has-[:checked]/opt:border-error group-has-[:checked]/opt:border-success group-has-[:checked]/opt:border-info group-has-[:checked]/opt:border-pink group-has-[:checked]/opt:text-primary group-has-[:checked]/opt:text-cohorts group-has-[:checked]/opt:text-warning group-has-[:checked]/opt:text-error group-has-[:checked]/opt:text-success group-has-[:checked]/opt:text-info group-has-[:checked]/opt:text-pink #} {% extends 'base.html.twig' %} {% block title %}{{ isAdmin ? 'Gestion des utilisateurs - Admin' : 'Utilisateurs - ' ~ managerCompany.name }}{% endblock %} {% block body %}
{# ================================================================ HEADER ================================================================ #}
{% include '_molecules/page-header/page-header.html.twig' with { title: isAdmin ? 'Gestion des utilisateurs' : 'Utilisateurs', subtitle: isAdmin ? 'Consultez et gérez l\'ensemble des comptes utilisateurs. Créez de nouveaux profils, attribuez des rôles, activez ou désactivez des accès.' : 'Gérez les membres de ' ~ managerCompany.name ~ '. Créez de nouveaux comptes, attribuez des rôles et suivez l\'activité de votre équipe.', icon: 'users', backHref: path('app_dashboard'), backLabel: 'Retour au dashboard' } only %} {% if isAdmin %} {% include '_atoms/button/button.html.twig' with { label: 'Nouvel utilisateur', variant: 'primary', size: 'lg', icon: 'plus', href: path('admin_user_new') } only %} {% elseif app.user.isActive %} {% if stats.remainingSlots is defined and stats.remainingSlots <= 0 %}
{% include '_atoms/button/button.html.twig' with { label: 'Nouvel utilisateur', variant: 'primary', size: 'lg', icon: 'plus', disabled: true } only %} {% include '_atoms/badge/badge.html.twig' with { label: 'Limite atteinte (' ~ stats.maxUsers ~ '/' ~ stats.maxUsers ~ ')', variant: 'warning', size: 'sm' } only %}
{% else %} {% include '_atoms/button/button.html.twig' with { label: 'Nouvel utilisateur', variant: 'primary', size: 'lg', icon: 'plus', href: path('admin_user_new') } only %} {% endif %} {% endif %}
{# ================================================================ STATISTIQUES ================================================================ #} {% if isAdmin %} {# Stats Admin : vue globale plateforme #}
{% include '_molecules/stat-card/stat-card.html.twig' with { label: 'Utilisateurs', value: stats.total, icon: 'users', variant: 'primary', size: 'sm', layout: 'horizontal' } only %} {% include '_molecules/stat-card/stat-card.html.twig' with { label: 'Actifs', value: stats.active, icon: 'check-circle', variant: 'success', size: 'sm', layout: 'horizontal' } only %} {% include '_molecules/stat-card/stat-card.html.twig' with { label: 'Désactivés', value: stats.inactive, icon: 'power', variant: 'error', size: 'sm', layout: 'horizontal' } only %} {% include '_molecules/stat-card/stat-card.html.twig' with { label: 'Nouveaux (30j)', value: stats.recentCount|default(0), icon: 'clock', variant: 'primary', detail: 'Ce mois-ci', size: 'sm', layout: 'horizontal' } only %}
{% else %} {# Stats Manager : quota utilisateurs #}
{% include '_molecules/stat-card/stat-card.html.twig' with { label: 'Utilisateurs actifs', value: stats.active ~ ' / ' ~ stats.maxUsers, icon: 'users', variant: 'primary', size: 'sm', layout: 'horizontal' } only %} {% include '_molecules/stat-card/stat-card.html.twig' with { label: stats.remainingSlots > 1 ? 'Places disponibles' : 'Place disponible', value: stats.remainingSlots, icon: 'plus', variant: stats.remainingSlots > 0 ? 'success' : 'warning', size: 'sm', layout: 'horizontal' } only %} {% include '_molecules/stat-card/stat-card.html.twig' with { label: 'Désactivés', value: stats.inactive, icon: 'power', variant: 'error', size: 'sm', layout: 'horizontal' } only %}
{% endif %} {# ================================================================ TABLEAU DES UTILISATEURS ================================================================ #}
{# Header avec filtres #}

Liste des utilisateurs ({{ pagination.totalItems }})

{# ---- Tailwind class variables for DRY ---- #} {% set triggerBase = 'w-full sm:w-auto flex items-center gap-1.5 px-3 h-[38px] border-[1.5px] rounded-lg text-sm bg-white cursor-pointer whitespace-nowrap transition-all duration-200 group-[.filter-multiselect--open]/filter:border-primary group-[.filter-multiselect--open]/filter:shadow-[0_0_0_3px_rgba(12,129,228,0.1)]' %} {% set chevronSvg = 'text-gray-400 shrink-0 transition-transform duration-200 group-[.filter-multiselect--open]/filter:rotate-180' %} {% set dropdownBase = 'hidden group-[.filter-multiselect--open]/filter:block absolute top-[calc(100%+4px)] left-0 min-w-[220px] bg-white border-[1.5px] border-gray-200 rounded-xl shadow-lg z-50 py-1.5 max-h-[280px] overflow-y-auto' %} {% set optBase = 'group/opt flex items-center gap-2.5 py-2 px-3.5 mx-1.5 rounded-md cursor-pointer text-sm text-gray-700 transition-colors duration-150 hover:bg-primary/[0.07] has-[:checked]:bg-gradient-to-r has-[:checked]:from-primary/[0.08] has-[:checked]:to-primary/[0.03]' %} {% set optBaseNoColor = 'group/opt flex items-center gap-2.5 py-2 px-3.5 mx-1.5 rounded-md cursor-pointer text-sm text-gray-700 transition-colors duration-150' %} {% set radioBase = 'w-[18px] h-[18px] border-2 border-gray-300 rounded-full shrink-0 flex items-center justify-center transition-all duration-200 group-hover/opt:border-primary group-has-[:checked]/opt:border-primary group-has-[:checked]/opt:shadow-[0_2px_6px_rgba(12,129,228,0.3)]' %} {% set radioDot = 'w-2 h-2 rounded-full bg-transparent transition-all duration-200 group-has-[:checked]/opt:bg-primary' %} {% set optLabel = 'flex-1 leading-snug group-has-[:checked]/opt:text-primary group-has-[:checked]/opt:font-semibold' %} {% set iconBase = 'w-[26px] h-[26px] rounded-md shrink-0 flex items-center justify-center transition-all duration-200' %} {# Filtres inline #}
{# ---- Filtre rôle ---- #}
{# ---- Filtre statut ---- #}
{# ---- Filtre entreprise (admin) ---- #} {% if isAdmin %}
{% endif %} {# ---- Filtre plan (admin only) ---- #} {% if isAdmin %}
{% endif %} {# Barre de recherche #} {# Bouton reset #} {% if filters.search or filters.role or filters.status or filters.company or filters.plan %} {% endif %}
{# Tableau #} {% if users|length > 0 %}
{% if isAdmin %} {% endif %} {# Tokens — en-tête triable (asc/desc), filtres courants préservés #} {% set _tokNextDir = (sort == 'tokens' and dir == 'desc') ? 'asc' : 'desc' %} {# Consommé (30 j) — en-tête triable (asc/desc), filtres courants préservés #} {% set _consNextDir = (sort == 'consumed' and dir == 'desc') ? 'asc' : 'desc' %} {% for user in users %} {# Utilisateur #} {# Rôle #} {# Entreprise (admin only) #} {% if isAdmin %} {# Plan (admin only) #} {% endif %} {# Tokens #} {# Tokens consommés sur les 30 derniers jours #} {# Statut #} {# Actions #} {% endfor %}
Utilisateur Rôle Statut Actions
{% include '_atoms/avatar/avatar.html.twig' with { name: user.fullName, size: 'sm' } only %}
{{ user.fullName }} {% if user.isBetaTester or 'ROLE_BETA_TESTER' in user.roles %} {% include '_atoms/icon/icon.html.twig' with { name: 'star', size: 'xs', color: 'warning' } only %} {% endif %}
{{ user.email }} {% if isAdmin and user.googleId %} G: {{ user.googleId }} {% endif %} {{ user.createdAt|date('d/m/Y') }}
{% if 'ROLE_ADMIN' in user.roles %} {% include '_atoms/badge/badge.html.twig' with { label: 'Admin', variant: 'error', size: 'sm' } only %} {% elseif 'ROLE_COMPANY_MANAGER' in user.roles %} {% include '_atoms/badge/badge.html.twig' with { label: 'Gestionnaire', variant: 'cohorts', size: 'sm' } only %} {% elseif 'ROLE_COMMERCIAL' in user.roles %} {% include '_atoms/badge/badge.html.twig' with { label: 'Commercial', variant: 'success', size: 'sm' } only %} {% else %} {% include '_atoms/badge/badge.html.twig' with { label: 'Utilisateur', variant: 'primary', size: 'sm' } only %} {% endif %}
{% include '_atoms/badge/badge.html.twig' with { preset: user.isActive ? 'user-active' : 'user-inactive', size: 'sm' } only %} {% if not user.isEmailVerified %} {% include '_atoms/badge/badge.html.twig' with { label: 'Email non vérifié', variant: 'warning', size: 'xs' } only %} {% endif %}
{% if user != app.user %} {% endif %}
{# Pagination - Utilise la molécule atomique #} {% if pagination.total > 1 %}
Page {{ pagination.current }} sur {{ pagination.total }} ({{ pagination.totalItems }} utilisateurs) {# Conserve tous les filtres actifs (recherche, rôle, statut, entreprise, plan) ainsi que le tri courant lors du changement de page. #} {% set paginationFilters = filters|filter(v => v is not null and v != '') %} {% if sort %}{% set paginationFilters = paginationFilters|merge({sort: sort, dir: dir}) %}{% endif %} {% include '_molecules/pagination/pagination.html.twig' with { currentPage: pagination.current, totalPages: pagination.total, baseUrl: path('admin_user_list', paginationFilters), size: 'sm', showInfo: false, showFirstLast: true } only %}
{% endif %} {% else %}
{% include '_molecules/empty-state/empty-state.html.twig' with { title: 'Aucun utilisateur trouvé', description: 'Aucun utilisateur ne correspond à vos critères de recherche.', icon: 'users', variant: 'minimal', actions: (isAdmin or app.user.isActive) ? [{ label: 'Créer un utilisateur', href: path('admin_user_new'), variant: 'primary', icon: 'plus' }] : [] } only %}
{% endif %}
{% endblock %}