From 965a92da8f8527cf16d9b0250eade927fc3f1985 Mon Sep 17 00:00:00 2001 From: ordinarthur <@arthurbarre.js@gmail.com> Date: Wed, 6 May 2026 11:35:59 +0200 Subject: [PATCH] =?UTF-8?q?feat(web):=20/factures/import=20=E2=80=94=20pag?= =?UTF-8?q?e=20focused=20d'import=20via=20bouton=20topbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le bouton '+ Importer factures' du topbar avait un Button inerte. Il ouvre maintenant une vraie page focused dédiée : - Route /factures/import (factures_.import.tsx) avec breadcrumb, eyebrow, H1 'Importer *plusieurs* factures.', lede explicatif, dropzone full-page avec mutation upload câblée - Drop-catcher de page comme sur /factures (drop n'importe où marche) - 3 hints discrets en bas (Formats / Confidentiel / Reprenable) pour rassurer le user au moment décisif de l'upload Routing nesting fix : - Renommé factures_.import.\$batchId.tsx → factures_.import_.\$batchId.tsx - Trailing underscore sur 'import_' escape la nouvelle landing parent - Les 2 routes sont maintenant siblings sous _app : · /factures/import → factures_.import.tsx · /factures/import/\$batchId → factures_.import_.\$batchId.tsx Topbar AppLayout : - '+ Importer factures' = Button asChild + Link to /factures/import (middle-click / cmd-click / right-click ouvrent un nouvel onglet) - '+ Saisir' reste disabled (placeholder modale 2.3, prochaine étape) Bundle prod : 117.56 KB gzip core (stable, +0.06 vs avant). Co-Authored-By: Claude Opus 4.7 --- apps/web/src/components/layout/AppLayout.tsx | 9 +- apps/web/src/routes/_app/factures_.import.tsx | 123 ++++++++++++++++++ ...hId.tsx => factures_.import_.$batchId.tsx} | 2 +- 3 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 apps/web/src/routes/_app/factures_.import.tsx rename apps/web/src/routes/_app/{factures_.import.$batchId.tsx => factures_.import_.$batchId.tsx} (99%) 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 ( +
+ +
+ ); +} + +function Hint({ eyebrow, body }: { eyebrow: string; body: string }) { + return ( +
+

+ {eyebrow} +

+

{body}

+
+ ); +} diff --git a/apps/web/src/routes/_app/factures_.import.$batchId.tsx b/apps/web/src/routes/_app/factures_.import_.$batchId.tsx similarity index 99% rename from apps/web/src/routes/_app/factures_.import.$batchId.tsx rename to apps/web/src/routes/_app/factures_.import_.$batchId.tsx index bc6419b..3a90659 100644 --- a/apps/web/src/routes/_app/factures_.import.$batchId.tsx +++ b/apps/web/src/routes/_app/factures_.import_.$batchId.tsx @@ -63,7 +63,7 @@ type ImportBatch = { createdAt: string; }; -export const Route = createFileRoute("/_app/factures_/import/$batchId")({ +export const Route = createFileRoute("/_app/factures_/import_/$batchId")({ component: ImportReviewPage, loader: ({ context, params }) => { void context.queryClient.prefetchQuery({