441 lines
38 KiB
HTML
441 lines
38 KiB
HTML
<!DOCTYPE html>
|
|
<html xmlns:th="http://www.thymeleaf.org">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Adhérents - AS Talange</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://unpkg.com/htmx.org@1.9.11"></script>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
body { font-family: 'Inter', sans-serif; }
|
|
</style>
|
|
<meta name="_csrf" th:content="${_csrf.token}"/>
|
|
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>
|
|
</head>
|
|
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
|
|
|
|
<!-- Sidebar -->
|
|
<div th:replace="~{fragments/sidebar :: sidebar}"></div>
|
|
|
|
<!-- Main Content -->
|
|
<main class="flex-1 flex flex-col h-screen overflow-hidden">
|
|
<!-- Header -->
|
|
<header class="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6">
|
|
<h2 class="text-lg font-semibold text-gray-800">Gestion des Adhérents</h2>
|
|
<button class="md:hidden text-gray-500 hover:text-gray-700">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
|
|
</button>
|
|
</header>
|
|
|
|
<!-- Main section -->
|
|
<div class="flex-1 overflow-auto p-6">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h3 class="text-xl font-bold text-gray-900">Liste des Adhérents</h3>
|
|
<div class="flex items-center space-x-3">
|
|
<form th:action="@{/adherents/sporteasy-invite-batch}" method="post" onsubmit="return confirm('Envoyer les invitations SportEasy à tous les adhérents non invités de la saison active ?');">
|
|
<button type="submit" class="bg-indigo-50 text-indigo-700 hover:bg-indigo-100 border border-indigo-200 px-4 py-2 rounded-lg text-sm font-medium transition-colors flex items-center space-x-1.5 shadow-2xs">
|
|
<svg class="w-4 h-4 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>
|
|
<span>Inviter sur SportEasy</span>
|
|
</button>
|
|
</form>
|
|
<a th:href="@{/adherents/new}" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors inline-block">
|
|
+ Nouvel Adhérent
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Messages d'information -->
|
|
<div th:if="${successMessage}" class="mb-4 p-4 bg-green-50 border-l-4 border-green-500 rounded-lg text-sm text-green-800 font-medium">
|
|
<span th:text="${successMessage}"></span>
|
|
</div>
|
|
<div th:if="${errorMessage}" class="mb-4 p-4 bg-red-50 border-l-4 border-red-500 rounded-lg text-sm text-red-800 font-medium">
|
|
<span th:text="${errorMessage}"></span>
|
|
</div>
|
|
<div th:if="${infoMessage}" class="mb-4 p-4 bg-blue-50 border-l-4 border-blue-500 rounded-lg text-sm text-blue-800 font-medium">
|
|
<span th:text="${infoMessage}"></span>
|
|
</div>
|
|
|
|
<!-- Table Container with Wrapper-level HTMX triggers for filters -->
|
|
<div id="adherents-table-container"
|
|
hx-get="/adherents"
|
|
hx-target="this"
|
|
hx-select="#adherents-table-container"
|
|
hx-trigger="change from:#filter-row, keyup changed delay:300ms from:#filter-row"
|
|
hx-include="#filter-row input, #filter-row select, input[name='query']"
|
|
hx-sync="this:replace"
|
|
hx-on::config-request="if (event.detail.trigger.tagName !== 'BUTTON') { event.detail.parameters['page'] = 0; }"
|
|
class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
|
|
|
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
|
|
<form hx-get="/adherents" hx-target="#adherents-table-container" hx-select="#adherents-table-container" class="flex items-center space-x-2">
|
|
<input type="text" name="query" th:value="${searchQuery}" placeholder="Nom, Prénom ou N° Licence..."
|
|
class="border border-gray-300 rounded-lg px-4 py-2 text-sm w-64 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
<button type="submit" class="bg-gray-100 text-gray-700 px-4 py-2 rounded-lg text-sm font-medium hover:bg-gray-200 border border-gray-300 transition-colors">Chercher</button>
|
|
<a th:if="${searchQuery != null and !searchQuery.isEmpty()}" th:href="@{/adherents}" class="text-sm text-gray-500 hover:text-gray-700 underline underline-offset-2 ml-2">Effacer</a>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-left border-collapse min-w-[1150px]">
|
|
<thead>
|
|
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
|
|
<th class="py-3 px-6 font-medium text-left cursor-pointer hover:bg-gray-100 transition-colors group select-none" onclick="updateSort('nom')">
|
|
<div class="flex items-center space-x-1">
|
|
<span>Nom</span>
|
|
<span th:if="${sortField != 'nom'}"><svg class="w-3.5 h-3.5 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l4-4 4 4m0 6l-4 4-4-4"></path></svg></span>
|
|
<span th:if="${sortField == 'nom'}">
|
|
<svg th:if="${sortDirection == 'asc'}" class="w-3.5 h-3.5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 15l7-7 7 7"></path></svg>
|
|
<svg th:if="${sortDirection == 'desc'}" class="w-3.5 h-3.5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"></path></svg>
|
|
</span>
|
|
</div>
|
|
</th>
|
|
<th class="py-3 px-6 font-medium text-left cursor-pointer hover:bg-gray-100 transition-colors group select-none" onclick="updateSort('prenom')">
|
|
<div class="flex items-center space-x-1">
|
|
<span>Prénom</span>
|
|
<span th:if="${sortField != 'prenom'}"><svg class="w-3.5 h-3.5 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l4-4 4 4m0 6l-4 4-4-4"></path></svg></span>
|
|
<span th:if="${sortField == 'prenom'}">
|
|
<svg th:if="${sortDirection == 'asc'}" class="w-3.5 h-3.5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 15l7-7 7 7"></path></svg>
|
|
<svg th:if="${sortDirection == 'desc'}" class="w-3.5 h-3.5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"></path></svg>
|
|
</span>
|
|
</div>
|
|
</th>
|
|
<th class="py-3 px-6 font-medium text-left cursor-pointer hover:bg-gray-100 transition-colors group select-none" onclick="updateSort('categorie')">
|
|
<div class="flex items-center space-x-1">
|
|
<span>Catégorie</span>
|
|
<span th:if="${sortField != 'categorie'}"><svg class="w-3.5 h-3.5 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l4-4 4 4m0 6l-4 4-4-4"></path></svg></span>
|
|
<span th:if="${sortField == 'categorie'}">
|
|
<svg th:if="${sortDirection == 'asc'}" class="w-3.5 h-3.5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 15l7-7 7 7"></path></svg>
|
|
<svg th:if="${sortDirection == 'desc'}" class="w-3.5 h-3.5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"></path></svg>
|
|
</span>
|
|
</div>
|
|
</th>
|
|
<th class="py-3 px-4 font-medium text-center">Équipements</th>
|
|
<th class="py-3 px-6 font-medium text-center cursor-pointer hover:bg-gray-100 transition-colors group select-none" onclick="updateSort('licence')">
|
|
<div class="flex items-center justify-center space-x-1">
|
|
<span>N° Licence</span>
|
|
<span th:if="${sortField != 'licence'}"><svg class="w-3.5 h-3.5 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l4-4 4 4m0 6l-4 4-4-4"></path></svg></span>
|
|
<span th:if="${sortField == 'licence'}">
|
|
<svg th:if="${sortDirection == 'asc'}" class="w-3.5 h-3.5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 15l7-7 7 7"></path></svg>
|
|
<svg th:if="${sortDirection == 'desc'}" class="w-3.5 h-3.5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"></path></svg>
|
|
</span>
|
|
</div>
|
|
</th>
|
|
<th class="py-3 px-6 font-medium text-left cursor-pointer hover:bg-gray-100 transition-colors group select-none" onclick="updateSort('email')">
|
|
<div class="flex items-center space-x-1">
|
|
<span>Email</span>
|
|
<span th:if="${sortField != 'email'}"><svg class="w-3.5 h-3.5 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l4-4 4 4m0 6l-4 4-4-4"></path></svg></span>
|
|
<span th:if="${sortField == 'email'}">
|
|
<svg th:if="${sortDirection == 'asc'}" class="w-3.5 h-3.5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 15l7-7 7 7"></path></svg>
|
|
<svg th:if="${sortDirection == 'desc'}" class="w-3.5 h-3.5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"></path></svg>
|
|
</span>
|
|
</div>
|
|
</th>
|
|
<th class="py-3 px-6 font-medium text-center">Paiement</th>
|
|
<th class="py-3 px-4 font-medium text-center">SportEasy</th>
|
|
<th class="py-3 px-6 font-medium text-right">Actions</th>
|
|
</tr>
|
|
<!-- Filter Row -->
|
|
<tr id="filter-row" class="bg-gray-100 border-b border-gray-200 text-xs">
|
|
<input type="hidden" name="sortField" id="sortField" th:value="${sortField}">
|
|
<input type="hidden" name="sortDirection" id="sortDirection" th:value="${sortDirection}">
|
|
<td class="py-2 px-3">
|
|
<input type="text" id="filterNom" name="nom" th:value="${nomFilter}" placeholder="Filtrer Nom..." class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none">
|
|
</td>
|
|
<td class="py-2 px-3">
|
|
<input type="text" id="filterPrenom" name="prenom" th:value="${prenomFilter}" placeholder="Filtrer Prénom..." class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none">
|
|
</td>
|
|
<td class="py-2 px-3">
|
|
<select id="filterCategorie" name="categorie" class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none">
|
|
<option value="">Toutes</option>
|
|
<option th:each="cat : ${categories}" th:value="${cat.nom}" th:text="${cat.nom}" th:selected="${categorieFilter == cat.nom}"></option>
|
|
</select>
|
|
</td>
|
|
<td class="py-2 px-3">
|
|
<select id="filterEquipements" name="equipements" class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none">
|
|
<option value="">Tous</option>
|
|
<option value="TOUT_FOURNI" th:selected="${equipementsFilter == 'TOUT_FOURNI'}">Complet (Tous)</option>
|
|
<option value="PARTIEL" th:selected="${equipementsFilter == 'PARTIEL'}">Partiel</option>
|
|
<option value="NON_FOURNI" th:selected="${equipementsFilter == 'NON_FOURNI'}">Non fourni</option>
|
|
</select>
|
|
</td>
|
|
<td class="py-2 px-3">
|
|
<input type="text" id="filterLicence" name="licence" th:value="${licenceFilter}" placeholder="Filtrer N°..." class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none">
|
|
</td>
|
|
<td class="py-2 px-3">
|
|
<input type="text" id="filterEmail" name="email" th:value="${emailFilter}" placeholder="Filtrer Email..." class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none">
|
|
</td>
|
|
<td class="py-2 px-3">
|
|
<select id="filterPaiement" name="paiement" class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none">
|
|
<option value="">Tous</option>
|
|
<option value="A_JOUR" th:selected="${paiementFilter == 'A_JOUR'}">À jour</option>
|
|
<option value="RESTE" th:selected="${paiementFilter == 'RESTE'}">Reste à payer</option>
|
|
<option value="AUCUNE" th:selected="${paiementFilter == 'AUCUNE'}">Aucune licence</option>
|
|
</select>
|
|
</td>
|
|
<td class="py-2 px-3">
|
|
<select id="filterSportEasy" name="sporteasy" class="w-full border border-gray-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500 focus:outline-none">
|
|
<option value="">Tous</option>
|
|
<option value="NON_INVITE" th:selected="${sporteasyFilter == 'NON_INVITE'}">Non invité</option>
|
|
<option value="INVITE" th:selected="${sporteasyFilter == 'INVITE'}">Invité</option>
|
|
<option value="RENOUVELLEMENT" th:selected="${sporteasyFilter == 'RENOUVELLEMENT'}">Renouvellement</option>
|
|
</select>
|
|
</td>
|
|
<td class="py-2 px-3"></td>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-200 text-sm">
|
|
<tr th:if="${#lists.isEmpty(adherents)}">
|
|
<td colspan="9" class="py-8 text-center text-gray-500">Aucun adhérent enregistré.</td>
|
|
</tr>
|
|
<tr th:each="adherent : ${adherents}" class="hover:bg-gray-50 transition-colors adherent-row">
|
|
<td class="py-4 px-6 font-medium text-gray-900">
|
|
<span th:text="${adherent.nom}">Dupont</span>
|
|
<div class="text-[10px] text-gray-400 font-normal mt-0.5">
|
|
<span th:text="${adherent.sexe == 'FEMININ' ? 'Féminin' : 'Masculin'}">Masculin</span>
|
|
<span class="mx-1">|</span>
|
|
<span th:text="${adherent.typeMaillot == 'GARDIEN' ? 'Gardien' : 'Joueur'}">Joueur</span>
|
|
</div>
|
|
</td>
|
|
<td class="py-4 px-6 font-medium text-gray-900" th:text="${adherent.prenom}">Jean</td>
|
|
<td class="py-4 px-6 text-gray-600 font-medium"
|
|
th:text="${adherent.getLicenceActuelle() != null ? adherent.getLicenceActuelle().getCategorie().getNom() + (adherent.getLicenceActuelle().getEquipe() != null ? ' (' + adherent.getLicenceActuelle().getEquipe().getNom() + ')' : '') : '-'}">-</td>
|
|
<!-- Équipements Column -->
|
|
<td class="py-3 px-4 text-center">
|
|
<div th:with="choisis=${adherent.getLicenceActuelle() != null ? adherent.getLicenceActuelle().dotations.?[choisi == true] : null}">
|
|
<div th:if="${choisis != null and !#lists.isEmpty(choisis)}" class="relative inline-block text-left">
|
|
<button type="button"
|
|
class="px-2.5 py-1 text-[9px] font-bold rounded-full border uppercase tracking-wider cursor-pointer hover:ring-2 hover:ring-offset-1 hover:ring-indigo-300 transition-all flex items-center space-x-1 mx-auto"
|
|
th:with="total=${#lists.size(choisis)},
|
|
fournis=${#lists.size(choisis.?[fourni == true])}"
|
|
th:classappend="${fournis == total ? 'bg-emerald-100 text-emerald-800 border-emerald-200' : (fournis > 0 ? 'bg-amber-100 text-amber-800 border-amber-200' : 'bg-rose-100 text-rose-800 border-rose-200')}"
|
|
onclick="toggleEquipementsPopover(this, event)">
|
|
<span th:text="${fournis == total ? '🟢 Complet (' + fournis + '/' + total + ')' : (fournis > 0 ? '🟠 Partiel (' + fournis + '/' + total + ')' : '🔴 Non fourni (0/' + total + ')')}"
|
|
th:data-total="${total}" th:data-fournis="${fournis}">
|
|
Complet (3/3)
|
|
</span>
|
|
<svg class="w-3 h-3 text-current opacity-70" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
|
</button>
|
|
|
|
<!-- Popover dropdown -->
|
|
<div class="hidden absolute left-1/2 -translate-x-1/2 mt-2 w-64 bg-white rounded-xl shadow-xl border border-gray-200 p-3 z-50 text-left transition-all equipement-popover">
|
|
<div class="flex items-center justify-between pb-2 mb-2 border-b border-gray-100">
|
|
<span class="text-xs font-semibold text-gray-800 flex items-center space-x-1">
|
|
<span>Détail Équipements</span>
|
|
<span class="text-[10px] font-bold text-gray-500 bg-gray-100 px-1.5 py-0.5 rounded-full" th:text="${#lists.size(choisis.?[fourni == true]) + '/' + #lists.size(choisis)}">3/3</span>
|
|
</span>
|
|
<button type="button" class="text-gray-400 hover:text-gray-600 text-xs font-bold px-1" onclick="closeAllEquipementPopovers()">×</button>
|
|
</div>
|
|
<div class="space-y-1.5 max-h-48 overflow-y-auto pr-0.5">
|
|
<div th:each="dot : ${choisis}"
|
|
class="flex items-center justify-between p-2 rounded-lg text-xs border"
|
|
th:classappend="${dot.fourni ? 'bg-emerald-50/60 border-emerald-100 text-emerald-900' : 'bg-gray-50 border-gray-200 text-gray-700'}">
|
|
<div class="flex flex-col">
|
|
<span class="font-medium text-gray-900 leading-tight" th:text="${dot.equipement != null ? dot.equipement.nom : 'Équipement'}">Maillot</span>
|
|
<div class="text-[10px] text-gray-500 flex flex-wrap items-center gap-1 mt-0.5" th:if="${(dot.taille != null and !dot.taille.isEmpty()) or (dot.flocage != null and !dot.flocage.isEmpty()) or (dot.numero != null and !dot.numero.isEmpty())}">
|
|
<span th:if="${dot.taille != null and !dot.taille.isEmpty()}" class="font-mono bg-white/80 px-1 py-0.2 rounded border border-gray-200" th:text="'Taille: ' + ${dot.taille}">Taille: M</span>
|
|
<span th:if="${dot.flocage != null and !dot.flocage.isEmpty()}" class="italic" th:text="${dot.flocage}">Flocage</span>
|
|
<span th:if="${dot.numero != null and !dot.numero.isEmpty()}" class="font-mono font-bold" th:text="'#' + ${dot.numero}">#10</span>
|
|
</div>
|
|
</div>
|
|
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-bold flex-shrink-0"
|
|
th:classappend="${dot.fourni ? 'bg-emerald-100 text-emerald-800' : 'bg-gray-200 text-gray-600'}">
|
|
<span th:text="${dot.fourni ? '✓ Fourni' : '✗ Non fourni'}">✓ Fourni</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div th:if="${choisis == null or #lists.isEmpty(choisis)}" class="text-xs text-gray-400 italic">
|
|
-
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="py-4 px-6 text-center text-gray-600 font-mono text-xs"
|
|
th:text="${adherent.getLicenceActuelle() != null and adherent.getLicenceActuelle().numeroLicence != null and !adherent.getLicenceActuelle().numeroLicence.isEmpty() ? adherent.getLicenceActuelle().numeroLicence : '-'}">-</td>
|
|
<td class="py-4 px-6 text-gray-600" th:text="${adherent.email}">jean@example.com</td>
|
|
<td class="py-3 px-4">
|
|
<!-- Aucune licence -->
|
|
<div th:if="${adherent.getLicenceActuelle() == null}" class="text-center text-gray-400 text-xs italic">
|
|
Aucune licence
|
|
</div>
|
|
<!-- Détails financiers -->
|
|
<div th:if="${adherent.getLicenceActuelle() != null}" class="flex flex-col items-center space-y-1">
|
|
<!-- Badge Statut -->
|
|
<span class="px-2 py-0.5 text-[9px] font-bold rounded-full uppercase tracking-wider"
|
|
th:classappend="${adherent.getLicenceActuelle().getResteAPayer().compareTo(T(java.math.BigDecimal).ZERO) == 0 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}"
|
|
th:text="${adherent.getLicenceActuelle().getResteAPayer().compareTo(T(java.math.BigDecimal).ZERO) == 0 ? 'À jour' : 'Reste à payer'}">
|
|
À jour
|
|
</span>
|
|
<!-- Tableau/Liste miniature des montants -->
|
|
<div class="text-[11px] font-mono text-gray-600 bg-gray-50 p-1.5 rounded-lg border border-gray-100 w-28 space-y-0.5 shadow-2xs mx-auto">
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-400">Dû:</span>
|
|
<span class="font-semibold text-gray-800" th:text="|${adherent.getLicenceActuelle().getPrixTotal()} €|">150 €</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-400">Payé:</span>
|
|
<span class="font-semibold text-green-600" th:text="|${adherent.getLicenceActuelle().getSommePayee()} €|">100 €</span>
|
|
</div>
|
|
<div class="flex justify-between border-t border-gray-200 pt-0.5 mt-0.5">
|
|
<span class="text-gray-500 font-medium">Reste:</span>
|
|
<span class="font-bold" th:classappend="${adherent.getLicenceActuelle().getResteAPayer().compareTo(T(java.math.BigDecimal).ZERO) == 0 ? 'text-gray-800' : 'text-red-600'}" th:text="|${adherent.getLicenceActuelle().getResteAPayer()} €|">50 €</span>
|
|
</div>
|
|
</div>
|
|
<!-- Bouton Relancer Solde -->
|
|
<div th:if="${adherent.getLicenceActuelle().getResteAPayer().compareTo(T(java.math.BigDecimal).ZERO) > 0}" class="mt-1 text-center" th:id="'relance-paiement-container-' + ${adherent.id}">
|
|
<button type="button"
|
|
th:attr="hx-post=@{/adherents/{id}/relancer-paiement-htmx(id=${adherent.id})}, hx-target=|#relance-paiement-container-${adherent.id}|"
|
|
hx-swap="innerHTML"
|
|
class="text-[10px] text-amber-700 hover:text-amber-900 bg-amber-50 hover:bg-amber-100 border border-amber-200 px-2 py-0.5 rounded transition-colors inline-flex items-center gap-1 font-medium">
|
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 002-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
|
|
Relancer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<!-- SportEasy Status Column -->
|
|
<td class="py-3 px-4 text-center">
|
|
<div th:if="${adherent.getLicenceActuelle() != null}">
|
|
<div th:if="${adherent.getLicenceActuelle().isRenouvellement()}" class="text-xs text-gray-400 italic">
|
|
Renouvellement
|
|
</div>
|
|
<div th:unless="${adherent.getLicenceActuelle().isRenouvellement()}">
|
|
<div th:if="${adherent.getLicenceActuelle().getSportEasyEmailSent()}" class="flex flex-col items-center space-y-1">
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold bg-emerald-100 text-emerald-800 border border-emerald-200"
|
|
th:title="${adherent.getLicenceActuelle().getSportEasyEmailSentAt() != null ? 'Envoyé le ' + #temporals.format(adherent.getLicenceActuelle().getSportEasyEmailSentAt(), 'dd/MM/yyyy HH:mm') : 'Invité'}">
|
|
🟢 Invité
|
|
</span>
|
|
<form th:action="@{/adherents/{id}/sporteasy-invite(id=${adherent.id})}" method="post">
|
|
<input type="hidden" name="force" value="true" />
|
|
<button type="submit" class="text-[10px] text-gray-500 hover:text-blue-600 underline">Renvoyer</button>
|
|
</form>
|
|
</div>
|
|
<div th:if="${!adherent.getLicenceActuelle().getSportEasyEmailSent()}" class="flex flex-col items-center space-y-1">
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-600 border border-gray-200">
|
|
⚪ Non invité
|
|
</span>
|
|
<form th:action="@{/adherents/{id}/sporteasy-invite(id=${adherent.id})}" method="post">
|
|
<button type="submit" class="text-xs font-medium text-indigo-600 hover:text-indigo-800 bg-indigo-50 hover:bg-indigo-100 px-2.5 py-1 rounded-md border border-indigo-200 transition-colors">Inviter</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div th:if="${adherent.getLicenceActuelle() == null}" class="text-xs text-gray-400 italic">
|
|
-
|
|
</div>
|
|
</td>
|
|
<td class="py-4 px-6 text-right flex justify-end space-x-3 items-center">
|
|
<a th:href="@{/adherents/{id}/edit(id=${adherent.id})}" class="text-indigo-600 hover:text-indigo-900 font-medium bg-indigo-50 px-3 py-1 rounded-lg">Voir / Modifier</a>
|
|
<form th:action="@{/adherents/{id}/delete(id=${adherent.id})}" method="post" onsubmit="return confirm('Supprimer cet adhérent ?');">
|
|
<button type="submit" class="text-red-600 hover:text-red-800 font-medium">Supprimer</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination Footer -->
|
|
<div class="px-6 py-4 border-t border-gray-200 flex items-center justify-between bg-gray-50 text-sm text-gray-700">
|
|
<div th:if="${totalElements > 0}">
|
|
Affichage de <span class="font-semibold" th:text="${currentPage * pageSize + 1}">1</span> à
|
|
<span class="font-semibold" th:text="${T(java.lang.Math).min((currentPage + 1) * pageSize, totalElements)}">20</span> sur
|
|
<span class="font-semibold" th:text="${totalElements}">100</span> adhérents
|
|
</div>
|
|
<div th:if="${totalElements == 0}">
|
|
Aucun adhérent à afficher
|
|
</div>
|
|
<div class="flex items-center space-x-2" th:if="${totalPages > 1}">
|
|
<!-- Page Précédente -->
|
|
<button type="button"
|
|
th:if="${currentPage > 0}"
|
|
hx-get="/adherents"
|
|
hx-target="#adherents-table-container"
|
|
hx-select="#adherents-table-container"
|
|
hx-include="#filter-row input, #filter-row select, input[name='query']"
|
|
th:attr="hx-vals=|{"page": ${currentPage - 1}}|"
|
|
class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors">
|
|
Précédent
|
|
</button>
|
|
<span th:if="${currentPage == 0}" class="px-3 py-1 border border-gray-200 rounded text-gray-400 bg-gray-50 cursor-not-allowed">
|
|
Précédent
|
|
</span>
|
|
|
|
<!-- Numéros de pages -->
|
|
<th:block th:each="pageNum : ${#numbers.sequence(0, totalPages - 1)}" th:if="${totalPages > 0}">
|
|
<button type="button"
|
|
hx-get="/adherents"
|
|
hx-target="#adherents-table-container"
|
|
hx-select="#adherents-table-container"
|
|
hx-include="#filter-row input, #filter-row select, input[name='query']"
|
|
th:attr="hx-vals=|{"page": ${pageNum}}|"
|
|
th:text="${pageNum + 1}"
|
|
class="px-3 py-1 rounded transition-colors"
|
|
th:classappend="${currentPage == pageNum ? 'bg-blue-600 text-white' : 'border border-gray-300 hover:bg-gray-100'}">
|
|
1
|
|
</button>
|
|
</th:block>
|
|
|
|
<!-- Page Suivante -->
|
|
<button type="button"
|
|
th:if="${currentPage < totalPages - 1}"
|
|
hx-get="/adherents"
|
|
hx-target="#adherents-table-container"
|
|
hx-select="#adherents-table-container"
|
|
hx-include="#filter-row input, #filter-row select, input[name='query']"
|
|
th:attr="hx-vals=|{"page": ${currentPage + 1}}|"
|
|
class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors">
|
|
Suivant
|
|
</button>
|
|
<span th:if="${currentPage == totalPages - 1}" class="px-3 py-1 border border-gray-200 rounded text-gray-400 bg-gray-50 cursor-not-allowed">
|
|
Suivant
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
<script>
|
|
function updateSort(field) {
|
|
let sf = document.getElementById('sortField');
|
|
let sd = document.getElementById('sortDirection');
|
|
if (sf.value === field) {
|
|
sd.value = sd.value === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
sf.value = field;
|
|
sd.value = 'asc';
|
|
}
|
|
sf.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
|
|
function toggleEquipementsPopover(btn, event) {
|
|
if (event) event.stopPropagation();
|
|
const popover = btn.nextElementSibling;
|
|
const isVisible = popover && !popover.classList.contains('hidden');
|
|
closeAllEquipementPopovers();
|
|
if (popover && !isVisible) {
|
|
popover.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
function closeAllEquipementPopovers() {
|
|
document.querySelectorAll('.equipement-popover').forEach(p => p.classList.add('hidden'));
|
|
}
|
|
|
|
document.addEventListener('click', function(event) {
|
|
if (!event.target.closest('.equipement-popover')) {
|
|
closeAllEquipementPopovers();
|
|
}
|
|
});
|
|
|
|
document.body.addEventListener('htmx:configRequest', function(evt) {
|
|
var csrfHeader = document.querySelector('meta[name="_csrf_header"]');
|
|
var csrfToken = document.querySelector('meta[name="_csrf"]');
|
|
if (csrfHeader && csrfToken) {
|
|
evt.detail.headers[csrfHeader.content] = csrfToken.content;
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|