feat: flag products whose last registered supplier differs from current view

Instead of unreliable preference=1, use artfou1.no_id DESC (most recently
added artfou1 entry) to identify the last supplier for each product.
Shows an amber warning icon in the Désignation column when the last
supplier ≠ current supplier view, with tooltip showing who to attribute
the sales to.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Sonnet 4.6 committed 2026-04-03 11:55:42 +02:00
1 parent 6ef23de317
commit 6b26676836
4 files changed
+61 -31

No files matched your search

@@ -70,6 +70,8 @@ export async function getProductRows(input: GetProductRowsInput): Promise<Produc
codein: art.codein, codein: art.codein,
codeFournisseur: art.codefou ?? codeFournisseur, codeFournisseur: art.codefou ?? codeFournisseur,
nomFournisseur: art.nomfou ?? "", nomFournisseur: art.nomfou ?? "",
fournisseurPrincipalCode: art.codefou_principal ?? undefined,
fournisseurPrincipalNom: art.nomfou_principal ?? undefined,
libelle1: art.libelle1 ?? "", libelle1: art.libelle1 ?? "",
gtin: art.gtin ?? "", gtin: art.gtin ?? "",
reference: art.reference ?? "", reference: art.reference ?? "",
+39 -27
View File
@@ -13,7 +13,7 @@ import {
RowSelectionState, RowSelectionState,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { useVirtualizer } from "@tanstack/react-virtual"; import { useVirtualizer } from "@tanstack/react-virtual";
import { ChevronUp, ChevronDown, ChevronsUpDown, Copy, Check, Store, SlidersHorizontal, ShoppingCart, PackageOpen, Warehouse } from "lucide-react"; import { ChevronUp, ChevronDown, ChevronsUpDown, Copy, Check, Store, SlidersHorizontal, ShoppingCart, PackageOpen, Warehouse, AlertTriangle } from "lucide-react";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
@@ -430,33 +430,45 @@ export function HeatmapGrid({ onSelectionChange, isAdmin }: HeatmapGridProps) {
accessorKey: "libelle1", accessorKey: "libelle1",
header: "Désignation", header: "Désignation",
size: 280, size: 280,
cell: ({ row }) => ( cell: ({ row }) => {
<div className="flex items-center gap-2 overflow-hidden w-full pr-1"> const r = row.original;
<span className="text-[13px] font-bold truncate flex-1" title={row.original.libelle1} style={{ color: "var(--text-primary)" }}> const isDifferentSupplier = r.fournisseurPrincipalCode && r.fournisseurPrincipalCode !== r.codeFournisseur;
{row.original.libelle1} const warnTitle = isDifferentSupplier
</span> ? `Dernier fournisseur enregistré : ${r.fournisseurPrincipalNom ?? r.fournisseurPrincipalCode} (${r.fournisseurPrincipalCode}) — les ventes sont à attribuer à ce fournisseur`
<div className="flex gap-1.5 shrink-0"> : undefined;
{row.original.workingStores.map((magasin) => { return (
const config = getStoreConfig(magasin); <div className="flex items-center gap-2 overflow-hidden w-full pr-1">
return ( {isDifferentSupplier && (
<div <span title={warnTitle} className="shrink-0 flex items-center">
key={magasin} <AlertTriangle className="w-3.5 h-3.5 text-amber-500" />
title={`Travaillé par : ${SITE_LABELS[magasin]?.nom ?? magasin}`} </span>
className="px-1.5 py-0.5 rounded-md flex items-center gap-1 text-[10px] font-black border shadow-sm transition-transform hover:scale-110" )}
style={{ <span className="text-[13px] font-bold truncate flex-1" title={r.libelle1} style={{ color: "var(--text-primary)" }}>
background: config.bg, {r.libelle1}
borderColor: config.border, </span>
color: config.text, <div className="flex gap-1.5 shrink-0">
}} {r.workingStores.map((magasin) => {
> const config = getStoreConfig(magasin);
<Store className="w-2.5 h-2.5" strokeWidth={3} /> return (
<span>{config.label}</span> <div
</div> key={magasin}
); title={`Travaillé par : ${SITE_LABELS[magasin]?.nom ?? magasin}`}
})} className="px-1.5 py-0.5 rounded-md flex items-center gap-1 text-[10px] font-black border shadow-sm transition-transform hover:scale-110"
style={{
background: config.bg,
borderColor: config.border,
color: config.text,
}}
>
<Store className="w-2.5 h-2.5" strokeWidth={3} />
<span>{config.label}</span>
</div>
);
})}
</div>
</div> </div>
</div> );
), },
}, },
{ {
accessorKey: "libelle3", accessorKey: "libelle3",
+17 -4
View File
@@ -42,9 +42,11 @@ export interface PgArticle {
pcb?: number; pcb?: number;
reference?: string; reference?: string;
ean13?: string; ean13?: string;
gtin?: string; // art_gtin.gtin (préférentiel) gtin?: string; // art_gtin.gtin (préférentiel)
pv_central?: number; // article_infosup.prix_vente_mini pv_central?: number; // article_infosup.prix_vente_mini
pa?: number; // cube_pa.pa pa?: number; // cube_pa.pa
codefou_principal?: string; // artfou1.code où preference=1
nomfou_principal?: string; // nom du fournisseur principal
} }
export interface PgMensuelRow { export interface PgMensuelRow {
@@ -117,6 +119,7 @@ export async function pgGetArticlesByFournisseur(codefou: string): Promise<PgArt
// Pas de filtre "actif" : on retourne tous les articles pour ne pas exclure les articles // Pas de filtre "actif" : on retourne tous les articles pour ne pas exclure les articles
// d'une session précédente (snapshot) qui n'ont pas de gamme DB ni de ventes récentes. // d'une session précédente (snapshot) qui n'ont pas de gamme DB ni de ventes récentes.
// Le filtrage gamme-Y sans ventes est fait en aval dans getProductRows (Phase 10). // Le filtrage gamme-Y sans ventes est fait en aval dans getProductRows (Phase 10).
// afpref : fournisseur préférentiel (preference=1) de l'article — peut différer du fournisseur consulté.
const result = await pgNoParallel(sql` const result = await pgNoParallel(sql`
SELECT DISTINCT ON (a.no_id) SELECT DISTINCT ON (a.no_id)
a.no_id, a.no_id,
@@ -129,13 +132,23 @@ export async function pgGetArticlesByFournisseur(codefou: string): Promise<PgArt
af.ean13, af.ean13,
COALESCE(ag.gtin, af.ean13) AS gtin, COALESCE(ag.gtin, af.ean13) AS gtin,
ai.prix_vente_mini AS pv_central, ai.prix_vente_mini AS pv_central,
pa.pa pa.pa,
last_fournisseur.codefou_dernier AS codefou_principal,
last_fournisseur.nomfou_dernier AS nomfou_principal
FROM artfou1 af FROM artfou1 af
JOIN articles a ON a.no_id = af.art_no_id JOIN articles a ON a.no_id = af.art_no_id
LEFT JOIN fouident fi ON fi.code = af.code LEFT JOIN fouident fi ON fi.code = af.code
LEFT JOIN article_infosup ai ON ai.artnoid = a.no_id LEFT JOIN article_infosup ai ON ai.artnoid = a.no_id
LEFT JOIN cube_pa pa ON pa.artnoid = a.no_id LEFT JOIN cube_pa pa ON pa.artnoid = a.no_id
LEFT JOIN art_gtin ag ON ag.idarticle = a.no_id AND ag.preferentiel = 1 LEFT JOIN art_gtin ag ON ag.idarticle = a.no_id AND ag.preferentiel = 1
LEFT JOIN LATERAL (
SELECT af2.code AS codefou_dernier, fi2.nom AS nomfou_dernier
FROM artfou1 af2
LEFT JOIN fouident fi2 ON fi2.code = af2.code
WHERE af2.art_no_id = a.no_id
ORDER BY af2.no_id DESC
LIMIT 1
) last_fournisseur ON true
WHERE af.code = ${codefou} WHERE af.code = ${codefou}
AND a.codein IS NOT NULL AND a.codein IS NOT NULL
ORDER BY a.no_id, af.no_id ORDER BY a.no_id, af.no_id
+3
View File
@@ -33,6 +33,9 @@ export interface ProductRow {
codein: string; codein: string;
codeFournisseur: string; codeFournisseur: string;
nomFournisseur: string; nomFournisseur: string;
/** Dernier fournisseur enregistré pour cet article (artfou1.no_id DESC). Peut différer du fournisseur consulté. */
fournisseurPrincipalCode?: string;
fournisseurPrincipalNom?: string;
libelle1: string; libelle1: string;
gtin: string; gtin: string;
reference: string; reference: string;