Ajouter les filtres du journal métier
Permet de filtrer l'historique métier par période, membre, domaine, action et recherche texte. Ajoute les contrôles correspondants dans l'onglet Historique du module Journal.
This commit is contained in:
parent
6677b0f57b
commit
c24b0625d3
2 changed files with 116 additions and 12 deletions
|
|
@ -1,6 +1,8 @@
|
|||
"""Consultation du journal d'audit."""
|
||||
from datetime import date, datetime, time, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
|
|
@ -46,13 +48,36 @@ async def journal_actions(
|
|||
@router.get("/historique")
|
||||
async def historique_modifications(
|
||||
limite: int = 100,
|
||||
date_debut: date | None = None,
|
||||
date_fin: date | None = None,
|
||||
membre_id: str | None = None,
|
||||
table_cible: str | None = None,
|
||||
action: str | None = None,
|
||||
q: str | None = None,
|
||||
executif=Depends(require_executif),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
limite = max(1, min(limite, 500))
|
||||
query = select(HistoriqueModification).where(HistoriqueModification.groupe_id == executif.groupe_id)
|
||||
if date_debut:
|
||||
query = query.where(HistoriqueModification.cree_le >= datetime.combine(date_debut, time.min, tzinfo=timezone.utc))
|
||||
if date_fin:
|
||||
query = query.where(HistoriqueModification.cree_le <= datetime.combine(date_fin, time.max, tzinfo=timezone.utc))
|
||||
if membre_id:
|
||||
query = query.where(HistoriqueModification.membre_id == membre_id)
|
||||
if table_cible:
|
||||
query = query.where(HistoriqueModification.table_cible == table_cible)
|
||||
if action:
|
||||
query = query.where(HistoriqueModification.action == action)
|
||||
if q:
|
||||
terme = f"%{q.strip()}%"
|
||||
query = query.where(or_(
|
||||
HistoriqueModification.raison.ilike(terme),
|
||||
HistoriqueModification.action.ilike(terme),
|
||||
HistoriqueModification.table_cible.ilike(terme),
|
||||
))
|
||||
result = await db.execute(
|
||||
select(HistoriqueModification)
|
||||
.where(HistoriqueModification.groupe_id == executif.groupe_id)
|
||||
query
|
||||
.order_by(HistoriqueModification.cree_le.desc())
|
||||
.limit(limite)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,28 +2,71 @@ import React, { useEffect, useState } from 'react';
|
|||
import { useStore } from '../stores/store';
|
||||
import { api } from '../stores/api';
|
||||
|
||||
const DOMAINES = [
|
||||
['', 'Tous les domaines'],
|
||||
['propositions', 'Gouvernance'],
|
||||
['pv_reunions', 'Procès-verbaux'],
|
||||
['collectes', 'Collectes'],
|
||||
['depenses', 'Dépenses'],
|
||||
['envois_contributions', 'Contributions'],
|
||||
['reserves', 'Réserves'],
|
||||
['operations_bancaires', 'Opérations bancaires'],
|
||||
['mouvements_reserves', 'Virements réserves'],
|
||||
['litterature', 'Littérature'],
|
||||
['reunions', 'Rencontres'],
|
||||
['destinataires_contributions', 'Destinataires'],
|
||||
];
|
||||
|
||||
export default function JournalActions() {
|
||||
const { setPage } = useStore();
|
||||
const [actions, setActions] = useState([]);
|
||||
const [historique, setHistorique] = useState([]);
|
||||
const [membres, setMembres] = useState([]);
|
||||
const [tab, setTab] = useState('historique');
|
||||
const [filtres, setFiltres] = useState({ date_debut: '', date_fin: '', membre_id: '', table_cible: '', action: '', q: '' });
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
api.get('/journal/historique?limite=100'),
|
||||
api.get('/journal/actions?limite=100'),
|
||||
])
|
||||
.then(([hist, acts]) => { setHistorique(hist); setActions(acts); })
|
||||
.catch((e) => setError(e.message))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
const historiqueQuery = (source = filtres) => {
|
||||
const params = new URLSearchParams({ limite: '100' });
|
||||
Object.entries(source).forEach(([key, value]) => {
|
||||
if (value) params.set(key, value);
|
||||
});
|
||||
return `/journal/historique?${params.toString()}`;
|
||||
};
|
||||
|
||||
const charger = async (sourceFiltres = filtres) => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const [hist, acts, mbrs] = await Promise.all([
|
||||
api.get(historiqueQuery(sourceFiltres)),
|
||||
api.get('/journal/actions?limite=100'),
|
||||
api.get('/membres/actifs'),
|
||||
]);
|
||||
setHistorique(hist);
|
||||
setActions(acts);
|
||||
setMembres(mbrs);
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { charger(); }, []);
|
||||
|
||||
const formatDate = (value) => new Date(value).toLocaleString('fr-CA', {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
});
|
||||
const updateFiltre = (key, value) => setFiltres((f) => ({ ...f, [key]: value }));
|
||||
const resetFiltres = () => {
|
||||
const vides = { date_debut: '', date_fin: '', membre_id: '', table_cible: '', action: '', q: '' };
|
||||
setFiltres(vides);
|
||||
charger(vides);
|
||||
};
|
||||
const actionsDisponibles = [...new Set(historique.map((item) => item.action))].sort();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100">
|
||||
|
|
@ -44,6 +87,42 @@ export default function JournalActions() {
|
|||
Technique
|
||||
</button>
|
||||
</div>
|
||||
{tab === 'historique' && (
|
||||
<div className="bg-white rounded-2xl p-4 shadow-sm space-y-3">
|
||||
<input
|
||||
value={filtres.q}
|
||||
onChange={(e) => updateFiltre('q', e.target.value)}
|
||||
placeholder="Rechercher dans les raisons"
|
||||
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue text-sm"
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<input type="date" value={filtres.date_debut} onChange={(e) => updateFiltre('date_debut', e.target.value)}
|
||||
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue text-sm" />
|
||||
<input type="date" value={filtres.date_fin} onChange={(e) => updateFiltre('date_fin', e.target.value)}
|
||||
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue text-sm" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<select value={filtres.membre_id} onChange={(e) => updateFiltre('membre_id', e.target.value)}
|
||||
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue text-sm">
|
||||
<option value="">Tous les membres</option>
|
||||
{membres.map((m) => <option key={m.id} value={m.id}>{m.prenom}</option>)}
|
||||
</select>
|
||||
<select value={filtres.table_cible} onChange={(e) => updateFiltre('table_cible', e.target.value)}
|
||||
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue text-sm">
|
||||
{DOMAINES.map(([value, label]) => <option key={value || 'all'} value={value}>{label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<select value={filtres.action} onChange={(e) => updateFiltre('action', e.target.value)}
|
||||
className="w-full p-3 bg-gray-50 rounded-xl border-0 focus:ring-2 focus:ring-aa-blue text-sm">
|
||||
<option value="">Toutes les actions</option>
|
||||
{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={resetFiltres} className="p-3 bg-gray-100 text-gray-600 rounded-xl text-sm font-bold">Réinitialiser</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{loading && <p className="text-gray-500 text-center py-8">Chargement...</p>}
|
||||
{error && <div className="bg-red-50 text-red-600 p-3 rounded-xl text-sm">{error}</div>}
|
||||
{!loading && tab === 'historique' && historique.length === 0 && (
|
||||
|
|
|
|||
Loading…
Reference in a new issue