diff --git a/backend/app/routers/journal.py b/backend/app/routers/journal.py index ce4fd50..58d9dd0 100644 --- a/backend/app/routers/journal.py +++ b/backend/app/routers/journal.py @@ -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) ) diff --git a/frontend/src/pages/JournalActions.jsx b/frontend/src/pages/JournalActions.jsx index 477575d..2510794 100644 --- a/frontend/src/pages/JournalActions.jsx +++ b/frontend/src/pages/JournalActions.jsx @@ -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 (
@@ -44,6 +87,42 @@ export default function JournalActions() { Technique
+ {tab === 'historique' && ( +
+ 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" + /> +
+ 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" /> + 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" /> +
+
+ + +
+ +
+ + +
+
+ )} {loading &&

Chargement...

} {error &&
{error}
} {!loading && tab === 'historique' && historique.length === 0 && (