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.
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
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 = [
['', 'Tous les domaines'],
['membres', 'Membres'],
['propositions', 'Gouvernance'],
['pv_reunions', 'Procès-verbaux'],
['collectes', 'Collectes'],
@ -62,9 +63,11 @@ export default function JournalActions() {
dateStyle: 'medium',
timeStyle: 'short',
});
const formatDateCompacte = (value) => new Date(value).toLocaleString('fr-CA', {
const formatJour = (value) => new Date(value).toLocaleDateString('fr-CA', {
month: 'short',
day: 'numeric',
});
const formatHeure = (value) => new Date(value).toLocaleTimeString('fr-CA', {
hour: '2-digit',
minute: '2-digit',
});
@ -81,17 +84,6 @@ export default function JournalActions() {
.map((cle) => [cle, avant?.[cle], apres?.[cle]])
.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 updateFiltre = (key, value) => setFiltres((f) => ({ ...f, [key]: value }));
const resetFiltres = () => {
@ -154,7 +146,7 @@ export default function JournalActions() {
{actionsDisponibles.map((action) => <option key={action} value={action}>{action}</option>)}
</select>
<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>
</div>
</div>
@ -177,17 +169,16 @@ export default function JournalActions() {
onClick={() => setOuvertHistorique(ouvert ? null : item.id)}
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="flex-1 min-w-0 text-sm font-medium text-gray-800 truncate">{resumeChangements(item)}</span>
<span className="w-[70px] shrink-0 text-xs font-bold text-gray-500">{formatJour(item.cree_le)}</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>
</button>
{ouvert && (
<div className="px-4 pb-4 space-y-3 border-t border-gray-100">
<div className="pt-3">
<p className="font-bold text-gray-800">{item.raison}</p>
<p className="text-xs text-gray-400 mt-1">
{formatDate(item.cree_le)} · {item.membre_prenom || item.membre_id || 'Membre inconnu'}
</p>
<p className="text-xs text-gray-400 mt-1">Par {item.membre_prenom || item.membre_id || 'membre inconnu'}</p>
</div>
{lignes.length > 0 && (
<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 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 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>}
@ -230,13 +220,14 @@ export default function JournalActions() {
onClick={() => setOuvertTechnique(ouvert ? null : action.id)}
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="flex-1 min-w-0 text-sm font-medium text-gray-800 truncate">{action.methode} {action.chemin}</span>
<span className="w-[70px] shrink-0 text-xs font-bold text-gray-500">{formatJour(action.cree_le)}</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>
</button>
{ouvert && (
<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>
{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>}