mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
feat: implement PostgreSQL client for bulk data retrieval to replace per-article HTTP requests and configure Claude permissions
This commit is contained in:
1 parent
868886db87
commit
7e4b370267
6 files changed
+91
-19
No files matched your search
@@ -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`);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user