234 lines
10 KiB
JavaScript
234 lines
10 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import { api } from '../stores/api';
|
|
import PageHeader from '../components/PageHeader';
|
|
import SuccessCheck from '../components/SuccessCheck';
|
|
|
|
export default function RedigerPV() {
|
|
const [reunions, setReunions] = useState([]);
|
|
const [pvList, setPvList] = useState([]);
|
|
const [propositions, setPropositions] = useState([]);
|
|
const [reunionId, setReunionId] = useState('');
|
|
const [contenu, setContenu] = useState('');
|
|
const [decisions, setDecisions] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [success, setSuccess] = useState(false);
|
|
const [viewPv, setViewPv] = useState(null);
|
|
const [error, setError] = useState('');
|
|
|
|
useEffect(() => {
|
|
Promise.all([
|
|
api.get('/presences/reunions'),
|
|
api.get('/pv/'),
|
|
api.get('/propositions/?statut=secondee'),
|
|
]).then(([r, p, props]) => {
|
|
setReunions(r);
|
|
setPvList(p);
|
|
setPropositions(props);
|
|
setLoading(false);
|
|
}).catch(() => setLoading(false));
|
|
}, []);
|
|
|
|
// Auto-renseigner les décisions depuis les propositions secondées
|
|
const chargerPropositions = () => {
|
|
if (propositions.length === 0) return;
|
|
const decisionsAuto = propositions.map((p) => ({
|
|
sujet: p.sujet,
|
|
decision: p.description || '',
|
|
proposeur: p.proposeur_prenom,
|
|
secondeur: p.secondeur_prenom || '',
|
|
statut: 'en_attente',
|
|
proposition_id: p.id,
|
|
}));
|
|
setDecisions((prev) => {
|
|
// Ne pas dupliquer les propositions déjà ajoutées
|
|
const existants = new Set(prev.map((d) => d.proposition_id).filter(Boolean));
|
|
const nouvelles = decisionsAuto.filter((d) => !existants.has(d.proposition_id));
|
|
return [...prev, ...nouvelles];
|
|
});
|
|
};
|
|
|
|
const ajouterDecision = () => {
|
|
setDecisions([...decisions, { sujet: '', decision: '', proposeur: '', secondeur: '', statut: 'en_attente' }]);
|
|
};
|
|
|
|
const updateDecision = (idx, field, value) => {
|
|
const copy = [...decisions];
|
|
copy[idx][field] = value;
|
|
setDecisions(copy);
|
|
};
|
|
|
|
const supprimerDecision = (idx) => {
|
|
setDecisions(decisions.filter((_, i) => i !== idx));
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
if (!reunionId) { setError("Sélectionnez une réunion d'affaires"); return; }
|
|
if (!contenu.trim()) { setError('Le contenu du PV est obligatoire'); return; }
|
|
setError('');
|
|
try {
|
|
const validDecisions = decisions.filter((d) => d.sujet.trim());
|
|
await api.post('/pv/', {
|
|
reunion_id: reunionId,
|
|
contenu: contenu.trim(),
|
|
decisions: validDecisions.length > 0 ? validDecisions : null,
|
|
});
|
|
setSuccess(true);
|
|
} catch (e) { setError(e.message); }
|
|
};
|
|
|
|
if (success) return <SuccessCheck message="Procès-verbal enregistré ✓" />;
|
|
|
|
// Vue lecture d'un PV existant
|
|
if (viewPv) {
|
|
return (
|
|
<div className="min-h-screen bg-gray-100">
|
|
<div className="bg-aa-blue text-white p-4 flex items-center gap-3">
|
|
<button onClick={() => setViewPv(null)} className="text-2xl">←</button>
|
|
<div>
|
|
<h1 className="text-lg font-bold">Procès-verbal</h1>
|
|
<p className="text-blue-200 text-xs">
|
|
{viewPv.date_reunion ? new Date(viewPv.date_reunion).toLocaleDateString('fr-CA') : ''}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="p-4 space-y-4">
|
|
<div className="bg-white rounded-2xl p-4 shadow-sm">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<p className="text-xs text-gray-400">Rédigé par {viewPv.redacteur_prenom}</p>
|
|
{viewPv.approuve && (
|
|
<span className="bg-green-100 text-green-700 text-xs px-2 py-1 rounded-full font-bold">Approuvé</span>
|
|
)}
|
|
</div>
|
|
<div className="text-gray-800 whitespace-pre-wrap text-sm leading-relaxed">{viewPv.contenu}</div>
|
|
</div>
|
|
{viewPv.decisions && viewPv.decisions.length > 0 && (
|
|
<div className="bg-white rounded-2xl p-4 shadow-sm">
|
|
<h3 className="font-bold text-gray-700 mb-3">Décisions</h3>
|
|
{viewPv.decisions.map((d, i) => (
|
|
<div key={i} className="py-2 border-b border-gray-100 last:border-0">
|
|
<p className="font-medium text-gray-800">{d.sujet}</p>
|
|
<p className="text-sm text-gray-600">{d.decision}</p>
|
|
<p className="text-xs text-gray-400 mt-1">
|
|
{d.proposeur && `Proposé par ${d.proposeur}`}
|
|
{d.secondeur && `, secondé par ${d.secondeur}`}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-100">
|
|
<PageHeader titre="Rédiger un PV" sousTitre="M1 — Procès-verbal" />
|
|
|
|
<div className="p-4 space-y-4">
|
|
{/* PV existants */}
|
|
{pvList.length > 0 && (
|
|
<div className="bg-white rounded-2xl p-4 shadow-sm">
|
|
<h2 className="text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">PV récents</h2>
|
|
{pvList.slice(0, 5).map((p) => (
|
|
<button key={p.id} onClick={() => setViewPv(p)}
|
|
className="w-full flex items-center justify-between py-2 border-b border-gray-50 last:border-0 active:bg-gray-50">
|
|
<div>
|
|
<p className="font-medium text-sm text-gray-800">
|
|
{p.date_reunion ? new Date(p.date_reunion).toLocaleDateString('fr-CA') : 'Sans date'}
|
|
</p>
|
|
<p className="text-xs text-gray-400">Par {p.redacteur_prenom}</p>
|
|
</div>
|
|
<span className="text-gray-300">→</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Nouveau PV */}
|
|
<div className="bg-white rounded-2xl p-4 shadow-sm">
|
|
<h2 className="font-bold text-gray-700 mb-3">Nouveau procès-verbal</h2>
|
|
|
|
<label className="text-xs text-gray-400 block mb-1">Réunion d'affaires</label>
|
|
<select value={reunionId} onChange={(e) => setReunionId(e.target.value)}
|
|
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue mb-3">
|
|
<option value="">Sélectionner</option>
|
|
{reunions.map((r) => (
|
|
<option key={r.id} value={r.id}>
|
|
{new Date(r.date_reunion).toLocaleDateString('fr-CA')}
|
|
{r.presences?.length > 0 ? ` (${r.presences.filter(p => p.present).length} présents)` : ''}
|
|
</option>
|
|
))}
|
|
</select>
|
|
|
|
<label className="text-xs text-gray-400 block mb-1">Contenu du PV</label>
|
|
<textarea value={contenu} onChange={(e) => setContenu(e.target.value)} rows={8}
|
|
placeholder={"Ouverture de la réunion à 19h00...\n\nLecture du PV précédent...\n\nRapport du trésorier...\n\nRapport du RSG..."}
|
|
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue resize-none text-sm" />
|
|
</div>
|
|
|
|
{/* Décisions */}
|
|
<div className="bg-white rounded-2xl p-4 shadow-sm">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<h2 className="font-bold text-gray-700">Affaires nouvelles / Décisions</h2>
|
|
<div className="flex gap-2">
|
|
{propositions.length > 0 && (
|
|
<button onClick={chargerPropositions}
|
|
className="text-purple-600 text-xs font-medium bg-purple-50 px-2 py-1 rounded-lg active:opacity-60">
|
|
📋 Importer propositions ({propositions.length})
|
|
</button>
|
|
)}
|
|
<button onClick={ajouterDecision}
|
|
className="text-aa-blue text-sm font-medium active:opacity-60">+ Ajouter</button>
|
|
</div>
|
|
</div>
|
|
{decisions.length === 0 ? (
|
|
<div className="text-gray-400 text-sm">
|
|
<p>Aucune décision.</p>
|
|
{propositions.length > 0 && (
|
|
<p className="mt-1 text-purple-500 text-xs">
|
|
{propositions.length} proposition{propositions.length > 1 ? 's' : ''} secondée{propositions.length > 1 ? 's' : ''} à traiter — cliquez « Importer propositions ».
|
|
</p>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{decisions.map((d, i) => (
|
|
<div key={i} className={`rounded-xl p-3 space-y-2 ${d.proposition_id ? 'bg-purple-50' : 'bg-gray-50'}`}>
|
|
<div className="flex justify-between">
|
|
<span className="text-xs text-gray-400">
|
|
{d.proposition_id ? '📋 Proposition importée' : `Décision ${i + 1}`}
|
|
</span>
|
|
<button onClick={() => supprimerDecision(i)} className="text-red-400 text-xs">Supprimer</button>
|
|
</div>
|
|
<input type="text" placeholder="Sujet" value={d.sujet}
|
|
onChange={(e) => updateDecision(i, 'sujet', e.target.value)}
|
|
className="w-full p-2 bg-white rounded-lg border-0 text-sm focus:ring-2 focus:ring-aa-blue" />
|
|
<input type="text" placeholder="Décision prise" value={d.decision}
|
|
onChange={(e) => updateDecision(i, 'decision', e.target.value)}
|
|
className="w-full p-2 bg-white rounded-lg border-0 text-sm focus:ring-2 focus:ring-aa-blue" />
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<input type="text" placeholder="Proposeur" value={d.proposeur}
|
|
onChange={(e) => updateDecision(i, 'proposeur', e.target.value)}
|
|
className="p-2 bg-white rounded-lg border-0 text-sm focus:ring-2 focus:ring-aa-blue" />
|
|
<input type="text" placeholder="Secondeur" value={d.secondeur}
|
|
onChange={(e) => updateDecision(i, 'secondeur', e.target.value)}
|
|
className="p-2 bg-white rounded-lg border-0 text-sm focus:ring-2 focus:ring-aa-blue" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{error && <p className="text-red-500 text-sm text-center bg-red-50 p-3 rounded-lg">{error}</p>}
|
|
|
|
<button onClick={handleSubmit}
|
|
disabled={!reunionId || !contenu.trim()}
|
|
className="w-full p-5 bg-indigo-600 text-white rounded-2xl text-xl font-bold disabled:opacity-40 active:scale-[0.97] transition shadow-lg">
|
|
📝 Enregistrer le PV
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|