v1.0
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user