mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-18 12:46:32 +02:00
API : affecter ou changer la gamme d'un produit
Deux endpoints d'écriture dans /api/v1 :
- PUT /products/{codein}/gamme { gamme, fournisseur? } pour un article ;
- POST /gammes { fournisseur, changes[] } pour plusieurs articles d'un
fournisseur, en tout ou rien.
Ils passent par le même enregistrement que la Grille (extrait de
saveDraftChanges dans enregistrerGammes) : snapshot du fournisseur, cache
de la Grille et instantané grid_rows. Gamme validée contre la liste des
gammes (A, B, C, Y, Z) ; un article déjà dans la gamme demandée n'est pas
réécrit. Spec OpenAPI et page de connexion à l'API mises à jour ; la
légende des gammes de la spec, qui citait une gamme D inexistante, suit
désormais lib/gammes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VEXT61tGbsXHUDNBeghCXQ
This commit is contained in:
9 files changed
+544
-54
No files matched your search
@@ -79,6 +79,8 @@ const ENDPOINTS: Array<{ method: string; path: string; desc: string }> = [
|
||||
{ method: "GET", path: "/nomenclatures?fournisseur=CODE", desc: "Postes de nomenclature d'un fournisseur, avec nombre d'articles et chiffre d'affaires" },
|
||||
{ method: "GET", path: "/products/search?q=terme", desc: "Recherche de produits, tous fournisseurs confondus" },
|
||||
{ method: "GET", path: "/products/{codein}", desc: "Fiche complète d'un produit" },
|
||||
{ method: "PUT", path: "/products/{codein}/gamme", desc: "Affecter ou changer la gamme d'un produit — corps : { \"gamme\": \"A\" }" },
|
||||
{ method: "POST", path: "/gammes", desc: "Affecter ou changer la gamme de plusieurs produits d'un fournisseur en un appel" },
|
||||
{ method: "GET", path: "/network/{codeCentrale}", desc: "Ventes du réseau (Qlik) et courbe sur 12 mois" },
|
||||
{ method: "GET", path: "/openapi.json", desc: "Description de l'API lisible par un programme (format OpenAPI)" },
|
||||
];
|
||||
@@ -203,6 +205,7 @@ export function ApiConnectionInfo() {
|
||||
</Repli>
|
||||
|
||||
<Repli titre="Exemples d'appels">
|
||||
<CodeLine>{`curl -X PUT -H "X-API-Key: VOTRE_CLE" -H "Content-Type: application/json" \\\n -d '{"gamme":"B"}' "${base}/products/123456/gamme"`}</CodeLine>
|
||||
<CodeLine>{`curl -H "X-API-Key: VOTRE_CLE" \\\n "${base}/products/search?q=tapis&limit=20"`}</CodeLine>
|
||||
<CodeLine>{`curl -H "X-API-Key: VOTRE_CLE" \\\n "${base}/grid?fournisseur=FOU001&fields=codein,libelle1,totalCa,codeGammeServeur"`}</CodeLine>
|
||||
<p className="pt-1 text-[13px] text-[var(--text-secondary)]">
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import "server-only";
|
||||
|
||||
import { db } from "@/db";
|
||||
import { sessionSnapshots } from "@/db/schema";
|
||||
import { eq, desc } from "drizzle-orm";
|
||||
import { patchGridRowsCache } from "./get-product-rows";
|
||||
import { updateGridRowsGamme } from "@/lib/grid-store";
|
||||
|
||||
export interface ChangementGamme {
|
||||
codein: string;
|
||||
/** Gamme avant la modification (état serveur), conservée pour l'historique. */
|
||||
codeGammeBefore: string | null;
|
||||
codeGamme: string;
|
||||
}
|
||||
|
||||
export interface EnregistrementGammes {
|
||||
codeFournisseur: string;
|
||||
nomFournisseur?: string | null;
|
||||
magasin: string;
|
||||
changes: ChangementGamme[];
|
||||
userId: number | null;
|
||||
/** Libellé du snapshot, « Draft — date » par défaut. */
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre des gammes pour un fournisseur — chemin unique de la Grille et de /api/v1.
|
||||
*
|
||||
* Les changements sont fusionnés par-dessus le dernier snapshot du fournisseur,
|
||||
* puis reportés dans le cache mémoire de la Grille et dans l'instantané `grid_rows`.
|
||||
* Lève une erreur si l'écriture du snapshot échoue.
|
||||
*/
|
||||
export async function enregistrerGammes(input: EnregistrementGammes): Promise<number> {
|
||||
const { codeFournisseur, nomFournisseur, magasin, changes, userId } = input;
|
||||
if (changes.length === 0) return 0;
|
||||
|
||||
// Charger le dernier snapshot existant pour merger les changements
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(sessionSnapshots)
|
||||
.where(eq(sessionSnapshots.codeFournisseur, codeFournisseur))
|
||||
.orderBy(desc(sessionSnapshots.createdAt))
|
||||
.limit(1);
|
||||
|
||||
const prevChanges: Record<string, { before: string | null; after: string }> =
|
||||
existing.length > 0
|
||||
? (existing[0].changes as Record<string, { before: string | null; after: string }>)
|
||||
: {};
|
||||
|
||||
// Merger les nouveaux changements par-dessus l'existant
|
||||
const mergedChanges = { ...prevChanges };
|
||||
for (const c of changes) {
|
||||
mergedChanges[c.codein] = {
|
||||
before: c.codeGammeBefore,
|
||||
after: c.codeGamme,
|
||||
};
|
||||
}
|
||||
|
||||
// Upsert : insérer un nouveau snapshot "draft" pour ce fournisseur
|
||||
await db.insert(sessionSnapshots).values({
|
||||
userId,
|
||||
codeFournisseur,
|
||||
nomFournisseur: nomFournisseur ?? null,
|
||||
magasin,
|
||||
label: input.label ?? `Draft — ${new Date().toLocaleDateString("fr-FR")}`,
|
||||
changes: mergedChanges,
|
||||
summaryJson: null,
|
||||
type: "snapshot",
|
||||
});
|
||||
|
||||
// Les lignes en cache reçoivent les nouvelles gammes au lieu d'être jetées :
|
||||
// l'invalidation imposait un recalcul complet (jusqu'à ~40 s) à la
|
||||
// réouverture suivante. L'instantané lu par /api/v1 suit, sans bloquer.
|
||||
patchGridRowsCache(codeFournisseur, changes);
|
||||
void updateGridRowsGamme(codeFournisseur, changes).catch((e) =>
|
||||
console.error("[enregistrerGammes] mise à jour grid_rows KO:", (e as Error).message?.slice(0, 200)),
|
||||
);
|
||||
|
||||
return changes.length;
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
"use server";
|
||||
|
||||
import { db } from "@/db";
|
||||
import { sessionSnapshots } from "@/db/schema";
|
||||
import { eq, desc } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { verifierSession } from "@/lib/authz";
|
||||
import { patchGridRowsCache } from "./get-product-rows";
|
||||
import { updateGridRowsGamme } from "@/lib/grid-store";
|
||||
import { enregistrerGammes } from "./enregistrer-gammes";
|
||||
|
||||
const SaveDraftsSchema = z.object({
|
||||
codeFournisseur: z.string(),
|
||||
@@ -39,49 +35,8 @@ export async function saveDraftChanges(
|
||||
const finalUserId = userId && !isNaN(userId) ? userId : null;
|
||||
|
||||
try {
|
||||
// Charger le dernier snapshot existant pour merger les changements
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(sessionSnapshots)
|
||||
.where(eq(sessionSnapshots.codeFournisseur, codeFournisseur))
|
||||
.orderBy(desc(sessionSnapshots.createdAt))
|
||||
.limit(1);
|
||||
|
||||
const prevChanges: Record<string, { before: string | null; after: string }> =
|
||||
existing.length > 0
|
||||
? (existing[0].changes as Record<string, { before: string | null; after: string }>)
|
||||
: {};
|
||||
|
||||
// Merger les nouveaux changements par-dessus l'existant
|
||||
const mergedChanges = { ...prevChanges };
|
||||
for (const c of changes) {
|
||||
mergedChanges[c.codein] = {
|
||||
before: c.codeGammeBefore,
|
||||
after: c.codeGamme,
|
||||
};
|
||||
}
|
||||
|
||||
// Upsert : insérer un nouveau snapshot "draft" pour ce fournisseur
|
||||
await db.insert(sessionSnapshots).values({
|
||||
userId: finalUserId,
|
||||
codeFournisseur,
|
||||
nomFournisseur: nomFournisseur ?? null,
|
||||
magasin,
|
||||
label: `Draft — ${new Date().toLocaleDateString("fr-FR")}`,
|
||||
changes: mergedChanges,
|
||||
summaryJson: null,
|
||||
type: "snapshot",
|
||||
});
|
||||
|
||||
// Les lignes en cache reçoivent les nouvelles gammes au lieu d'être jetées :
|
||||
// l'invalidation imposait un recalcul complet (jusqu'à ~40 s) à la
|
||||
// réouverture suivante. L'instantané lu par /api/v1 suit, sans bloquer.
|
||||
patchGridRowsCache(codeFournisseur, changes);
|
||||
void updateGridRowsGamme(codeFournisseur, changes).catch((e) =>
|
||||
console.error("[saveDraftChanges] mise à jour grid_rows KO:", (e as Error).message?.slice(0, 200)),
|
||||
);
|
||||
|
||||
return { success: true, saved: changes.length };
|
||||
const saved = await enregistrerGammes({ codeFournisseur, nomFournisseur, magasin, changes, userId: finalUserId });
|
||||
return { success: true, saved };
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : "Unknown error";
|
||||
return { success: false, saved: 0, error: msg };
|
||||
|
||||
Reference in new issue
Block a user