groupe-meditation/frontend/src/pages/RedigerPV.jsx
2026-04-17 04:04:04 -04:00

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>
);
}