2026-03-04 14:01:00 -05:00
import React , { useState , useEffect } from 'react' ;
import { api } from '../stores/api' ;
2026-05-25 17:39:38 -04:00
import { useStore } from '../stores/store' ;
2026-03-04 14:01:00 -05:00
import PageHeader from '../components/PageHeader' ;
export default function GererMembres ( ) {
2026-05-31 23:30:57 -04:00
const { membre , groupe , estSysadmin , impersoner } = useStore ( ) ;
2026-03-04 14:01:00 -05:00
const [ membres , setMembres ] = useState ( [ ] ) ;
const [ loading , setLoading ] = useState ( true ) ;
const [ detail , setDetail ] = useState ( null ) ;
const [ confirmAction , setConfirmAction ] = useState ( null ) ;
const [ msg , setMsg ] = useState ( '' ) ;
2026-05-15 16:41:17 -04:00
const [ code , setCode ] = useState ( null ) ;
const [ expiration , setExpiration ] = useState ( null ) ;
const [ inviteLoading , setInviteLoading ] = useState ( false ) ;
const [ copied , setCopied ] = useState ( false ) ;
Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.
Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.
Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.
Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00
const [ showCreate , setShowCreate ] = useState ( false ) ;
const [ createLoading , setCreateLoading ] = useState ( false ) ;
const [ createForm , setCreateForm ] = useState ( {
prenom : '' ,
nom : '' ,
telephone : '' ,
courriel : '' ,
pin : '' ,
date _abstinence : '' ,
} ) ;
2026-03-04 14:01:00 -05:00
const charger = async ( ) => {
setLoading ( true ) ;
try {
const data = await api . get ( '/membres/' ) ;
setMembres ( data ) ;
} catch { }
setLoading ( false ) ;
} ;
useEffect ( ( ) => { charger ( ) ; } , [ ] ) ;
2026-05-15 16:41:17 -04:00
const genererInvitation = async ( ) => {
setInviteLoading ( true ) ;
try {
const data = await api . post ( '/invitations/' ) ;
setCode ( data . code ) ;
setExpiration ( new Date ( data . expire _le ) . toLocaleString ( 'fr-CA' ) ) ;
} catch ( e ) { alert ( e . message ) ; }
setInviteLoading ( false ) ;
} ;
const copierInvitation = ( ) => {
navigator . clipboard . writeText ( code ) . then ( ( ) => {
setCopied ( true ) ;
setTimeout ( ( ) => setCopied ( false ) , 2000 ) ;
} ) ;
} ;
const partagerSMS = ( ) => {
2026-05-31 23:30:57 -04:00
const texte = ` Rejoins ${ groupe ? . nom || 'le groupe' } sur l'app. Ton code d'invitation : ${ code } ` ;
2026-05-15 16:41:17 -04:00
window . open ( ` sms:?body= ${ encodeURIComponent ( texte ) } ` ) ;
} ;
2026-03-04 14:01:00 -05:00
const desactiver = async ( id ) => {
try {
const res = await api . patch ( ` /membres/ ${ id } /desactiver ` ) ;
setMsg ( res . message ) ;
setConfirmAction ( null ) ;
setDetail ( null ) ;
charger ( ) ;
setTimeout ( ( ) => setMsg ( '' ) , 3000 ) ;
} catch ( e ) { alert ( e . message ) ; }
} ;
2026-05-31 12:28:04 -04:00
const reactiver = async ( id ) => {
try {
const res = await api . patch ( ` /membres/ ${ id } /reactiver ` ) ;
setMsg ( res . message ) ;
setConfirmAction ( null ) ;
setDetail ( null ) ;
charger ( ) ;
setTimeout ( ( ) => setMsg ( '' ) , 3000 ) ;
} catch ( e ) { alert ( e . message ) ; }
} ;
2026-03-04 14:01:00 -05:00
const droitOubli = async ( id ) => {
try {
const res = await api . request ( 'DELETE' , ` /membres/ ${ id } /oubli ` ) ;
setMsg ( res . message ) ;
setConfirmAction ( null ) ;
setDetail ( null ) ;
charger ( ) ;
setTimeout ( ( ) => setMsg ( '' ) , 3000 ) ;
} catch ( e ) { alert ( e . message ) ; }
} ;
2026-05-25 17:39:38 -04:00
const agirComme = async ( id ) => {
try {
await impersoner ( id ) ;
} catch ( e ) { alert ( e . message ) ; }
} ;
Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.
Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.
Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.
Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00
const creerCompte = async ( ) => {
setCreateLoading ( true ) ;
try {
const payload = {
... createForm ,
nom : createForm . nom . trim ( ) || null ,
date _abstinence : createForm . date _abstinence || null ,
} ;
const res = await api . post ( '/membres/' , payload ) ;
setMsg ( ` Compte créé pour ${ res . prenom } ` ) ;
setShowCreate ( false ) ;
setCreateForm ( { prenom : '' , nom : '' , telephone : '' , courriel : '' , pin : '' , date _abstinence : '' } ) ;
await charger ( ) ;
setTimeout ( ( ) => setMsg ( '' ) , 3000 ) ;
} catch ( e ) {
alert ( e . message ) ;
}
setCreateLoading ( false ) ;
} ;
2026-05-31 12:28:04 -04:00
const estAnonymise = ( m ) => Boolean ( m ? . anonymise || m ? . courriel ? . startsWith ( 'supprime_' ) ) ;
const telephoneVisible = ( m ) => m ? . telephone && m . telephone !== '000-000-0000' ;
const courrielVisible = ( m ) => m ? . courriel && ! m . courriel . startsWith ( 'supprime_' ) ;
2026-03-04 14:01:00 -05:00
// Vue détail d'un membre
if ( detail ) {
2026-05-31 12:28:04 -04:00
const anonymise = estAnonymise ( detail ) ;
const membreCourant = detail . id === membre ? . id ;
const actionsAdministrativesPermises = ! detail . is _sysadmin && ! membreCourant ;
2026-03-04 14:01:00 -05:00
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 = { ( ) => { setDetail ( null ) ; setConfirmAction ( null ) ; } } className = "text-2xl" > ← < / button >
< h1 className = "text-lg font-bold" > { detail . prenom } { detail . nom } < / h1 >
< / div >
< 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 > }
< div className = "bg-white rounded-2xl p-4 shadow-sm space-y-3" >
< div > < p className = "text-xs text-gray-400" > Prénom < / p > < p className = "font-medium" > { detail . prenom } < / p > < / div >
2026-05-31 12:28:04 -04:00
< div > < p className = "text-xs text-gray-400" > Nom < / p > < p className = "font-medium" > { detail . nom || '—' } < / p > < / div >
< div > < p className = "text-xs text-gray-400" > Téléphone < / p > < p className = "font-medium" > { telephoneVisible ( detail ) ? detail . telephone : '—' } < / p > < / div >
< div > < p className = "text-xs text-gray-400" > Courriel < / p > < p className = "font-medium" > { courrielVisible ( detail ) ? detail . courriel : '—' } < / p > < / div >
2026-03-04 14:01:00 -05:00
< div > < p className = "text-xs text-gray-400" > Date d 'abstinence</p><p className="font-medium">{detail.date_abstinence || ' — ' } < / p > < / div >
< div > < p className = "text-xs text-gray-400" > Statut < / p >
2026-05-31 12:28:04 -04:00
< span className = { ` inline-block px-2 py-1 rounded-full text-xs font-bold ${ detail . actif ? 'bg-green-100 text-green-700' : anonymise ? 'bg-gray-100 text-gray-600' : 'bg-red-100 text-red-700' } ` } >
{ anonymise ? 'Anonymisé' : detail . actif ? 'Actif' : 'Inactif' }
2026-03-04 14:01:00 -05:00
< / span >
< / div >
< / div >
{ /* Contact rapide */ }
< div className = "grid grid-cols-2 gap-2" >
2026-05-31 12:28:04 -04:00
{ telephoneVisible ( detail ) && (
2026-03-04 14:01:00 -05:00
< a href = { ` tel: ${ detail . telephone } ` }
className = "p-3 bg-green-50 border border-green-200 rounded-xl text-center text-green-700 font-bold text-sm" >
📞 Appeler
< / a >
) }
2026-05-31 12:28:04 -04:00
{ courrielVisible ( detail ) && (
2026-03-04 14:01:00 -05:00
< a href = { ` mailto: ${ detail . courriel } ` }
className = "p-3 bg-blue-50 border border-blue-200 rounded-xl text-center text-blue-700 font-bold text-sm" >
✉ ️ Courriel
< / a >
) }
< / div >
{ /* Actions */ }
2026-05-31 12:28:04 -04:00
{ estSysadmin && detail . id !== membre ? . id && ! anonymise && ! confirmAction && (
2026-05-25 17:39:38 -04:00
< button onClick = { ( ) => agirComme ( detail . id ) }
className = "w-full p-3 bg-aa-blue text-white rounded-xl font-bold text-sm" >
Agir comme ce membre
< / button >
) }
2026-05-31 12:28:04 -04:00
{ actionsAdministrativesPermises && detail . actif && ! confirmAction && (
2026-03-04 14:01:00 -05:00
< button onClick = { ( ) => setConfirmAction ( 'desactiver' ) }
className = "w-full p-3 bg-orange-50 border border-orange-200 text-orange-700 rounded-xl font-bold text-sm" >
Désactiver ce membre
< / button >
) }
2026-05-31 12:28:04 -04:00
{ actionsAdministrativesPermises && ! detail . actif && ! anonymise && ! confirmAction && (
< button onClick = { ( ) => setConfirmAction ( 'reactiver' ) }
className = "w-full p-3 bg-green-50 border border-green-200 text-green-700 rounded-xl font-bold text-sm" >
Réactiver ce membre
< / button >
) }
{ actionsAdministrativesPermises && ! anonymise && ! confirmAction && (
2026-03-04 14:01:00 -05:00
< button onClick = { ( ) => setConfirmAction ( 'oubli' ) }
className = "w-full p-3 bg-red-50 border border-red-200 text-red-700 rounded-xl font-bold text-sm" >
Droit à l ' oubli ( irréversible )
< / button >
) }
{ /* Confirmations */ }
{ confirmAction === 'desactiver' && (
< div className = "bg-orange-50 rounded-2xl p-4 border border-orange-200" >
< p className = "text-sm text-orange-800 mb-3" > Désactiver { detail . prenom } ? Le membre ne pourra plus se connecter . < / p >
< div className = "flex gap-2" >
< button onClick = { ( ) => desactiver ( detail . id ) } className = "flex-1 p-3 bg-orange-500 text-white rounded-xl font-bold" > Confirmer < / button >
< button onClick = { ( ) => setConfirmAction ( null ) } className = "flex-1 p-3 bg-gray-200 text-gray-700 rounded-xl font-bold" > Annuler < / button >
< / div >
< / div >
) }
2026-05-31 12:28:04 -04:00
{ confirmAction === 'reactiver' && (
< div className = "bg-green-50 rounded-2xl p-4 border border-green-200" >
< p className = "text-sm text-green-800 mb-3" > Réactiver { detail . prenom } ? Le membre pourra de nouveau se connecter avec son identifiant et son PIN existants . < / p >
< div className = "flex gap-2" >
< button onClick = { ( ) => reactiver ( detail . id ) } className = "flex-1 p-3 bg-green-600 text-white rounded-xl font-bold" > Confirmer < / button >
< button onClick = { ( ) => setConfirmAction ( null ) } className = "flex-1 p-3 bg-gray-200 text-gray-700 rounded-xl font-bold" > Annuler < / button >
< / div >
< / div >
) }
2026-03-04 14:01:00 -05:00
{ confirmAction === 'oubli' && (
< div className = "bg-red-50 rounded-2xl p-4 border border-red-200" >
2026-05-31 12:28:04 -04:00
< p className = "text-sm text-red-800 mb-3" > ⚠ ️ Cette action est irréversible . Le prénom de { detail . prenom } demeurera pour préserver l 'historique; le nom, le téléphone, l' identifiant , le PIN et la date d ' abstinence seront effacés ou neutralisés . < / p >
2026-03-04 14:01:00 -05:00
< div className = "flex gap-2" >
2026-05-31 12:28:04 -04:00
< button onClick = { ( ) => droitOubli ( detail . id ) } className = "flex-1 p-3 bg-red-600 text-white rounded-xl font-bold" > Anonymiser < / button >
2026-03-04 14:01:00 -05:00
< button onClick = { ( ) => setConfirmAction ( null ) } className = "flex-1 p-3 bg-gray-200 text-gray-700 rounded-xl font-bold" > Annuler < / button >
< / div >
< / div >
) }
< / div >
< / div >
) ;
}
return (
< div className = "min-h-screen bg-gray-100" >
2026-05-31 12:28:04 -04:00
< PageHeader titre = "Gestion des membres" sousTitre = "Membres, invitations, accès" / >
2026-05-15 16:41:17 -04:00
< div className = "p-4 space-y-4" >
2026-03-04 14:01:00 -05:00
{ msg && < div className = "bg-green-50 text-green-700 p-3 rounded-xl text-center text-sm font-medium mb-2" > { msg } < / div > }
2026-05-15 16:41:17 -04:00
< div className = "bg-white rounded-2xl p-4 shadow-sm space-y-3" >
< div className = "flex items-center justify-between gap-3" >
< div >
< h2 className = "font-bold text-gray-800" > Inviter un membre < / h2 >
< p className = "text-xs text-gray-400" > Code à usage unique , expiration automatique . < / p >
< / div >
< button onClick = { genererInvitation } disabled = { inviteLoading }
className = "px-4 py-2 bg-green-600 text-white rounded-xl text-sm font-bold disabled:opacity-40" >
{ inviteLoading ? '...' : 'Générer' }
< / button >
< / div >
{ code && (
< div className = "bg-green-50 rounded-xl p-3 border border-green-100" >
< p className = "text-xs text-green-700" > Code d ' invitation < / p >
< p className = "text-2xl font-mono font-bold tracking-widest text-green-800" > { code } < / p >
< p className = "text-xs text-green-600 mt-1" > Expire le { expiration } < / p >
< div className = "flex gap-2 mt-3" >
< button onClick = { copierInvitation }
className = "flex-1 py-2 bg-white text-green-700 rounded-xl text-sm font-bold border border-green-200" >
{ copied ? 'Copié' : 'Copier' }
< / button >
< button onClick = { partagerSMS }
className = "flex-1 py-2 bg-green-600 text-white rounded-xl text-sm font-bold" >
SMS
< / button >
< / div >
< / div >
) }
< / div >
Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.
Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.
Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.
Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00
< div className = "bg-white rounded-2xl p-4 shadow-sm space-y-3" >
< button onClick = { ( ) => setShowCreate ( ! showCreate ) }
className = "w-full flex items-center justify-between font-bold text-gray-800" >
< span > Créer un compte membre < / span >
< span className = "text-gray-400" > { showCreate ? '− ' : '+' } < / span >
< / button >
{ showCreate && (
< div className = "space-y-3" >
< div className = "grid grid-cols-2 gap-2" >
< Champ label = "Prénom" > < input value = { createForm . prenom } onChange = { ( e ) => setCreateForm ( { ... createForm , prenom : e . target . value } ) } className = "w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue" / > < / Champ >
< Champ label = "Nom" > < input value = { createForm . nom } onChange = { ( e ) => setCreateForm ( { ... createForm , nom : e . target . value } ) } className = "w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue" / > < / Champ >
< / div >
< Champ label = "Téléphone" > < input value = { createForm . telephone } onChange = { ( e ) => setCreateForm ( { ... createForm , telephone : e . target . value } ) } className = "w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue" / > < / Champ >
< Champ label = "Courriel" > < input type = "email" value = { createForm . courriel } onChange = { ( e ) => setCreateForm ( { ... createForm , courriel : e . target . value } ) } className = "w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue" / > < / Champ >
< div className = "grid grid-cols-2 gap-2" >
< Champ label = "PIN initial" > < input inputMode = "numeric" maxLength = { 4 } value = { createForm . pin } onChange = { ( e ) => setCreateForm ( { ... createForm , pin : e . target . value . replace ( /\D/g , '' ) . slice ( 0 , 4 ) } ) } className = "w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue" / > < / Champ >
< Champ label = "Date d'abstinence" > < input type = "date" value = { createForm . date _abstinence } onChange = { ( e ) => setCreateForm ( { ... createForm , date _abstinence : e . target . value } ) } className = "w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue" / > < / Champ >
< / div >
< button onClick = { creerCompte } disabled = { createLoading || ! createForm . prenom || ! createForm . telephone || ! createForm . courriel || createForm . pin . length !== 4 }
className = "w-full p-3 bg-aa-blue text-white rounded-xl font-bold disabled:opacity-40" >
{ createLoading ? '...' : 'Créer le compte' }
< / button >
< / div >
) }
< / div >
2026-03-04 14:01:00 -05:00
{ loading ? (
< p className = "text-center text-gray-400 py-8" > Chargement ... < / p >
) : (
membres . map ( ( m ) => (
< button key = { m . id } onClick = { ( ) => setDetail ( m ) }
className = "w-full bg-white rounded-2xl p-4 shadow-sm flex items-center justify-between active:scale-[0.98] transition" >
< div >
< p className = "font-bold text-gray-800" > { m . prenom } { m . nom } < / p >
2026-05-31 12:28:04 -04:00
< p className = "text-xs text-gray-400" >
{ estAnonymise ( m ) ? 'Anonymisé' : m . actif ? ( m . telephone || m . courriel || '—' ) : 'Inactif' }
< / p >
2026-03-04 14:01:00 -05:00
< / div >
< span className = "text-gray-300" > → < / span >
< / button >
) )
) }
< / div >
< / div >
) ;
}
Intégrer rencontres et assemblées pilotées par tuiles
Refond les pages Rencontres et Assemblées autour de tuiles ouvrables avec actions locales, PDF serveur, ventes, collectes, présences, décisions, postes et contributions.
Ajoute les invitations publiques d'assemblée en lecture seule avec lien expirant, l'adoption formelle du rapport du trésorier avec proposeur/secondeur, et bloque les contributions tant que ce rapport n'est pas adopté.
Uniformise la navigation et les entêtes, retire les modules redondants des contextes, ajoute la création de comptes membres par exécutif et les données démo reliées aux nouveaux processus.
Ce commit capture un lot fonctionnel important; une refactorisation ciblée du module Assemblées, puis Trésorerie/Gouvernance, est maintenant pertinente pour isoler les règles métier et faciliter les tests.
2026-05-31 09:02:02 -04:00
function Champ ( { label , children } ) {
return < label className = "block" > < span className = "text-xs text-gray-400 block mb-1" > { label } < / span > { children } < / label > ;
}