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({