Optimiser affichage du journal
This commit is contained in:
parent
b8b84841b7
commit
ab8b4ea487
1 changed files with 98 additions and 57 deletions
|
|
@ -26,6 +26,8 @@ export default function JournalActions() {
|
|||
const [filtres, setFiltres] = useState({ date_debut: '', date_fin: '', membre_id: '', table_cible: '', action: '', q: '' });
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [ouvertHistorique, setOuvertHistorique] = useState(null);
|
||||
const [ouvertTechnique, setOuvertTechnique] = useState(null);
|
||||
|
||||
const historiqueQuery = (source = filtres) => {
|
||||
const params = new URLSearchParams({ limite: '100' });
|
||||
|
|
@ -60,6 +62,12 @@ export default function JournalActions() {
|
|||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
});
|
||||
const formatDateCompacte = (value) => new Date(value).toLocaleString('fr-CA', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
const formatValeur = (value) => {
|
||||
if (value === null || value === undefined || value === '') return '—';
|
||||
if (typeof value === 'boolean') return value ? 'oui' : 'non';
|
||||
|
|
@ -73,6 +81,18 @@ 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 = () => {
|
||||
const vides = { date_debut: '', date_fin: '', membre_id: '', table_cible: '', action: '', q: '' };
|
||||
|
|
@ -144,66 +164,87 @@ export default function JournalActions() {
|
|||
{!loading && tab === 'technique' && actions.length === 0 && (
|
||||
<p className="text-gray-500 text-center py-8">Aucune action technique journalisée.</p>
|
||||
)}
|
||||
{tab === 'historique' && historique.map((item) => (
|
||||
<div key={item.id} className="bg-white rounded-2xl p-4 shadow-sm">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
<span className="text-xs font-bold rounded-full bg-blue-50 text-blue-700 px-2 py-1">
|
||||
{item.action}
|
||||
</span>
|
||||
</div>
|
||||
{item.apres && changements(item.avant, item.apres).length > 0 && (
|
||||
<div className="mt-3 rounded-xl bg-gray-50 p-3 text-xs text-gray-600 space-y-2">
|
||||
{changements(item.avant, item.apres).map(([cle, avant, apres]) => (
|
||||
<div key={cle} className="grid grid-cols-[90px_1fr] gap-2">
|
||||
<span className="font-bold text-gray-500">{cle}</span>
|
||||
<span>
|
||||
<span className="text-red-700">{formatValeur(avant)}</span>
|
||||
<span className="text-gray-400"> → </span>
|
||||
<span className="text-green-700">{formatValeur(apres)}</span>
|
||||
</span>
|
||||
{tab === 'historique' && historique.map((item) => {
|
||||
const ouvert = ouvertHistorique === item.id;
|
||||
const lignes = changements(item.avant, item.apres);
|
||||
return (
|
||||
<div key={item.id} className="bg-white rounded-2xl shadow-sm overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
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="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>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{item.avant && !item.apres && (
|
||||
<div className="mt-3 rounded-xl bg-red-50 p-3 text-xs text-red-700 space-y-1">
|
||||
{Object.entries(item.avant).map(([cle, valeur]) => (
|
||||
<p key={cle}><span className="font-bold">{cle}:</span> {formatValeur(valeur)}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3 text-xs text-gray-500 space-y-1">
|
||||
<p>Objet: {item.table_cible} · {item.objet_id}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{tab === 'technique' && actions.map((action) => (
|
||||
<div key={action.id} className="bg-white rounded-2xl p-4 shadow-sm">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="font-bold text-gray-800">{action.methode} {action.chemin}</p>
|
||||
<p className="text-xs text-gray-400 mt-1">{formatDate(action.cree_le)}</p>
|
||||
</div>
|
||||
<span className="text-xs font-bold rounded-full bg-gray-100 text-gray-600 px-2 py-1">
|
||||
{action.statut_http}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 text-xs text-gray-500 space-y-1">
|
||||
<p>Membre: {action.membre_id}</p>
|
||||
{action.admin_id && <p>Admin: {action.admin_id}</p>}
|
||||
{action.adresse_ip && <p>IP: {action.adresse_ip}</p>}
|
||||
{action.details?.impersonification && (
|
||||
<p className="text-amber-700 font-medium">Action faite en impersonification</p>
|
||||
{lignes.length > 0 && (
|
||||
<div className="rounded-xl bg-gray-50 p-3 text-xs text-gray-600 space-y-2">
|
||||
{lignes.map(([cle, avant, apres]) => (
|
||||
<div key={cle} className="grid grid-cols-[92px_1fr] gap-2">
|
||||
<span className="font-bold text-gray-500">{cle}</span>
|
||||
<span>
|
||||
<span className="text-red-700">{formatValeur(avant)}</span>
|
||||
<span className="text-gray-400"> → </span>
|
||||
<span className="text-green-700">{formatValeur(apres)}</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{item.avant && !item.apres && (
|
||||
<div className="rounded-xl bg-red-50 p-3 text-xs text-red-700 space-y-1">
|
||||
{Object.entries(item.avant).map(([cle, valeur]) => (
|
||||
<p key={cle}><span className="font-bold">{cle}:</span> {formatValeur(valeur)}</p>
|
||||
))}
|
||||
</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">Objet</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>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
{tab === 'technique' && actions.map((action) => {
|
||||
const ouvert = ouvertTechnique === action.id;
|
||||
return (
|
||||
<div key={action.id} className="bg-white rounded-2xl shadow-sm overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
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="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">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>}
|
||||
{action.details?.impersonification && (
|
||||
<p className="text-amber-700 font-medium">Action faite en impersonification</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue