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

326 lines
15 KiB
React
Raw Normal View History

2026-04-17 04:04:04 -04:00
import React, { useState, useEffect } from 'react';
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' },
];
export default function GererPostes() {
const [postes, setPostes] = useState([]);
const [taches, setTaches] = useState([]);
const [candidatures, setCandidatures] = useState([]);
const [tab, setTab] = useState('postes');
const [showForm, setShowForm] = useState(false);
const [showTacheForm, setShowTacheForm] = useState(false);
const [msg, setMsg] = useState('');
const [error, setError] = useState('');
// Form poste
const [nom, setNom] = useState('');
const [categorie, setCategorie] = useState('service');
const [description, setDescription] = useState('');
const [prerequis, setPrerequis] = useState('');
const [informations, setInformations] = useState('');
// Form tache
const [tacheNom, setTacheNom] = useState('');
const [tacheInfo, setTacheInfo] = useState('');
// Affectation tache
const [affectPosteId, setAffectPosteId] = useState('');
const [affectTacheId, setAffectTacheId] = useState('');
const charger = async () => {
try {
const [p, t, c] = await Promise.all([
api.get('/postes/?inclure_abolis=true'),
api.get('/postes/taches'),
api.get('/postes/candidatures'),
]);
setPostes(p);
setTaches(t);
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}» ?\nCette action est irréversible.`)) return;
try {
await api.request('DELETE', `/postes/${id}/permanent`);
flash('Poste supprimé définitivement');
await charger();
} catch (e) { setError(e.message); }
};
const creerTache = async () => {
if (!tacheNom.trim()) { setError('Nom requis'); return; }
try {
await api.post('/postes/taches', { nom: tacheNom.trim(), informations: tacheInfo.trim() || null });
setTacheNom(''); setTacheInfo(''); setShowTacheForm(false);
flash('Tâche créée ✓');
await charger();
} catch (e) { setError(e.message); }
};
const supprimerTache = async (id, nomT) => {
if (!confirm(`Supprimer la tâche «${nomT}» ?`)) return;
try {
await api.request('DELETE', `/postes/taches/${id}`);
flash('Tâche supprimée');
await charger();
} catch (e) { setError(e.message); }
};
const affecterTache = async () => {
if (!affectPosteId || !affectTacheId) { setError('Sélectionnez un poste et une tâche'); return; }
try {
await api.post(`/postes/${affectPosteId}/taches/${affectTacheId}`);
setAffectTacheId('');
flash('Tâche affectée ✓');
await charger();
} catch (e) { setError(e.message); }
};
const desaffecterTache = async (posteId, tacheId) => {
try {
await api.request('DELETE', `/postes/${posteId}/taches/${tacheId}`);
flash('Tâche retirée');
await charger();
} catch (e) { setError(e.message); }
};
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 postesActifs = postes.filter((p) => p.actif);
const CAT_ICON = { executif: '🔵', service: '🟣', physique: '🟢' };
return (
<div className="min-h-screen bg-gray-100">
<PageHeader titre="Gestion des postes" sousTitre="Postes, tâches, candidatures" />
<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})`],
['taches', `🔧 Tâches (${taches.length})`],
['candidatures', `🙋 Candid. (${candidatures.length})`],
].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>
{/* ── Postes ── */}
{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 active:scale-[0.97]">
+ Créer un poste
</button>
{showForm && (
<div className="bg-white rounded-2xl p-4 shadow-sm space-y-3">
<input type="text" 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" autoFocus />
<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 type="text" 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 type="text" 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 complémentaires" rows={2}
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue resize-none" />
<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>}
{p.informations && <p className="text-xs text-gray-400 mt-1 italic">{p.informations}</p>}
{/* Tâches associées */}
{p.taches && p.taches.length > 0 && (
<div className="mt-2 space-y-1">
{p.taches.map((t) => (
<div key={t.id} className="flex items-center justify-between bg-gray-50 rounded-lg px-2 py-1">
<span className="text-xs text-gray-600">🔧 {t.nom}</span>
{p.actif && (
<button onClick={() => desaffecterTache(p.id, t.id)}
className="text-xs text-red-400 active:text-red-600">✕</button>
)}
</div>
))}
</div>
)}
</div>
<div className="flex flex-col gap-1">
{p.actif ? (
<button onClick={() => abolir(p.id, p.nom)}
className="text-xs text-red-500 font-medium px-2 py-1 rounded-lg active:bg-red-50">Abolir</button>
) : (
<>
<button onClick={() => reactiver(p.id)}
className="text-xs text-green-600 font-medium px-2 py-1 rounded-lg active:bg-green-50">Réactiver</button>
<button onClick={() => supprimerPermanent(p.id, p.nom)}
className="text-xs text-red-500 font-medium px-2 py-1 rounded-lg active:bg-red-50">Supprimer</button>
</>
)}
</div>
</div>
</div>
))}
</div>
)}
{/* ── Tâches ── */}
{tab === 'taches' && (
<div className="space-y-3">
<button onClick={() => setShowTacheForm(!showTacheForm)}
className="w-full py-3 bg-green-50 text-green-700 rounded-xl font-bold border border-green-200 active:scale-[0.97]">
+ Créer une tâche
</button>
{showTacheForm && (
<div className="bg-white rounded-2xl p-4 shadow-sm space-y-3">
<input type="text" value={tacheNom} onChange={(e) => setTacheNom(e.target.value)}
placeholder="Nom de la tâche" className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue" autoFocus />
<textarea value={tacheInfo} onChange={(e) => setTacheInfo(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" />
<button onClick={creerTache} disabled={!tacheNom.trim()}
className="w-full py-3 bg-aa-blue text-white rounded-xl font-bold disabled:opacity-40">Créer</button>
</div>
)}
{/* Affecter tâche → poste */}
{taches.length > 0 && postesActifs.length > 0 && (
<div className="bg-white rounded-2xl p-4 shadow-sm space-y-3">
<p className="text-xs font-bold text-gray-400 uppercase tracking-wider">Affecter une tâche à un poste</p>
<select value={affectTacheId} onChange={(e) => setAffectTacheId(e.target.value)}
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue text-sm">
<option value="">— Tâche —</option>
{taches.map((t) => <option key={t.id} value={t.id}>{t.nom}</option>)}
</select>
<select value={affectPosteId} onChange={(e) => setAffectPosteId(e.target.value)}
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue text-sm">
<option value="">— Poste —</option>
{postesActifs.map((p) => <option key={p.id} value={p.id}>{CAT_ICON[p.categorie]} {p.nom}</option>)}
</select>
<button onClick={affecterTache} disabled={!affectPosteId || !affectTacheId}
className="w-full py-2 bg-purple-600 text-white rounded-xl font-bold disabled:opacity-40 text-sm">
🔧 Affecter
</button>
</div>
)}
{/* Liste des tâches */}
{taches.map((t) => (
<div key={t.id} className="bg-white rounded-2xl p-4 shadow-sm">
<div className="flex items-start justify-between gap-2">
<div className="flex-1">
<p className="font-bold text-gray-800">🔧 {t.nom}</p>
{t.informations && <p className="text-xs text-gray-500 mt-1">{t.informations}</p>}
{t.postes.length > 0 && (
<p className="text-xs text-purple-500 mt-1">→ {t.postes.join(', ')}</p>
)}
</div>
<button onClick={() => supprimerTache(t.id, t.nom)}
className="text-xs text-red-500 font-medium px-2 py-1 rounded-lg active:bg-red-50">Supprimer</button>
</div>
</div>
))}
</div>
)}
{/* ── Candidatures ── */}
{tab === 'candidatures' && (
candidatures.length === 0 ? (
<div className="text-center py-12">
<p className="text-4xl mb-2">🙋</p>
<p className="text-gray-500">Aucune candidature en attente</p>
</div>
) : (
<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>}
<p className="text-xs text-gray-400 mt-1">{new Date(c.cree_le).toLocaleDateString('fr-CA')}</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 active:scale-[0.97]">
✓ Accepter
</button>
<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 active:scale-[0.97]">
✗ Refuser
</button>
</div>
</div>
))}
</div>
)
)}
</div>
</div>
);
}