326 lines
15 KiB
React
326 lines
15 KiB
React
|
|
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>
|
||
|
|
);
|
||
|
|
}
|