feat: implement PostgreSQL client for bulk data retrieval to replace per-article HTTP requests and configure Claude permissions

This commit is contained in:
Michael committed 2026-04-02 16:31:11 +02:00
1 parent 868886db87
commit 7e4b370267
6 files changed
+91 -19

No files matched your search

+2 -1
View File
@@ -33,7 +33,8 @@ export default async function GridPage({ searchParams }: GridPageProps) {
return <SupplierSelectionLanding fournisseurs={fournisseurs} />;
}
// 3. Load real product data for the selected supplier
// 3. Load product data — résultat mis en cache 5 min par fournisseur (unstable_cache).
// La première requête est lente mais les suivantes (refresh, navigation) sont instantanées.
console.log(`\n>>> [GridPage] Fetching data for: ${codeFournisseur}, shop: ${magasin}`);
const rows = await getGridData(codeFournisseur, magasin, filters);
console.log(`>>> [GridPage] Received ${rows.length} rows from getGridData`);
+4 -2
View File
@@ -21,14 +21,16 @@ export async function getMagasins() {
/**
* Nomenclature filter is disabled — code3 is not available from the API.
* Returns an empty hierarchy so the sidebar filter is hidden gracefully.
*/
export async function getAvailableNomenclature() {
return {};
}
/**
* Get product data for a specific supplier and store.
* Retourne tous les produits d'un fournisseur.
* Le résultat est mis en cache 5 min côté serveur (unstable_cache par fournisseur).
* La première requête est lente (6 SQL + agrégation mémoire).
* Toutes les requêtes suivantes dans la fenêtre de 5 min sont instantanées.
*/
export async function getGridData(
codeFournisseur: string,
+44 -4
View File
@@ -16,15 +16,26 @@ import {
import { db } from "@/db";
import { sessionSnapshots } from "@/db/schema";
import { eq, desc } from "drizzle-orm";
import { unstable_cache } from "next/cache";
interface GetProductRowsInput {
// ─── Tag de cache par fournisseur ─────────────────────────────────────────────
// Permet d'invalider précisément le cache d'un seul fournisseur lors d'une sauvegarde.
export function getGridCacheTag(codeFournisseur: string): string {
return `grid-rows-${codeFournisseur}`;
}
export interface GetProductRowsInput {
codeFournisseur: string;
magasin?: string;
filters?: Partial<GridFilters>;
}
export async function getProductRows(input: GetProductRowsInput): Promise<ProductRow[]> {
const { codeFournisseur, magasin = "TOTAL" } = input;
// ─── Fonction interne : fetch complet non-paginé (cachée) ───────────────────
// Cette fonction est le « dataloader » central. Elle est enveloppée dans
// unstable_cache pour ne jamais refaire les 6 requêtes SQL tant que le cache
// est valide (revalidate : 300s). Le tag permet une invalidation ciblée.
async function _fetchAllProductRows(codeFournisseur: string): Promise<ProductRow[]> {
const magasin = "TOTAL"; // Le cache est toujours TOTAL ; le switch magasin est client-side
console.log(`\n>>> [getProductRows] supplier: ${codeFournisseur}, magasin: ${magasin}`);
try {
@@ -299,7 +310,36 @@ export async function getProductRows(input: GetProductRowsInput): Promise<Produc
return computeProductScores(rows);
} catch (error) {
console.error(`[getProductRows] Error for ${codeFournisseur}:`, error);
console.error(`[_fetchAllProductRows] Error for ${codeFournisseur}:`, error);
return [];
}
}
// ─── Fonction publique (Option B : cache pur, pas de pagination) ─────────────
/**
* Point d'entrée unique pour la grille.
* - Met en cache le résultat complet par fournisseur (5 min, tag invalidable)
* - Retourne toutes les lignes (filtrage/tri restent côté client)
*
* La première requête pour un fournisseur donné est lente (6 SQL + agrégation).
* Toutes les requêtes suivantes dans la fenêtre de 5 min sont instantanées.
*/
export async function getProductRows(input: GetProductRowsInput): Promise<ProductRow[]> {
const { codeFournisseur } = input;
// Créer dynamiquement la fonction cachée avec le tag spécifique au fournisseur.
// unstable_cache mémoïse par le tableau de clés [`grid-rows-${codeFournisseur}`].
const cachedFn = unstable_cache(
async () => _fetchAllProductRows(codeFournisseur),
[`grid-rows-${codeFournisseur}`],
{ revalidate: 300, tags: [`grid-rows-${codeFournisseur}`] }
);
try {
return await cachedFn();
} catch (e) {
console.error(`[getProductRows] Cache error for ${codeFournisseur}:`, e);
// Fallback sans cache si unstable_cache échoue
return _fetchAllProductRows(codeFournisseur);
}
}
+4 -6
View File
@@ -11,9 +11,7 @@ import { useSaveDrafts } from "@/features/grid/hooks/use-save-drafts";
import type { ProductRow } from "@/types/grid";
import { CheckCircle, AlertCircle, Loader2 } from "lucide-react";
import { useSession } from "next-auth/react";
import { useSearchParams } from "next/navigation";
import { computeProductScores } from "@/lib/score-engine";
interface GridClientProps {
initialRows: ProductRow[];
@@ -26,7 +24,7 @@ interface GridClientProps {
export function GridClient({ initialRows, codeFournisseur, nomFournisseur, fournisseurs, magasins, magasin }: GridClientProps) {
const { data: session } = useSession();
const isAdmin = (session?.user as any)?.role === "admin";
const isAdmin = (session?.user as { role?: string })?.role === "admin";
const setRows = useGridStore((s) => s.setRows);
const setActiveGridQuery = useGridStore((s) => s.setActiveGridQuery);
const setFilter = useGridStore((s) => s.setFilter);
@@ -50,7 +48,6 @@ export function GridClient({ initialRows, codeFournisseur, nomFournisseur, fourn
useEffect(() => {
if (!isMounted) return;
const currentQueryString = searchParams.toString();
// Set the active query, ensuring it starts with ? if not empty
setActiveGridQuery(currentQueryString ? `?${currentQueryString}` : "");
}, [searchParams, setActiveGridQuery, isMounted]);
@@ -63,8 +60,9 @@ export function GridClient({ initialRows, codeFournisseur, nomFournisseur, fourn
setFilter("code3", null);
prevFournisseurRef.current = codeFournisseur;
}
const scoredRows = computeProductScores([...initialRows]);
setRows(scoredRows);
// Les scores ont déjà été calculés côté serveur dans _fetchAllProductRows.
// On pousse directement les lignes dans le store sans recalcul côté client.
setRows(initialRows);
}, [codeFournisseur, initialRows, setRows, setFilter, isMounted]);
// Synchroniser le magasin actif depuis la prop URL (changement de magasin sans rechargement)
+12 -5
View File
@@ -7,6 +7,8 @@ import type { GammeCode, GridFilters, GridSummary, ProductRow } from "@/types/gr
interface GridState {
/** Source data from server */
rows: ProductRow[];
/** Index O(1) pour les lookups par codein (Map<codein, ProductRow>) */
rowsIndex: Map<string, ProductRow>;
/** Draft edits: codein → new GammeCode */
draftChanges: Record<string, GammeCode>;
filters: GridFilters;
@@ -64,6 +66,7 @@ export const useGridStore = create<GridState>()(
persist(
(set, get) => ({
rows: [],
rowsIndex: new Map(),
activeMagasin: "TOTAL",
draftChanges: {},
filters: {
@@ -94,12 +97,15 @@ export const useGridStore = create<GridState>()(
},
setRows: (rows) => {
set({ rows, summary: computeSummary(rows, get().draftChanges, get().activeMagasin) });
// Construire l'index en O(n) une seule fois au lieu de O(n) à chaque lookup.
const rowsIndex = new Map<string, ProductRow>(rows.map(r => [r.codein, r]));
set({ rows, rowsIndex, summary: computeSummary(rows, get().draftChanges, get().activeMagasin) });
},
setDraftGamme: (codein, gamme) => {
const { rows, draftChanges: oldDrafts } = get();
const originalRow = rows.find(r => r.codein === codein);
const { rows, rowsIndex, draftChanges: oldDrafts } = get();
// Lookup O(1) via l'index au lieu de O(n) via .find()
const originalRow = rowsIndex.get(codein);
const originalGamme = originalRow?.codeGamme;
const draftChanges = { ...oldDrafts };
@@ -144,11 +150,12 @@ export const useGridStore = create<GridState>()(
},
batchSetDraftGamme: (newChanges) => {
const { rows, draftChanges: oldDrafts } = get();
const { rows, rowsIndex, draftChanges: oldDrafts } = get();
const updatedDrafts = { ...oldDrafts };
Object.entries(newChanges).forEach(([codein, gamme]) => {
const originalRow = rows.find(r => r.codein === codein);
// Lookup O(1) via l'index au lieu de O(n) via .find()
const originalRow = rowsIndex.get(codein);
const originalGamme = originalRow?.codeGamme;
if (gamme === originalGamme) {