Ajoute le suivi des demandes d'adhésion

This commit is contained in:
Michael committed 2026-07-15 11:02:13 +02:00
1 parent b4a3019098
commit c49c45e65f
6 files changed
+291 -7

No files matched your search

+124
View File
@@ -0,0 +1,124 @@
import { useEffect, useMemo, useState } from 'react';
import { api } from '../../lib/api.js';
import { CredentialsModal } from './AccessControls.jsx';
const STATUS = {
nouvelle: { label: 'Nouvelle', className: 'badge-red' },
contactee: { label: 'Contact pris', className: 'badge-amber' },
validee: { label: 'Membre validé', className: 'badge-green' },
};
const formatDate = (value) => new Date(value).toLocaleDateString('fr-FR', {
day: '2-digit', month: 'short', year: 'numeric',
});
export function RequestsTab() {
const [requests, setRequests] = useState([]);
const [filter, setFilter] = useState('pending');
const [busyId, setBusyId] = useState(null);
const [error, setError] = useState('');
const [credentials, setCredentials] = useState(null);
const reload = () => api.get('/api/admin/demandes')
.then((data) => setRequests(data.demandes))
.catch((err) => setError(err.message));
useEffect(() => { reload(); }, []);
const visible = useMemo(() => requests.filter((request) => (
filter === 'completed' ? request.statut === 'validee' : request.statut !== 'validee'
)), [filter, requests]);
const pendingCount = requests.filter((request) => request.statut !== 'validee').length;
const newCount = requests.filter((request) => request.statut === 'nouvelle').length;
const run = async (request, action) => {
setBusyId(request.id);
setError('');
try {
const result = await api.post(`/api/admin/demandes/${request.id}/${action}`);
if (result.tempPassword) {
setCredentials({ recipient: result.email, tempPassword: result.tempPassword });
} else if (result.accessError) {
setError(`Membre validé, mais accès non créé : ${result.accessError}`);
}
await reload();
} catch (err) {
setError(err.message);
} finally {
setBusyId(null);
}
};
return (
<div className="requests-layout">
<div className="requests-summary">
<div className="card request-stat"><span>À traiter</span><strong>{pendingCount}</strong></div>
<div className="card request-stat request-stat-accent"><span>Nouvelles</span><strong>{newCount}</strong></div>
<div className="card request-stat"><span>Validées</span><strong>{requests.length - pendingCount}</strong></div>
</div>
<section className="card requests-panel">
<div className="requests-toolbar">
<div>
<h3 className="serif">Demandes d’adhésion</h3>
<p>Contactez chaque personne avant validation membre.</p>
</div>
<div className="requests-filter" aria-label="Filtrer les demandes">
<button className={filter === 'pending' ? 'active' : ''} onClick={() => setFilter('pending')}>En cours</button>
<button className={filter === 'completed' ? 'active' : ''} onClick={() => setFilter('completed')}>Terminées</button>
</div>
</div>
{error && <p className="error-text requests-error">{error}</p>}
<div className="requests-list">
{visible.map((request) => {
const status = STATUS[request.statut] || STATUS.nouvelle;
const busy = busyId === request.id;
return (
<article className="request-row" key={request.id}>
<div className="request-date"><span>Reçue</span><strong>{formatDate(request.created_at)}</strong></div>
<div className="request-identity">
<div className="request-title-line">
<h4>{request.nom}</h4>
<span className={`badge ${status.className}`}>{status.label}</span>
</div>
<p>{request.fonction || 'Fonction non précisée'} · <strong>{request.entreprise}</strong></p>
<div className="request-contact">
<a href={`mailto:${request.email}`}>{request.email}</a>
{request.tel && <a href={`tel:${request.tel}`}>{request.tel}</a>}
</div>
</div>
<div className="request-actions">
{request.statut === 'nouvelle' && (
<button className="btn btn-outline-soft btn-sm" disabled={busy} onClick={() => run(request, 'contact')}>
{busy ? 'Enregistrement…' : '✓ Contact pris'}
</button>
)}
{request.statut === 'contactee' && (
<button className="btn btn-red btn-sm" disabled={busy || !request.email} onClick={() => run(request, 'validate')}>
{busy ? 'Validation…' : 'Valider comme membre'}
</button>
)}
{request.statut === 'validee' && (
<span className="request-member-link">Membre : {request.membre_nom || request.entreprise}</span>
)}
</div>
</article>
);
})}
{visible.length === 0 && (
<div className="requests-empty">
<span>✓</span>
<h4>Aucune demande {filter === 'pending' ? 'en cours' : 'terminée'}</h4>
<p>Les nouvelles demandes apparaîtront ici.</p>
</div>
)}
</div>
</section>
{credentials && (
<CredentialsModal recipient={credentials.recipient} tempPassword={credentials.tempPassword} onClose={() => setCredentials(null)} />
)}
</div>
);
}
+4 -1
View File
@@ -7,14 +7,16 @@ import { CategoriesTab, ContenuTab } from '../components/admin/AdminContent.jsx'
import { PastEventsTab } from '../components/admin/AdminPast.jsx';
import { UsersTab } from '../components/admin/AdminUsers.jsx';
import { EmailCreatorTab } from '../components/admin/AdminEmail.jsx';
import { RequestsTab } from '../components/admin/AdminRequests.jsx';
// Moderators only get members/rencontres/inscriptions/rencontres passées
// Moderators get members/membership requests/rencontres/inscriptions/rencontres passées
// (create + edit, never delete) — everything else (dashboard, taxonomy,
// site content, staff account management) is admin-only, enforced both
// here (sidebar) and server-side (routes).
const TABS = [
{ key: 'dashboard', icon: '◧', label: 'Tableau de bord', title: 'Tableau de bord', roles: ['admin'] },
{ key: 'membres', icon: '▤', label: 'Membres', title: 'Gestion des membres', roles: ['admin', 'moderator'] },
{ key: 'demandes', icon: '◌', label: 'Demandes', title: "Demandes d’adhésion", roles: ['admin', 'moderator'] },
{ key: 'rencontres', icon: '◈', label: 'Rencontres', title: 'Rencontres', roles: ['admin', 'moderator'] },
{ key: 'passees', icon: '▦', label: 'Rencontres passées', title: 'Rencontres passées', roles: ['admin', 'moderator'] },
{ key: 'inscriptions', icon: '✎', label: 'Inscriptions', title: 'Inscriptions', roles: ['admin', 'moderator'] },
@@ -147,6 +149,7 @@ export function AdminShell() {
<div className="admin-content-body" style={{ padding: '36px 40px' }}>
{tab === 'dashboard' && <Dashboard />}
{tab === 'membres' && <MembersTab />}
{tab === 'demandes' && <RequestsTab />}
{tab === 'rencontres' && <RencontresTab />}
{tab === 'passees' && <PastEventsTab />}
{tab === 'inscriptions' && <InscriptionsTab />}
+39
View File
@@ -124,6 +124,41 @@ input:focus, textarea:focus, select:focus { outline: none; border-color: var(--r
}
.admin-nav-btn.active { background: rgba(193, 39, 45, .18); color: #fff; border-left-color: var(--red); font-weight: 600; }
/* Membership request workflow */
.requests-layout { display: flex; flex-direction: column; gap: 20px; }
.requests-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
.request-stat { padding: 20px 22px; border-radius: 6px; display: flex; align-items: flex-end; justify-content: space-between; }
.request-stat span { font-size: 12.5px; color: var(--gray-light); }
.request-stat strong { font-family: Georgia, serif; font-size: 32px; line-height: 1; }
.request-stat-accent { background: var(--dark); color: #fff; }
.request-stat-accent span { color: #B7AFA6; }
.requests-panel { border-radius: 6px; overflow: hidden; }
.requests-toolbar { padding: 22px 24px; display: flex; align-items: center; justify-content: space-between; gap: 20px; border-bottom: 1px solid var(--border); }
.requests-toolbar h3 { font-size: 20px; font-weight: 600; }
.requests-toolbar p { margin-top: 4px; color: var(--gray-light); font-size: 12.5px; }
.requests-filter { display: flex; padding: 3px; background: var(--admin-bg); border-radius: 5px; }
.requests-filter button { border: 0; background: transparent; color: var(--gray); padding: 8px 13px; border-radius: 3px; font-size: 12.5px; font-weight: 600; }
.requests-filter button.active { background: #fff; color: var(--ink); box-shadow: 0 1px 4px rgba(27,27,27,.1); }
.requests-error { margin: 14px 24px 0; }
.requests-list { display: flex; flex-direction: column; }
.request-row { display: grid; grid-template-columns: 110px minmax(260px, 1fr) auto; gap: 22px; align-items: center; padding: 22px 24px; border-top: 1px solid var(--border-soft); }
.request-row:first-child { border-top: 0; }
.request-row:hover { background: #FCFAF7; }
.request-date { display: flex; flex-direction: column; gap: 4px; color: var(--gray-light); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }
.request-date strong { color: var(--gray); font-size: 12px; letter-spacing: 0; text-transform: none; }
.request-title-line { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.request-title-line h4 { font-family: Georgia, serif; font-size: 18px; color: var(--ink); }
.request-identity > p { margin-top: 5px; color: var(--gray); font-size: 13px; }
.request-contact { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 10px; }
.request-contact a { color: var(--red); font-size: 12.5px; text-decoration: none; font-weight: 600; }
.request-contact a:hover { text-decoration: underline; }
.request-actions { min-width: 170px; text-align: right; }
.request-member-link { color: var(--green); font-size: 12.5px; font-weight: 600; }
.requests-empty { padding: 60px 24px; text-align: center; color: var(--gray-light); }
.requests-empty > span { display: inline-flex; width: 44px; height: 44px; align-items: center; justify-content: center; border-radius: 50%; background: #E8F3EC; color: var(--green); font-size: 20px; }
.requests-empty h4 { margin-top: 14px; color: var(--ink); font-family: Georgia, serif; font-size: 18px; }
.requests-empty p { margin-top: 5px; font-size: 13px; }
/* Header nav */
.nav-btn { background: none; border: none; font-size: 14.5px; font-weight: 500; color: var(--gray-mid); padding: 10px 14px; border-radius: 2px; }
.nav-btn:hover, .nav-btn.active { color: var(--red); }
@@ -157,6 +192,10 @@ input:focus, textarea:focus, select:focus { outline: none; border-color: var(--r
.admin-sidebar { width: 100% !important; height: auto !important; position: static !important; }
.admin-content-header { padding: 16px 20px !important; }
.admin-content-body { padding: 20px !important; }
.requests-summary { grid-template-columns: 1fr; }
.requests-toolbar { align-items: flex-start; flex-direction: column; }
.request-row { grid-template-columns: 1fr; gap: 12px; }
.request-actions { min-width: 0; text-align: left; }
}
@media (max-width: 640px) {