diff --git a/src/features/grid/components/floating-summary-bar.tsx b/src/features/grid/components/floating-summary-bar.tsx index a1426e5..448c3ba 100644 --- a/src/features/grid/components/floating-summary-bar.tsx +++ b/src/features/grid/components/floating-summary-bar.tsx @@ -27,6 +27,7 @@ import { exporterGammeA, exporterTousProduits, imprimerPdf, + LIGNES_MAX_PAR_FICHIER_GAMMES, lignesModifiees, } from "@/features/grid/lib/exports"; @@ -42,6 +43,14 @@ function Stat({ label, value, sub, subColor }: { label: string; value: string; s ); } +/** Prévient quand le fichier d'import des gammes a été découpé ; vrai si quelque chose a été exporté. */ +function signalerDecoupage(nbFichiers: number): boolean { + if (nbFichiers > 1) { + toast.info(`Export découpé en ${nbFichiers} fichiers de ${fmtEntier(LIGNES_MAX_PAR_FICHIER_GAMMES)} lignes maximum : intégrez-les un par un.`); + } + return nbFichiers > 0; +} + function messageErreur(err: unknown): string { if (isStaleServerActionError(err)) return STALE_ACTION_MESSAGE; return err instanceof Error ? err.message : String(err); @@ -217,7 +226,7 @@ function FloatingSummaryBarInner({ isAdmin, nomFournisseur }: { isAdmin: boolean const nom = lignesModifiees()[0].nomFournisseur; // Copie dans l'Historique, onglet « Exports » await handleSnapshot(`Export ${nom} — ${new Date().toLocaleString("fr-FR", { dateStyle: "short", timeStyle: "short" })}`, "export"); - return exporterFichierGammes(true); + return exporterFichierGammes(true).then(signalerDecoupage); }, "Aucune gamme n'a été modifiée par rapport à FF.")} > @@ -228,7 +237,7 @@ function FloatingSummaryBarInner({ isAdmin, nomFournisseur }: { isAdmin: boolean lancer("complet", () => exporterFichierGammes(false), "Aucun produit à exporter.")} + onSelect={() => lancer("complet", () => exporterFichierGammes(false).then(signalerDecoupage), "Aucun produit à exporter.")} >
diff --git a/src/features/grid/lib/exports.ts b/src/features/grid/lib/exports.ts index c03542d..d48a1cb 100644 --- a/src/features/grid/lib/exports.ts +++ b/src/features/grid/lib/exports.ts @@ -32,14 +32,24 @@ export function lignesModifiees(): ProductRow[] { } /** - * Fichier d'import des gammes (CODE FOURNISSEUR, GENCOD, GAMME), généré par le - * serveur. `seulementModifiees` : uniquement les gammes changées. - * Renvoie `false` s'il n'y a rien à exporter. + * Nombre maximal de produits par fichier d'import des gammes. Le logiciel de + * mise à jour des gammes refuse les fichiers trop gros (~28 000 lignes) : au-delà, + * l'export est découpé en plusieurs fichiers à intégrer un par un. */ -export async function exporterFichierGammes(seulementModifiees: boolean): Promise { +export const LIGNES_MAX_PAR_FICHIER_GAMMES = 5000; + +const pause = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +/** + * Fichier d'import des gammes (CODE FOURNISSEUR, CODEIN, GAMME), généré par le + * serveur. `seulementModifiees` : uniquement les gammes changées. + * Découpé en plusieurs fichiers de `LIGNES_MAX_PAR_FICHIER_GAMMES` lignes au plus. + * Renvoie le nombre de fichiers téléchargés (0 s'il n'y a rien à exporter). + */ +export async function exporterFichierGammes(seulementModifiees: boolean): Promise { const { rows, draftChanges } = useGridStore.getState(); const lignes = seulementModifiees ? lignesModifiees() : rows; - if (lignes.length === 0) return false; + if (lignes.length === 0) return 0; const nomFournisseur = lignes[0].nomFournisseur; const changes = lignes.map((r) => ({ @@ -49,16 +59,25 @@ export async function exporterFichierGammes(seulementModifiees: boolean): Promis gamme: gammeEffective(r, draftChanges) as string, })); - const res = await fetch("/api/export/modified-gammes", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ nomFournisseur, changes }), - }); - if (!res.ok) throw new Error(`le serveur a répondu ${res.status}`); - const prefixe = seulementModifiees ? "Modifications_Gammes" : "Export_Complet"; - telecharger(await res.blob(), `${prefixe}_${nomFichierSur(nomFournisseur)}_${aujourdhui()}.xlsx`); - return true; + const base = `${prefixe}_${nomFichierSur(nomFournisseur)}_${aujourdhui()}`; + const nbFichiers = Math.ceil(changes.length / LIGNES_MAX_PAR_FICHIER_GAMMES); + + for (let i = 0; i < nbFichiers; i++) { + const paquet = changes.slice(i * LIGNES_MAX_PAR_FICHIER_GAMMES, (i + 1) * LIGNES_MAX_PAR_FICHIER_GAMMES); + const res = await fetch("/api/export/modified-gammes", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ nomFournisseur, changes: paquet }), + }); + if (!res.ok) throw new Error(`le serveur a répondu ${res.status}`); + + const suffixe = nbFichiers > 1 ? `_partie_${i + 1}_sur_${nbFichiers}` : ""; + telecharger(await res.blob(), `${base}${suffixe}.xlsx`); + // Les navigateurs ignorent les téléchargements trop rapprochés + if (i < nbFichiers - 1) await pause(500); + } + return nbFichiers; } /** Tous les produits avec gamme avant/après et chiffres sur 12 mois (Excel, généré par le serveur). */