mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-15 19:28:52 +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,39 +1,10 @@
|
||||
"use server";
|
||||
/**
|
||||
* Suppression d'un enregistrement de l'historique — action exécutée par le
|
||||
* serveur Rust : `POST /api/actions/deleteSnapshot`.
|
||||
*/
|
||||
|
||||
import { db } from "@/db";
|
||||
import { sessionSnapshots } from "@/db/schema";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import { verifierSession } from "@/lib/authz";
|
||||
import { callAction } from "@/lib/rpc";
|
||||
|
||||
export async function deleteSnapshot(id: number) {
|
||||
const acces = await verifierSession();
|
||||
if (!acces.ok) return { success: false, error: acces.message };
|
||||
|
||||
const rawUserId = acces.utilisateur.id;
|
||||
const userId = rawUserId ? parseInt(rawUserId, 10) : null;
|
||||
|
||||
console.log(`[deleteSnapshot] Request for ID: ${id}, Session UserId: ${userId}`);
|
||||
|
||||
try {
|
||||
// Condition de sécurité : match ID ET match le propriétaire (ou anonyme)
|
||||
const userCondition = userId && !isNaN(userId)
|
||||
? eq(sessionSnapshots.userId, userId)
|
||||
: isNull(sessionSnapshots.userId);
|
||||
|
||||
const result = await db.delete(sessionSnapshots)
|
||||
.where(and(eq(sessionSnapshots.id, id), userCondition))
|
||||
.returning({ deletedId: sessionSnapshots.id });
|
||||
|
||||
if (result.length === 0) {
|
||||
console.warn(`[deleteSnapshot] No rows deleted for ID ${id}. Snapshot may not exist or belongs to another user.`);
|
||||
return { success: false, error: "Snapshot non trouvé ou non autorisé" };
|
||||
}
|
||||
|
||||
console.log(`[deleteSnapshot] Successfully deleted ID: ${id}`);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
// Le détail technique reste dans les journaux du serveur.
|
||||
console.error("[deleteSnapshot] DB Error:", err);
|
||||
return { success: false, error: "erreur technique sur le serveur" };
|
||||
}
|
||||
export async function deleteSnapshot(id: number): Promise<{ success: boolean; error?: string }> {
|
||||
return callAction("deleteSnapshot", [id]);
|
||||
}
|
||||
@@ -1,34 +1,26 @@
|
||||
"use server";
|
||||
/**
|
||||
* Historique des sessions de révision et des exports — action exécutée par le
|
||||
* serveur Rust : `POST /api/actions/getSnapshots`.
|
||||
*/
|
||||
|
||||
import { db } from "@/db";
|
||||
import { sessionSnapshots } from "@/db/schema";
|
||||
import { requireSession } from "@/lib/authz";
|
||||
import { and, desc, eq, isNull } from "drizzle-orm";
|
||||
import { callAction } from "@/lib/rpc";
|
||||
|
||||
export async function getSnapshots(type?: "snapshot" | "export") {
|
||||
const utilisateur = await requireSession();
|
||||
try {
|
||||
const rawUserId = utilisateur.id;
|
||||
const userId = rawUserId ? parseInt(rawUserId, 10) : null;
|
||||
/** Une ligne de `session_snapshots`, telle que le serveur la renvoie. */
|
||||
export interface SnapshotRow {
|
||||
id: number;
|
||||
userId: number | null;
|
||||
codeFournisseur: string;
|
||||
nomFournisseur: string | null;
|
||||
magasin: string;
|
||||
/** codein → { before, after } */
|
||||
changes: unknown;
|
||||
summaryJson: unknown;
|
||||
label: string | null;
|
||||
type: string | null;
|
||||
/** Date ISO de création. */
|
||||
createdAt: string | null;
|
||||
}
|
||||
|
||||
// Si on a un userId, on filtre par celui-ci.
|
||||
// Sinon, on filtre par userId IS NULL (mode anonyme/local)
|
||||
const userCondition = userId && !isNaN(userId)
|
||||
? eq(sessionSnapshots.userId, userId)
|
||||
: isNull(sessionSnapshots.userId);
|
||||
|
||||
const conditions = [userCondition];
|
||||
if (type) conditions.push(eq(sessionSnapshots.type, type));
|
||||
|
||||
return await db.select()
|
||||
.from(sessionSnapshots)
|
||||
.where(and(...conditions))
|
||||
.orderBy(desc(sessionSnapshots.createdAt));
|
||||
} catch (err) {
|
||||
const msg = ((err as Error)?.message || String(err)).split("\n")[0];
|
||||
console.error(`[getSnapshots] ERROR (type=${type}):`, msg, err);
|
||||
// Remonter l'erreur : une liste vide faisait afficher « Aucun snapshot »
|
||||
// au lieu de signaler que le chargement a échoué.
|
||||
throw new Error("Chargement de l'historique impossible.");
|
||||
}
|
||||
export async function getSnapshots(type?: "snapshot" | "export"): Promise<SnapshotRow[]> {
|
||||
return callAction("getSnapshots", [type]);
|
||||
}
|
||||
@@ -1,114 +1,27 @@
|
||||
"use server";
|
||||
/**
|
||||
* Enregistrement d'une session de révision ou d'un export — action exécutée par
|
||||
* le serveur Rust : `POST /api/actions/saveSnapshot` (validation des données
|
||||
* comprise).
|
||||
*/
|
||||
|
||||
import { db } from "@/db";
|
||||
import { sessionSnapshots } from "@/db/schema";
|
||||
import { z } from "zod";
|
||||
import { sql, and, eq } from "drizzle-orm";
|
||||
import { verifierSession } from "@/lib/authz";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { callAction } from "@/lib/rpc";
|
||||
|
||||
const SaveSnapshotSchema = z.object({
|
||||
codeFournisseur: z.string(),
|
||||
nomFournisseur: z.string().optional(),
|
||||
magasin: z.string(),
|
||||
label: z.string().optional(),
|
||||
changes: z.record(z.string(), z.object({
|
||||
before: z.string().nullable(),
|
||||
after: z.string(),
|
||||
})),
|
||||
type: z.enum(["snapshot", "export"]).optional(),
|
||||
summary: z.object({
|
||||
totalRows: z.number(),
|
||||
totalCa: z.number(),
|
||||
totalMarge: z.number(),
|
||||
tauxMargeGlobal: z.number(),
|
||||
}).optional(),
|
||||
});
|
||||
|
||||
export async function saveSnapshot(raw: unknown) {
|
||||
const acces = await verifierSession();
|
||||
if (!acces.ok) return { success: false, error: acces.message };
|
||||
|
||||
const parsed = SaveSnapshotSchema.safeParse(raw);
|
||||
if (!parsed.success) {
|
||||
console.error("Snapshot validation failed:", parsed.error.format());
|
||||
return { success: false, error: "Validation failed: " + parsed.error.issues.map((i: any) => i.message).join(", ") };
|
||||
}
|
||||
|
||||
const { codeFournisseur, nomFournisseur, magasin, label, changes, summary, type } = parsed.data;
|
||||
const rawUserId = acces.utilisateur.id;
|
||||
const userId = rawUserId ? parseInt(rawUserId, 10) : null;
|
||||
|
||||
console.log(`[saveSnapshot] User: ${userId} (raw: ${rawUserId}), Supplier: ${codeFournisseur}, Type: ${type}`);
|
||||
|
||||
const finalUserId = (userId && !isNaN(userId)) ? userId : null;
|
||||
|
||||
try {
|
||||
const [created] = await db
|
||||
.insert(sessionSnapshots)
|
||||
.values({
|
||||
userId: finalUserId,
|
||||
codeFournisseur,
|
||||
nomFournisseur: nomFournisseur ?? null,
|
||||
magasin,
|
||||
label: label ?? `${type === 'export' ? 'Export' : 'Snapshot'} — ${new Date().toLocaleDateString("fr-FR")}`,
|
||||
changes,
|
||||
summaryJson: summary ?? null,
|
||||
type: type ?? "snapshot",
|
||||
})
|
||||
.returning({ id: sessionSnapshots.id });
|
||||
|
||||
revalidatePath("/snapshots");
|
||||
revalidatePath("/exports");
|
||||
return { success: true, snapshotId: created?.id };
|
||||
} catch (err) {
|
||||
console.error("Initial snapshot save failed, attempting auto-repair...", err);
|
||||
|
||||
try {
|
||||
// Tentative systématique de création de table (no-op si déjà là)
|
||||
await db.execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS session_snapshots (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER,
|
||||
code_fournisseur VARCHAR(20) NOT NULL,
|
||||
nom_fournisseur VARCHAR(255),
|
||||
magasin VARCHAR(20) NOT NULL,
|
||||
changes JSONB NOT NULL,
|
||||
summary_json JSONB,
|
||||
label TEXT,
|
||||
type VARCHAR(20) DEFAULT 'snapshot',
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
`);
|
||||
|
||||
// Si la table existait déjà mais sans la colonne 'type' ou 'user_id', on les ajoute
|
||||
try {
|
||||
await db.execute(sql`ALTER TABLE session_snapshots ADD COLUMN IF NOT EXISTS type VARCHAR(20) DEFAULT 'snapshot'`);
|
||||
await db.execute(sql`ALTER TABLE session_snapshots ADD COLUMN IF NOT EXISTS user_id INTEGER`);
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
// Deuxième tentative d'insertion
|
||||
const [retryCreated] = await db
|
||||
.insert(sessionSnapshots)
|
||||
.values({
|
||||
userId: finalUserId,
|
||||
codeFournisseur,
|
||||
nomFournisseur: nomFournisseur ?? null,
|
||||
magasin,
|
||||
label: label ?? `${type === 'export' ? 'Export' : 'Snapshot'} — ${new Date().toLocaleDateString("fr-FR")}`,
|
||||
changes,
|
||||
summaryJson: summary ?? null,
|
||||
type: type ?? "snapshot",
|
||||
})
|
||||
.returning({ id: sessionSnapshots.id });
|
||||
|
||||
return { success: true, snapshotId: retryCreated?.id };
|
||||
} catch (repairErr) {
|
||||
// Le détail technique (les deux erreurs) reste dans les journaux du serveur.
|
||||
console.error("Snapshot auto-repair/retry failed:", repairErr);
|
||||
return { success: false, error: "erreur technique sur le serveur" };
|
||||
}
|
||||
}
|
||||
/** Données attendues par le serveur (ex-schéma zod `SaveSnapshotSchema`). */
|
||||
export interface SaveSnapshotInput {
|
||||
codeFournisseur: string;
|
||||
nomFournisseur?: string;
|
||||
magasin: string;
|
||||
label?: string;
|
||||
changes: Record<string, { before: string | null; after: string }>;
|
||||
type?: "snapshot" | "export";
|
||||
summary?: {
|
||||
totalRows: number;
|
||||
totalCa: number;
|
||||
totalMarge: number;
|
||||
tauxMargeGlobal: number;
|
||||
};
|
||||
}
|
||||
|
||||
export async function saveSnapshot(raw: unknown): Promise<{ success: boolean; snapshotId?: number; error?: string }> {
|
||||
return callAction("saveSnapshot", [raw]);
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
|
||||
Reference in new issue
Block a user