Ajoute le suivi des demandes d'adhésion
This commit is contained in:
1 parent
b4a3019098
commit
c49c45e65f
6 files changed
+291
-7
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user