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,
codeFournisseur: art.codefou ?? codeFournisseur,
nomFournisseur: art.nomfou ?? "",
fournisseurPrincipalCode: art.codefou_principal ?? undefined,
fournisseurPrincipalNom: art.nomfou_principal ?? undefined,
libelle1: art.libelle1 ?? "",
gtin: art.gtin ?? "",
reference: art.reference ?? "",
+39 -27
View File
@@ -13,7 +13,7 @@ import {
RowSelectionState,
} from "@tanstack/react-table";
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 {
DropdownMenu,
DropdownMenuCheckboxItem,
@@ -430,33 +430,45 @@ export function HeatmapGrid({ onSelectionChange, isAdmin }: HeatmapGridProps) {
accessorKey: "libelle1",
header: "Désignation",
size: 280,
cell: ({ row }) => (
<div className="flex items-center gap-2 overflow-hidden w-full pr-1">
<span className="text-[13px] font-bold truncate flex-1" title={row.original.libelle1} style={{ color: "var(--text-primary)" }}>
{row.original.libelle1}
</span>
<div className="flex gap-1.5 shrink-0">
{row.original.workingStores.map((magasin) => {
const config = getStoreConfig(magasin);
return (
<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>
);
})}
cell: ({ row }) => {
const r = row.original;
const isDifferentSupplier = r.fournisseurPrincipalCode && r.fournisseurPrincipalCode !== r.codeFournisseur;
const warnTitle = isDifferentSupplier
? `Dernier fournisseur enregistré : ${r.fournisseurPrincipalNom ?? r.fournisseurPrincipalCode} (${r.fournisseurPrincipalCode}) — les ventes sont à attribuer à ce fournisseur`
: undefined;
return (
<div className="flex items-center gap-2 overflow-hidden w-full pr-1">
{isDifferentSupplier && (
<span title={warnTitle} className="shrink-0 flex items-center">
<AlertTriangle className="w-3.5 h-3.5 text-amber-500" />
</span>
)}
<span className="text-[13px] font-bold truncate flex-1" title={r.libelle1} style={{ color: "var(--text-primary)" }}>
{r.libelle1}
</span>
<div className="flex gap-1.5 shrink-0">
{r.workingStores.map((magasin) => {
const config = getStoreConfig(magasin);
return (
<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>
),
);
},
},
{
accessorKey: "libelle3",
+17 -4
View File
@@ -42,9 +42,11 @@ export interface PgArticle {
pcb?: number;
reference?: string;
ean13?: string;
gtin?: string; // art_gtin.gtin (préférentiel)
pv_central?: number; // article_infosup.prix_vente_mini
pa?: number; // cube_pa.pa
gtin?: string; // art_gtin.gtin (préférentiel)
pv_central?: number; // article_infosup.prix_vente_mini
pa?: number; // cube_pa.pa
codefou_principal?: string; // artfou1.code où preference=1
nomfou_principal?: string; // nom du fournisseur principal
}
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
// 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).
// afpref : fournisseur préférentiel (preference=1) de l'article — peut différer du fournisseur consulté.
const result = await pgNoParallel(sql`
SELECT DISTINCT ON (a.no_id)
a.no_id,
@@ -129,13 +132,23 @@ export async function pgGetArticlesByFournisseur(codefou: string): Promise<PgArt
af.ean13,
COALESCE(ag.gtin, af.ean13) AS gtin,
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
JOIN articles a ON a.no_id = af.art_no_id
LEFT JOIN fouident fi ON fi.code = af.code
LEFT JOIN article_infosup ai ON ai.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 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}
AND a.codein IS NOT NULL
ORDER BY a.no_id, af.no_id
+3
View File
@@ -33,6 +33,9 @@ export interface ProductRow {
codein: string;
codeFournisseur: 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;
gtin: string;
reference: string;