From b7aa2749218f580afe3777507fda25dbfafa596f Mon Sep 17 00:00:00 2001 From: Manus Date: Mon, 16 Mar 2026 14:45:02 -0400 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Ajout=20des=20zones=20de=20t?= =?UTF-8?q?=C3=A9l=C3=A9versement=20drag=20&=20drop=20dans=20la=20page=20P?= =?UTF-8?q?aram=C3=A8tres=20lorsque=20la=20source=20"Fichier=20local"=20es?= =?UTF-8?q?t=20s=C3=A9lectionn=C3=A9e,=20avec=20r=C3=A9sultat=20d'import?= =?UTF-8?q?=20affich=C3=A9=20imm=C3=A9diatement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/Settings.tsx | 284 +++++++++++++++++++++++++++++----- todo.md | 1 + 2 files changed, 245 insertions(+), 40 deletions(-) diff --git a/client/src/pages/Settings.tsx b/client/src/pages/Settings.tsx index 733a4c2..7605dec 100644 --- a/client/src/pages/Settings.tsx +++ b/client/src/pages/Settings.tsx @@ -1,13 +1,12 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import { trpc } from "@/lib/trpc"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Badge } from "@/components/ui/badge"; import { Switch } from "@/components/ui/switch"; +import { Badge } from "@/components/ui/badge"; import { Settings as SettingsIcon, HardDrive, @@ -17,9 +16,13 @@ import { Save, RefreshCw, CheckCircle, + XCircle, Clock, Loader2, Info, + Upload, + FileSpreadsheet, + AlertCircle, } from "lucide-react"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; @@ -40,6 +43,149 @@ const SOURCE_LABELS: Record = { sharepoint: "SharePoint", }; +// ─── Composant UploadZone (réutilisé depuis ImportLogs) ─────────────────────── + +interface UploadResult { + success: boolean; + fileType: "veille" | "aap"; + fileName: string; + totalRows: number; + newRows: number; + skippedRows: number; + errors: string[]; + status: "success" | "partial" | "error"; + error?: string; +} + +function UploadZone({ + fileType, + label, + accentColor, + onSuccess, +}: { + fileType: "veille" | "aap"; + label: string; + accentColor: 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 / .xls +

+
+ +
+ )} +
+ + {/* Résultat */} + {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 SettingsPage() { const settingsQuery = trpc.settings.get.useQuery(); const saveMutation = trpc.settings.save.useMutation({ @@ -83,11 +229,7 @@ export default function SettingsPage() { }, [settingsQuery.data]); const set = (key: string, value: string) => setForm((prev) => ({ ...prev, [key]: value })); - - const handleSave = () => { - saveMutation.mutate(form as Parameters[0]); - }; - + const handleSave = () => saveMutation.mutate(form as Parameters[0]); const sourceType = (form.source_type || "local") as SourceType; return ( @@ -116,6 +258,8 @@ export default function SettingsPage() { {/* ─── Source des fichiers ─────────────────────────────────────── */} + + {/* Sélecteur de type de source */} Type de source @@ -142,37 +286,98 @@ export default function SettingsPage() { - {/* Chemins des fichiers */} - - - Chemins des fichiers - - {sourceType === "local" - ? "Chemin absolu vers les fichiers sur le serveur" - : sourceType === "ftp" - ? "Chemin relatif sur le serveur FTP" - : "URL complète vers les fichiers"} - - - -
- - set("veille_file_path", e.target.value)} + {/* ── SOURCE LOCALE : upload direct ───────────────────────────── */} + {sourceType === "local" && ( + + + + + Import manuel par téléversement + + + Déposez vos fichiers Excel directement depuis votre ordinateur. + Les nouvelles entrées sont ajoutées immédiatement, les doublons ignorés. + + + + -
-
- - set("aap_file_path", e.target.value)} + -
-
-
+ + + )} + + {/* ── SOURCE LOCALE : chemin serveur (optionnel pour cron) ──── */} + {sourceType === "local" && ( + + + + + Chemin serveur (import automatique) + + + Optionnel — renseignez un chemin absolu sur le serveur pour l'import quotidien automatique + + + +
+ + set("veille_file_path", e.target.value)} + /> +
+
+ + set("aap_file_path", e.target.value)} + /> +
+
+
+ )} + + {/* ── SOURCES DISTANTES : chemins ─────────────────────────────── */} + {sourceType !== "local" && ( + + + Chemins des fichiers + + {sourceType === "ftp" + ? "Chemin relatif sur le serveur FTP" + : "URL complète vers les fichiers"} + + + +
+ + set("veille_file_path", e.target.value)} + /> +
+
+ + set("aap_file_path", e.target.value)} + /> +
+
+
+ )} {/* Configuration FTP */} {sourceType === "ftp" && ( @@ -264,7 +469,6 @@ export default function SettingsPage() { type="time" value={form.import_time || "06:00"} onChange={(e) => set("import_time", e.target.value)} - className="h-11" />

Prochain import : demain à {form.import_time || "06:00"} @@ -275,8 +479,8 @@ export default function SettingsPage() { - Import manuel - Déclenchez un import immédiat des deux fichiers + Import manuel depuis chemin configuré + Déclenchez un import immédiat depuis la source configurée (chemin serveur, FTP, OneDrive, SharePoint)

diff --git a/todo.md b/todo.md index c360bfb..646fc36 100644 --- a/todo.md +++ b/todo.md @@ -37,3 +37,4 @@ - [x] Frontend : bouton "Importer un fichier" dans la page Logs d'import avec sélecteur veille/AAP - [x] Frontend : zone de dépôt (drag & drop) dans la page Paramètres pour les deux fichiers - [x] Frontend : afficher le résultat de l'import (nouvelles entrées, erreurs) après upload +- [x] Page Paramètres : afficher les zones d'upload (drag & drop) quand la source "local" est sélectionnée