diff --git a/apps/web/src/components/factures/Dropzone.tsx b/apps/web/src/components/factures/Dropzone.tsx index cf9f3b9..e183296 100644 --- a/apps/web/src/components/factures/Dropzone.tsx +++ b/apps/web/src/components/factures/Dropzone.tsx @@ -22,22 +22,36 @@ type DropzoneProps = { maxFiles?: number; /** Callback quand des fichiers valides ont été sélectionnés. */ onFiles?: (files: File[]) => void; + /** Mode chargement : disable input + remplace le call to action par un spinner. */ + isUploading?: boolean; className?: string; }; const ACCEPT_ATTR = ACCEPTED_INVOICE_MIME_TYPES.join(","); -function isAcceptableFile(file: File): boolean { - return ( - (ACCEPTED_INVOICE_MIME_TYPES as readonly string[]).includes(file.type) && - file.size <= MAX_INVOICE_FILE_SIZE_BYTES - ); +/** + * Vérification souple : on accepte par MIME OU par extension. Certains + * navigateurs (ou drag depuis Finder/Explorer) renvoient un `type` vide, + * il ne faut pas rejeter pour autant. La taille trop grande renvoie un + * message dédié, pas le générique "format non supporté". + */ +type FileCheckResult = { ok: true } | { ok: false; reason: "format" | "size" }; + +function checkFile(file: File): FileCheckResult { + const acceptableMime = + file.type === "" || + (ACCEPTED_INVOICE_MIME_TYPES as readonly string[]).includes(file.type); + const acceptableExt = /\.(pdf|png|jpe?g)$/iu.test(file.name); + if (!acceptableMime && !acceptableExt) return { ok: false, reason: "format" }; + if (file.size > MAX_INVOICE_FILE_SIZE_BYTES) return { ok: false, reason: "size" }; + return { ok: true }; } export function Dropzone({ variant = "full", maxFiles = 20, onFiles, + isUploading = false, className, }: DropzoneProps) { const [isDragging, setIsDragging] = useState(false); @@ -46,22 +60,32 @@ export function Dropzone({ const handleFiles = useCallback( (fileList: FileList | null) => { - if (!fileList) return; + if (!fileList || isUploading) return; const files = Array.from(fileList); + if (files.length === 0) return; if (files.length > maxFiles) { setError(`Maximum ${maxFiles} fichiers en un seul drop.`); return; } - const valid = files.filter(isAcceptableFile); - const rejected = files.length - valid.length; - if (rejected > 0 && valid.length === 0) { - setError("Format non supporté. PDF, PNG, JPG seulement."); + const checks = files.map((f) => ({ file: f, ...checkFile(f) })); + const valid = checks.filter((c) => c.ok).map((c) => c.file); + const tooBig = checks.filter((c) => !c.ok && c.reason === "size").length; + const wrongFormat = checks.filter( + (c) => !c.ok && c.reason === "format", + ).length; + + if (valid.length === 0) { + if (tooBig > 0) { + setError(`Fichier trop lourd · 10 Mo maximum par fichier.`); + } else if (wrongFormat > 0) { + setError("Format non supporté. PDF, PNG, JPG uniquement."); + } return; } setError(null); onFiles?.(valid); }, - [maxFiles, onFiles], + [maxFiles, onFiles, isUploading], ); const onDragOver = (e: React.DragEvent) => { @@ -116,7 +140,11 @@ export function Dropzone({ isFull ? "text-[22px] font-semibold" : "text-[17px] font-semibold", )} > - {isDragging ? ( + {isUploading ? ( + <> + On lit vos factures… + + ) : isDragging ? ( <> Lâchez ici, on s'occupe du reste. @@ -136,10 +164,12 @@ export function Dropzone({ size="sm" onClick={() => inputRef.current?.click()} type="button" + loading={isUploading} + disabled={isUploading} >