2026-05-25 17:39:38 -04:00
import React , { useEffect , useState } from 'react' ;
import { api } from '../stores/api' ;
import { useStore } from '../stores/store' ;
import PageHeader from '../components/PageHeader' ;
import { dateHeureReunionIso , datesAssembleesAffaires , dateLocale , labelDateReunion } from '../utils/datesReunion' ;
const INPUT = 'w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue' ;
export default function AssembleeAffaires ( ) {
const { postes , groupe } = useStore ( ) ;
const estExecutif = postes . some ( ( p ) => [ 'RSG' , 'RSG Substitut(e)' , 'Trésorier(ière)' , 'Secrétaire' ] . includes ( p ) ) ;
const choixDates = datesAssembleesAffaires ( groupe ) ;
const [ membres , setMembres ] = useState ( [ ] ) ;
const [ reunions , setReunions ] = useState ( [ ] ) ;
const [ pvs , setPvs ] = useState ( [ ] ) ;
const [ propositions , setPropositions ] = useState ( [ ] ) ;
const [ resolutions , setResolutions ] = useState ( [ ] ) ;
const [ postesVacants , setPostesVacants ] = useState ( [ ] ) ;
const [ reunionId , setReunionId ] = useState ( '' ) ;
const [ dateSelection , setDateSelection ] = useState ( dateLocale ( ) ) ;
const [ dateReunion , setDateReunion ] = useState ( dateLocale ( ) ) ;
const [ note , setNote ] = useState ( '' ) ;
const [ presencesMap , setPresencesMap ] = useState ( { } ) ;
const [ contenu , setContenu ] = useState ( '' ) ;
const [ pvPrecedent , setPvPrecedent ] = useState ( null ) ;
const [ showPvPrecedent , setShowPvPrecedent ] = useState ( false ) ;
2026-05-26 09:05:58 -04:00
const [ adoption , setAdoption ] = useState ( { proposeur _id : '' , secondeur _id : '' } ) ;
2026-05-25 17:39:38 -04:00
const [ msg , setMsg ] = useState ( '' ) ;
const [ error , setError ] = useState ( '' ) ;
const [ loading , setLoading ] = useState ( false ) ;
const charger = async ( ) => {
try {
const [ m , r , pv , props , res , postesDispo ] = await Promise . all ( [
api . get ( '/membres/actifs' ) ,
api . get ( '/presences/reunions' ) ,
api . get ( '/pv/' ) ,
api . get ( '/propositions/?statut=secondee&limite=100' ) ,
api . get ( '/propositions/resolutions' ) ,
api . get ( '/postes/disponibles' ) ,
] ) ;
setMembres ( m ) ;
setReunions ( r ) ;
setPvs ( pv ) ;
setPropositions ( props ) ;
setResolutions ( res ) ;
setPostesVacants ( postesDispo ) ;
const map = { } ;
m . forEach ( ( mb ) => { map [ mb . id ] = true ; } ) ;
setPresencesMap ( map ) ;
const duJour = r . find ( ( x ) => x . date _reunion ? . slice ( 0 , 10 ) === dateSelection ) ;
if ( duJour ) setReunionId ( duJour . id ) ;
} catch ( e ) { setError ( e . message ) ; }
} ;
useEffect ( ( ) => { charger ( ) ; } , [ ] ) ;
useEffect ( ( ) => {
if ( choixDates . length > 0 ) {
if ( ! choixDates . includes ( dateSelection ) ) setDateSelection ( choixDates . find ( ( d ) => d >= dateLocale ( ) ) || choixDates [ choixDates . length - 1 ] ) ;
if ( ! choixDates . includes ( dateReunion ) ) setDateReunion ( choixDates . find ( ( d ) => d >= dateLocale ( ) ) || choixDates [ choixDates . length - 1 ] ) ;
}
} , [ groupe ? . jour _reunion , groupe ? . assemblee _affaires _ordre ] ) ;
useEffect ( ( ) => {
const duJour = reunions . find ( ( x ) => x . date _reunion ? . slice ( 0 , 10 ) === dateSelection ) ;
setReunionId ( duJour ? . id || '' ) ;
const precedents = pvs . filter ( ( p ) => p . date _reunion && p . date _reunion . slice ( 0 , 10 ) < dateSelection ) ;
setPvPrecedent ( precedents [ 0 ] || null ) ;
} , [ dateSelection , reunions , pvs ] ) ;
const reunion = reunions . find ( ( r ) => r . id === reunionId ) ;
const pvJour = pvs . find ( ( p ) => p . date _reunion ? . slice ( 0 , 10 ) === dateSelection ) ;
const resolutionsJour = resolutions . filter ( ( r ) => ( r . date _vote || r . cree _le ? . slice ( 0 , 10 ) ) === dateSelection ) ;
const nbPresents = Object . values ( presencesMap ) . filter ( Boolean ) . length ;
const toggle = ( membreId ) => setPresencesMap ( { ... presencesMap , [ membreId ] : ! presencesMap [ membreId ] } ) ;
const imprimer = ( ) => window . print ( ) ;
const creerRapport = async ( ) => {
const res = await api . post ( '/presences/reunion' , {
date _reunion : dateHeureReunionIso ( dateReunion , groupe ) ,
note : note . trim ( ) || null ,
} ) ;
setReunionId ( res . id ) ;
setReunions ( [ res , ... reunions ] ) ;
setDateSelection ( res . date _reunion . slice ( 0 , 10 ) ) ;
return res . id ;
} ;
const enregistrer = async ( ) => {
setError ( '' ) ;
setMsg ( '' ) ;
setLoading ( true ) ;
try {
const id = reunionId || await creerRapport ( ) ;
const presences = Object . entries ( presencesMap ) . map ( ( [ membre _id , present ] ) => ( { membre _id , present } ) ) ;
await api . post ( '/presences/' , { reunion _id : id , presences } ) ;
if ( contenu . trim ( ) ) {
await api . post ( '/pv/' , {
reunion _id : id ,
contenu : contenu . trim ( ) ,
decisions : null ,
} ) ;
}
setMsg ( contenu . trim ( ) ? 'Présences et PV enregistrés' : 'Présences enregistrées' ) ;
await charger ( ) ;
} catch ( e ) { setError ( e . message ) ; }
setLoading ( false ) ;
} ;
const adopterPvPrecedent = async ( ) => {
if ( ! pvPrecedent ) return ;
2026-05-26 09:05:58 -04:00
if ( ! adoption . proposeur _id || ! adoption . secondeur _id ) {
setError ( 'Proposeur et secondeur requis' ) ;
return ;
}
2026-05-25 17:39:38 -04:00
setError ( '' ) ;
try {
await api . patch ( ` /pv/ ${ pvPrecedent . id } /adopter ` , adoption ) ;
setMsg ( 'PV précédent adopté' ) ;
await charger ( ) ;
} catch ( e ) { setError ( e . message ) ; }
} ;
const textePvAuto = [
'Ouverture de l’ assemblée.' ,
'' ,
pvPrecedent ? ` Lecture du procès-verbal précédent ( ${ new Date ( pvPrecedent . date _reunion ) . toLocaleDateString ( 'fr-CA' ) } ). ` : 'Lecture du procès-verbal précédent.' ,
'' ,
'Réflexions en cours:' ,
... propositions . map ( ( p ) => ` - ${ p . sujet } ( ${ p . proposeur _prenom } / ${ p . secondeur _prenom || 'à confirmer' } ) ` ) ,
'' ,
'Décisions prises pendant cette assemblée:' ,
... resolutionsJour . map ( ( r ) => ` - ${ r . numero _resolution || '' } ${ r . sujet } ` . trim ( ) ) ,
'' ,
'Postes disponibles:' ,
... postesVacants . map ( ( p ) => ` - ${ p . nom } ` ) ,
] . join ( '\n' ) ;
return (
< div className = "min-h-screen bg-gray-100" >
< PageHeader titre = "Assemblée d'affaires" sousTitre = "Assistant et rapport imprimable" / >
< 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 > }
< Card titre = "Consulter le rapport" >
< Champ label = "Date" >
< SelectDate value = { dateSelection } onChange = { setDateSelection } dates = { choixDates } / >
< / Champ >
< div className = "grid grid-cols-2 gap-2" >
< button onClick = { imprimer } className = "p-3 bg-gray-900 text-white rounded-xl text-sm font-bold" > Imprimer < / button >
{ pvPrecedent && (
< button onClick = { ( ) => setShowPvPrecedent ( ! showPvPrecedent ) } className = "p-3 bg-blue-50 text-blue-700 rounded-xl text-sm font-bold border border-blue-100" >
PV précédent
< / button >
) }
< / div >
< / Card >
{ showPvPrecedent && pvPrecedent && (
< Card titre = "Procès-verbal précédent" >
< p className = "text-xs text-gray-400" > { new Date ( pvPrecedent . date _reunion ) . toLocaleDateString ( 'fr-CA' ) } < / p >
< p className = "text-sm whitespace-pre-wrap text-gray-700" > { pvPrecedent . contenu } < / p >
{ estExecutif && ! pvPrecedent . approuve && (
< div className = "space-y-2 pt-2 border-t border-gray-100" >
< div className = "grid grid-cols-2 gap-2" >
2026-05-26 09:05:58 -04:00
< select value = { adoption . proposeur _id } onChange = { ( e ) => setAdoption ( { ... adoption , proposeur _id : e . target . value } ) } className = { INPUT } >
< option value = "" > Proposeur < / option >
{ membres . map ( ( m ) => < option key = { m . id } value = { m . id } > { m . prenom } < / option > ) }
< / select >
< select value = { adoption . secondeur _id } onChange = { ( e ) => setAdoption ( { ... adoption , secondeur _id : e . target . value } ) } className = { INPUT } >
< option value = "" > Secondeur < / option >
{ membres . map ( ( m ) => < option key = { m . id } value = { m . id } > { m . prenom } < / option > ) }
< / select >
2026-05-25 17:39:38 -04:00
< / div >
2026-05-26 09:05:58 -04:00
< button onClick = { adopterPvPrecedent } disabled = { ! adoption . proposeur _id || ! adoption . secondeur _id }
className = "w-full p-3 bg-green-50 text-green-700 rounded-xl text-sm font-bold border border-green-100 disabled:opacity-40" >
Adopter le PV précédent
< / button >
2026-05-25 17:39:38 -04:00
< / div >
) }
< / Card >
) }
< div className = "bg-white rounded-2xl p-5 shadow-sm print:shadow-none print:rounded-none print:p-0" >
< h2 className = "text-xl font-bold text-gray-900" > Rapport d ’ assemblée d ’ affaires < / h2 >
< p className = "text-sm text-gray-500" > { dateSelection } < / p >
< Section titre = "Présences" >
{ reunion ? . presences ? . length ? reunion . presences . filter ( ( p ) => p . present ) . map ( ( p ) => < li key = { p . membre _id } > { p . membre _prenom } < / li > ) : < li > Présences à cocher sur papier < / li > }
< / Section >
< Section titre = "Réflexions en cours" >
{ propositions . length ? propositions . map ( ( p ) => < li key = { p . id } > { p . sujet } ( { p . proposeur _prenom } / { p . secondeur _prenom || 'à confirmer' } ) < / li > ) : < li > Aucune proposition secondée non résolue < / li > }
< / Section >
< Section titre = "Décisions prises" >
{ resolutionsJour . length ? resolutionsJour . map ( ( r ) => < li key = { r . id } > { r . numero _resolution } — { r . sujet } < / li > ) : < li > Aucune décision enregistrée pour cette date < / li > }
< / Section >
< Section titre = "Postes disponibles" >
{ postesVacants . length ? postesVacants . map ( ( p ) => < li key = { p . id } > { p . nom } < / li > ) : < li > Aucun poste vacant < / li > }
< / Section >
< div className = "mt-4" >
< h3 className = "font-bold text-gray-800" > Procès - verbal < / h3 >
< p className = "text-sm whitespace-pre-wrap mt-1" > { pvJour ? . contenu || textePvAuto } < / p >
< / div >
< / div >
{ estExecutif && (
< >
< Card titre = "Créer ou renseigner l’ assemblée" >
{ reunions . length > 0 && (
< Champ label = "Assemblée existante" >
< select value = { reunionId } onChange = { ( e ) => setReunionId ( e . target . value ) } className = { INPUT } >
< option value = "" > Nouvelle assemblée < / option >
{ reunions . map ( ( r ) => < option key = { r . id } value = { r . id } > { new Date ( r . date _reunion ) . toLocaleDateString ( 'fr-CA' ) } < / option > ) }
< / select >
< / Champ >
) }
{ ! reunionId && (
< >
< Champ label = "Date" > < SelectDate value = { dateReunion } onChange = { setDateReunion } dates = { choixDates } / > < / Champ >
< Champ label = "Note" > < input value = { note } onChange = { ( e ) => setNote ( e . target . value ) } placeholder = "Note optionnelle" className = { INPUT } / > < / Champ >
< button onClick = { creerRapport } className = "w-full p-3 bg-indigo-50 text-indigo-700 rounded-xl text-sm font-bold border border-indigo-100" > Créer le rapport d ’ assemblée < / button >
< / >
) }
< / Card >
< Card titre = { ` Présences ( ${ nbPresents } / ${ membres . length } ) ` } >
< div className = "space-y-1" > { membres . map ( ( m ) => (
< button key = { m . id } onClick = { ( ) => toggle ( m . id ) } className = { ` w-full flex items-center gap-3 p-3 rounded-xl transition ${ presencesMap [ m . id ] ? 'bg-green-50 border border-green-200' : 'bg-gray-50 border border-gray-100 opacity-50' } ` } >
< span className = { ` w-6 h-6 rounded-lg flex items-center justify-center text-sm font-bold ${ presencesMap [ m . id ] ? 'bg-green-500 text-white' : 'bg-gray-200 text-gray-400' } ` } > { presencesMap [ m . id ] ? '✓' : '' } < / span >
< span className = "font-medium text-gray-800" > { m . prenom } < / span >
< / button >
) ) } < / div >
< / Card >
< Card titre = "Procès-verbal" >
< textarea value = { contenu } onChange = { ( e ) => setContenu ( e . target . value ) } rows = { 10 } placeholder = { textePvAuto } className = { ` ${ INPUT } resize-none text-sm ` } / >
< / Card >
< button onClick = { enregistrer } disabled = { loading } className = "w-full p-5 bg-indigo-600 text-white rounded-2xl text-lg font-bold disabled:opacity-40 active:scale-[0.97] transition shadow-lg" > { loading ? '...' : 'Enregistrer l’ assemblée' } < / button >
< / >
) }
< / div >
< / div >
) ;
}
function Card ( { titre , children } ) {
return < div className = "bg-white rounded-2xl p-4 shadow-sm space-y-3" > < h2 className = "font-bold text-gray-800" > { titre } < / h2 > { children } < / div > ;
}
function Champ ( { label , children } ) {
return < label className = "block" > < span className = "text-xs text-gray-400 block mb-1" > { label } < / span > { children } < / label > ;
}
function Section ( { titre , children } ) {
return < div className = "mt-4" > < h3 className = "font-bold text-gray-800" > { titre } < / h3 > < ul className = "list-disc ml-5 text-sm mt-1 space-y-1" > { children } < / ul > < / div > ;
}
function SelectDate ( { value , onChange , dates } ) {
if ( ! dates . length ) {
return < div className = "rounded-xl bg-gray-50 p-3 text-sm text-gray-500" > Configurez le jour de réunion dans les paramètres du groupe . < / div > ;
}
return (
< select value = { value } onChange = { ( e ) => onChange ( e . target . value ) } className = { INPUT } >
{ dates . map ( ( d ) => < option key = { d } value = { d } > { labelDateReunion ( d ) } < / option > ) }
< / select >
) ;
}