groupe-meditation/frontend/src/pages/GererPostes.jsx

263 lines
12 KiB
React
Raw Normal View History

2026-05-25 17:39:38 -04:00
import React, { useState, useEffect, useMemo } from 'react';
2026-04-17 04:04:04 -04:00
import { api } from '../stores/api';
import PageHeader from '../components/PageHeader';
const CATEGORIES = [
{ value: 'executif', label: 'Exécutif' },
{ value: 'service', label: 'Service' },
{ value: 'physique', label: 'Physique' },
];
const MODULES = [
2026-05-25 17:39:38 -04:00
['S0', 'Rencontre hebdomadaire'], ['M0', "Assemblée d'affaires"],
['S1', 'Collecte 7e'], ['C5', 'Gouvernance'],
['S1T', 'Trésorerie'], ['M4', 'Envoi contributions'],
['M1', 'Rédiger PV'], ['M2', 'Présences'], ['M3', 'Rapport RSG'],
['S2', 'Remise jeton'], ['M6', 'Commander jetons'],
['S3', 'Vente littérature'], ['M5', 'Ajuster inventaire'], ['S4', 'Rapport réunion'],
2026-05-25 17:39:38 -04:00
['G2', 'Gestion membres'], ['G9', 'Paramètres'],
['G11', 'Gestion mandats'], ['G12', 'Gestion postes'],
];
2026-05-25 17:39:38 -04:00
const GROUPES_MODULES = [
{ titre: 'Réunion', codes: ['S0', 'S1', 'S2', 'S3', 'S4'] },
{ titre: 'Affaires', codes: ['M0', 'M1', 'M2', 'M3', 'C5'] },
{ titre: 'Finances', codes: ['S1T', 'S5', 'M4'] },
{ titre: 'Inventaire', codes: ['C4', 'M5', 'M6'] },
{ titre: 'Administration', codes: ['G2', 'G9', 'G11', 'G12'] },
];
2026-04-17 04:04:04 -04:00
export default function GererPostes() {
const [postes, setPostes] = useState([]);
const [modulesPostes, setModulesPostes] = useState({});
2026-04-17 04:04:04 -04:00
const [candidatures, setCandidatures] = useState([]);
const [tab, setTab] = useState('postes');
2026-05-25 17:39:38 -04:00
const [posteOuvert, setPosteOuvert] = useState(null);
2026-04-17 04:04:04 -04:00
const [showForm, setShowForm] = useState(false);
const [msg, setMsg] = useState('');
const [error, setError] = useState('');
const [nom, setNom] = useState('');
const [categorie, setCategorie] = useState('service');
const [description, setDescription] = useState('');
const [prerequis, setPrerequis] = useState('');
const [informations, setInformations] = useState('');
const charger = async () => {
try {
const [p, mp, c] = await Promise.all([
2026-04-17 04:04:04 -04:00
api.get('/postes/?inclure_abolis=true'),
api.get('/postes/modules'),
2026-04-17 04:04:04 -04:00
api.get('/postes/candidatures'),
]);
setPostes(p);
setModulesPostes(mp);
2026-04-17 04:04:04 -04:00
setCandidatures(c);
} catch (e) { setError(e.message); }
};
useEffect(() => { charger(); }, []);
const flash = (m) => { setMsg(m); setTimeout(() => setMsg(''), 2500); };
const creerPoste = async () => {
if (!nom.trim()) { setError('Nom requis'); return; }
setError('');
try {
await api.post('/postes/', {
nom: nom.trim(), categorie,
description: description.trim() || null,
prerequis: prerequis.trim() || null,
informations: informations.trim() || null,
});
setNom(''); setDescription(''); setPrerequis(''); setInformations(''); setShowForm(false);
flash('Poste créé ✓');
await charger();
} catch (e) { setError(e.message); }
};
const abolir = async (id, nomPoste) => {
if (!confirm(`Abolir le poste «${nomPoste}» ?\nTous les mandats actifs seront terminés.`)) return;
try {
const res = await api.request('DELETE', `/postes/${id}`);
flash(`Poste aboli — ${res.mandats_termines} mandat(s) terminé(s)`);
await charger();
} catch (e) { setError(e.message); }
};
const reactiver = async (id) => {
try {
await api.post(`/postes/${id}/reactiver`);
flash('Poste réactivé ✓');
await charger();
} catch (e) { setError(e.message); }
};
const supprimerPermanent = async (id, nomPoste) => {
if (!confirm(`Supprimer définitivement le poste «${nomPoste}» ?`)) return;
2026-04-17 04:04:04 -04:00
try {
await api.request('DELETE', `/postes/${id}/permanent`);
flash('Poste supprimé');
2026-04-17 04:04:04 -04:00
await charger();
} catch (e) { setError(e.message); }
};
const toggleModule = async (posteId, code) => {
const actuel = new Set(modulesPostes[posteId] || []);
if (actuel.has(code)) actuel.delete(code);
else actuel.add(code);
const codes = [...actuel];
setModulesPostes({ ...modulesPostes, [posteId]: codes });
2026-04-17 04:04:04 -04:00
try {
await api.request('PUT', `/postes/${posteId}/modules`, codes);
flash('Accès mis à jour ✓');
} catch (e) { setError(e.message); await charger(); }
2026-04-17 04:04:04 -04:00
};
const traiterCandidature = async (id, statut) => {
try {
await api.patch(`/postes/candidatures/${id}?statut=${statut}`);
flash(statut === 'acceptee' ? 'Candidature acceptée ✓' : 'Candidature refusée');
await charger();
} catch (e) { setError(e.message); }
};
const CAT_ICON = { executif: '🔵', service: '🟣', physique: '🟢' };
2026-05-25 17:39:38 -04:00
const moduleParCode = useMemo(() => new Map(MODULES.map(([code, label]) => [code, label])), []);
const groupesPourPoste = (posteId) => GROUPES_MODULES.map((groupe) => ({
...groupe,
modules: groupe.codes
.filter((code) => moduleParCode.has(code))
.map((code) => [code, moduleParCode.get(code), (modulesPostes[posteId] || []).includes(code)]),
})).filter((groupe) => groupe.modules.length > 0);
2026-04-17 04:04:04 -04:00
return (
<div className="min-h-screen bg-gray-100">
<PageHeader titre="Gestion des postes" sousTitre="Postes, candidatures, accès aux modules" />
2026-04-17 04:04:04 -04:00
<div className="p-4 space-y-4">
{msg && <div className="bg-green-50 text-green-700 p-3 rounded-xl text-center text-sm font-medium">{msg}</div>}
{error && <div className="bg-red-50 text-red-600 p-3 rounded-xl text-center text-sm">{error}</div>}
<div className="flex gap-1 overflow-x-auto">
{[
['postes', `Postes (${postes.length})`],
['candidatures', `Candidatures (${candidatures.length})`],
2026-04-17 04:04:04 -04:00
].map(([t, label]) => (
<button key={t} onClick={() => { setTab(t); setError(''); }}
className={`flex-1 py-2 rounded-xl text-xs font-bold transition whitespace-nowrap px-2 ${
tab === t ? 'bg-aa-blue text-white' : 'bg-white text-gray-500'
}`}>{label}</button>
))}
</div>
{tab === 'postes' && (
<div className="space-y-3">
<button onClick={() => setShowForm(!showForm)}
className="w-full py-3 bg-green-50 text-green-700 rounded-xl font-bold border border-green-200">
2026-04-17 04:04:04 -04:00
+ Créer un poste
</button>
{showForm && (
<div className="bg-white rounded-2xl p-4 shadow-sm space-y-3">
<input value={nom} onChange={(e) => setNom(e.target.value)} placeholder="Nom du poste"
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue" />
2026-04-17 04:04:04 -04:00
<select value={categorie} onChange={(e) => setCategorie(e.target.value)}
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue">
{CATEGORIES.map((c) => <option key={c.value} value={c.value}>{c.label}</option>)}
</select>
<input value={description} onChange={(e) => setDescription(e.target.value)} placeholder="Description"
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue" />
<input value={prerequis} onChange={(e) => setPrerequis(e.target.value)} placeholder="Prérequis"
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue" />
<textarea value={informations} onChange={(e) => setInformations(e.target.value)} placeholder="Informations"
rows={2} className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue resize-none" />
2026-04-17 04:04:04 -04:00
<button onClick={creerPoste} disabled={!nom.trim()}
className="w-full py-3 bg-aa-blue text-white rounded-xl font-bold disabled:opacity-40">Créer</button>
</div>
)}
{postes.map((p) => (
<div key={p.id} className={`bg-white rounded-2xl p-4 shadow-sm ${!p.actif ? 'border-2 border-dashed border-gray-300' : ''}`}>
<div className="flex items-start justify-between gap-2">
<div className="flex-1">
<p className="font-bold text-gray-800">{CAT_ICON[p.categorie] || '⚪'} {p.nom}</p>
{p.description && <p className="text-xs text-gray-500 mt-1">{p.description}</p>}
2026-05-25 17:39:38 -04:00
{p.prerequis && <p className="text-xs text-gray-500 mt-1"><span className="font-bold">Prérequis :</span> {p.prerequis}</p>}
2026-04-17 04:04:04 -04:00
{p.informations && <p className="text-xs text-gray-400 mt-1 italic">{p.informations}</p>}
</div>
{p.actif ? (
<button onClick={() => abolir(p.id, p.nom)} className="text-xs text-red-500 font-medium px-2 py-1">Abolir</button>
) : (
<div className="flex flex-col gap-1">
<button onClick={() => reactiver(p.id)} className="text-xs text-green-600 font-medium px-2 py-1">Réactiver</button>
<button onClick={() => supprimerPermanent(p.id, p.nom)} className="text-xs text-red-500 font-medium px-2 py-1">Supprimer</button>
</div>
)}
2026-04-17 04:04:04 -04:00
</div>
2026-05-25 17:39:38 -04:00
<div className="mt-3 pt-3 border-t border-gray-100">
<button
type="button"
onClick={() => setPosteOuvert(posteOuvert === p.id ? null : p.id)}
className={`w-full min-h-[44px] rounded-xl text-sm font-bold border ${
posteOuvert === p.id ? 'bg-aa-blue text-white border-aa-blue' : 'bg-gray-50 text-gray-700 border-gray-200'
}`}
>
Rôles associés ({(modulesPostes[p.id] || []).length})
</button>
{posteOuvert === p.id && (
<div className="mt-3 space-y-4">
{groupesPourPoste(p.id).map((groupe) => (
<div key={groupe.titre} className="space-y-2">
<p className="text-xs font-bold text-gray-400 uppercase tracking-wider">{groupe.titre}</p>
<div className="grid grid-cols-2 gap-2">
{groupe.modules.map(([code, label, actif]) => (
<button
key={code}
type="button"
onClick={() => toggleModule(p.id, code)}
disabled={!p.actif}
className={`min-h-[44px] p-2 rounded-xl text-xs font-bold border ${
actif ? 'bg-aa-blue text-white border-aa-blue' : 'bg-gray-50 text-gray-500 border-gray-200'
} disabled:opacity-40`}
>
{code} · {label}
</button>
))}
</div>
</div>
))}
</div>
)}
2026-04-17 04:04:04 -04:00
</div>
</div>
))}
</div>
)}
{tab === 'candidatures' && (
candidatures.length === 0 ? (
<div className="text-center py-12"><p className="text-gray-500">Aucune candidature en attente</p></div>
2026-04-17 04:04:04 -04:00
) : (
<div className="space-y-3">
{candidatures.map((c) => (
<div key={c.id} className="bg-white rounded-2xl p-4 shadow-sm">
<p className="font-bold text-gray-800">{c.membre_prenom} → {c.poste_nom}</p>
{c.motivation && <p className="text-sm text-gray-500 mt-1 italic">«{c.motivation}»</p>}
<div className="flex gap-2 mt-3">
<button onClick={() => traiterCandidature(c.id, 'acceptee')}
className="flex-1 py-2 bg-green-50 text-green-700 rounded-xl text-sm font-bold border border-green-200">Accepter</button>
2026-04-17 04:04:04 -04:00
<button onClick={() => traiterCandidature(c.id, 'refusee')}
className="flex-1 py-2 bg-red-50 text-red-700 rounded-xl text-sm font-bold border border-red-200">Refuser</button>
2026-04-17 04:04:04 -04:00
</div>
</div>
))}
</div>
)
)}
</div>
</div>
);
}