import { useState, useRef, useCallback } from "react"; import { trpc } from "@/lib/trpc"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Activity, CheckCircle, XCircle, Loader2, RefreshCw, ChevronLeft, ChevronRight, Clock, FileText, Upload, FileSpreadsheet, AlertCircle, } from "lucide-react"; import { format } from "date-fns"; import { fr } from "date-fns/locale"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; interface ImportLog { id: number; fileType: "veille" | "aap"; status: "success" | "error" | "partial"; newRows: number | null; skippedRows: number | null; totalRows: number | null; errorMessage: string | null; startedAt: Date; completedAt: Date | null; source: string | null; } interface UploadResult { success: boolean; fileType: "veille" | "aap"; fileName: string; totalRows: number; newRows: number; skippedRows: number; errors: string[]; status: "success" | "partial" | "error"; error?: string; } const STATUS_CONFIG = { success: { label: "Succès", color: "bg-emerald-100 text-emerald-800 border-emerald-200", icon: }, error: { label: "Erreur", color: "bg-red-100 text-red-800 border-red-200", icon: }, partial: { label: "Partiel", color: "bg-amber-100 text-amber-800 border-amber-200", icon: }, }; const FILE_CONFIG = { veille: { label: "Veille Stratégique", color: "bg-blue-100 text-blue-800 border-blue-200" }, aap: { label: "Appels à Projets", color: "bg-violet-100 text-violet-800 border-violet-200" }, }; const PAGE_SIZE = 20; // ─── Composant UploadZone ───────────────────────────────────────────────────── function UploadZone({ fileType, label, color, onSuccess, }: { fileType: "veille" | "aap"; label: string; color: string; onSuccess: () => void; }) { const [isDragging, setIsDragging] = useState(false); const [isUploading, setIsUploading] = useState(false); const [lastResult, setLastResult] = useState(null); const inputRef = useRef(null); const handleFile = useCallback( async (file: File) => { if (!file) return; if (!file.name.endsWith(".xlsx") && !file.name.endsWith(".xls")) { toast.error("Seuls les fichiers Excel (.xlsx, .xls) sont acceptés"); return; } setIsUploading(true); setLastResult(null); try { const formData = new FormData(); formData.append("file", file); formData.append("fileType", fileType); const res = await fetch("/api/upload-excel", { method: "POST", body: formData, }); const data: UploadResult = await res.json(); if (!res.ok || !data.success) { throw new Error(data.error || "Erreur lors de l'import"); } setLastResult(data); onSuccess(); if (data.newRows > 0) { toast.success(`Import réussi — ${data.newRows} nouvelle(s) entrée(s) ajoutée(s)`); } else { toast.info(`Import terminé — aucune nouvelle entrée (${data.skippedRows} déjà présentes)`); } } catch (e: unknown) { const msg = e instanceof Error ? e.message : String(e); toast.error(`Erreur : ${msg}`); setLastResult({ success: false, fileType, fileName: file.name, totalRows: 0, newRows: 0, skippedRows: 0, errors: [msg], status: "error", error: msg }); } finally { setIsUploading(false); } }, [fileType, onSuccess] ); const onDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); const file = e.dataTransfer.files[0]; if (file) handleFile(file); }, [handleFile] ); return (
{ e.preventDefault(); setIsDragging(true); }} onDragLeave={() => setIsDragging(false)} onDrop={onDrop} onClick={() => inputRef.current?.click()} > { const f = e.target.files?.[0]; if (f) handleFile(f); e.target.value = ""; }} /> {isUploading ? (

Import en cours…

) : (

{label}

Glissez-déposez ou parcourez

.xlsx ou .xls · max 50 MB

)}
{/* Résultat du dernier upload */} {lastResult && (
{lastResult.status === "success" ? : lastResult.status === "partial" ? : }

{lastResult.fileName}

{lastResult.newRows} nouvelle(s) · {lastResult.skippedRows} ignorée(s) · {lastResult.totalRows} total

{lastResult.errors.length > 0 && (

{lastResult.errors[0]}

)}
)}
); } // ─── Page principale ────────────────────────────────────────────────────────── export default function ImportLogs() { const [page, setPage] = useState(1); const logsQuery = trpc.import.logs.useQuery({ page, pageSize: PAGE_SIZE }); const importMutation = trpc.import.run.useMutation({ onSuccess: () => logsQuery.refetch(), }); const logs = (logsQuery.data?.logs ?? []) as unknown as ImportLog[]; const total = logsQuery.data?.total ?? 0; const totalPages = Math.ceil(total / PAGE_SIZE); const handleUploadSuccess = () => { logsQuery.refetch(); }; return (
{/* En-tête */}

Logs d'import

Historique des imports automatiques et manuels

{/* Upload direct */} Import direct depuis votre ordinateur

Déposez vos fichiers Excel directement — les nouvelles entrées seront ajoutées immédiatement

{/* Stats rapides */} {logsQuery.data?.stats && (
{[ { label: "Total imports", value: logsQuery.data.stats.total, icon: , color: "text-primary" }, { label: "Succès", value: logsQuery.data.stats.success, icon: , color: "text-emerald-600" }, { label: "Erreurs", value: logsQuery.data.stats.errors, icon: , color: "text-red-500" }, { label: "Nouvelles entrées", value: logsQuery.data.stats.totalNewRows, icon: , color: "text-accent" }, ].map((stat) => (
{stat.icon} {stat.label}

{stat.value}

))}
)} {/* Tableau des logs */} {logsQuery.isLoading ? (
) : logs.length === 0 ? (

Aucun log d'import

Les imports apparaîtront ici

) : (
{logs.map((log) => { const status = STATUS_CONFIG[log.status] || STATUS_CONFIG.error; const fileConf = FILE_CONFIG[log.fileType] || FILE_CONFIG.veille; return ( ); })}
Date Fichier Statut Nouvelles Ignorées Total Durée Source
{format(new Date(log.startedAt), "d MMM yyyy HH:mm", { locale: fr })}
{fileConf.label} {status.icon} {status.label} +{log.newRows ?? 0} {log.skippedRows ?? 0} {log.totalRows ?? 0} {log.startedAt && log.completedAt ? `${((new Date(log.completedAt).getTime() - new Date(log.startedAt).getTime()) / 1000).toFixed(1)}s` : "—"} {log.source ? log.source.split(/[\\/]/).pop() || log.source : "—"}
)}
{/* Pagination */} {totalPages > 1 && (
Page {page} / {totalPages}
)}
); }