diff --git a/apps/web/src/components/layout/AppLayout.tsx b/apps/web/src/components/layout/AppLayout.tsx
index 4e396ab..1dac995 100644
--- a/apps/web/src/components/layout/AppLayout.tsx
+++ b/apps/web/src/components/layout/AppLayout.tsx
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/Button";
import { AppSidebar } from "./AppSidebar";
import { AppTopbar } from "./AppTopbar";
import { MobileTabBar } from "./MobileTabBar";
+import { Link } from "@tanstack/react-router";
/**
* Shell de l'app authentifiée :
@@ -51,11 +52,13 @@ export function AppLayout({ children, title, subtitle, actions }: AppLayoutProps
// chaque route gère ses propres CTA en tête de contenu (cf. wireframe 4.3).
const defaultActions = (
-
);
diff --git a/apps/web/src/routes/_app/factures_.import.tsx b/apps/web/src/routes/_app/factures_.import.tsx
new file mode 100644
index 0000000..59208b0
--- /dev/null
+++ b/apps/web/src/routes/_app/factures_.import.tsx
@@ -0,0 +1,123 @@
+import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
+import { useMutation } from "@tanstack/react-query";
+import { ArrowLeft, FilePlus } from "lucide-react";
+import { toast } from "sonner";
+
+import { api } from "@/lib/api";
+import { Button } from "@/components/ui/Button";
+import { Eyebrow } from "@/components/ui/Eyebrow";
+import { Dropzone } from "@/components/factures/Dropzone";
+
+type ImportBatchResponse = {
+ id: string;
+ drafts: Array<{ id: string; filename: string }>;
+};
+
+export const Route = createFileRoute("/_app/factures_/import")({
+ component: ImportLandingPage,
+});
+
+function ImportLandingPage() {
+ const navigate = useNavigate();
+
+ const upload = useMutation({
+ mutationFn: (files: File[]) =>
+ api.post("/api/v1/invoices/upload", {
+ filenames: files.map((f) => f.name),
+ }),
+ onSuccess: (batch) => {
+ toast.success(
+ `${batch.drafts.length} facture${batch.drafts.length > 1 ? "s" : ""} extraite${
+ batch.drafts.length > 1 ? "s" : ""
+ }. Vérifions ensemble.`,
+ );
+ void navigate({
+ to: "/factures/import/$batchId",
+ params: { batchId: batch.id },
+ });
+ },
+ onError: () => {
+ toast.error("L'upload a échoué. Réessayez dans un instant.");
+ },
+ });
+
+ // Drop-catcher de page (au cas où le user dropperait à côté de la dropzone).
+ const onPageDragOver = (e: React.DragEvent) => {
+ if (e.dataTransfer.types.includes("Files")) e.preventDefault();
+ };
+ const onPageDrop = (e: React.DragEvent) => {
+ if (!e.dataTransfer.types.includes("Files")) return;
+ e.preventDefault();
+ const files = Array.from(e.dataTransfer.files).filter(
+ (f) => /\.(pdf|png|jpe?g)$/iu.test(f.name) || f.type !== "",
+ );
+ if (files.length > 0 && !upload.isPending) upload.mutate(files);
+ };
+
+ return (
+
+
+ Factures
+
+
+
+
+ Import
+
+ Importer plusieurs factures.
+
+
+ Glissez vos PDFs et images de factures — l'OCR extrait les
+ champs, vous validez en 30 secondes, on programme les relances.
+ Jusqu'à 20 fichiers en un seul drop.
+