Files
crowdlending-app/frontend/src/pages/admin/JobLogsSection.jsx
T
Olivier CROGUENNEC 48ed7fe65e Initial commit
2026-06-13 14:57:15 +02:00

129 lines
5.2 KiB
React

import { useState, useEffect, useCallback } from 'react';
import { api } from '../../api.js';
import { fmt, StatusBadge } from './adminHelpers.jsx';
const KNOWN_JOBS = [
{ name: 'auto_statut_retard', label: 'Passage automatique en retard' },
];
export default function JobLogsSection() {
const [logs, setLogs] = useState([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [running, setRunning] = useState(null);
const [runResult, setRunResult] = useState(null);
const [err, setErr] = useState(null);
const [page, setPage] = useState(0);
const PER_PAGE = 20;
const load = useCallback(async () => {
try {
setLoading(true);
const data = await api.get('/admin/job-logs', { limit: PER_PAGE, offset: page * PER_PAGE });
setLogs(data.rows);
setTotal(data.total);
} catch (e) { setErr(e.message); }
finally { setLoading(false); }
}, [page]);
useEffect(() => { load(); }, [load]);
const runJob = async (jobName) => {
setRunning(jobName); setRunResult(null);
try {
const r = await api.post(`/admin/jobs/${jobName}/run`, {});
setRunResult({ ok: true, msg: `Exécution terminée — ${r.nb_changes} modification(s)` });
load();
} catch (e) {
setRunResult({ ok: false, msg: e.message });
} finally { setRunning(null); }
};
const pages = Math.ceil(total / PER_PAGE);
return (
<div className="card">
<h3 style={{ margin: '0 0 4px' }}>Logs des jobs automatiques</h3>
<p className="text-muted" style={{ margin: '0 0 20px', fontSize: 'var(--fs-sm)' }}>
Historique d'exécution des tâches planifiées et lancement manuel.
</p>
<div style={{ display: 'flex', gap: 12, alignItems: 'center', marginBottom: 20, flexWrap: 'wrap' }}>
{KNOWN_JOBS.map(j => (
<button
key={j.name}
className="btn btn-outline"
style={{ display: 'flex', alignItems: 'center', gap: 7 }}
disabled={running === j.name}
onClick={() => runJob(j.name)}
>
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor"
strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<polygon points="4,2 14,8 4,14"/>
</svg>
{running === j.name ? 'Exécution' : `Lancer : ${j.label}`}
</button>
))}
{runResult && (
<span style={{
fontSize: 13, padding: '4px 12px', borderRadius: 6,
background: runResult.ok ? 'rgba(34,197,94,.1)' : 'rgba(239,68,68,.1)',
color: runResult.ok ? '#16a34a' : '#dc2626',
border: `1px solid ${runResult.ok ? 'rgba(34,197,94,.3)' : 'rgba(239,68,68,.3)'}`,
}}>
{runResult.msg}
</span>
)}
</div>
{loading && <p style={{ color: 'var(--text-muted)' }}>Chargement…</p>}
{err && <p style={{ color: '#ef4444' }}>{err}</p>}
{!loading && !err && !logs.length && <p style={{ color: 'var(--text-muted)' }}>Aucun log disponible.</p>}
{logs.length > 0 && (
<>
<p style={{ fontSize: 13, color: 'var(--text-muted)', marginBottom: 12 }}>
{total} entrée{total > 1 ? 's' : ''} au total
</p>
<table style={{ fontSize: 13 }}>
<thead>
<tr>
<th>Date</th><th>Job</th><th>Statut</th>
<th className="num">Modifs</th><th>Détails</th><th>Erreur</th>
</tr>
</thead>
<tbody>
{logs.map(l => (
<tr key={l.id}>
<td style={{ whiteSpace: 'nowrap', color: 'var(--text-muted)' }}>{fmt(l.run_at)}</td>
<td style={{ fontFamily: 'monospace', fontSize: 12 }}>{l.job_name}</td>
<td><StatusBadge status={l.status} /></td>
<td className="num">
{l.nb_changes > 0
? <span style={{ fontWeight: 700, color: '#f97316' }}>{l.nb_changes}</span>
: <span style={{ color: 'var(--text-muted)' }}>0</span>
}
</td>
<td style={{ maxWidth: 280, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={l.details || ''}>
{l.details || <span style={{ color: 'var(--text-muted)' }}>—</span>}
</td>
<td style={{ color: '#ef4444', fontSize: 12 }}>
{l.error_msg || <span style={{ color: 'var(--text-muted)' }}>—</span>}
</td>
</tr>
))}
</tbody>
</table>
{pages > 1 && (
<div style={{ display: 'flex', gap: 8, marginTop: 16, alignItems: 'center' }}>
<button className="btn btn-sm btn-outline" disabled={page === 0} onClick={() => setPage(p => p - 1)}>← Précédent</button>
<span style={{ fontSize: 13, color: 'var(--text-muted)' }}>Page {page + 1} / {pages}</span>
<button className="btn btn-sm btn-outline" disabled={page >= pages - 1} onClick={() => setPage(p => p + 1)}>Suivant </button>
</div>
)}
</>
)}
</div>
);
}