```
✨ feat(templates/cota.twig): Ajoute template pour confirmation cotisation ✨ feat(templates/admin/dashboard.twig): Affiche stats membres et commandes 🐛 fix(src/Controller/WebhooksController.php): Gère paiement et reçu cotisation ✨ feat(src/Service/Payments/PaymentClient.php): Ajoute paiement cotisation ✨ feat(.env): Met à jour URL de dev ✨ feat(src/Controller/Admin/AdminController.php): Ajoute validation et lien paiement ✨ feat(src/Controller/DonsController.php): Ajoute route validation cotisation ✨ feat(assets/admin.js): Ajoute assets admin ✨ feat(templates/form_admin.twig): Ajoute thème formulaire admin ✨ feat(assets/admin.scss): Ajoute style admin ✨ feat(src/Service/Pdf/CotaReceiptGenerator.php): Génère reçu de cotisation ✨ feat(src/Form/MembersType.php): Ajoute champs et options formulaire membre ✨ feat(templates/admin/base.twig): Ajoute base admin ✨ feat(templates/admin/member/add.twig): Ajoute template ajout/édition membre ✨ feat(src/Entity/Members.php): Ajoute champs et relations entité Membre ✨ feat(templates/admin/members.twig): Affiche liste membres ✨ feat(templates/mails/coti_payment.twig): Ajoute template mail paiement cotisation ✨ feat(src/Controller/MembersController.php): Filtre membres actifs ✨ feat(templates/mails/cota_validation.twig): Ajoute template mail validation cota ```
This commit is contained in:
@@ -1,138 +1,144 @@
|
||||
{# Assurez-vous d'utiliser une version de Tailwind CSS qui supporte ces classes #}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Administration{% endblock %} - E-Cosplay</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
{# 🛑 NO INDEX DIRECTIVE #}
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<title>{% block title %}Admin Dashboard{% endblock %} | Mon App</title>
|
||||
<!-- Chargement de l'entrée principale de Vite pour les CSS et le JS -->
|
||||
{{ vite_asset('admin.js', []) }}
|
||||
|
||||
{# 🎨 Tailwind CSS Integration (Utilisation du CDN pour la démo) #}
|
||||
{{ vite_asset('app.js',[]) }}
|
||||
<!-- Tailwind CSS est inclus via le fichier app.css/app.js géré par Vite -->
|
||||
{% block stylesheets %}{% endblock %}
|
||||
|
||||
|
||||
</head>
|
||||
<!-- DARK MODE: bg-gray-900 pour le fond principal -->
|
||||
<!-- La body reste un conteneur flex horizontal -->
|
||||
<body class="bg-gray-900 min-h-screen flex antialiased">
|
||||
|
||||
{# Utiliser un layout en grille ou flex pour organiser la sidebar et le contenu #}
|
||||
<body class="bg-gray-900 font-sans">
|
||||
<!-- 1. Sidebar (Barre Latérale) - bg-gray-800 -->
|
||||
|
||||
<div id="admin-layout" class="flex h-screen">
|
||||
<!-- MODIFICATION MAJEURE : Suppression de 'fixed' et 'h-screen' de la sidebar.
|
||||
Elle défilera donc avec le reste du contenu de la page.
|
||||
L'overflow-y-auto n'est plus nécessaire ici car c'est le <body> qui gère le scroll. -->
|
||||
|
||||
{# 1. BARRE LATÉRALE (SIDEBAR - FOND BLANC) #}
|
||||
<aside class="w-64 bg-gray-700 shadow-xl flex-shrink-0">
|
||||
<div class="h-full flex flex-col justify-between">
|
||||
<aside id="sidebar" class="w-64 bg-gray-800 text-white flex-shrink-0 transition-transform duration-300 transform -translate-x-full md:translate-x-0 z-40">
|
||||
<div class="p-6">
|
||||
<h1 class="text-3xl font-bold tracking-wider">E-Cosplay</h1>
|
||||
</div>
|
||||
|
||||
{# Contenu de la navigation #}
|
||||
<nav class="p-4 space-y-6 flex-1 overflow-y-auto">
|
||||
<nav class="mt-8">
|
||||
<!-- Dashboard -->
|
||||
<a href="{{ path('admin_dashboard') }}" class="flex items-center py-2 px-6 text-gray-400 hover:bg-gray-700 hover:text-white transition duration-200 rounded-r-lg">
|
||||
<svg class="w-5 h-5 mr-3" 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="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg>
|
||||
Dashboard
|
||||
</a>
|
||||
|
||||
{# Titre/Logo #}
|
||||
<div class="text-3xl font-extrabold text-indigo-600 border-b border-gray-200 pb-4 mb-2">
|
||||
E-Cosplay
|
||||
</div>
|
||||
<!-- Membres (Utilisateurs) -->
|
||||
<a href="{{ path('admin_members') }}" class="flex items-center py-2 px-6 text-gray-400 hover:bg-gray-700 hover:text-white transition duration-200 mt-1 rounded-r-lg">
|
||||
<svg class="w-5 h-5 mr-3" 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 20h5v-2a3 3 0 00-5.356-1.857M9 20H4v-2a3 3 0 015-2.236M9 20v-2a3 3 0 00-5-2.236M9 20h5m-5 0h5M12 4a4 4 0 100 8 4 4 0 000-8z"></path></svg>
|
||||
Membres
|
||||
</a>
|
||||
|
||||
{# --- Lien Principal : Dashboard --- #}
|
||||
<p class="text-xs font-semibold uppercase text-white pt-4 pb-2">Général</p>
|
||||
<!-- Produits -->
|
||||
<a href="{{ path('admin_products') }}" class="flex items-center py-2 px-6 text-gray-400 hover:bg-gray-700 hover:text-white transition duration-200 mt-1 rounded-r-lg">
|
||||
<svg class="w-5 h-5 mr-3" 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="M12 8c1.657 0 3 .895 3 2s-1.343 2-3 2-3 .895-3 2 1.343 2 3 2m0-8h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
Produits
|
||||
</a>
|
||||
|
||||
<a href="{{ path('admin_dashboard') }}"
|
||||
class="block px-3 py-2 rounded-lg transition duration-150 ease-in-out text-white
|
||||
{% if app.request.attributes.get('_route') == 'admin_dashboard' %}
|
||||
bg-indigo-500 font-bold
|
||||
{% else %}
|
||||
hover:bg-gray-700
|
||||
{% endif %}">
|
||||
Dashboard
|
||||
</a>
|
||||
<!-- Événements -->
|
||||
<a href="{{ path('admin_events') }}" class="flex items-center py-2 px-6 text-gray-400 hover:bg-gray-700 hover:text-white transition duration-200 mt-1 rounded-r-lg">
|
||||
<svg class="w-5 h-5 mr-3" 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="M8 7V3m8 4V3m-9 8h.01M16 11h.01M9 16h.01M15 16h.01M16 12h-8m8 4H8m8 4H8m-5 4h18a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>
|
||||
Événements
|
||||
</a>
|
||||
|
||||
{# --- SECTION : COMMUNAUTÉ --- #}
|
||||
<p class="text-xs font-semibold uppercase text-white pt-4 pb-2">Gestion Communauté</p>
|
||||
<!-- AG (Assemblée Générale) -->
|
||||
<a href="{{ path('admin_ag') }}" class="flex items-center py-2 px-6 text-gray-400 hover:bg-gray-700 hover:text-white transition duration-200 mt-1 rounded-r-lg">
|
||||
<svg class="w-5 h-5 mr-3" 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="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.523 5.754 18 7.5 18s3.332.477 4.5 1.247m0-13C13.168 5.477 14.754 5 16.5 5s3.332.477 4.5 1.253v13C19.832 18.523 18.246 18 16.5 18s-3.332.477-4.5 1.247"></path></svg>
|
||||
AG (Assemblée Générale)
|
||||
</a>
|
||||
|
||||
{# MEMBRES #}
|
||||
<a href="{{ path('admin_members') }}"
|
||||
class="block px-3 py-2 rounded-lg transition duration-150 ease-in-out text-white
|
||||
{% if 'members' in app.request.attributes.get('_route') %}
|
||||
bg-indigo-500 font-bold
|
||||
{% else %}
|
||||
hover:bg-gray-700
|
||||
{% endif %}">
|
||||
Membres
|
||||
</a>
|
||||
{# MEMBRES #}
|
||||
<a href="{{ path('admin_products') }}"
|
||||
class="block px-3 py-2 rounded-lg transition duration-150 ease-in-out text-white
|
||||
{% if 'product' in app.request.attributes.get('_route') %}
|
||||
bg-indigo-500 font-bold
|
||||
{% else %}
|
||||
hover:bg-gray-700
|
||||
{% endif %}">
|
||||
Produits
|
||||
</a>
|
||||
{# ÉVÉNEMENTS #}
|
||||
<a href="{{ path('admin_events') }}"
|
||||
class="block px-3 py-2 rounded-lg transition duration-150 ease-in-out text-white
|
||||
{% if 'events' in app.request.attributes.get('_route') %}
|
||||
bg-indigo-500 font-bold
|
||||
{% else %}
|
||||
hover:bg-gray-700
|
||||
{% endif %}">
|
||||
Événements
|
||||
</a>
|
||||
{# Le lien Paramètres a été supprimé #}
|
||||
|
||||
{# --- SECTION : ADMINISTRATION --- #}
|
||||
<p class="text-xs font-semibold uppercase text-white pt-4 pb-2">Administration</p>
|
||||
{# Ajoutez d'autres liens ici #}
|
||||
</nav>
|
||||
|
||||
{# COMPTES ADMINISTRATEUR #}
|
||||
<a href="{{ path('admin_accounts_list') }}"
|
||||
class="block px-3 py-2 rounded-lg transition duration-150 ease-in-out text-white
|
||||
{% if 'accounts_list' in app.request.attributes.get('_route') %}
|
||||
bg-indigo-500 font-bold
|
||||
{% else %}
|
||||
hover:bg-gray-700
|
||||
{% endif %}">
|
||||
Comptes Administrateur
|
||||
</a>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{# Bouton Déconnexion (pied de page de la sidebar) #}
|
||||
<div class="p-4 border-t border-gray-200">
|
||||
<a href="{{ path('app_logout') }}"
|
||||
class="block px-3 py-2 rounded-lg text-red-500 hover:bg-red-50 transition duration-150 ease-in-out">
|
||||
<!-- 2. Contenu Principal et Navigation Supérieure -->
|
||||
|
||||
<!-- Le conteneur principal reste flex-col et prend l'espace restant -->
|
||||
|
||||
<div id="main-content" class="flex flex-col flex-1">
|
||||
|
||||
<!-- 2.1. Navbar (Barre de Navigation Supérieure) - bg-gray-800 -->
|
||||
<!-- La navbar est toujours sticky pour rester en haut de l'écran lors du défilement -->
|
||||
<header class="bg-gray-800 shadow-xl p-4 flex justify-between items-center sticky top-0 z-30">
|
||||
<!-- Bouton pour afficher/cacher la sidebar sur mobile -->
|
||||
<button id="sidebar-toggle" class="text-gray-400 md:hidden p-2 rounded-md hover:bg-gray-700 focus:outline-none">
|
||||
<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 12h16m-7 6h7"></path></svg>
|
||||
</button>
|
||||
|
||||
<!-- Titre de la page (Visible uniquement ici pour la cohérence) -->
|
||||
<h2 class="text-xl font-semibold text-white hidden sm:block">
|
||||
{% block page_title %}Tableau de Bord{% endblock %}
|
||||
</h2>
|
||||
|
||||
<!-- Profil et Déconnexion -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex items-center cursor-pointer group">
|
||||
<span class="text-gray-200 font-medium hidden sm:inline">
|
||||
{# Assurez-vous que app.user est défini #}
|
||||
{% if app.user is defined and app.user %}
|
||||
{{ app.user.username }}
|
||||
{% else %}
|
||||
Admin
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
<!-- Bouton Déconnexion -->
|
||||
<a href="{{ path('app_logout') }}" class="ml-4 p-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition duration-150 flex items-center shadow-md">
|
||||
<svg class="w-5 h-5 mr-1" 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 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path></svg>
|
||||
Déconnexion
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</header>
|
||||
|
||||
{# 2. CONTENU PRINCIPAL (Inclut la Topbar) #}
|
||||
<div class="flex-1 flex flex-col overflow-hidden">
|
||||
<!-- 2.2. Contenu de la Page (Titre et Corps) -->
|
||||
<!-- Retrait de overflow-y-auto pour que le <body> défile -->
|
||||
<main class="p-6 flex-1">
|
||||
|
||||
{# 3. TOP BAR (BARRE SUPÉRIEURE) #}
|
||||
<header class="w-full bg-gray-800 shadow-md p-4 flex justify-end items-center flex-shrink-0">
|
||||
<!-- Affichage du Titre de la Page (pour les écrans larges) -->
|
||||
<h1 class="text-3xl font-bold text-white mb-6 hidden sm:block">
|
||||
{{ block('page_title') }}
|
||||
</h1>
|
||||
|
||||
{# Remplacer 'current_user.name' par la variable Twig de votre session #}
|
||||
<div class="text-white font-medium">
|
||||
Bienvenue, {{ app.user.username }}
|
||||
</div>
|
||||
<!-- Le contenu réel de la page s'insère ici -->
|
||||
<div class="w-full">
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{# Vous pourriez ajouter ici un bouton de profil ou un dropdown #}
|
||||
</header>
|
||||
<!-- 2.3. Footer (Pied de Page) - bg-gray-800 -->
|
||||
<footer class="p-4 bg-gray-800 border-t border-gray-700 text-center text-sm text-gray-400 flex-shrink-0">
|
||||
Développé par SARL SITECONSEIL. Tous droits réservés.
|
||||
<a href="https://www.siteconseil.fr/" target="_blank" rel="noopener noreferrer" class="text-indigo-400 hover:text-indigo-300 transition duration-150 font-medium ml-1">
|
||||
https://www.siteconseil.fr/
|
||||
</a>
|
||||
</footer>
|
||||
|
||||
{# ZONE DE CONTENU PRINCIPALE #}
|
||||
<main class="flex-1 overflow-x-hidden overflow-y-auto p-6">
|
||||
<h2 class="text-3xl font-semibold text-white mb-6">
|
||||
{% block page_title %}{% endblock %}
|
||||
</h2>
|
||||
|
||||
<div class="bg-gray-700 shadow-md rounded-lg min-h-full">
|
||||
{% block body %}
|
||||
|
||||
{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Script pour la gestion du menu mobile -->
|
||||
|
||||
|
||||
{% block javascripts %}{% endblock %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,3 +1,77 @@
|
||||
{% extends 'admin/base.twig' %}
|
||||
{% block title %}Tableau de bord{% endblock %}
|
||||
{% block page_title %}Tableau de bord{% endblock %}
|
||||
|
||||
{# Définition des titres pour le navigateur et l'affichage #}
|
||||
{% block title 'Tableau de bord' %}
|
||||
{% block page_title 'Tableau de bord' %}
|
||||
|
||||
{# Variables de données simulées (MOCK DATA) - À REMPLACER par les vraies variables passées au template #}
|
||||
{% set pendingOrdersCount = 0 %}
|
||||
|
||||
{% block body %}
|
||||
<div class="space-y-8">
|
||||
{# Section des Cartes de Statistiques (KPIs) #}
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
|
||||
{# Carte 1: Nombre de Membres #}
|
||||
<div class="bg-gray-800 p-6 rounded-xl shadow-2xl border border-gray-700 hover:border-indigo-500 transition duration-300">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-indigo-400">
|
||||
<svg class="w-8 h-8" 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 20h5v-2a3 3 0 00-5.356-1.857M9 20H4v-2a3 3 0 015-2.236M9 20v-2a3 3 0 00-5-2.236M9 20h5m-5 0h5M12 4a4 4 0 100 8 4 4 0 000-8z"></path></svg>
|
||||
</div>
|
||||
<p class="text-sm font-medium text-gray-400">
|
||||
Membres de l'association
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p class="text-4xl font-extrabold text-white">
|
||||
{{ memberCount | number_format(0, ',', ' ') }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Total des inscriptions actives.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Carte 2: Commandes en Attente #}
|
||||
<div class="bg-gray-800 p-6 rounded-xl shadow-2xl border border-gray-700 hover:border-red-500 transition duration-300">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-red-400">
|
||||
<svg class="w-8 h-8" 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 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"></path></svg>
|
||||
</div>
|
||||
<p class="text-sm font-medium text-gray-400">
|
||||
Commandes en attente
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p class="text-4xl font-extrabold text-white">
|
||||
{{ pendingOrdersCount }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Nécessitent un traitement.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{# Carte 4: Espace pour un autre KPI (ex: Nouveaux événements) #}
|
||||
<div class="bg-gray-800 p-6 rounded-xl shadow-2xl border border-gray-700 hover:border-yellow-500 transition duration-300">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-yellow-400">
|
||||
<svg class="w-8 h-8" 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="M8 7V3m8 4V3m-9 8h.01M16 11h.01M9 16h.01M15 16h.01M16 12h-8m8 4H8m8 4H8m-5 4h18a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>
|
||||
</div>
|
||||
<p class="text-sm font-medium text-gray-400">
|
||||
Événements à venir
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p class="text-4xl font-extrabold text-white">
|
||||
0
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Planifiés pour les 3 prochains mois.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,99 +1,138 @@
|
||||
{% extends 'admin/base.twig' %}
|
||||
|
||||
{% block title %}Ajouter/Éditer un Membre{% endblock %}
|
||||
{% block page_title %}
|
||||
{{ form.vars.value.id ? 'Éditer le Membre' : 'Créer un nouveau Membre' }}
|
||||
{{ form.vars.value.id ? 'Éditer le Membre: ' ~ form.vars.value.pseudo : 'Créer un nouveau Membre' }}
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<div class="mx-auto bg-white p-8 rounded-lg">
|
||||
{# --- APPLICATION DU THÈME DE FORMULAIRE DEMANDÉ --- #}
|
||||
{% form_theme form 'form_admin.twig' %}
|
||||
|
||||
{{ form_start(form, {'attr': {'class': 'space-y-6'}}) }}
|
||||
{# Définir l'entité pour la rendre plus facile à référencer, notamment pour vich_uploader_asset #}
|
||||
{% set member = form.vars.value %}
|
||||
|
||||
{# --- SECTION 1: Informations de base --- #}
|
||||
<h3 class="text-xl font-semibold text-gray-700 border-b pb-2 mb-4">Informations Principales</h3>
|
||||
{# Conteneur principal #}
|
||||
<div class="w-full mx-auto bg-gray-800 p-4 sm:p-8 md:p-10 rounded-xl shadow-2xl border border-gray-700">
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{{ form_start(form, {'attr': {'class': 'space-y-8'}}) }}
|
||||
|
||||
{# Pseudo #}
|
||||
{{ form_row(form.pseudo, {'attr': {'class': 'w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500'}}) }}
|
||||
{# --- SECTION 1: Informations de base (Grille de 3 colonnes sur grand écran) --- #}
|
||||
<h3 class="text-2xl font-bold text-white border-b border-gray-700 pb-3 mb-6">
|
||||
Informations Principales
|
||||
</h3>
|
||||
|
||||
{# Rôle #}
|
||||
{{ form_row(form.role, {'attr': {'class': 'w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500'}}) }}
|
||||
{# Grille de 3 colonnes sur les grands écrans (lg:grid-cols-3) #}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
|
||||
{# Pseudo - Utilise le style défini dans form_admin.twig #}
|
||||
<div>
|
||||
{{ form_row(form.pseudo) }}
|
||||
</div>
|
||||
{# Rôle - Utilise le style défini dans form_admin.twig #}
|
||||
<div>
|
||||
{{ form_row(form.role) }}
|
||||
</div>
|
||||
|
||||
{# Date de Rejoint - Utilise le style défini dans form_admin.twig #}
|
||||
<div>
|
||||
{{ form_row(form.joinedAt) }}
|
||||
</div>
|
||||
|
||||
{# Statut - Utilise le style défini dans form_admin.twig #}
|
||||
<div>
|
||||
{{ form_row(form.status) }}
|
||||
</div>
|
||||
|
||||
{# Orientation - Utilise le style défini dans form_admin.twig #}
|
||||
<div>
|
||||
{{ form_row(form.orientation) }}
|
||||
</div>
|
||||
<div>
|
||||
{{ form_row(form.email) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# L'élément LABEL est stylisé pour devenir le cercle cliquable #}
|
||||
<label for="{{ form.members.vars.id }}"
|
||||
class="relative cursor-pointer bg-gray-200 rounded-full w-28 h-28
|
||||
flex items-center justify-center border-2 border-dashed border-gray-400
|
||||
hover:border-indigo-600 transition duration-300 overflow-hidden">
|
||||
{# --- SECTION IMAGE DE PROFIL / UPLOAD --- #}
|
||||
<h3 class="text-2xl font-bold text-white border-b border-gray-700 pb-3 pt-6 mb-6">
|
||||
Photo de Profil
|
||||
</h3>
|
||||
|
||||
{# 1. AFFICHAGE DE L'IMAGE EXISTANTE (si présente) #}
|
||||
{% set currentImageUrl = vich_uploader_asset(member,'members') %}
|
||||
{% if currentImageUrl %}
|
||||
<img src="{{ asset(currentImageUrl) }}"
|
||||
alt="Photo de profil actuelle"
|
||||
class="w-full h-full object-cover">
|
||||
<div class="flex items-center space-x-6">
|
||||
|
||||
{# Overlay pour indiquer que c'est cliquable #}
|
||||
<div class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center
|
||||
opacity-0 hover:opacity-100 transition duration-300 text-white font-bold text-xs">
|
||||
{# Zone cliquable pour l'image existante, le placeholder ou la prévisualisation #}
|
||||
<label for="{{ form.members.vars.id }}"
|
||||
class="relative cursor-pointer bg-gray-700 rounded-full w-32 h-32
|
||||
flex items-center justify-center border-2 border-dashed border-gray-600
|
||||
hover:border-indigo-500 transition duration-300 overflow-hidden shadow-lg"
|
||||
id="preview-container">
|
||||
|
||||
{% set currentImageUrl = member.memberFileName ? asset(vich_uploader_asset(member, 'members')) : '' %}
|
||||
|
||||
{# Image de prévisualisation ou Image actuelle #}
|
||||
<img id="profile-preview-img"
|
||||
src="{{ currentImageUrl }}"
|
||||
alt="Photo de profil"
|
||||
class="w-full h-full object-cover {{ currentImageUrl ? '' : 'hidden' }}">
|
||||
|
||||
{# Icône par défaut (visible seulement si aucune image actuelle) #}
|
||||
<svg id="default-user-icon"
|
||||
class="h-12 w-12 text-gray-500 {{ currentImageUrl ? 'hidden' : '' }}"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
||||
</svg>
|
||||
|
||||
|
||||
{# Overlay pour indiquer que c'est cliquable (s'applique à l'ensemble du label) #}
|
||||
<div class="absolute inset-0 bg-black bg-opacity-40 flex items-center justify-center
|
||||
opacity-0 hover:opacity-100 transition duration-300 text-white font-bold text-xs p-2 text-center">
|
||||
Cliquer pour changer
|
||||
</div>
|
||||
{% else %}
|
||||
{# 2. AFFICHAGE DE L'ICÔNE PAR DÉFAUT (si aucune image) #}
|
||||
<svg class="h-10 w-10 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 18m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
{% endif %}
|
||||
|
||||
</label>
|
||||
</label>
|
||||
|
||||
{# Champ de fichier réel (caché) #}
|
||||
{{ form_widget(form.members, {'attr': {'class': 'sr-only'}}) }}
|
||||
|
||||
<div class="flex-1">
|
||||
<p class="text-sm text-gray-400">
|
||||
Sélectionnez une nouvelle photo de profil. Le fichier actuel sera remplacé. (Max 2Mo).
|
||||
</p>
|
||||
{# Affichage des erreurs spécifiques au champ image #}
|
||||
{{ form_errors(form.members) }}
|
||||
</div>
|
||||
|
||||
{{ form_widget(form.members, {
|
||||
'attr': {
|
||||
'class': 'sr-only',
|
||||
}
|
||||
}) }}
|
||||
<div class="mt-6">
|
||||
{{ form_row(form.orientation, {'attr': {'class': 'w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500'}}) }}
|
||||
</div>
|
||||
|
||||
|
||||
{# --- SECTION 2: Tags / Booléens (Boutons Radio) --- #}
|
||||
<h3 class="text-xl font-semibold text-gray-700 border-b pb-2 pt-6 mb-4">Tags et Caractéristiques</h3>
|
||||
<h3 class="text-2xl font-bold text-white border-b border-gray-700 pb-3 pt-6 mb-6">
|
||||
Tags et Caractéristiques
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{# Utilisation d'une grille pour aligner les 3 groupes de boutons radio #}
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
|
||||
{# Crosscosplayer #}
|
||||
{{ form_row(form.crosscosplayer, {
|
||||
'label': 'Crosscosplayer ?',
|
||||
'row_attr': {'class': 'space-y-4'},
|
||||
'label_attr': {'class': 'block text-sm font-medium text-gray-700 mb-2'}
|
||||
}) }}
|
||||
{% set boolFields = [form.crosscosplayer, form.cosplayer, form.trans] %}
|
||||
|
||||
{# Cosplayer #}
|
||||
{{ form_row(form.cosplayer, {
|
||||
'label': 'Cosplayer ?',
|
||||
'row_attr': {'class': 'space-y-4'},
|
||||
'label_attr': {'class': 'block text-sm font-medium text-gray-700 mb-2'}
|
||||
}) }}
|
||||
|
||||
{# Trans #}
|
||||
{{ form_row(form.trans, {
|
||||
'label': 'Transgenre ?',
|
||||
'row_attr': {'class': 'space-y-4'},
|
||||
'label_attr': {'class': 'block text-sm font-medium text-gray-700 mb-2'}
|
||||
}) }}
|
||||
{# Utilisation de form_row() pour chaque champ booléen.
|
||||
Le thème form_admin.twig gère le conteneur de carte sombre, le label et les options radio via 'choice_widget_expanded'. #}
|
||||
{% for field in boolFields %}
|
||||
{{ form_row(field) }}
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
||||
{# --- Bouton de Soumission --- #}
|
||||
<div class="pt-6 border-t border-gray-200 mt-8">
|
||||
<button type="submit" class="w-full inline-flex justify-center py-3 px-6 border border-transparent
|
||||
rounded-md shadow-sm text-lg font-medium text-white bg-indigo-600
|
||||
hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2
|
||||
focus:ring-indigo-500 transition duration-150">
|
||||
{{ form.vars.value.id ? 'Sauvegarder les Modifications' : 'Créer le Membre' }}
|
||||
<div class="pt-8 border-t border-gray-700 mt-8">
|
||||
<button type="submit" class="w-full inline-flex justify-center py-4 px-6 border border-transparent
|
||||
rounded-xl shadow-lg text-lg font-bold text-white bg-indigo-600
|
||||
hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2
|
||||
focus:ring-indigo-500 focus:ring-offset-gray-800 transition duration-150 transform hover:scale-[1.01]">
|
||||
{{ member.id ? 'Sauvegarder les Modifications' : 'Créer le Membre' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -101,7 +140,118 @@
|
||||
|
||||
</div>
|
||||
|
||||
{# --- Rendu des erreurs et champs cachés --- #}
|
||||
{{ form_end(form) }}
|
||||
{# --- NOUVELLE SECTION: LISTE DES COTISATIONS --- #}
|
||||
{% if member.membersCotisations is defined %}
|
||||
<div class="w-full mx-auto bg-gray-800 p-4 sm:p-8 md:p-10 rounded-xl shadow-2xl border border-gray-700 mt-8">
|
||||
<h3 class="text-2xl font-bold text-white border-b border-gray-700 pb-3 mb-6">
|
||||
Liste des Cotisations
|
||||
</h3>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Période
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Montant
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Statut
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-center text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-gray-800 divide-y divide-gray-700 text-white">
|
||||
|
||||
{# Itération sur la collection de cotisations #}
|
||||
{% for cotisation in member.membersCotisations %}
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-300">
|
||||
{{ cotisation.startDate|date('d/m/Y') }} - {{ cotisation.endDate|date('d/m/Y') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">
|
||||
{{ cotisation.amount|format_currency('EUR', locale='fr') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||
{% if cotisation.isPaid %}
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-700 text-green-100">
|
||||
Payée
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-700 text-red-100">
|
||||
Non payée
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
{% if not cotisation.isPaid %}
|
||||
<a href="{{ path('admin_member_edit', {'id': member.id,'idValidateCota':cotisation.id}) }}"
|
||||
class="text-green-400 hover:text-green-200 transition duration-150 mr-3">
|
||||
Valider le paiement
|
||||
</a>
|
||||
<a href="{{ path('admin_member_edit', {'id': member.id,'idLinkCota':cotisation.id}) }}"
|
||||
class="text-indigo-400 hover:text-indigo-200 transition duration-150">
|
||||
Envoyer lien
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-gray-500">Aucune action requise</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-4 text-center text-gray-500">
|
||||
Aucune cotisation trouvée pour ce membre.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{# --- SCRIPT JAVASCRIPT POUR LA PRÉVISUALISATION D'IMAGE --- #}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Récupère l'ID réel du champ de fichier généré par Symfony
|
||||
const fileInput = document.getElementById('{{ form.members.vars.id }}');
|
||||
const previewImg = document.getElementById('profile-preview-img');
|
||||
const defaultIcon = document.getElementById('default-user-icon');
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', function() {
|
||||
// Vérifie si un fichier a été sélectionné
|
||||
if (this.files && this.files[0]) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function(e) {
|
||||
// 1. Met à jour l'attribut src de l'image avec la data URL du fichier
|
||||
previewImg.src = e.target.result;
|
||||
|
||||
// 2. Assure que l'image est visible
|
||||
previewImg.classList.remove('hidden');
|
||||
|
||||
// 3. Cache l'icône par défaut si elle est là
|
||||
if (defaultIcon) {
|
||||
defaultIcon.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Lit le contenu du fichier
|
||||
reader.readAsDataURL(this.files[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,63 +1,103 @@
|
||||
{% extends 'admin/base.twig' %}
|
||||
{% block title %}Membre(s){% endblock %}
|
||||
{% block page_title %}Liste des Membres{% endblock %}
|
||||
|
||||
{% block title 'Membre(s)' %}
|
||||
{% block page_title 'Liste des Membres' %}
|
||||
|
||||
{% block body %}
|
||||
<style>
|
||||
.dz{
|
||||
display: block;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<div class="flex justify-end">
|
||||
{# Définition d'un mappage pour traduire les rôles stockés vers les libellés affichés #}
|
||||
{% set roleDisplay = {
|
||||
'President': 'Président(e)',
|
||||
'Tresorier': 'Trésorier(e)',
|
||||
'Secretaire': 'Secrétaire(e)',
|
||||
'VicePresident': 'Vice-Président(e)',
|
||||
'TresorierAdjoint': 'Trésorier(e) Adjoint',
|
||||
'SecretaireAdjoint': 'Secrétaire(e) Adjoint',
|
||||
'Membre': 'Membre',
|
||||
} %}
|
||||
|
||||
<div class="flex justify-end mb-6">
|
||||
<a href="{{ path('admin_member_create') }}"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium
|
||||
shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2
|
||||
focus:ring-offset-2 focus:ring-indigo-500 transition duration-150 dz">
|
||||
{# Vous pouvez ajouter ici une icône si vous le souhaitez, par exemple : [Icône] #}
|
||||
class="dz items-center px-4 py-2 border border-transparent text-sm font-medium
|
||||
rounded-lg shadow-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2
|
||||
focus:ring-offset-2 focus:ring-indigo-500 transition duration-150">
|
||||
<svg class="w-5 h-5 mr-2" 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="M12 4v16m8-8H4"></path></svg>
|
||||
Créer un membre
|
||||
</a>
|
||||
</div>
|
||||
<div class="overflow-x-auto bg-gray-700">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
|
||||
|
||||
<div class="overflow-x-auto rounded-lg shadow-xl border border-gray-700">
|
||||
<table class="min-w-full divide-y divide-gray-700">
|
||||
|
||||
{# --- EN-TÊTE DU TABLEAU (HEAD) --- #}
|
||||
<thead class="bg-gray-800">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-white uppercase tracking-wider">
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Pseudo
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-white uppercase tracking-wider">
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Rôle
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-white uppercase tracking-wider">
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Rejoint le
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Statut
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{# --- CORPS DU TABLEAU (BODY) --- #}
|
||||
<tbody class="bg-gray-600 divide-y divide-gray-200">
|
||||
<tbody class="bg-gray-800 divide-y divide-gray-700">
|
||||
|
||||
{# Démonstration: Boucle sur une liste de membres (members) passée par votre contrôleur #}
|
||||
{% if members is not empty %}
|
||||
{% for member in members %}
|
||||
<tr>
|
||||
<tr class="hover:bg-gray-700 transition duration-150">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-white">{{ member.pseudo }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{# Utilisation d'un badge Tailwind pour le rôle #}
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
||||
{{ member.role }}
|
||||
</span>
|
||||
{# Détermination de la couleur en fonction du Rôle stocké #}
|
||||
{% set roleKey = member.role %}
|
||||
{% set roleColor = (roleKey == 'President' or roleKey == 'VicePresident') ? 'bg-red-500' :
|
||||
(roleKey == 'Secretaire' or roleKey == 'Tresorier') ? 'bg-indigo-500' :
|
||||
(roleKey == 'Membre') ? 'bg-gray-500' : 'bg-green-600' %}
|
||||
|
||||
{# Affichage du libellé français grâce au mappage #}
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full text-white {{ roleColor }}">
|
||||
{{ roleDisplay[roleKey] | default(roleKey) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-300">
|
||||
{# Assurez-vous que joined_at est un objet DateTime ou une chaîne Twig #}
|
||||
{{ member.joinedAt | date('d/m/Y') }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{# Détermination du badge de statut (actif, inactif, suspendu) #}
|
||||
{% set statusColor = (member.status == 'actif') ? 'bg-green-500' :
|
||||
(member.status == 'inactif') ? 'bg-yellow-500' :
|
||||
'bg-red-600' %}
|
||||
|
||||
{% set statusText = (member.status == 'actif') ? 'Actif' :
|
||||
(member.status == 'inactif') ? 'Inactif' :
|
||||
'Suspendu' %}
|
||||
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full text-white {{ statusColor }}">
|
||||
{{ statusText }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<a href="{{ path('admin_member_edit', {id: member.id}) }}" class="text-indigo-900 hover:text-indigo-900 mr-4">
|
||||
<a href="{{ path('admin_member_edit', {id: member.id}) }}"
|
||||
class="text-indigo-400 hover:text-indigo-300 mr-4 transition duration-150">
|
||||
Éditer
|
||||
</a>
|
||||
<a href="{{ path('admin_member_delete', {id: member.id}) }}" class="text-red-900 hover:text-red-900">
|
||||
<a href="{{ path('admin_member_delete', {id: member.id}) }}"
|
||||
class="text-red-400 hover:text-red-300 transition duration-150">
|
||||
Supprimer
|
||||
</a>
|
||||
</td>
|
||||
@@ -66,14 +106,15 @@
|
||||
{% else %}
|
||||
{# Message si la liste est vide #}
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-12 text-center text-gray-500">
|
||||
Aucun membre trouvé.
|
||||
<td colspan="5" class="px-6 py-12 text-center text-gray-500">
|
||||
Aucun membre trouvé. Veuillez créer un nouveau membre.
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
54
templates/cota.twig
Normal file
54
templates/cota.twig
Normal file
@@ -0,0 +1,54 @@
|
||||
{% extends 'base.twig' %}
|
||||
|
||||
{% block title %}
|
||||
Confirmation de Paiement | Votre Cotisation
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="min-h-screen bg-gray-100 flex items-center justify-center p-4">
|
||||
<div class="max-w-xl w-full bg-white shadow-2xl rounded-lg overflow-hidden border-t-4 border-green-500">
|
||||
|
||||
{# Header #}
|
||||
<div class="p-6 bg-green-500 text-white text-center">
|
||||
<svg class="w-16 h-16 mx-auto mb-3" 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 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<h1 class="text-3xl font-bold">Paiement Reçu !</h1>
|
||||
<p class="text-green-100 mt-1">Confirmation de votre Cotisation</p>
|
||||
</div>
|
||||
|
||||
{# Body Content #}
|
||||
<div class="p-6 md:p-8 space-y-4">
|
||||
<p class="text-gray-700">Cher membre,</p>
|
||||
|
||||
<p class="text-gray-800">
|
||||
Nous vous remercions sincèrement. Le paiement de votre **cotisation** a été effectué avec succès et a été enregistré dans nos systèmes.
|
||||
Votre compte est désormais à jour.
|
||||
</p>
|
||||
|
||||
{# Transaction Details #}
|
||||
<div class="bg-gray-50 border border-gray-200 p-4 rounded-lg">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-3 border-b pb-2">Détails de la transaction</h2>
|
||||
|
||||
<dl class="space-y-2 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<dt class="font-medium text-gray-600">Montant payé :</dt>
|
||||
<dd class="font-bold text-green-600">{{ payment.amount|number_format(2, ',', ' ') }} €</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{# Footer Text #}
|
||||
<p class="pt-4 text-gray-600 border-t mt-6">
|
||||
Pour toute question concernant cette transaction ou votre adhésion, n'hésitez pas à contacter notre support.
|
||||
</p>
|
||||
|
||||
<p class="text-gray-700 font-semibold">
|
||||
Cordialement,<br>
|
||||
L'équipe E-Cosplay
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
148
templates/form_admin.twig
Normal file
148
templates/form_admin.twig
Normal file
@@ -0,0 +1,148 @@
|
||||
{% use 'form_div_layout.html.twig' %}
|
||||
|
||||
{# ---------- FORM START / END ---------- #}
|
||||
{% block form_start -%}
|
||||
{{ parent() }}
|
||||
{%- endblock %}
|
||||
{% block form_end -%}
|
||||
{{ parent() }}
|
||||
{%- endblock %}
|
||||
|
||||
{# ---------- ROW ---------- #}
|
||||
{% block form_row %}
|
||||
{# La ROW est le conteneur du label, du widget et des erreurs. #}
|
||||
<div class="mb-5">
|
||||
{{ form_label(form) }}
|
||||
<div class="mt-1">
|
||||
{{ form_widget(form) }}
|
||||
</div>
|
||||
{% if not compound and not form.vars.valid %}
|
||||
{# Affiche l'erreur en bas du champ simple #}
|
||||
<p class="text-sm text-red-400 mt-1">{{ form_errors(form) }}</p>
|
||||
{% else %}
|
||||
{# Affiche l'erreur pour les champs composés (si form_errors n'est pas déjà dans le widget) #}
|
||||
{{ form_errors(form) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{# ---------- LABEL ---------- #}
|
||||
{% block form_label %}
|
||||
{% if label is not same as(false) %}
|
||||
<label for="{{ id }}" class="block text-sm font-medium text-gray-300">
|
||||
{{ label|trans({}, translation_domain) }}
|
||||
{% if required %}
|
||||
<span class="text-red-400">*</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{# ---------- ERRORS ---------- #}
|
||||
{% block form_errors %}
|
||||
{% if errors|length > 0 %}
|
||||
{# Utilisation de red-400 pour mieux ressortir sur un fond sombre #}
|
||||
<ul class="mt-1 text-sm text-red-400 list-disc list-inside">
|
||||
{% for error in errors %}
|
||||
<li>{{ error.message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{# ---------- WIDGET DISPATCH ---------- #}
|
||||
{% block form_widget %}
|
||||
{% if compound %}
|
||||
{{ block('form_widget_compound') }}
|
||||
{% else %}
|
||||
{{ block('form_widget_simple') }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{# --- STYLE COMMUN POUR WIDGETS (DARK MODE) --- #}
|
||||
|
||||
{# ---------- SIMPLE INPUTS (text, email, number...) ---------- #}
|
||||
{% block form_widget_simple %}
|
||||
{% set type = type|default('text') %}
|
||||
<input
|
||||
type="{{ type }}"
|
||||
{{ block('widget_attributes') }}
|
||||
value="{{ value }}"
|
||||
{# DARK MODE: bg-gray-700, border-gray-600, text-white #}
|
||||
class="form-input mt-1 block w-full px-3 py-2 bg-gray-700 border border-gray-600 text-white placeholder-gray-400 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm transition duration-150"
|
||||
/>
|
||||
{% endblock %}
|
||||
|
||||
{# ---------- TEXTAREA ---------- #}
|
||||
{% block textarea_widget %}
|
||||
<textarea
|
||||
{{ block('widget_attributes') }}
|
||||
{# DARK MODE: bg-gray-700, border-gray-600, text-white #}
|
||||
class="form-textarea form-input mt-1 block w-full px-3 py-2 bg-gray-700 border border-gray-600 text-white placeholder-gray-400 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm transition duration-150"
|
||||
>{{ value }}</textarea>
|
||||
{% endblock %}
|
||||
|
||||
{# ---------- SELECT ---------- #}
|
||||
{% block choice_widget_collapsed %}
|
||||
<select
|
||||
{{ block('widget_attributes') }}
|
||||
{# DARK MODE: bg-gray-700, border-gray-600, text-white #}
|
||||
class="form-select form-input mt-1 block w-full px-3 py-2 bg-gray-700 border border-gray-600 text-white rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm transition duration-150"
|
||||
>
|
||||
{% if placeholder is not none %}
|
||||
<option value="" {% if required and value is empty %}selected{% endif %}>
|
||||
{{ placeholder != '' ? (placeholder|trans({}, translation_domain)) : '' }}
|
||||
</option>
|
||||
{% endif %}
|
||||
{% for group_label, choice in choices %}
|
||||
{% if choice is iterable %}
|
||||
<optgroup label="{{ group_label|trans({}, translation_domain) }}">
|
||||
{% for nested_choice in choice %}
|
||||
<option value="{{ nested_choice.value }}" {% if nested_choice is selectedchoice(value) %}selected{% endif %}>
|
||||
{{ nested_choice.label|trans({}, translation_domain) }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% else %}
|
||||
<option value="{{ choice.value }}" {% if choice is selectedchoice(value) %}selected{% endif %}>
|
||||
{{ choice.label|trans({}, translation_domain) }}
|
||||
</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endblock %}
|
||||
|
||||
{# ---------- CHECKBOX ---------- #}
|
||||
{% block checkbox_widget %}
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox"
|
||||
{{ block('widget_attributes') }}
|
||||
{% if value not in ['', null] %} value="{{ value }}"{% endif %}
|
||||
{% if checked %}checked="checked"{% endif %}
|
||||
{# DARK MODE: bg-gray-700, border-gray-600 #}
|
||||
class="form-checkbox h-5 w-5 text-indigo-500 border-gray-600 bg-gray-700 rounded focus:ring-indigo-500">
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{# ---------- RADIO ---------- #}
|
||||
{% block radio_widget %}
|
||||
<input type="radio"
|
||||
{{ block('widget_attributes') }}
|
||||
value="{{ value }}"
|
||||
{% if checked %}checked="checked"{% endif %}
|
||||
{# DARK MODE: bg-gray-700, border-gray-600 #}
|
||||
class="form-radio h-5 w-5 text-indigo-500 border-gray-600 bg-gray-700 focus:ring-indigo-500">
|
||||
{% endblock %}
|
||||
|
||||
{# ---------- FILE ---------- #}
|
||||
{% block file_widget %}
|
||||
<input type="file"
|
||||
{{ block('widget_attributes') }}
|
||||
{# DARK MODE: Outer bg-gray-700, border-gray-600, text-gray-300 #}
|
||||
class="block w-full text-sm text-gray-300 file:mr-4 file:py-2 file:px-4
|
||||
file:rounded-md file:border-0
|
||||
file:text-sm file:font-semibold
|
||||
file:bg-indigo-600 file:text-white
|
||||
hover:file:bg-indigo-700
|
||||
bg-gray-700 border border-gray-600 rounded-md shadow-sm">
|
||||
{% endblock %}
|
||||
52
templates/mails/cota_validation.twig
Normal file
52
templates/mails/cota_validation.twig
Normal file
@@ -0,0 +1,52 @@
|
||||
{% extends 'mails/base.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<mj-section background-color="#f3f4f6" border-radius="12px" padding="20px">
|
||||
|
||||
<mj-column>
|
||||
<mj-text font-family="Helvetica, Arial, sans-serif" color="#10b981" font-size="28px" font-weight="bold" align="center" padding-bottom="20px">
|
||||
✅ Paiement Confirmé
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
|
||||
<mj-wrapper background-color="#ffffff" border-radius="8px" padding="20px" border="1px solid #e5e7eb" padding-bottom="20px">
|
||||
<mj-section padding="0">
|
||||
<mj-column width="100%">
|
||||
<mj-text font-family="Helvetica, Arial, sans-serif" color="#374151" font-size="16px" line-height="1.6" padding-bottom="0">
|
||||
Bonjour <strong>{{ datas.pseudo }}</strong>,
|
||||
</mj-text>
|
||||
|
||||
<mj-text font-family="Helvetica, Arial, sans-serif" color="#374151" font-size="16px" line-height="1.6" padding-top="0" padding-bottom="20px">
|
||||
Nous vous confirmons la bonne réception de votre paiement de cotisation. Merci beaucoup pour votre soutien ! Votre adhésion est maintenant active.
|
||||
</mj-text>
|
||||
|
||||
<mj-table padding="5px 0" width="100%" color="#374151" font-size="14px">
|
||||
<tr style="border-bottom: 1px solid #e5e7eb;">
|
||||
<td style="font-weight: bold; padding: 10px 0;">Période d'Adhésion :</td>
|
||||
<td style="text-align: right; padding: 10px 0;">Du {{ datas.start_at|date('d/m/Y') }} au {{ datas.end_at|date('d/m/Y') }}</td>
|
||||
</tr>
|
||||
</mj-table>
|
||||
|
||||
<mj-table padding="5px 0" width="100%">
|
||||
<tr>
|
||||
<td style="font-weight: bold; padding: 10px 0; color: #374151; font-size: 14px;">Montant Payé :</td>
|
||||
<td style="text-align: right; padding: 10px 0; color: #10b981; font-size: 18px; font-weight: bold;">{{ datas.amount|format_currency('EUR', locale='fr') }}</td>
|
||||
</tr>
|
||||
</mj-table>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-wrapper>
|
||||
|
||||
<mj-column>
|
||||
|
||||
<mj-text font-family="Helvetica, Arial, sans-serif" color="#6b7280" font-size="12px" align="center" padding-top="15px">
|
||||
Une question ? Contactez-nous.
|
||||
</mj-text>
|
||||
|
||||
<mj-text font-family="Helvetica, Arial, sans-serif" color="#6b7280" font-size="12px" align="center" padding-top="10px">
|
||||
Cet e-mail est envoyé automatiquement.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
|
||||
</mj-section>
|
||||
{% endblock %}
|
||||
70
templates/mails/coti_payment.twig
Normal file
70
templates/mails/coti_payment.twig
Normal file
@@ -0,0 +1,70 @@
|
||||
{% extends 'mails/base.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Section principale - Fond clair (Light Gray) -->
|
||||
<mj-section background-color="#f3f4f6" border-radius="12px" padding="20px">
|
||||
|
||||
<!-- Colonne pour le Titre -->
|
||||
<mj-column>
|
||||
<!-- Titre -->
|
||||
<mj-text font-family="Helvetica, Arial, sans-serif" color="#4f46e5" font-size="28px" font-weight="bold" align="center" padding-bottom="20px">
|
||||
Votre Cotisation Annuelle
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
|
||||
<!-- Carte d'information (fond blanc) - Correction: Remplacement de <mj-group> par <mj-wrapper> -->
|
||||
<mj-wrapper background-color="#ffffff" border-radius="8px" padding="20px" css-class="card-container" border="1px solid #e5e7eb" padding-bottom="20px">
|
||||
<mj-section padding="0">
|
||||
<mj-column width="100%">
|
||||
<!-- Texte principal : couleur sombre -->
|
||||
<mj-text font-family="Helvetica, Arial, sans-serif" color="#374151" font-size="16px" line-height="1.6" padding-bottom="0">
|
||||
Bonjour <strong>{{ datas.pseudo }}</strong>,
|
||||
</mj-text>
|
||||
|
||||
<mj-text font-family="Helvetica, Arial, sans-serif" color="#374151" font-size="16px" line-height="1.6" padding-top="0" padding-bottom="20px">
|
||||
Nous vous remercions de votre engagement. Veuillez trouver ci-dessous les détails de votre cotisation pour la période à venir.
|
||||
</mj-text>
|
||||
|
||||
<!-- Détails de la Cotisation : Période -->
|
||||
<mj-table padding="5px 0" width="100%" color="#374151" font-size="14px">
|
||||
<!-- Bordure très claire -->
|
||||
<tr style="border-bottom: 1px solid #e5e7eb;">
|
||||
<td style="font-weight: bold; padding: 10px 0;">Période :</td>
|
||||
<td style="text-align: right; padding: 10px 0;">Du {{ datas.start_at|date('d/m/Y') }} au {{ datas.end_at|date('d/m/Y') }}</td>
|
||||
</tr>
|
||||
</mj-table>
|
||||
|
||||
<!-- Détails de la Cotisation : Montant -->
|
||||
<mj-table padding="5px 0" width="100%">
|
||||
<tr>
|
||||
<td style="font-weight: bold; padding: 10px 0; color: #374151; font-size: 14px;">Montant total :</td>
|
||||
<!-- Couleur d'accentuation verte conservée -->
|
||||
<td style="text-align: right; padding: 10px 0; color: #10b981; font-size: 18px; font-weight: bold;">{{ datas.amount|format_currency('EUR', locale='fr') }}</td>
|
||||
</tr>
|
||||
</mj-table>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-wrapper>
|
||||
|
||||
<!-- Colonne pour le Bouton et le bas de page -->
|
||||
<mj-column>
|
||||
<!-- Bouton de Paiement (couleur d'accentuation conservée) -->
|
||||
<mj-button href="{{ datas.link }}" background-color="#4f46e5" color="#ffffff" font-size="16px" font-weight="bold" border-radius="8px" padding-top="25px" padding-bottom="25px">
|
||||
Procéder au Paiement Sécurisé
|
||||
</mj-button>
|
||||
|
||||
<!-- Note de bas de page : couleur de texte gris moyen -->
|
||||
<mj-text font-family="Helvetica, Arial, sans-serif" color="#6b7280" font-size="12px" align="center" padding-top="15px">
|
||||
Si le bouton ne fonctionne pas, veuillez copier-coller ce lien dans votre navigateur :<br>
|
||||
<a href="{{ datas.link }}" style="color: #4f46e5; text-decoration: underline; word-break: break-all;">{{ datas.link }}</a>
|
||||
</mj-text>
|
||||
|
||||
<mj-text font-family="Helvetica, Arial, sans-serif" color="#6b7280" font-size="12px" align="center" padding-top="10px">
|
||||
Cet e-mail est envoyé automatiquement. Merci de ne pas y répondre.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
|
||||
</mj-section>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user