Corriger filtres et lecture du journal

This commit is contained in:
Daniel Allaire 2026-05-31 17:56:00 -04:00
parent 57e489e118
commit 9b45a3110f
2 changed files with 15 additions and 22 deletions

View file

@ -576,6 +576,8 @@ Les consultations simples ne sont pas journalisées comme actions modificatrices
Le journal doit servir à comprendre les changements et protéger l'historique, pas à surveiller les membres. Le journal doit servir à comprendre les changements et protéger l'historique, pas à surveiller les membres.
Tous les modules ne sont pas équivalents en journalisation. Les actions techniques sont enregistrées largement par l'API. L'historique métier détaillé, avec raison et valeurs avant/après, existe pour les processus sensibles déjà instrumentés : trésorerie, réserves, collectes, dépenses, gouvernance, procès-verbaux, rencontres, contributions, littérature et gestion des membres.
## 14. Modifications simultanées ## 14. Modifications simultanées
Il peut arriver que deux membres exécutifs travaillent sur la même information en même temps. Il peut arriver que deux membres exécutifs travaillent sur la même information en même temps.

View file

@ -4,6 +4,7 @@ import { api } from '../stores/api';
const DOMAINES = [ const DOMAINES = [
['', 'Tous les domaines'], ['', 'Tous les domaines'],
['membres', 'Membres'],
['propositions', 'Gouvernance'], ['propositions', 'Gouvernance'],
['pv_reunions', 'Procès-verbaux'], ['pv_reunions', 'Procès-verbaux'],
['collectes', 'Collectes'], ['collectes', 'Collectes'],
@ -62,9 +63,11 @@ export default function JournalActions() {
dateStyle: 'medium', dateStyle: 'medium',
timeStyle: 'short', timeStyle: 'short',
}); });
const formatDateCompacte = (value) => new Date(value).toLocaleString('fr-CA', { const formatJour = (value) => new Date(value).toLocaleDateString('fr-CA', {
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
});
const formatHeure = (value) => new Date(value).toLocaleTimeString('fr-CA', {
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
}); });
@ -81,17 +84,6 @@ export default function JournalActions() {
.map((cle) => [cle, avant?.[cle], apres?.[cle]]) .map((cle) => [cle, avant?.[cle], apres?.[cle]])
.filter(([, a, b]) => JSON.stringify(a) !== JSON.stringify(b)); .filter(([, a, b]) => JSON.stringify(a) !== JSON.stringify(b));
}; };
const resumeChangements = (item) => {
const lignes = changements(item.avant, item.apres);
if (lignes.length === 0 && item.avant && !item.apres) {
return Object.keys(item.avant).slice(0, 2).join(', ') || item.raison;
}
if (lignes.length === 0) return item.raison;
const resume = lignes.slice(0, 2)
.map(([cle, avant, apres]) => `${cle}: ${formatValeur(avant)} → ${formatValeur(apres)}`)
.join(' · ');
return lignes.length > 2 ? `${resume} · +${lignes.length - 2}` : resume;
};
const domaineLabel = (value) => DOMAINES.find(([id]) => id === value)?.[1] || value || '—'; const domaineLabel = (value) => DOMAINES.find(([id]) => id === value)?.[1] || value || '—';
const updateFiltre = (key, value) => setFiltres((f) => ({ ...f, [key]: value })); const updateFiltre = (key, value) => setFiltres((f) => ({ ...f, [key]: value }));
const resetFiltres = () => { const resetFiltres = () => {
@ -154,7 +146,7 @@ export default function JournalActions() {
{actionsDisponibles.map((action) => <option key={action} value={action}>{action}</option>)} {actionsDisponibles.map((action) => <option key={action} value={action}>{action}</option>)}
</select> </select>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<button onClick={charger} className="p-3 bg-aa-blue text-white rounded-xl text-sm font-bold">Filtrer</button> <button onClick={() => charger()} className="p-3 bg-aa-blue text-white rounded-xl text-sm font-bold">Filtrer</button>
<button onClick={resetFiltres} className="p-3 bg-gray-100 text-gray-600 rounded-xl text-sm font-bold">Réinitialiser</button> <button onClick={resetFiltres} className="p-3 bg-gray-100 text-gray-600 rounded-xl text-sm font-bold">Réinitialiser</button>
</div> </div>
</div> </div>
@ -177,17 +169,16 @@ export default function JournalActions() {
onClick={() => setOuvertHistorique(ouvert ? null : item.id)} onClick={() => setOuvertHistorique(ouvert ? null : item.id)}
className="w-full p-3 text-left flex items-center gap-3 active:bg-gray-50" className="w-full p-3 text-left flex items-center gap-3 active:bg-gray-50"
> >
<span className="w-[84px] shrink-0 text-xs font-bold text-gray-500">{formatDateCompacte(item.cree_le)}</span> <span className="w-[70px] shrink-0 text-xs font-bold text-gray-500">{formatJour(item.cree_le)}</span>
<span className="flex-1 min-w-0 text-sm font-medium text-gray-800 truncate">{resumeChangements(item)}</span> <span className="w-[50px] shrink-0 text-xs font-bold text-gray-500">{formatHeure(item.cree_le)}</span>
<span className="flex-1 min-w-0 text-sm font-bold text-gray-800 truncate">{domaineLabel(item.table_cible)}</span>
<span className="text-gray-300 text-lg">{ouvert ? '⌃' : '⌄'}</span> <span className="text-gray-300 text-lg">{ouvert ? '⌃' : '⌄'}</span>
</button> </button>
{ouvert && ( {ouvert && (
<div className="px-4 pb-4 space-y-3 border-t border-gray-100"> <div className="px-4 pb-4 space-y-3 border-t border-gray-100">
<div className="pt-3"> <div className="pt-3">
<p className="font-bold text-gray-800">{item.raison}</p> <p className="font-bold text-gray-800">{item.raison}</p>
<p className="text-xs text-gray-400 mt-1"> <p className="text-xs text-gray-400 mt-1">Par {item.membre_prenom || item.membre_id || 'membre inconnu'}</p>
{formatDate(item.cree_le)} · {item.membre_prenom || item.membre_id || 'Membre inconnu'}
</p>
</div> </div>
{lignes.length > 0 && ( {lignes.length > 0 && (
<div className="rounded-xl bg-gray-50 p-3 text-xs text-gray-600 space-y-2"> <div className="rounded-xl bg-gray-50 p-3 text-xs text-gray-600 space-y-2">
@ -211,7 +202,6 @@ export default function JournalActions() {
</div> </div>
)} )}
<div className="grid grid-cols-2 gap-2 text-xs text-gray-500"> <div className="grid grid-cols-2 gap-2 text-xs text-gray-500">
<p><span className="font-bold text-gray-600">Domaine</span><br />{domaineLabel(item.table_cible)}</p>
<p><span className="font-bold text-gray-600">Action</span><br />{item.action}</p> <p><span className="font-bold text-gray-600">Action</span><br />{item.action}</p>
<p className="col-span-2 break-all"><span className="font-bold text-gray-600">Référence technique</span><br />{item.objet_id}</p> <p className="col-span-2 break-all"><span className="font-bold text-gray-600">Référence technique</span><br />{item.objet_id}</p>
{item.chemin_api && <p className="col-span-2 break-all"><span className="font-bold text-gray-600">API</span><br />{item.chemin_api}</p>} {item.chemin_api && <p className="col-span-2 break-all"><span className="font-bold text-gray-600">API</span><br />{item.chemin_api}</p>}
@ -230,13 +220,14 @@ export default function JournalActions() {
onClick={() => setOuvertTechnique(ouvert ? null : action.id)} onClick={() => setOuvertTechnique(ouvert ? null : action.id)}
className="w-full p-3 text-left flex items-center gap-3 active:bg-gray-50" className="w-full p-3 text-left flex items-center gap-3 active:bg-gray-50"
> >
<span className="w-[84px] shrink-0 text-xs font-bold text-gray-500">{formatDateCompacte(action.cree_le)}</span> <span className="w-[70px] shrink-0 text-xs font-bold text-gray-500">{formatJour(action.cree_le)}</span>
<span className="flex-1 min-w-0 text-sm font-medium text-gray-800 truncate">{action.methode} {action.chemin}</span> <span className="w-[50px] shrink-0 text-xs font-bold text-gray-500">{formatHeure(action.cree_le)}</span>
<span className="flex-1 min-w-0 text-sm font-bold text-gray-800 truncate">Technique</span>
<span className="text-xs font-bold rounded-full bg-gray-100 text-gray-600 px-2 py-1">{action.statut_http}</span> <span className="text-xs font-bold rounded-full bg-gray-100 text-gray-600 px-2 py-1">{action.statut_http}</span>
</button> </button>
{ouvert && ( {ouvert && (
<div className="px-4 pb-4 pt-3 border-t border-gray-100 text-xs text-gray-500 space-y-1"> <div className="px-4 pb-4 pt-3 border-t border-gray-100 text-xs text-gray-500 space-y-1">
<p><span className="font-bold text-gray-600">Date</span>: {formatDate(action.cree_le)}</p> <p className="break-all"><span className="font-bold text-gray-600">Action</span>: {action.methode} {action.chemin}</p>
<p className="break-all"><span className="font-bold text-gray-600">Membre</span>: {action.membre_id}</p> <p className="break-all"><span className="font-bold text-gray-600">Membre</span>: {action.membre_id}</p>
{action.admin_id && <p className="break-all"><span className="font-bold text-gray-600">Admin</span>: {action.admin_id}</p>} {action.admin_id && <p className="break-all"><span className="font-bold text-gray-600">Admin</span>: {action.admin_id}</p>}
{action.adresse_ip && <p><span className="font-bold text-gray-600">IP</span>: {action.adresse_ip}</p>} {action.adresse_ip && <p><span className="font-bold text-gray-600">IP</span>: {action.adresse_ip}</p>}