From 4a11bdc3b0ee783cb02d1ef9506bae3fcaad7120 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 07:50:59 +0000 Subject: [PATCH] Afficher les montants au centime dans toute l'application MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les CA, marges et valeurs de stock étaient arrondis à l'euro (fmtEur0, fmtEntier suivi de « € », Math.round dans les exports). Tous les montants sont désormais affichés avec deux décimales : accueil, grille, fiche produit, hit-parade, publicités, analytics, snapshots, export PDF et export Excel des manques d'assortiment. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VEXT61tGbsXHUDNBeghCXQ --- src/app/(dashboard)/analytics/client.tsx | 4 ++-- src/app/(dashboard)/dashboard/page.tsx | 16 +++++++-------- src/app/(dashboard)/hit-parade/client.tsx | 6 +++--- src/app/(dashboard)/produits/fiche.tsx | 20 +++++++++---------- src/app/(dashboard)/produits/results.tsx | 4 ++-- src/app/(dashboard)/publicites/client.tsx | 10 +++++----- .../grid/components/assortment-gaps-modal.tsx | 9 ++++----- .../grid/components/floating-summary-bar.tsx | 6 +++--- src/features/grid/components/heatmap-grid.tsx | 14 ++++++------- .../grid/components/network-monthly-modal.tsx | 4 ++-- src/features/grid/lib/exports.ts | 5 +++-- .../snapshots/components/snapshot-list.tsx | 4 ++-- src/lib/format.ts | 11 ++++++---- 13 files changed, 58 insertions(+), 55 deletions(-) diff --git a/src/app/(dashboard)/analytics/client.tsx b/src/app/(dashboard)/analytics/client.tsx index 4291bf9..6dfd35e 100644 --- a/src/app/(dashboard)/analytics/client.tsx +++ b/src/app/(dashboard)/analytics/client.tsx @@ -9,7 +9,7 @@ import { Select } from "@/components/ui/form-controls"; import { DeltaBadge } from "@/components/ui/badge"; import { Terme } from "@/components/ui/tooltip"; import { MAGASINS, nomMagasin } from "@/lib/magasins"; -import { fmtEur0 } from "@/lib/format"; +import { fmtEur2 } from "@/lib/format"; import { telechargerExcel } from "@/lib/export-excel"; import { cn } from "@/lib/utils"; @@ -81,7 +81,7 @@ const evolution = (ca: number, caN1: number) => (caN1 > 0 ? ((ca - caN1) / caN1) const recherche = (r: AnalyticsRow) => [r.label, r.key]; function Montant({ v }: { v: number }) { - return {fmtEur0(v)}; + return {fmtEur2(v)}; } /** En-tête sur deux lignes : magasin au-dessus, mesure en dessous. */ diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx index 557ae24..75e21a9 100644 --- a/src/app/(dashboard)/dashboard/page.tsx +++ b/src/app/(dashboard)/dashboard/page.tsx @@ -10,7 +10,7 @@ import { Badge, DeltaBadge } from "@/components/ui/badge"; import { EmptyState, ErrorState } from "@/components/ui/states"; import { Terme } from "@/components/ui/tooltip"; import { LIBELLE_TOUS_MAGASINS, nomMagasin } from "@/lib/magasins"; -import { fmtDecimal1, fmtEntier, fmtEur0, fmtEur2 } from "@/lib/format"; +import { fmtDecimal1, fmtEntier, fmtEur2 } from "@/lib/format"; import { couleurMarge } from "@/lib/marge"; export const metadata: Metadata = { title: "Accueil" }; @@ -76,8 +76,8 @@ function CarteMagasin({ site }: { site: DashboardSiteStats }) {
0 ? fmtEur0(site.ca_n1) : null} + valeur={fmtEur2(site.ca_hier)} + valeurN1={site.ca_n1 > 0 ? fmtEur2(site.ca_n1) : null} delta={evolution(site.ca_hier, site.ca_n1)} /> = { function valeurPrincipale(item: DashboardTopItem, critere: Critere): string { if (critere === "qte") return `${fmtEntier(item.qte)} vendus`; - if (critere === "marge") return fmtEur0(item.marge); - return fmtEur0(item.ca); + if (critere === "marge") return fmtEur2(item.marge); + return fmtEur2(item.ca); } function ListeTop10({ items, critere }: { items: DashboardTopItem[]; critere: Critere }) { @@ -150,7 +150,7 @@ function ListeTop10({ items, critere }: { items: DashboardTopItem[]; critere: Cr
{item.fournisseur} - {critere !== "ca" && CA {fmtEur0(item.ca)}} + {critere !== "ca" && CA {fmtEur2(item.ca)}} {critere !== "qte" && {fmtEntier(item.qte)} vendus} 0 ? `N-1 : ${fmtEur0(totalCaN1)}` : undefined} + hint={totalCaN1 > 0 ? `N-1 : ${fmtEur2(totalCaN1)}` : undefined} icon={Euro} /> [] { hint: `Chiffre d'affaires TTC (${g.nom}) sur la période, retours déduits`, align: "right", sortValue: r => r[g.ca], - cell: r => (r[g.ca] !== 0 ? fmtEur0(r[g.ca]) : vide), - footer: rows => fmtEur0(somme(rows, g.ca)), + cell: r => (r[g.ca] !== 0 ? fmtEur2(r[g.ca]) : vide), + footer: rows => fmtEur2(somme(rows, g.ca)), className: cn("whitespace-nowrap", total && "font-semibold"), }, { diff --git a/src/app/(dashboard)/produits/fiche.tsx b/src/app/(dashboard)/produits/fiche.tsx index dc3107a..65ba6b2 100644 --- a/src/app/(dashboard)/produits/fiche.tsx +++ b/src/app/(dashboard)/produits/fiche.tsx @@ -26,7 +26,7 @@ import { EmptyState } from "@/components/ui/states"; import { Terme, Tooltip } from "@/components/ui/tooltip"; import { toast } from "@/components/ui/feedback"; import { CODE_TOUS_MAGASINS, LIBELLE_TOUS_MAGASINS, nomMagasin } from "@/lib/magasins"; -import { fmtDecimal1, fmtEntier, fmtEur0, fmtEur2 } from "@/lib/format"; +import { fmtDecimal1, fmtDecimal2, fmtEntier, fmtEur2 } from "@/lib/format"; import { couleurMarge } from "@/lib/marge"; import { cn } from "@/lib/utils"; import { OpportunitesFamille } from "./opportunites"; @@ -129,7 +129,7 @@ const LIGNES_MENSUELLES_RESEAU: Array<{ { label: "Quantité vendue", get: (v) => v.qte, fmt: fmtEntier }, { label: "Magasins vendeurs", get: (v) => v.nbMag, fmt: fmtEntier }, { label: "Quantité par magasin", get: (v) => (v.nbMag && v.nbMag > 0 ? v.qte / v.nbMag : undefined), fmt: fmtDecimal1 }, - { label: "Chiffre d'affaires (€)", get: (v) => v.ca, fmt: fmtEntier }, + { label: "Chiffre d'affaires (€)", get: (v) => v.ca, fmt: fmtDecimal2 }, { label: "Prix moyen", get: (v) => (v.ca != null && v.qte > 0 ? v.ca / v.qte : undefined), fmt: fmtEur2 }, { label: "Taux de marge", get: (v) => normalizeMargePct(v.margePct) ?? undefined, fmt: (n) => `${fmtDecimal1(n)} %` }, ]; @@ -327,8 +327,8 @@ export function ProduitFicheView({ fiche, backQuery }: { fiche: ProduitFiche; ba value={prixMoyenReseau != null ? fmtEur2(prixMoyenReseau) : "—"} hint="chiffre d'affaires ÷ quantité" /> - - } value={fmtEur0(reseau.caParMagasinReseau)} /> + + } value={fmtEur2(reseau.caParMagasinReseau)} /> Taux de marge} value={ @@ -556,7 +556,7 @@ export function ProduitFicheView({ fiche, backQuery }: { fiche: ProduitFiche; ba
{fmtEntier(s.stockdispo)} - {fmtEur0(s.valstock)} + {fmtEur2(s.valstock)} }>{s.prmp > 0 ? fmtEur2(s.prmp) : "—"} {formatDate(s.dernierevente)} {formatDate(s.dernierereception)} @@ -587,8 +587,8 @@ export function ProduitFicheView({ fiche, backQuery }: { fiche: ProduitFiche; ba >
- - } value={fmtEur0(totauxAffiches.marge)} /> + + } value={fmtEur2(totauxAffiches.marge)} /> Chiffre d'affaires TTC (€) {months.map((m) => ( - {serie[m]?.ca ? fmtEntier(serie[m].ca) : —} + {serie[m]?.ca ? fmtDecimal2(serie[m].ca) : —} ))} @@ -690,8 +690,8 @@ export function ProduitFicheView({ fiche, backQuery }: { fiche: ProduitFiche; ba
Chiffre d'affaires par magasin} - nous={fmtEur0(comparatif.caLocalParMagasin)} - reseau={comparatif.caReseauParMagasin != null ? fmtEur0(comparatif.caReseauParMagasin) : "—"} + nous={fmtEur2(comparatif.caLocalParMagasin)} + reseau={comparatif.caReseauParMagasin != null ? fmtEur2(comparatif.caReseauParMagasin) : "—"} /> Taux de marge} diff --git a/src/app/(dashboard)/produits/results.tsx b/src/app/(dashboard)/produits/results.tsx index 0e65558..ed8d4fe 100644 --- a/src/app/(dashboard)/produits/results.tsx +++ b/src/app/(dashboard)/produits/results.tsx @@ -12,7 +12,7 @@ import { DataTable, type DataColumn } from "@/components/ui/data-table"; import { EmptyState, ErrorState } from "@/components/ui/states"; import { Terme } from "@/components/ui/tooltip"; import { GLOSSAIRE } from "@/lib/glossaire"; -import { fmtDecimal1, fmtEntier, fmtEur0, fmtEur2 } from "@/lib/format"; +import { fmtDecimal1, fmtEntier, fmtEur2 } from "@/lib/format"; import { couleurMarge } from "@/lib/marge"; /** Intervalle d'interrogation de l'état du job de recherche. */ @@ -401,7 +401,7 @@ function ResultTable({ rows, query }: { rows: ProduitRechercheRow[]; query: stri hint: GLOSSAIRE.caParMagasin.definition, align: "right", sortValue: (r) => (r.caParMagasinReseau > 0 ? r.caParMagasinReseau : null), - cell: (r) => (r.caParMagasinReseau > 0 ? fmtEur0(r.caParMagasinReseau) : "—"), + cell: (r) => (r.caParMagasinReseau > 0 ? fmtEur2(r.caParMagasinReseau) : "—"), }, { id: "marge", diff --git a/src/app/(dashboard)/publicites/client.tsx b/src/app/(dashboard)/publicites/client.tsx index 4d842d4..a15f7f8 100644 --- a/src/app/(dashboard)/publicites/client.tsx +++ b/src/app/(dashboard)/publicites/client.tsx @@ -12,7 +12,7 @@ import { Pagination } from "@/components/ui/pagination"; import { ErrorState } from "@/components/ui/states"; import { Tabs, useUrlTab } from "@/components/ui/tabs"; import { Terme, InfoBulle } from "@/components/ui/tooltip"; -import { fmtDecimal1, fmtEntier, fmtEur0 } from "@/lib/format"; +import { fmtDecimal1, fmtEntier, fmtEur2 } from "@/lib/format"; import { nomMagasin } from "@/lib/magasins"; import { cn } from "@/lib/utils"; @@ -205,8 +205,8 @@ export function PublicitesClient({ header: CA pub période, align: "right", sortValue: (p) => nombre(p.ca_pub_periode_pub), - cell: (p) => {fmtEur0(nombre(p.ca_pub_periode_pub))}, - footer: (rows) => fmtEur0(somme(rows, (r) => r.ca_pub_periode_pub)), + cell: (p) => {fmtEur2(nombre(p.ca_pub_periode_pub))}, + footer: (rows) => fmtEur2(somme(rows, (r) => r.ca_pub_periode_pub)), }, { id: "partCa", @@ -297,8 +297,8 @@ export function PublicitesClient({ hint: "Chiffre d'affaires de l'opération, tous magasins confondus.", align: "right", sortValue: (h) => nombre(h.ca_total), - cell: (h) => {fmtEur0(nombre(h.ca_total))}, - footer: (rows) => fmtEur0(somme(rows, (r) => r.ca_total)), + cell: (h) => {fmtEur2(nombre(h.ca_total))}, + footer: (rows) => fmtEur2(somme(rows, (r) => r.ca_total)), }, { id: "quantite", diff --git a/src/features/grid/components/assortment-gaps-modal.tsx b/src/features/grid/components/assortment-gaps-modal.tsx index bf8fbec..d691a23 100644 --- a/src/features/grid/components/assortment-gaps-modal.tsx +++ b/src/features/grid/components/assortment-gaps-modal.tsx @@ -30,7 +30,6 @@ const PROFONDEURS = [100, 200, 500] as const; export const PROFONDEUR_MAX = PROFONDEURS[PROFONDEURS.length - 1]; const nb = (v: number) => Math.round(v).toLocaleString("fr-FR"); -const eur0 = (v: number) => `${Math.round(v).toLocaleString("fr-FR")} €`; const eur2 = (v: number) => v.toLocaleString("fr-FR", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + " €"; /** @@ -51,18 +50,18 @@ interface Critere { } const CRITERES: Record = { - caReseau: { label: "CA réseau", format: eur0, xlsx: '#,##0 "€"' }, + caReseau: { label: "CA réseau", format: eur2, xlsx: '#,##0.00 "€"' }, qteReseau: { label: "Qté réseau", format: nb, xlsx: "#,##0" }, nbMagasinsReseau: { label: "Magasins réseau", format: nb, xlsx: "#,##0" }, tendanceReseau: { label: "Tendance réseau", format: (v) => `${v >= 0 ? "+" : ""}${Math.round(v * 100)} %`, xlsx: "0 %" }, prixMoyenReseau: { label: "PV moyen réseau", format: eur2, xlsx: '#,##0.00 "€"' }, prixVente: { label: "PV magasin", format: eur2, xlsx: '#,##0.00 "€"' }, - caParMagasinReseau: { label: "CA / magasin réseau", format: eur0, xlsx: '#,##0 "€"' }, + caParMagasinReseau: { label: "CA / magasin réseau", format: eur2, xlsx: '#,##0.00 "€"' }, margePctReseau: { label: "Marge % réseau", format: (v) => `${(Math.abs(v) <= 1 ? v * 100 : v).toFixed(1)} %`, xlsx: '0.0 "%"', brut: (v) => (Math.abs(v) <= 1 ? v * 100 : v) }, totalQuantite: { label: "Qté 12 m", format: nb, xlsx: "#,##0" }, - totalCa: { label: "CA 12 m", format: eur0, xlsx: '#,##0 "€"' }, - totalMarge: { label: "Marge 12 m", format: eur0, xlsx: '#,##0 "€"' }, + totalCa: { label: "CA 12 m", format: eur2, xlsx: '#,##0.00 "€"' }, + totalMarge: { label: "Marge 12 m", format: eur2, xlsx: '#,##0.00 "€"' }, }; function libelleCritere(id: string | null): string { diff --git a/src/features/grid/components/floating-summary-bar.tsx b/src/features/grid/components/floating-summary-bar.tsx index 7fb2e5a..aa3b854 100644 --- a/src/features/grid/components/floating-summary-bar.tsx +++ b/src/features/grid/components/floating-summary-bar.tsx @@ -19,7 +19,7 @@ import { } from "@/components/ui/dropdown-menu"; import { Button } from "@/components/ui/button"; import { confirmer, toast } from "@/components/ui/feedback"; -import { fmtDecimal1, fmtEntier, fmtEur0 } from "@/lib/format"; +import { fmtDecimal1, fmtEntier, fmtEur2 } from "@/lib/format"; import { couleurMarge } from "@/lib/marge"; import { exporterFichierGammes, @@ -165,10 +165,10 @@ function FloatingSummaryBarInner({ isAdmin, nomFournisseur }: { isAdmin: boolean
- + diff --git a/src/features/grid/components/heatmap-grid.tsx b/src/features/grid/components/heatmap-grid.tsx index 0b7f0b2..71e76e9 100644 --- a/src/features/grid/components/heatmap-grid.tsx +++ b/src/features/grid/components/heatmap-grid.tsx @@ -45,7 +45,7 @@ import { } from "@/features/grid/lib/network-trend"; import { TrendSparkline } from "@/features/grid/components/network-charts"; import type { NetworkTrend } from "@/features/grid/lib/network-trend"; -import { fmtDecimal1, fmtEntier, fmtEur0, fmtEur2 } from "@/lib/format"; +import { fmtDecimal1, fmtEntier, fmtEur2 } from "@/lib/format"; import { CLASSES_SANS_GAMME, trouverGamme } from "@/lib/gammes"; import { couleurMarge } from "@/lib/marge"; import { Button } from "@/components/ui/button"; @@ -891,7 +891,7 @@ function HeatmapGridInner({ codeFournisseur, onSelectionChange, isAdmin, nomFour const val = row.original.caReseau; return (
- {val != null ? fmtEur0(val) : "-"} + {val != null ? fmtEur2(val) : "-"}
); }, @@ -1020,7 +1020,7 @@ function HeatmapGridInner({ codeFournisseur, onSelectionChange, isAdmin, nomFour const val = row.original.caParMagasinReseau; return (
- {val != null ? fmtEur0(val) : "-"} + {val != null ? fmtEur2(val) : "-"}
); }, @@ -1110,14 +1110,14 @@ function HeatmapGridInner({ codeFournisseur, onSelectionChange, isAdmin, nomFour id: "totalCa", accessorFn: (row: ProductRow) => activeMagasin === "TOTAL" ? row.totalCa : (row.caByStore?.[activeMagasin] ?? 0), header: () =>
CA
, - size: 90, + size: 105, cell: ({ row }: { row: { original: ProductRow } }) => { const ca = activeMagasin === "TOTAL" ? row.original.totalCa : (row.original.caByStore?.[activeMagasin] ?? 0); return (
- {fmtEntier(ca)} € + {fmtEur2(ca)}
); }, @@ -1126,7 +1126,7 @@ function HeatmapGridInner({ codeFournisseur, onSelectionChange, isAdmin, nomFour id: "totalMarge", accessorFn: (row: ProductRow) => activeMagasin === "TOTAL" ? row.totalMarge : (row.margeByStore?.[activeMagasin] ?? 0), header: () =>
Marge
, - size: 110, + size: 115, cell: ({ row }: { row: { original: ProductRow } }) => { const marge = activeMagasin === "TOTAL" ? row.original.totalMarge @@ -1138,7 +1138,7 @@ function HeatmapGridInner({ codeFournisseur, onSelectionChange, isAdmin, nomFour return (
- {fmtEntier(marge)} € + {fmtEur2(marge)} {taux.toFixed(1)}% diff --git a/src/features/grid/components/network-monthly-modal.tsx b/src/features/grid/components/network-monthly-modal.tsx index 8e6b4d7..26d4499 100644 --- a/src/features/grid/components/network-monthly-modal.tsx +++ b/src/features/grid/components/network-monthly-modal.tsx @@ -33,7 +33,7 @@ import { } from "@/features/grid/lib/network-trend"; import { formatDate, formatMonthLabel, qlikMonthToFf } from "@/features/grid/lib/months"; import { MAGASINS, LIBELLE_TOUS_MAGASINS } from "@/lib/magasins"; -import { fmtEur0 } from "@/lib/format"; +import { fmtEur2 } from "@/lib/format"; import type { ProductRow } from "@/types/grid"; const fmt = (v: number, d = 0) => v.toLocaleString("fr-FR", { minimumFractionDigits: d, maximumFractionDigits: d }); @@ -285,7 +285,7 @@ export function NetworkMonthlyModal({ row, trend, mois, onClose }: { {row.caReseau != null && row.caReseau > 0 && ( )} diff --git a/src/features/grid/lib/exports.ts b/src/features/grid/lib/exports.ts index 989eed2..c03542d 100644 --- a/src/features/grid/lib/exports.ts +++ b/src/features/grid/lib/exports.ts @@ -143,6 +143,7 @@ export async function imprimerPdf(nomFournisseur: string): Promise { doc.setTextColor(100); doc.text(`Export généré le : ${new Date().toLocaleDateString("fr-FR")}`, 14, 30); + const eur = (v: number) => v.toLocaleString("fr-FR", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const head = [["Code-barres", "Code article", "Réf.", "Libellé", "Magasins réseau", "Qté", "CA", "Marge", "Gamme"]]; const body = rows.map((r) => [ r.gtin || r.codein, @@ -151,8 +152,8 @@ export async function imprimerPdf(nomFournisseur: string): Promise { r.libelle1 ? r.libelle1.substring(0, 60) + (r.libelle1.length > 60 ? "..." : "") : "", r.nbMagasinsReseau != null ? r.nbMagasinsReseau.toString() : "-", Math.round(r.totalQuantite).toLocaleString("fr-FR"), - `${Math.round(r.totalCa).toLocaleString("fr-FR")} €`, - `${Math.round(r.totalMarge).toLocaleString("fr-FR")} €\n(${r.tauxMarge.toFixed(1)}%)`, + `${eur(r.totalCa)} €`, + `${eur(r.totalMarge)} €\n(${r.tauxMarge.toFixed(1)}%)`, gammeEffective(r, draftChanges) || "-", ]); diff --git a/src/features/snapshots/components/snapshot-list.tsx b/src/features/snapshots/components/snapshot-list.tsx index 63328bb..20aa972 100644 --- a/src/features/snapshots/components/snapshot-list.tsx +++ b/src/features/snapshots/components/snapshot-list.tsx @@ -19,7 +19,7 @@ import { Button } from "@/components/ui/button"; import { StoreBadge } from "@/components/ui/badge"; import { EmptyState, ErrorState, Skeleton } from "@/components/ui/states"; import { confirmer, toast } from "@/components/ui/feedback"; -import { fmtDecimal1, fmtEntier, fmtEur0 } from "@/lib/format"; +import { fmtDecimal1, fmtEntier, fmtEur2 } from "@/lib/format"; import { couleurMarge } from "@/lib/marge"; import { isStaleServerActionError, STALE_ACTION_MESSAGE } from "@/lib/stale-action"; @@ -228,7 +228,7 @@ export function SnapshotList({ type }: SnapshotListProps) {
Chiffre d'affaires
- {fmtEur0(resume.totalCa ?? 0)} + {fmtEur2(resume.totalCa ?? 0)}
{taux != null && Number.isFinite(taux) && ( diff --git a/src/lib/format.ts b/src/lib/format.ts index 32f16c9..3759b3f 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -9,7 +9,7 @@ const nfEntier = new Intl.NumberFormat("fr-FR"); const nfDecimal1 = new Intl.NumberFormat("fr-FR", { maximumFractionDigits: 1 }); -const nfEur0 = new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR", maximumFractionDigits: 0 }); +const nfDecimal2 = new Intl.NumberFormat("fr-FR", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const nfEur2 = new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR", @@ -23,8 +23,11 @@ export const fmtEntier = (v: number): string => nfEntier.format(Math.round(v)); /** Nombre avec au plus une décimale : `12,5`. */ export const fmtDecimal1 = (v: number): string => nfDecimal1.format(v); -/** Montant en euros sans centimes : `12 345 €`. */ -export const fmtEur0 = (v: number): string => nfEur0.format(v); +/** + * Montant au centime, sans symbole (colonnes déjà libellées « (€) ») : `12 345,67`. + * Les montants ne sont jamais arrondis à l'euro dans l'application. + */ +export const fmtDecimal2 = (v: number): string => nfDecimal2.format(v); -/** Montant en euros au centime (prix unitaires) : `3,49 €`. */ +/** Montant en euros au centime (prix, CA, marge) : `12 345,67 €`. */ export const fmtEur2 = (v: number): string => nfEur2.format(v);