mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-22 06:28:09 +02:00
Merge pull request #19 from R0m1k3/claude/eloquent-cannon-2mm04o
Afficher les montants au centime dans toute l'application
This commit is contained in:
13 files changed
+58
-55
No files matched your search
@@ -9,7 +9,7 @@ import { Select } from "@/components/ui/form-controls";
|
|||||||
import { DeltaBadge } from "@/components/ui/badge";
|
import { DeltaBadge } from "@/components/ui/badge";
|
||||||
import { Terme } from "@/components/ui/tooltip";
|
import { Terme } from "@/components/ui/tooltip";
|
||||||
import { MAGASINS, nomMagasin } from "@/lib/magasins";
|
import { MAGASINS, nomMagasin } from "@/lib/magasins";
|
||||||
import { fmtEur0 } from "@/lib/format";
|
import { fmtEur2 } from "@/lib/format";
|
||||||
import { telechargerExcel } from "@/lib/export-excel";
|
import { telechargerExcel } from "@/lib/export-excel";
|
||||||
import { cn } from "@/lib/utils";
|
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];
|
const recherche = (r: AnalyticsRow) => [r.label, r.key];
|
||||||
|
|
||||||
function Montant({ v }: { v: number }) {
|
function Montant({ v }: { v: number }) {
|
||||||
return <span className={cn("whitespace-nowrap", v === 0 && "text-[var(--text-muted)]")}>{fmtEur0(v)}</span>;
|
return <span className={cn("whitespace-nowrap", v === 0 && "text-[var(--text-muted)]")}>{fmtEur2(v)}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** En-tête sur deux lignes : magasin au-dessus, mesure en dessous. */
|
/** En-tête sur deux lignes : magasin au-dessus, mesure en dessous. */
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Badge, DeltaBadge } from "@/components/ui/badge";
|
|||||||
import { EmptyState, ErrorState } from "@/components/ui/states";
|
import { EmptyState, ErrorState } from "@/components/ui/states";
|
||||||
import { Terme } from "@/components/ui/tooltip";
|
import { Terme } from "@/components/ui/tooltip";
|
||||||
import { LIBELLE_TOUS_MAGASINS, nomMagasin } from "@/lib/magasins";
|
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";
|
import { couleurMarge } from "@/lib/marge";
|
||||||
|
|
||||||
export const metadata: Metadata = { title: "Accueil" };
|
export const metadata: Metadata = { title: "Accueil" };
|
||||||
@@ -76,8 +76,8 @@ function CarteMagasin({ site }: { site: DashboardSiteStats }) {
|
|||||||
<div className="grid gap-3 p-4 sm:grid-cols-3">
|
<div className="grid gap-3 p-4 sm:grid-cols-3">
|
||||||
<Indicateur
|
<Indicateur
|
||||||
label="Chiffre d'affaires"
|
label="Chiffre d'affaires"
|
||||||
valeur={fmtEur0(site.ca_hier)}
|
valeur={fmtEur2(site.ca_hier)}
|
||||||
valeurN1={site.ca_n1 > 0 ? fmtEur0(site.ca_n1) : null}
|
valeurN1={site.ca_n1 > 0 ? fmtEur2(site.ca_n1) : null}
|
||||||
delta={evolution(site.ca_hier, site.ca_n1)}
|
delta={evolution(site.ca_hier, site.ca_n1)}
|
||||||
/>
|
/>
|
||||||
<Indicateur
|
<Indicateur
|
||||||
@@ -107,8 +107,8 @@ const CRITERES: Record<Critere, { titre: string; icone: typeof Euro }> = {
|
|||||||
|
|
||||||
function valeurPrincipale(item: DashboardTopItem, critere: Critere): string {
|
function valeurPrincipale(item: DashboardTopItem, critere: Critere): string {
|
||||||
if (critere === "qte") return `${fmtEntier(item.qte)} vendus`;
|
if (critere === "qte") return `${fmtEntier(item.qte)} vendus`;
|
||||||
if (critere === "marge") return fmtEur0(item.marge);
|
if (critere === "marge") return fmtEur2(item.marge);
|
||||||
return fmtEur0(item.ca);
|
return fmtEur2(item.ca);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ListeTop10({ items, critere }: { items: DashboardTopItem[]; critere: Critere }) {
|
function ListeTop10({ items, critere }: { items: DashboardTopItem[]; critere: Critere }) {
|
||||||
@@ -150,7 +150,7 @@ function ListeTop10({ items, critere }: { items: DashboardTopItem[]; critere: Cr
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-[var(--text-muted)]">
|
<div className="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-[var(--text-muted)]">
|
||||||
<span className="truncate">{item.fournisseur}</span>
|
<span className="truncate">{item.fournisseur}</span>
|
||||||
{critere !== "ca" && <span>CA {fmtEur0(item.ca)}</span>}
|
{critere !== "ca" && <span>CA {fmtEur2(item.ca)}</span>}
|
||||||
{critere !== "qte" && <span>{fmtEntier(item.qte)} vendus</span>}
|
{critere !== "qte" && <span>{fmtEntier(item.qte)} vendus</span>}
|
||||||
<span
|
<span
|
||||||
style={{ color: item.stock <= 0 ? "var(--accent-error)" : undefined }}
|
style={{ color: item.stock <= 0 ? "var(--accent-error)" : undefined }}
|
||||||
@@ -259,9 +259,9 @@ export default async function DashboardPage() {
|
|||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Chiffre d'affaires"
|
label="Chiffre d'affaires"
|
||||||
value={fmtEur0(totalCa)}
|
value={fmtEur2(totalCa)}
|
||||||
delta={evolution(totalCa, totalCaN1)}
|
delta={evolution(totalCa, totalCaN1)}
|
||||||
hint={totalCaN1 > 0 ? `N-1 : ${fmtEur0(totalCaN1)}` : undefined}
|
hint={totalCaN1 > 0 ? `N-1 : ${fmtEur2(totalCaN1)}` : undefined}
|
||||||
icon={Euro}
|
icon={Euro}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { DataTable, type DataColumn, type DataFilter } from "@/components/ui/dat
|
|||||||
import { Input, Label } from "@/components/ui/form-controls";
|
import { Input, Label } from "@/components/ui/form-controls";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { MAGASINS, nomMagasin } from "@/lib/magasins";
|
import { MAGASINS, nomMagasin } from "@/lib/magasins";
|
||||||
import { fmtDecimal1, fmtEntier, fmtEur0 } from "@/lib/format";
|
import { fmtDecimal1, fmtEntier, fmtEur2 } from "@/lib/format";
|
||||||
import { couleurMarge } from "@/lib/marge";
|
import { couleurMarge } from "@/lib/marge";
|
||||||
import { telechargerExcel } from "@/lib/export-excel";
|
import { telechargerExcel } from "@/lib/export-excel";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -99,8 +99,8 @@ function colonnesGroupe(g: Groupe): DataColumn<Ligne>[] {
|
|||||||
hint: `Chiffre d'affaires TTC (${g.nom}) sur la période, retours déduits`,
|
hint: `Chiffre d'affaires TTC (${g.nom}) sur la période, retours déduits`,
|
||||||
align: "right",
|
align: "right",
|
||||||
sortValue: r => r[g.ca],
|
sortValue: r => r[g.ca],
|
||||||
cell: r => (r[g.ca] !== 0 ? fmtEur0(r[g.ca]) : vide),
|
cell: r => (r[g.ca] !== 0 ? fmtEur2(r[g.ca]) : vide),
|
||||||
footer: rows => fmtEur0(somme(rows, g.ca)),
|
footer: rows => fmtEur2(somme(rows, g.ca)),
|
||||||
className: cn("whitespace-nowrap", total && "font-semibold"),
|
className: cn("whitespace-nowrap", total && "font-semibold"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { EmptyState } from "@/components/ui/states";
|
|||||||
import { Terme, Tooltip } from "@/components/ui/tooltip";
|
import { Terme, Tooltip } from "@/components/ui/tooltip";
|
||||||
import { toast } from "@/components/ui/feedback";
|
import { toast } from "@/components/ui/feedback";
|
||||||
import { CODE_TOUS_MAGASINS, LIBELLE_TOUS_MAGASINS, nomMagasin } from "@/lib/magasins";
|
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 { couleurMarge } from "@/lib/marge";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { OpportunitesFamille } from "./opportunites";
|
import { OpportunitesFamille } from "./opportunites";
|
||||||
@@ -129,7 +129,7 @@ const LIGNES_MENSUELLES_RESEAU: Array<{
|
|||||||
{ label: "Quantité vendue", get: (v) => v.qte, fmt: fmtEntier },
|
{ label: "Quantité vendue", get: (v) => v.qte, fmt: fmtEntier },
|
||||||
{ label: "Magasins vendeurs", get: (v) => v.nbMag, 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: "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: "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)} %` },
|
{ 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) : "—"}
|
value={prixMoyenReseau != null ? fmtEur2(prixMoyenReseau) : "—"}
|
||||||
hint="chiffre d'affaires ÷ quantité"
|
hint="chiffre d'affaires ÷ quantité"
|
||||||
/>
|
/>
|
||||||
<StatCard label="Chiffre d'affaires" value={fmtEur0(reseau.caReseau)} hint="de tout le réseau" />
|
<StatCard label="Chiffre d'affaires" value={fmtEur2(reseau.caReseau)} hint="de tout le réseau" />
|
||||||
<StatCard label={<Terme id="caParMagasin" />} value={fmtEur0(reseau.caParMagasinReseau)} />
|
<StatCard label={<Terme id="caParMagasin" />} value={fmtEur2(reseau.caParMagasinReseau)} />
|
||||||
<StatCard
|
<StatCard
|
||||||
label={<Terme id="marge">Taux de marge</Terme>}
|
label={<Terme id="marge">Taux de marge</Terme>}
|
||||||
value={
|
value={
|
||||||
@@ -556,7 +556,7 @@ export function ProduitFicheView({ fiche, backQuery }: { fiche: ProduitFiche; ba
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
||||||
<Field label="Stock disponible"><span className="tabular-nums">{fmtEntier(s.stockdispo)}</span></Field>
|
<Field label="Stock disponible"><span className="tabular-nums">{fmtEntier(s.stockdispo)}</span></Field>
|
||||||
<Field label="Valeur du stock"><span className="tabular-nums">{fmtEur0(s.valstock)}</span></Field>
|
<Field label="Valeur du stock"><span className="tabular-nums">{fmtEur2(s.valstock)}</span></Field>
|
||||||
<Field label={<Terme id="prmp" />}><span className="tabular-nums">{s.prmp > 0 ? fmtEur2(s.prmp) : "—"}</span></Field>
|
<Field label={<Terme id="prmp" />}><span className="tabular-nums">{s.prmp > 0 ? fmtEur2(s.prmp) : "—"}</span></Field>
|
||||||
<Field label="Dernière vente">{formatDate(s.dernierevente)}</Field>
|
<Field label="Dernière vente">{formatDate(s.dernierevente)}</Field>
|
||||||
<Field label="Dernière réception">{formatDate(s.dernierereception)}</Field>
|
<Field label="Dernière réception">{formatDate(s.dernierereception)}</Field>
|
||||||
@@ -587,8 +587,8 @@ export function ProduitFicheView({ fiche, backQuery }: { fiche: ProduitFiche; ba
|
|||||||
>
|
>
|
||||||
<div className="mb-5 grid grid-cols-2 gap-3 lg:grid-cols-4">
|
<div className="mb-5 grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||||
<StatCard label="Quantité vendue" value={fmtEntier(totauxAffiches.qte)} />
|
<StatCard label="Quantité vendue" value={fmtEntier(totauxAffiches.qte)} />
|
||||||
<StatCard label="Chiffre d'affaires TTC" value={fmtEur0(totauxAffiches.ca)} />
|
<StatCard label="Chiffre d'affaires TTC" value={fmtEur2(totauxAffiches.ca)} />
|
||||||
<StatCard label={<Terme id="marge" />} value={fmtEur0(totauxAffiches.marge)} />
|
<StatCard label={<Terme id="marge" />} value={fmtEur2(totauxAffiches.marge)} />
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Taux de marge"
|
label="Taux de marge"
|
||||||
value={
|
value={
|
||||||
@@ -626,7 +626,7 @@ export function ProduitFicheView({ fiche, backQuery }: { fiche: ProduitFiche; ba
|
|||||||
<th scope="row" className={TH_LIGNE}>Chiffre d'affaires TTC (€)</th>
|
<th scope="row" className={TH_LIGNE}>Chiffre d'affaires TTC (€)</th>
|
||||||
{months.map((m) => (
|
{months.map((m) => (
|
||||||
<td key={m} className={TD}>
|
<td key={m} className={TD}>
|
||||||
{serie[m]?.ca ? fmtEntier(serie[m].ca) : <span className="text-[var(--text-muted)]">—</span>}
|
{serie[m]?.ca ? fmtDecimal2(serie[m].ca) : <span className="text-[var(--text-muted)]">—</span>}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -690,8 +690,8 @@ export function ProduitFicheView({ fiche, backQuery }: { fiche: ProduitFiche; ba
|
|||||||
<div className="mt-4 grid grid-cols-1 gap-3 md:grid-cols-2">
|
<div className="mt-4 grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||||
<Comparaison
|
<Comparaison
|
||||||
titre={<Terme id="caParMagasin">Chiffre d'affaires par magasin</Terme>}
|
titre={<Terme id="caParMagasin">Chiffre d'affaires par magasin</Terme>}
|
||||||
nous={fmtEur0(comparatif.caLocalParMagasin)}
|
nous={fmtEur2(comparatif.caLocalParMagasin)}
|
||||||
reseau={comparatif.caReseauParMagasin != null ? fmtEur0(comparatif.caReseauParMagasin) : "—"}
|
reseau={comparatif.caReseauParMagasin != null ? fmtEur2(comparatif.caReseauParMagasin) : "—"}
|
||||||
/>
|
/>
|
||||||
<Comparaison
|
<Comparaison
|
||||||
titre={<Terme id="marge">Taux de marge</Terme>}
|
titre={<Terme id="marge">Taux de marge</Terme>}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { DataTable, type DataColumn } from "@/components/ui/data-table";
|
|||||||
import { EmptyState, ErrorState } from "@/components/ui/states";
|
import { EmptyState, ErrorState } from "@/components/ui/states";
|
||||||
import { Terme } from "@/components/ui/tooltip";
|
import { Terme } from "@/components/ui/tooltip";
|
||||||
import { GLOSSAIRE } from "@/lib/glossaire";
|
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";
|
import { couleurMarge } from "@/lib/marge";
|
||||||
|
|
||||||
/** Intervalle d'interrogation de l'état du job de recherche. */
|
/** 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,
|
hint: GLOSSAIRE.caParMagasin.definition,
|
||||||
align: "right",
|
align: "right",
|
||||||
sortValue: (r) => (r.caParMagasinReseau > 0 ? r.caParMagasinReseau : null),
|
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",
|
id: "marge",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { Pagination } from "@/components/ui/pagination";
|
|||||||
import { ErrorState } from "@/components/ui/states";
|
import { ErrorState } from "@/components/ui/states";
|
||||||
import { Tabs, useUrlTab } from "@/components/ui/tabs";
|
import { Tabs, useUrlTab } from "@/components/ui/tabs";
|
||||||
import { Terme, InfoBulle } from "@/components/ui/tooltip";
|
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 { nomMagasin } from "@/lib/magasins";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
@@ -205,8 +205,8 @@ export function PublicitesClient({
|
|||||||
header: <InfoBulle explication={EXPLICATION_CA_PUB}>CA pub période</InfoBulle>,
|
header: <InfoBulle explication={EXPLICATION_CA_PUB}>CA pub période</InfoBulle>,
|
||||||
align: "right",
|
align: "right",
|
||||||
sortValue: (p) => nombre(p.ca_pub_periode_pub),
|
sortValue: (p) => nombre(p.ca_pub_periode_pub),
|
||||||
cell: (p) => <span className="font-semibold">{fmtEur0(nombre(p.ca_pub_periode_pub))}</span>,
|
cell: (p) => <span className="font-semibold">{fmtEur2(nombre(p.ca_pub_periode_pub))}</span>,
|
||||||
footer: (rows) => fmtEur0(somme(rows, (r) => r.ca_pub_periode_pub)),
|
footer: (rows) => fmtEur2(somme(rows, (r) => r.ca_pub_periode_pub)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "partCa",
|
id: "partCa",
|
||||||
@@ -297,8 +297,8 @@ export function PublicitesClient({
|
|||||||
hint: "Chiffre d'affaires de l'opération, tous magasins confondus.",
|
hint: "Chiffre d'affaires de l'opération, tous magasins confondus.",
|
||||||
align: "right",
|
align: "right",
|
||||||
sortValue: (h) => nombre(h.ca_total),
|
sortValue: (h) => nombre(h.ca_total),
|
||||||
cell: (h) => <span className="font-semibold">{fmtEur0(nombre(h.ca_total))}</span>,
|
cell: (h) => <span className="font-semibold">{fmtEur2(nombre(h.ca_total))}</span>,
|
||||||
footer: (rows) => fmtEur0(somme(rows, (r) => r.ca_total)),
|
footer: (rows) => fmtEur2(somme(rows, (r) => r.ca_total)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "quantite",
|
id: "quantite",
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ const PROFONDEURS = [100, 200, 500] as const;
|
|||||||
export const PROFONDEUR_MAX = PROFONDEURS[PROFONDEURS.length - 1];
|
export const PROFONDEUR_MAX = PROFONDEURS[PROFONDEURS.length - 1];
|
||||||
|
|
||||||
const nb = (v: number) => Math.round(v).toLocaleString("fr-FR");
|
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 }) + " €";
|
const eur2 = (v: number) => v.toLocaleString("fr-FR", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + " €";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,18 +50,18 @@ interface Critere {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CRITERES: Record<string, Critere> = {
|
const CRITERES: Record<string, Critere> = {
|
||||||
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" },
|
qteReseau: { label: "Qté réseau", format: nb, xlsx: "#,##0" },
|
||||||
nbMagasinsReseau: { label: "Magasins 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 %" },
|
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 "€"' },
|
prixMoyenReseau: { label: "PV moyen réseau", format: eur2, xlsx: '#,##0.00 "€"' },
|
||||||
prixVente: { label: "PV magasin", 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)} %`,
|
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) },
|
xlsx: '0.0 "%"', brut: (v) => (Math.abs(v) <= 1 ? v * 100 : v) },
|
||||||
totalQuantite: { label: "Qté 12 m", format: nb, xlsx: "#,##0" },
|
totalQuantite: { label: "Qté 12 m", format: nb, xlsx: "#,##0" },
|
||||||
totalCa: { label: "CA 12 m", format: eur0, xlsx: '#,##0 "€"' },
|
totalCa: { label: "CA 12 m", format: eur2, xlsx: '#,##0.00 "€"' },
|
||||||
totalMarge: { label: "Marge 12 m", format: eur0, xlsx: '#,##0 "€"' },
|
totalMarge: { label: "Marge 12 m", format: eur2, xlsx: '#,##0.00 "€"' },
|
||||||
};
|
};
|
||||||
|
|
||||||
function libelleCritere(id: string | null): string {
|
function libelleCritere(id: string | null): string {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { confirmer, toast } from "@/components/ui/feedback";
|
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 { couleurMarge } from "@/lib/marge";
|
||||||
import {
|
import {
|
||||||
exporterFichierGammes,
|
exporterFichierGammes,
|
||||||
@@ -165,10 +165,10 @@ function FloatingSummaryBarInner({ isAdmin, nomFournisseur }: { isAdmin: boolean
|
|||||||
<div className="flex flex-wrap items-center gap-x-8 gap-y-1">
|
<div className="flex flex-wrap items-center gap-x-8 gap-y-1">
|
||||||
<Stat label="Produits actifs" value={fmtEntier(summary.totalRows)} sub={summary.totalProducts !== summary.totalRows ? `sur ${fmtEntier(summary.totalProducts)}` : undefined} subColor="var(--text-muted)" />
|
<Stat label="Produits actifs" value={fmtEntier(summary.totalRows)} sub={summary.totalProducts !== summary.totalRows ? `sur ${fmtEntier(summary.totalProducts)}` : undefined} subColor="var(--text-muted)" />
|
||||||
<Stat label="Quantités vendues (12 mois)" value={fmtEntier(summary.totalQuantite)} />
|
<Stat label="Quantités vendues (12 mois)" value={fmtEntier(summary.totalQuantite)} />
|
||||||
<Stat label="Chiffre d'affaires" value={fmtEur0(summary.totalCa)} />
|
<Stat label="Chiffre d'affaires" value={fmtEur2(summary.totalCa)} />
|
||||||
<Stat
|
<Stat
|
||||||
label="Marge"
|
label="Marge"
|
||||||
value={fmtEur0(summary.totalMarge)}
|
value={fmtEur2(summary.totalMarge)}
|
||||||
sub={`${fmtDecimal1(summary.tauxMargeGlobal)} %`}
|
sub={`${fmtDecimal1(summary.tauxMargeGlobal)} %`}
|
||||||
subColor={couleurMarge(summary.tauxMargeGlobal)}
|
subColor={couleurMarge(summary.tauxMargeGlobal)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import {
|
|||||||
} from "@/features/grid/lib/network-trend";
|
} from "@/features/grid/lib/network-trend";
|
||||||
import { TrendSparkline } from "@/features/grid/components/network-charts";
|
import { TrendSparkline } from "@/features/grid/components/network-charts";
|
||||||
import type { NetworkTrend } from "@/features/grid/lib/network-trend";
|
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 { CLASSES_SANS_GAMME, trouverGamme } from "@/lib/gammes";
|
||||||
import { couleurMarge } from "@/lib/marge";
|
import { couleurMarge } from "@/lib/marge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -891,7 +891,7 @@ function HeatmapGridInner({ codeFournisseur, onSelectionChange, isAdmin, nomFour
|
|||||||
const val = row.original.caReseau;
|
const val = row.original.caReseau;
|
||||||
return (
|
return (
|
||||||
<div className="text-center tabular-nums text-[12px] font-bold text-emerald-600 dark:text-emerald-400">
|
<div className="text-center tabular-nums text-[12px] font-bold text-emerald-600 dark:text-emerald-400">
|
||||||
{val != null ? fmtEur0(val) : "-"}
|
{val != null ? fmtEur2(val) : "-"}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1020,7 +1020,7 @@ function HeatmapGridInner({ codeFournisseur, onSelectionChange, isAdmin, nomFour
|
|||||||
const val = row.original.caParMagasinReseau;
|
const val = row.original.caParMagasinReseau;
|
||||||
return (
|
return (
|
||||||
<div className="text-center tabular-nums text-[12px] font-bold" style={{ color: "var(--text-secondary)" }}>
|
<div className="text-center tabular-nums text-[12px] font-bold" style={{ color: "var(--text-secondary)" }}>
|
||||||
{val != null ? fmtEur0(val) : "-"}
|
{val != null ? fmtEur2(val) : "-"}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1110,14 +1110,14 @@ function HeatmapGridInner({ codeFournisseur, onSelectionChange, isAdmin, nomFour
|
|||||||
id: "totalCa",
|
id: "totalCa",
|
||||||
accessorFn: (row: ProductRow) => activeMagasin === "TOTAL" ? row.totalCa : (row.caByStore?.[activeMagasin] ?? 0),
|
accessorFn: (row: ProductRow) => activeMagasin === "TOTAL" ? row.totalCa : (row.caByStore?.[activeMagasin] ?? 0),
|
||||||
header: () => <div className="text-center w-full" title="Chiffre d'affaires sur 12 mois">CA</div>,
|
header: () => <div className="text-center w-full" title="Chiffre d'affaires sur 12 mois">CA</div>,
|
||||||
size: 90,
|
size: 105,
|
||||||
cell: ({ row }: { row: { original: ProductRow } }) => {
|
cell: ({ row }: { row: { original: ProductRow } }) => {
|
||||||
const ca = activeMagasin === "TOTAL"
|
const ca = activeMagasin === "TOTAL"
|
||||||
? row.original.totalCa
|
? row.original.totalCa
|
||||||
: (row.original.caByStore?.[activeMagasin] ?? 0);
|
: (row.original.caByStore?.[activeMagasin] ?? 0);
|
||||||
return (
|
return (
|
||||||
<div className="text-center tabular-nums text-[13px] font-bold" style={{ color: "var(--text-primary)" }}>
|
<div className="text-center tabular-nums text-[13px] font-bold" style={{ color: "var(--text-primary)" }}>
|
||||||
{fmtEntier(ca)} €
|
{fmtEur2(ca)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1126,7 +1126,7 @@ function HeatmapGridInner({ codeFournisseur, onSelectionChange, isAdmin, nomFour
|
|||||||
id: "totalMarge",
|
id: "totalMarge",
|
||||||
accessorFn: (row: ProductRow) => activeMagasin === "TOTAL" ? row.totalMarge : (row.margeByStore?.[activeMagasin] ?? 0),
|
accessorFn: (row: ProductRow) => activeMagasin === "TOTAL" ? row.totalMarge : (row.margeByStore?.[activeMagasin] ?? 0),
|
||||||
header: () => <div className="text-center w-full" title="Marge en euros et taux de marge sur 12 mois">Marge</div>,
|
header: () => <div className="text-center w-full" title="Marge en euros et taux de marge sur 12 mois">Marge</div>,
|
||||||
size: 110,
|
size: 115,
|
||||||
cell: ({ row }: { row: { original: ProductRow } }) => {
|
cell: ({ row }: { row: { original: ProductRow } }) => {
|
||||||
const marge = activeMagasin === "TOTAL"
|
const marge = activeMagasin === "TOTAL"
|
||||||
? row.original.totalMarge
|
? row.original.totalMarge
|
||||||
@@ -1138,7 +1138,7 @@ function HeatmapGridInner({ codeFournisseur, onSelectionChange, isAdmin, nomFour
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
<span className="tabular-nums text-[13px] font-bold" style={{ color: "var(--text-primary)" }}>
|
<span className="tabular-nums text-[13px] font-bold" style={{ color: "var(--text-primary)" }}>
|
||||||
{fmtEntier(marge)} €
|
{fmtEur2(marge)}
|
||||||
</span>
|
</span>
|
||||||
<span className="tabular-nums text-xs font-semibold" style={{ color: couleurMarge(taux) }}>
|
<span className="tabular-nums text-xs font-semibold" style={{ color: couleurMarge(taux) }}>
|
||||||
{taux.toFixed(1)}%
|
{taux.toFixed(1)}%
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
} from "@/features/grid/lib/network-trend";
|
} from "@/features/grid/lib/network-trend";
|
||||||
import { formatDate, formatMonthLabel, qlikMonthToFf } from "@/features/grid/lib/months";
|
import { formatDate, formatMonthLabel, qlikMonthToFf } from "@/features/grid/lib/months";
|
||||||
import { MAGASINS, LIBELLE_TOUS_MAGASINS } from "@/lib/magasins";
|
import { MAGASINS, LIBELLE_TOUS_MAGASINS } from "@/lib/magasins";
|
||||||
import { fmtEur0 } from "@/lib/format";
|
import { fmtEur2 } from "@/lib/format";
|
||||||
import type { ProductRow } from "@/types/grid";
|
import type { ProductRow } from "@/types/grid";
|
||||||
|
|
||||||
const fmt = (v: number, d = 0) => v.toLocaleString("fr-FR", { minimumFractionDigits: d, maximumFractionDigits: d });
|
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 && (
|
{row.caReseau != null && row.caReseau > 0 && (
|
||||||
<TuileStat
|
<TuileStat
|
||||||
label="CA réseau 12 m"
|
label="CA réseau 12 m"
|
||||||
valeur={fmtEur0(row.caReseau)}
|
valeur={fmtEur2(row.caReseau)}
|
||||||
indice={row.margePctReseau ? `marge ${fmt(row.margePctReseau * 100, 1)} %` : undefined}
|
indice={row.margePctReseau ? `marge ${fmt(row.margePctReseau * 100, 1)} %` : undefined}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ export async function imprimerPdf(nomFournisseur: string): Promise<boolean> {
|
|||||||
doc.setTextColor(100);
|
doc.setTextColor(100);
|
||||||
doc.text(`Export généré le : ${new Date().toLocaleDateString("fr-FR")}`, 14, 30);
|
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 head = [["Code-barres", "Code article", "Réf.", "Libellé", "Magasins réseau", "Qté", "CA", "Marge", "Gamme"]];
|
||||||
const body = rows.map((r) => [
|
const body = rows.map((r) => [
|
||||||
r.gtin || r.codein,
|
r.gtin || r.codein,
|
||||||
@@ -151,8 +152,8 @@ export async function imprimerPdf(nomFournisseur: string): Promise<boolean> {
|
|||||||
r.libelle1 ? r.libelle1.substring(0, 60) + (r.libelle1.length > 60 ? "..." : "") : "",
|
r.libelle1 ? r.libelle1.substring(0, 60) + (r.libelle1.length > 60 ? "..." : "") : "",
|
||||||
r.nbMagasinsReseau != null ? r.nbMagasinsReseau.toString() : "-",
|
r.nbMagasinsReseau != null ? r.nbMagasinsReseau.toString() : "-",
|
||||||
Math.round(r.totalQuantite).toLocaleString("fr-FR"),
|
Math.round(r.totalQuantite).toLocaleString("fr-FR"),
|
||||||
`${Math.round(r.totalCa).toLocaleString("fr-FR")} €`,
|
`${eur(r.totalCa)} €`,
|
||||||
`${Math.round(r.totalMarge).toLocaleString("fr-FR")} €\n(${r.tauxMarge.toFixed(1)}%)`,
|
`${eur(r.totalMarge)} €\n(${r.tauxMarge.toFixed(1)}%)`,
|
||||||
gammeEffective(r, draftChanges) || "-",
|
gammeEffective(r, draftChanges) || "-",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { StoreBadge } from "@/components/ui/badge";
|
import { StoreBadge } from "@/components/ui/badge";
|
||||||
import { EmptyState, ErrorState, Skeleton } from "@/components/ui/states";
|
import { EmptyState, ErrorState, Skeleton } from "@/components/ui/states";
|
||||||
import { confirmer, toast } from "@/components/ui/feedback";
|
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 { couleurMarge } from "@/lib/marge";
|
||||||
import { isStaleServerActionError, STALE_ACTION_MESSAGE } from "@/lib/stale-action";
|
import { isStaleServerActionError, STALE_ACTION_MESSAGE } from "@/lib/stale-action";
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ export function SnapshotList({ type }: SnapshotListProps) {
|
|||||||
<div>
|
<div>
|
||||||
<dt className="text-xs font-medium text-[var(--text-muted)]">Chiffre d'affaires</dt>
|
<dt className="text-xs font-medium text-[var(--text-muted)]">Chiffre d'affaires</dt>
|
||||||
<dd className="text-sm font-semibold tabular-nums text-[var(--text-primary)]">
|
<dd className="text-sm font-semibold tabular-nums text-[var(--text-primary)]">
|
||||||
{fmtEur0(resume.totalCa ?? 0)}
|
{fmtEur2(resume.totalCa ?? 0)}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
{taux != null && Number.isFinite(taux) && (
|
{taux != null && Number.isFinite(taux) && (
|
||||||
|
|||||||
+7
-4
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
const nfEntier = new Intl.NumberFormat("fr-FR");
|
const nfEntier = new Intl.NumberFormat("fr-FR");
|
||||||
const nfDecimal1 = new Intl.NumberFormat("fr-FR", { maximumFractionDigits: 1 });
|
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", {
|
const nfEur2 = new Intl.NumberFormat("fr-FR", {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: "EUR",
|
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`. */
|
/** Nombre avec au plus une décimale : `12,5`. */
|
||||||
export const fmtDecimal1 = (v: number): string => nfDecimal1.format(v);
|
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);
|
export const fmtEur2 = (v: number): string => nfEur2.format(v);
|
||||||
Reference in new issue
Block a user