This commit is contained in:
lejay
2026-02-13 00:21:53 +01:00
parent 81d2412835
commit 6923c319a1
85 changed files with 17836 additions and 0 deletions
+240
View File
@@ -0,0 +1,240 @@
import { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { fetchOrders, fetchSyncLogs, getFormexSyncStreamUrl } from '../api/client';
import styles from './Admin.module.css';
export default function Admin() {
const { isAdmin, logout } = useAuth();
const navigate = useNavigate();
const [orders, setOrders] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
// Formex sync state
const [syncLogs, setSyncLogs] = useState([]);
const [syncing, setSyncing] = useState(false);
const [syncResult, setSyncResult] = useState(null);
// SSE live progress state
const [liveMessages, setLiveMessages] = useState([]);
const [progress, setProgress] = useState({ current: 0, total: 0, collection: '' });
const logsEndRef = useRef(null);
useEffect(() => {
if (!isAdmin) {
navigate('/login?next=/admin', { replace: true });
return;
}
// Fetch orders and sync logs in parallel
Promise.all([fetchOrders(), fetchSyncLogs(10)])
.then(([ordersData, logsData]) => {
setOrders(ordersData);
setSyncLogs(logsData);
})
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, [isAdmin, navigate]);
// Auto-scroll to bottom of live logs
useEffect(() => {
if (logsEndRef.current) {
logsEndRef.current.scrollIntoView({ behavior: 'smooth' });
}
}, [liveMessages]);
const handleLogout = () => {
logout();
navigate('/', { replace: true });
};
const handleFormexSync = () => {
setSyncing(true);
setSyncResult(null);
setLiveMessages([]);
setProgress({ current: 0, total: 0, collection: '' });
const eventSource = new EventSource(getFormexSyncStreamUrl());
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'log') {
setLiveMessages(prev => [...prev, data.message]);
} else if (data.type === 'progress') {
setProgress({
current: data.current,
total: data.total,
collection: data.collection || ''
});
} else if (data.type === 'complete') {
setSyncResult(data.result);
setSyncing(false);
eventSource.close();
// Refresh sync logs
fetchSyncLogs(10).then(setSyncLogs).catch(() => {});
} else if (data.type === 'error') {
setSyncResult({ success: false, error: data.error });
setSyncing(false);
eventSource.close();
}
} catch (e) {
console.error('SSE parse error:', e);
}
};
eventSource.onerror = () => {
if (syncing) {
setSyncResult({ success: false, error: 'Connexion perdue' });
setSyncing(false);
}
eventSource.close();
};
};
const progressPercent = progress.total > 0
? Math.round((progress.current / progress.total) * 100)
: 0;
if (!isAdmin) return null;
return (
<div className={styles.page}>
<div className={styles.header}>
<h1>Administration</h1>
<button type="button" onClick={handleLogout} className={styles.logout}>
Déconnexion
</button>
</div>
{loading && <p className={styles.muted}>Chargement…</p>}
{error && <p className={styles.error}>{error}</p>}
{/* Formex Sync Section */}
{!loading && !error && (
<section className={styles.section}>
<h2>Synchronisation Formex</h2>
<div className={styles.syncActions}>
<button
type="button"
onClick={handleFormexSync}
disabled={syncing}
className={styles.syncBtn}
>
{syncing ? 'Synchronisation en cours…' : 'Synchroniser Formex'}
</button>
{syncResult && !syncing && (
<span className={syncResult.success ? styles.success : styles.error}>
{syncResult.success
? `${syncResult.itemsCount} produits synchronisés (${syncResult.duration}s)`
: `Erreur: ${syncResult.error}`}
</span>
)}
</div>
{/* Progress bar and live logs during sync */}
{syncing && (
<div className={styles.liveSync}>
<div className={styles.progressWrap}>
<div className={styles.progressInfo}>
<span>{progress.collection || 'Initialisation…'}</span>
<span>{progressPercent}%</span>
</div>
<div className={styles.progressBar}>
<div
className={styles.progressFill}
style={{ width: `${progressPercent}%` }}
/>
</div>
<div className={styles.progressStats}>
{progress.current} / {progress.total} collections
</div>
</div>
<div className={styles.liveLogsPanel}>
<div className={styles.liveLogsContent}>
{liveMessages.map((msg, idx) => (
<div key={idx} className={styles.logLine}>{msg}</div>
))}
<div ref={logsEndRef} />
</div>
</div>
</div>
)}
{syncLogs.length > 0 && !syncing && (
<div className={styles.logsWrap}>
<h3>Historique des synchronisations</h3>
<table className={styles.logsTable}>
<thead>
<tr>
<th>Date</th>
<th>Source</th>
<th>Statut</th>
<th>Produits</th>
<th>Erreur</th>
</tr>
</thead>
<tbody>
{syncLogs.map((log) => (
<tr key={log.id}>
<td>{log.created_at ? new Date(log.created_at).toLocaleString('fr-CH') : '—'}</td>
<td>{log.source}</td>
<td className={log.status === 'success' ? styles.success : styles.error}>
{log.status === 'success' ? 'Succès' : 'Erreur'}
</td>
<td>{log.items_count ?? '—'}</td>
<td>{log.error || '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</section>
)}
{/* Orders Section */}
{!loading && !error && (
<section className={styles.section}>
<h2>Commandes</h2>
<div className={styles.tableWrap}>
<table className={styles.table}>
<thead>
<tr>
<th>ID</th>
<th>Date</th>
<th>Total CHF</th>
<th>Nom</th>
<th>Email</th>
<th>Téléphone</th>
<th>Remarques</th>
</tr>
</thead>
<tbody>
{orders.length === 0 ? (
<tr>
<td colSpan={7} className={styles.empty}>
Aucune commande.
</td>
</tr>
) : (
orders.map((o) => (
<tr key={o.id}>
<td>{o.id}</td>
<td>{o.created_at ? new Date(o.created_at).toLocaleString('fr-CH') : '—'}</td>
<td>{o.total_chf != null ? Number(o.total_chf).toLocaleString('fr-CH') : '—'}</td>
<td>{o.name || '—'}</td>
<td>{o.email || '—'}</td>
<td>{o.phone || '—'}</td>
<td>{o.notes || '—'}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</section>
)}
</div>
);
}