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' },
|
|
|
|
|
];
|
|
|
|
|
|
2026-05-15 16:41:17 -04:00
|
|
|
const MODULES = [
|
2026-05-25 17:39:38 -04:00
|
|
|
['S0', 'Rencontre hebdomadaire'], ['M0', "Assemblée d'affaires"],
|
|
|
|
|
['S1', 'Collecte 7e'], ['C5', 'Gouvernance'],
|
2026-05-15 16:41:17 -04:00
|
|
|
['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'],
|
2026-05-15 16:41:17 -04:00
|
|
|
['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([]);
|
2026-05-15 16:41:17 -04:00
|
|
|
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 {
|
2026-05-15 16:41:17 -04:00
|
|
|
const [p, mp, c] = await Promise.all([
|
2026-04-17 04:04:04 -04:00
|
|
|
api.get('/postes/?inclure_abolis=true'),
|
2026-05-15 16:41:17 -04:00
|
|
|
api.get('/postes/modules'),
|
2026-04-17 04:04:04 -04:00
|
|
|
api.get('/postes/candidatures'),
|
|
|
|
|
]);
|
|
|
|
|
setPostes(p);
|
2026-05-15 16:41:17 -04:00
|
|
|
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) => {
|
2026-05-15 16:41:17 -04:00
|
|
|
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`);
|
2026-05-15 16:41:17 -04:00
|
|
|
flash('Poste supprimé');
|
2026-04-17 04:04:04 -04:00
|
|
|
await charger();
|
|
|
|
|
} catch (e) { setError(e.message); }
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-15 16:41:17 -04:00
|
|
|
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 {
|
2026-05-15 16:41:17 -04:00
|
|
|
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">
|
2026-05-15 16:41:17 -04:00
|
|
|
<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">
|
|
|
|
|
{[
|
2026-05-15 16:41:17 -04:00
|
|
|
['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)}
|
2026-05-15 16:41:17 -04:00
|
|
|
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">
|
2026-05-15 16:41:17 -04:00
|
|
|
<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>
|
2026-05-15 16:41:17 -04:00
|
|
|
<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>
|
2026-05-15 16:41:17 -04:00
|
|
|
{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 ? (
|
2026-05-15 16:41:17 -04:00
|
|
|
<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')}
|
2026-05-15 16:41:17 -04:00
|
|
|
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')}
|
2026-05-15 16:41:17 -04:00
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
}
|