Page /clients (liste) :
- Header dynamique : 'X factures en retard chez Y clients' en rubis-deep
s'il y en a, sinon 'Tout est calme côté clients.'
- Recherche par nom/email (param q côté serveur, debounce naturel via
TanStack Query staleTime: 10s)
- Table desktop / cards mobile (cohérent avec /factures)
- Tri serveur : retards d'abord (actionable), puis activité récente
- Empty state distincts (recherche vide vs jamais de clients)
- Lien depuis 'Voir tout' du panel TopLatePayers du dashboard fonctionne
- '+ Nouveau client' disabled (V2)
Page /clients/$id (détail) :
- Header : eyebrow contextuel, nom h1, infos contact (mail clickable,
phone, address) avec icônes Lucide ink-3
- 4 KPI cards en grille : Factures actives (avec sub-info 'N en retard'
rubis-deep si pertinent), En attente, Encaissé total, Factures payées
- Liste des factures du client (cliquables vers /factures/$id) avec
StatusBadge sans icône (compact)
- Notes internes : Textarea avec autosave on blur via PATCH /clients/:id
MSW :
- GET /clients?withStats=1&q= : enrichit avec compteurs + montants +
lastActivityAt. Tri par retards d'abord
- GET /clients/:id : détail enrichi + invoices triées plus récentes
- PATCH /clients/:id : édition Zod
- mockDb.updateClient(orgId, id, patch) ajouté
Persistance session mock (stay logged in après reload) :
- mocks/sessionStore.ts : helpers localStorage simulant le cookie
httpOnly côté serveur. TTL 30j (= refresh token typique). SPA n'y
accède jamais directement, seul MSW touche cette persistance.
- POST /auth/{login,signup} : sessionStore.set après succès
- POST /auth/logout : sessionStore.clear (clean disconnect)
- POST /auth/refresh : retourne la session stockée + recharge le user
depuis mockDb au cas où il a été modifié (signature post-onboarding etc.)
- main.tsx : bootstrapSession() avant le 1er render (silent refresh).
Évite le flash redirect /login pour les users déjà connectés.
Architecture : le SPA n'accède jamais directement à localStorage —
il passe toujours par HTTP (/auth/refresh). Quand on branchera le vrai
backend Adonis, on supprime juste mocks/sessionStore.ts et le pattern
continue à marcher (cookie httpOnly remplace localStorage côté serveur).
queryKeys.clients.list ajouté pour le param de recherche.
Bundle prod : 117.92 KB gzip core (stable +0.28 vs avant).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
101 lines
2.8 KiB
TypeScript
101 lines
2.8 KiB
TypeScript
import { StrictMode } from "react";
|
|
import { createRoot } from "react-dom/client";
|
|
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
|
|
import { routeTree } from "./routeTree.gen";
|
|
import { env } from "./lib/env";
|
|
import { api } from "./lib/api";
|
|
import { authStore } from "./lib/auth";
|
|
import type { AuthSession } from "@rubis/shared";
|
|
|
|
import "./styles/app.css";
|
|
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
staleTime: 30_000,
|
|
gcTime: 5 * 60_000,
|
|
retry: 1,
|
|
refetchOnWindowFocus: false,
|
|
},
|
|
mutations: {
|
|
retry: 0,
|
|
},
|
|
},
|
|
});
|
|
|
|
const router = createRouter({
|
|
routeTree,
|
|
context: { queryClient },
|
|
defaultPreload: "intent",
|
|
defaultPreloadStaleTime: 0,
|
|
});
|
|
|
|
declare module "@tanstack/react-router" {
|
|
interface Register {
|
|
router: typeof router;
|
|
}
|
|
}
|
|
|
|
async function enableMocking(): Promise<void> {
|
|
// import.meta.env.DEV est un booléen statique : Vite tree-shake la branche
|
|
// entière (et le chunk MSW avec) quand on build en mode production.
|
|
if (!import.meta.env.DEV || !env.VITE_USE_MOCKS) return;
|
|
const { worker } = await import("./mocks/browser");
|
|
await worker.start({
|
|
onUnhandledRequest: "bypass",
|
|
serviceWorker: {
|
|
url: "/mockServiceWorker.js",
|
|
},
|
|
});
|
|
// eslint-disable-next-line no-console
|
|
console.info(
|
|
"%c[MSW]%c Mocks API actifs — VITE_USE_MOCKS=true",
|
|
"background:#9F1239;color:white;padding:2px 6px;border-radius:3px;font-weight:600",
|
|
"color:#8A7F76",
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Tente de récupérer la session à partir du refresh token (cf. ADR-017).
|
|
* Si le serveur (ou MSW) confirme une session valide → on rehydrate l'authStore
|
|
* AVANT le 1er render, ce qui évite le flash redirect /login pour les users
|
|
* déjà connectés.
|
|
*
|
|
* En mode dev avec MSW, la "session" persistée est en localStorage (cf.
|
|
* mocks/sessionStore). En prod réel, c'est le cookie httpOnly côté Adonis.
|
|
*/
|
|
async function bootstrapSession(): Promise<void> {
|
|
try {
|
|
const session = await api.post<AuthSession>(
|
|
"/api/v1/auth/refresh",
|
|
undefined,
|
|
{ anonymous: true },
|
|
);
|
|
authStore.setSession(session.accessToken, session.user);
|
|
} catch {
|
|
// Pas de session valide → on reste anonyme. _app guard redirigera vers /login.
|
|
}
|
|
}
|
|
|
|
function render(): void {
|
|
const rootEl = document.getElementById("root");
|
|
if (!rootEl) throw new Error("#root introuvable dans index.html");
|
|
createRoot(rootEl).render(
|
|
<StrictMode>
|
|
<QueryClientProvider client={queryClient}>
|
|
<RouterProvider router={router} />
|
|
</QueryClientProvider>
|
|
</StrictMode>,
|
|
);
|
|
}
|
|
|
|
async function init(): Promise<void> {
|
|
await enableMocking();
|
|
await bootstrapSession();
|
|
render();
|
|
}
|
|
|
|
void init();
|