mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-16 11:49:18 +02:00
Interface : passage de Next.js à une SPA React (Vite + React Router)
Le serveur étant réécrit en Rust, l'interface devient une application
React 19 côté navigateur, compilée par Vite dans dist/ et servie par le
serveur Rust (STATIC_DIR). Composants, styles et logique cliente
inchangés : seule la plomberie Next.js est remplacée.
- Outillage : Vite 8, @vitejs/plugin-react, Tailwind 4 via PostCSS,
React Router 7 (mode déclaratif), index.html, src/main.tsx, alias @/,
proxy /api → localhost:5643 en développement, ESLint sans plugin Next.
- Adaptations (src/shims) : next/link, next/navigation (useRouter avec
refresh, usePathname/useSearchParams suivant aussi les replaceState de
l'interface), next-auth/react (session lue sur /api/auth/session,
déconnexion par /api/auth/logout).
- Actions serveur → POST /api/actions/{nom} (src/lib/rpc.ts) : mêmes noms,
mêmes signatures, mêmes types. La connexion passe par /api/auth/login.
- Pages → table des routes (src/router.tsx) : données lues sur
/api/pages/{nom} avec Suspense (loading.tsx au premier chargement,
page conservée pendant une navigation ou un router.refresh()),
error.tsx en frontière d'erreur, not-found.tsx, titres d'onglet,
pages d'administration réservées aux administrateurs.
- Suppression du code serveur Next.js (routes /api, accès base, Qlik,
authentification, middleware…) ; types utiles à l'interface recopiés
dans src/types/ff.ts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LG2LLJuswrrTTPKJmunEVf
This commit is contained in:
170 files changed
+2528
-24445
No files matched your search
@@ -1,11 +1,10 @@
|
||||
"use server";
|
||||
/**
|
||||
* Cadencier des commandes fournisseurs — actions exécutées par le serveur
|
||||
* Rust : `POST /api/actions/{nom}`.
|
||||
*/
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import { commandeCadences } from "@/db/schema";
|
||||
import { getDerniereReceptionCached, getFournisseursCached } from "@/lib/ff-cache";
|
||||
import { requireSession, verifierSession } from "@/lib/authz";
|
||||
import { callAction } from "@/lib/rpc";
|
||||
import { refreshPage } from "@/lib/page-data";
|
||||
|
||||
export type CadenceStatut = "a_commander" | "bientot" | "ok" | "inconnu";
|
||||
|
||||
@@ -25,21 +24,18 @@ export interface CadenceView {
|
||||
statut: CadenceStatut;
|
||||
}
|
||||
|
||||
const MS_PAR_JOUR = 24 * 60 * 60 * 1000;
|
||||
/** Seuil (en jours) en dessous duquel une cadence passe en "bientôt à commander" */
|
||||
const SEUIL_BIENTOT_JOURS = 7;
|
||||
|
||||
function calcStatut(joursRestants: number | null): CadenceStatut {
|
||||
if (joursRestants === null) return "inconnu";
|
||||
if (joursRestants <= 0) return "a_commander";
|
||||
if (joursRestants <= SEUIL_BIENTOT_JOURS) return "bientot";
|
||||
return "ok";
|
||||
/**
|
||||
* Les actions Next.js de modification appelaient `revalidatePath("/commandes-auto")`,
|
||||
* dont la réponse rafraîchissait la page : même effet ici après une réussite.
|
||||
*/
|
||||
function rafraichirSiOk<R extends { ok: boolean }>(resultat: R): R {
|
||||
if (resultat.ok) refreshPage();
|
||||
return resultat;
|
||||
}
|
||||
|
||||
/** Liste des fournisseurs (référentiel) pour le sélecteur de cadence. */
|
||||
export async function getFournisseursPourCadence(): Promise<{ code: string; nom: string }[]> {
|
||||
await requireSession();
|
||||
return getFournisseursCached();
|
||||
return callAction("getFournisseursPourCadence", []);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,45 +43,7 @@ export async function getFournisseursPourCadence(): Promise<{ code: string; nom:
|
||||
* de la dernière réception (FF Nancy).
|
||||
*/
|
||||
export async function listCadences(): Promise<CadenceView[]> {
|
||||
await requireSession();
|
||||
let rows: typeof commandeCadences.$inferSelect[] = [];
|
||||
let receptions = new Map<string, string>();
|
||||
try {
|
||||
rows = await db.select().from(commandeCadences);
|
||||
// Réceptions des seuls fournisseurs du cadencier : la lecture sur tout
|
||||
// l'historique de tous les fournisseurs était la plus lourde de la page.
|
||||
receptions = await getDerniereReceptionCached(rows.map((r) => r.codeFournisseur));
|
||||
} catch (e) {
|
||||
console.error("[cadence] listCadences error:", (e as Error).message);
|
||||
return [];
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
return rows.map((r) => {
|
||||
const receptionIso = receptions.get(`${r.codeFournisseur}|${r.site}`) ?? null;
|
||||
|
||||
let echeanceIso: string | null = null;
|
||||
let joursRestants: number | null = null;
|
||||
if (receptionIso) {
|
||||
const echeance = new Date(receptionIso).getTime() + r.intervalleSemaines * 7 * MS_PAR_JOUR;
|
||||
echeanceIso = new Date(echeance).toISOString();
|
||||
joursRestants = Math.ceil((echeance - now) / MS_PAR_JOUR);
|
||||
}
|
||||
|
||||
return {
|
||||
id: r.id,
|
||||
codefou: r.codeFournisseur,
|
||||
nomfou: r.nomFournisseur ?? r.codeFournisseur,
|
||||
site: r.site,
|
||||
intervalleSemaines: r.intervalleSemaines,
|
||||
actif: r.actif,
|
||||
derniereReception: receptionIso,
|
||||
echeance: echeanceIso,
|
||||
joursRestants,
|
||||
statut: r.actif ? calcStatut(joursRestants) : "inconnu",
|
||||
} satisfies CadenceView;
|
||||
});
|
||||
return callAction("listCadences", []);
|
||||
}
|
||||
|
||||
export interface UpsertCadenceInput {
|
||||
@@ -98,76 +56,15 @@ export interface UpsertCadenceInput {
|
||||
|
||||
/** Crée ou met à jour la cadence d'un fournisseur sur un site. */
|
||||
export async function upsertCadence(input: UpsertCadenceInput): Promise<{ ok: boolean; error?: string }> {
|
||||
const acces = await verifierSession();
|
||||
if (!acces.ok) return { ok: false, error: acces.message };
|
||||
|
||||
const codefou = input.codefou?.trim();
|
||||
const site = input.site?.trim();
|
||||
const intervalle = Math.round(Number(input.intervalleSemaines));
|
||||
|
||||
if (!codefou || !site) return { ok: false, error: "Fournisseur et magasin requis." };
|
||||
if (!Number.isFinite(intervalle) || intervalle < 1 || intervalle > 104) {
|
||||
return { ok: false, error: "L'intervalle doit être compris entre 1 et 104 semaines." };
|
||||
}
|
||||
|
||||
try {
|
||||
await db
|
||||
.insert(commandeCadences)
|
||||
.values({
|
||||
codeFournisseur: codefou,
|
||||
nomFournisseur: input.nomfou ?? null,
|
||||
site,
|
||||
intervalleSemaines: intervalle,
|
||||
actif: input.actif ?? true,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [commandeCadences.codeFournisseur, commandeCadences.site],
|
||||
set: {
|
||||
nomFournisseur: input.nomfou ?? null,
|
||||
intervalleSemaines: intervalle,
|
||||
actif: input.actif ?? true,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
revalidatePath("/commandes-auto");
|
||||
return { ok: true };
|
||||
} catch (e) {
|
||||
console.error("[cadence] upsertCadence error:", (e as Error).message);
|
||||
return { ok: false, error: "Échec de l'enregistrement." };
|
||||
}
|
||||
return rafraichirSiOk(await callAction<{ ok: boolean; error?: string }>("upsertCadence", [input]));
|
||||
}
|
||||
|
||||
/** Active / désactive une cadence sans la supprimer. */
|
||||
export async function toggleCadence(codefou: string, site: string, actif: boolean): Promise<{ ok: boolean; error?: string }> {
|
||||
const acces = await verifierSession();
|
||||
if (!acces.ok) return { ok: false, error: acces.message };
|
||||
|
||||
try {
|
||||
await db
|
||||
.update(commandeCadences)
|
||||
.set({ actif, updatedAt: new Date() })
|
||||
.where(and(eq(commandeCadences.codeFournisseur, codefou), eq(commandeCadences.site, site)));
|
||||
revalidatePath("/commandes-auto");
|
||||
return { ok: true };
|
||||
} catch (e) {
|
||||
console.error("[cadence] toggleCadence error:", (e as Error).message);
|
||||
return { ok: false };
|
||||
}
|
||||
return rafraichirSiOk(await callAction<{ ok: boolean; error?: string }>("toggleCadence", [codefou, site, actif]));
|
||||
}
|
||||
|
||||
/** Supprime définitivement une cadence. */
|
||||
export async function removeCadence(codefou: string, site: string): Promise<{ ok: boolean; error?: string }> {
|
||||
const acces = await verifierSession();
|
||||
if (!acces.ok) return { ok: false, error: acces.message };
|
||||
|
||||
try {
|
||||
await db
|
||||
.delete(commandeCadences)
|
||||
.where(and(eq(commandeCadences.codeFournisseur, codefou), eq(commandeCadences.site, site)));
|
||||
revalidatePath("/commandes-auto");
|
||||
return { ok: true };
|
||||
} catch (e) {
|
||||
console.error("[cadence] removeCadence error:", (e as Error).message);
|
||||
return { ok: false };
|
||||
}
|
||||
return rafraichirSiOk(await callAction<{ ok: boolean; error?: string }>("removeCadence", [codefou, site]));
|
||||
}
|
||||
Reference in new issue
Block a user