Export des gammes découpé en fichiers de 5 000 lignes maximum

Le logiciel de mise à jour des gammes refuse les fichiers trop gros
(ex. 28 590 lignes). L'export est désormais découpé en plusieurs
fichiers (_partie_1_sur_6, …) à intégrer un par un.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011jxxh5X1FHMUPFu2S2JpyT
This commit is contained in:
Claude committed 2026-10-11 07:56:43 +00:00
1 parent 60a1d38b6f
commit 1474c0cbb4
2 files changed
+44 -16

No files matched your search

@@ -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.")}
>
<FileSpreadsheet />
@@ -228,7 +237,7 @@ function FloatingSummaryBarInner({ isAdmin, nomFournisseur }: { isAdmin: boolean
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onSelect={() => lancer("complet", () => exporterFichierGammes(false), "Aucun produit à exporter.")}
onSelect={() => lancer("complet", () => exporterFichierGammes(false).then(signalerDecoupage), "Aucun produit à exporter.")}
>
<FileSpreadsheet />
<div className="flex flex-col">
+33 -14
View File
@@ -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<boolean> {
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<number> {
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). */