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 <noreply@anthropic.com>
85 lines
2.6 KiB
TypeScript
85 lines
2.6 KiB
TypeScript
import { useQuery } from "@tanstack/react-query";
|
|
import { Plus, Upload } from "lucide-react";
|
|
|
|
import { api } from "@/lib/api";
|
|
import { queryKeys } from "@/lib/queryKeys";
|
|
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 :
|
|
* - Desktop : sidebar fixe à gauche + main + topbar sticky en haut
|
|
* - Mobile : pas de sidebar, topbar avec brand, tab bar fixe en bas
|
|
*
|
|
* Le bottom-padding mobile (pb-20) évite que le tab bar masque le contenu.
|
|
*/
|
|
type AppLayoutProps = {
|
|
children: React.ReactNode;
|
|
/** Titre du topbar (sinon greeting + date). */
|
|
title?: string;
|
|
subtitle?: string;
|
|
/** Actions à droite du topbar. */
|
|
actions?: React.ReactNode;
|
|
};
|
|
|
|
type DashboardKpis = {
|
|
rubisCount: number;
|
|
rubisThisMonth: number;
|
|
hoursLiberatedThisMonth: number;
|
|
encaisseCents: number;
|
|
encaisseDeltaCents: number;
|
|
dsoDays: number;
|
|
dsoDeltaDays: number;
|
|
factureToRelance: number;
|
|
factureInRelance: number;
|
|
factureNewToday: number;
|
|
miseEnDemeurePending: number;
|
|
};
|
|
|
|
export function AppLayout({ children, title, subtitle, actions }: AppLayoutProps) {
|
|
// KPIs partagés layout ↔ dashboard : on les charge ici pour que le sidebar
|
|
// affiche le compteur sans attendre le rendu du dashboard.
|
|
const { data: kpis } = useQuery({
|
|
queryKey: queryKeys.dashboard.kpis(),
|
|
queryFn: () => api.get<DashboardKpis>("/api/v1/dashboard/kpis"),
|
|
staleTime: 30_000,
|
|
});
|
|
|
|
// Actions globales par défaut (visibles desktop seulement). Sur mobile,
|
|
// chaque route gère ses propres CTA en tête de contenu (cf. wireframe 4.3).
|
|
const defaultActions = (
|
|
<div className="hidden lg:flex items-center gap-2">
|
|
<Button size="sm" variant="secondary" disabled>
|
|
<Plus size={14} aria-hidden="true" /> Saisir
|
|
</Button>
|
|
<Button size="sm" asChild>
|
|
<Link to="/factures/import">
|
|
<Upload size={14} aria-hidden="true" /> Importer factures
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="min-h-screen flex bg-cream">
|
|
<AppSidebar rubisThisMonth={kpis?.rubisThisMonth ?? 0} />
|
|
|
|
<div className="flex flex-1 min-w-0 flex-col">
|
|
<AppTopbar
|
|
title={title}
|
|
subtitle={subtitle}
|
|
actions={actions ?? defaultActions}
|
|
/>
|
|
<main className="flex-1 px-5 py-6 pb-24 lg:px-8 lg:py-8 lg:pb-12">
|
|
{children}
|
|
</main>
|
|
</div>
|
|
|
|
<MobileTabBar />
|
|
</div>
|
|
);
|
|
}
|