filtered in card

This commit is contained in:
mickael 2026-01-20 21:20:46 +03:00
parent a2421c86ee
commit 1fc87ac59e
5 changed files with 309 additions and 214 deletions

View file

@ -124,11 +124,11 @@ const KPI_CONFIG: KPIConfig[] = [
const previousPeriodAvoirs = getPreviousPeriodItems(allAvoirs, period);
return Number(value) <= previousPeriodAvoirs.length ? 'up' : 'down';
},
filter: (avoirs) => avoirs.filter(a => a.statut === 1),
filter: (avoirs) => avoirs
},
{
id: 'validated',
title: 'Avoirs Validés',
title: 'Avoirs à Facturer',
icon: TrendingDown,
color: 'green',
getValue: (avoirs) => avoirs.filter(a => a.statut === 2).length,

View file

@ -54,9 +54,9 @@ const COLORS = {
const BL_STATUS_LABELS = {
0: { label: 'Saisi', color: COLORS.gray },
1: { label: 'Confirmé', color: COLORS.yellow },
2: { label: 'A préparer', color: COLORS.blue },
3: { label: 'Préparé', color: COLORS.green },
4: { label: 'Soldé', color: COLORS.purple },
2: { label: 'A Facturer', color: COLORS.blue },
// 3: { label: 'Facturé', color: COLORS.green },
// 4: { label: 'Soldé', color: COLORS.purple },
} as const;
export type StatusCode = keyof typeof BL_STATUS_LABELS;
@ -137,14 +137,14 @@ const KPI_CONFIG: KPIConfig[] = [
const previousPeriodBL = getPreviousPeriodItems(allBLs, period);
return Number(value) >= previousPeriodBL.length ? 'up' : 'down';
},
filter: (bls) => bls.filter(b => b.statut === 1),
filter: (bls) => bls,
},
{
id: 'delivered',
title: 'BL Livrés',
title: 'BL à facturer',
icon: CheckCircle,
color: 'green',
getValue: (bls) => bls.filter(b => b.statut === 3 || b.statut === 4).length,
getValue: (bls) => bls.filter(b => b.statut === 2).length,
getSubtitle: (bls) => {
const delivered = bls.filter(b => b.statut === 3 || b.statut === 4);
const deliveredAmount = delivered.reduce((sum, item) => sum + item.total_ht, 0);
@ -161,29 +161,7 @@ const KPI_CONFIG: KPIConfig[] = [
const previousDelivered = previousPeriodBL.filter(b => b.statut === 3 || b.statut === 4);
return Number(value) >= previousDelivered.length ? 'up' : 'down';
},
filter: (bls) => bls.filter(b => b.statut === 3 || b.statut === 4),
},
{
id: 'toInvoice',
title: 'À Facturer',
icon: Clock,
color: 'purple',
getValue: (bls) => bls.filter(b => b.statut === 3).length,
getSubtitle: (bls) => {
const toInvoice = bls.filter(b => b.statut === 3);
const toInvoiceAmount = toInvoice.reduce((sum, item) => sum + item.total_ht, 0);
return `${toInvoiceAmount.toLocaleString('fr-FR', { maximumFractionDigits: 0 })}€`;
},
getChange: (bls) => {
const toInvoice = bls.filter(b => b.statut === 3);
const toInvoiceAmount = toInvoice.reduce((sum, item) => sum + item.total_ht, 0);
return `${toInvoiceAmount.toLocaleString('fr-FR', { maximumFractionDigits: 0 })}€`;
},
getTrend: (bls) => {
const toInvoice = bls.filter(b => b.statut === 3);
return toInvoice.length > 0 ? 'neutral' : 'up';
},
filter: (bls) => bls.filter(b => b.statut === 3),
filter: (bls) => bls.filter(b => b.statut === 2),
},
{
id: 'invoiced',
@ -191,16 +169,16 @@ const KPI_CONFIG: KPIConfig[] = [
icon: TrendingUp,
color: 'blue',
getValue: (bls) => {
const invoiced = bls.filter(b => b.statut === 4);
const invoiced = bls.filter(b => b.statut === 2);
return Math.round(invoiced.reduce((sum, item) => sum + item.total_ht, 0));
},
getSubtitle: (bls) => {
const invoiced = bls.filter(b => b.statut === 4);
const invoiced = bls.filter(b => b.statut === 2);
return `${invoiced.length} BL facturé${invoiced.length > 1 ? 's' : ''}`;
},
getChange: (bls, value, period, allBLs) => {
const previousPeriodBL = getPreviousPeriodItems(allBLs, period);
const previousInvoiced = previousPeriodBL.filter(b => b.statut === 4);
const previousInvoiced = previousPeriodBL.filter(b => b.statut === 2);
const previousInvoicedAmount = previousInvoiced.reduce((sum, item) => sum + item.total_ht, 0);
return previousInvoicedAmount > 0
? (((Number(value) - previousInvoicedAmount) / previousInvoicedAmount) * 100).toFixed(1)
@ -208,11 +186,11 @@ const KPI_CONFIG: KPIConfig[] = [
},
getTrend: (bls, value, period, allBLs) => {
const previousPeriodBL = getPreviousPeriodItems(allBLs, period);
const previousInvoiced = previousPeriodBL.filter(b => b.statut === 4);
const previousInvoiced = previousPeriodBL.filter(b => b.statut === 2);
const previousInvoicedAmount = previousInvoiced.reduce((sum, item) => sum + item.total_ht, 0);
return Number(value) >= previousInvoicedAmount ? 'up' : 'down';
},
filter: (bls) => bls.filter(b => b.statut === 4),
filter: (bls) => bls.filter(b => b.statut === 2),
tooltip: { content: 'Total des BL facturés sur la période.', source: 'Ventes > Bon de livraisons' },
},
];

View file

@ -1,6 +1,6 @@
import React, { ReactNode, useEffect, useMemo, useState } from 'react';
import { Helmet } from 'react-helmet';
import { Plus, ShoppingCart, CheckSquare, AlertCircle, Euro, Eye, Copy } from 'lucide-react';
import { Plus, ShoppingCart, CheckSquare, AlertCircle, Euro, Eye, Copy, X } from 'lucide-react';
import KPIBar, { PeriodType } from '@/components/KPIBar';
import PrimaryButton_v2 from '@/components/PrimaryButton_v2';
import DataTable from '@/components/DataTable';
@ -43,6 +43,25 @@ export const STATUS_LABELS_COMMANDE = {
export type StatusCodeCommande = keyof typeof STATUS_LABELS_COMMANDE;
// ============================================
// TYPES
// ============================================
type FilterType = 'all' | 'count' | 'delivered' | 'pending' | 'deliveredAmount';
interface KPIConfig {
id: FilterType;
title: string;
icon: React.ElementType;
color: string;
getValue: (commandes: Commande[]) => number;
getSubtitle: (commandes: Commande[], value: number) => string;
getChange: (commandes: Commande[], value: number, period: PeriodType, allCommandes: Commande[]) => string;
getTrend: (commandes: Commande[], value: number, period: PeriodType, allCommandes: Commande[]) => 'up' | 'down' | 'neutral';
filter: (commandes: Commande[]) => Commande[];
tooltip?: { content: string; source: string };
}
// ============================================
// CONFIGURATION DES COLONNES
// ============================================
@ -57,6 +76,140 @@ const DEFAULT_COLUMNS: ColumnConfig[] = [
{ key: 'statut', label: 'Statut', visible: true },
];
// ============================================
// CONFIGURATION DES KPIs
// ============================================
const KPI_CONFIG: KPIConfig[] = [
{
id: 'all',
title: 'Total HT',
icon: Euro,
color: 'blue',
getValue: (commandes) => Math.round(commandes.reduce((sum, item) => sum + item.total_ht, 0)),
getSubtitle: (commandes) => {
const totalTTC = commandes.reduce((sum, item) => sum + item.total_ttc, 0);
return `${totalTTC.toLocaleString('fr-FR')}€ TTC`;
},
getChange: (commandes, value, period, allCommandes) => {
const previousPeriodCommandes = getPreviousPeriodItems(allCommandes, period);
const previousTotalHT = previousPeriodCommandes.reduce((sum, item) => sum + item.total_ht, 0);
return previousTotalHT > 0 ? (((value - previousTotalHT) / previousTotalHT) * 100).toFixed(1) : '0';
},
getTrend: (commandes, value, period, allCommandes) => {
const previousPeriodCommandes = getPreviousPeriodItems(allCommandes, period);
const previousTotalHT = previousPeriodCommandes.reduce((sum, item) => sum + item.total_ht, 0);
return value >= previousTotalHT ? 'up' : 'down';
},
filter: (commandes) => commandes,
tooltip: { content: "Total des commandes sur la période.", source: "Ventes > Commandes" }
},
{
id: 'count',
title: 'Nombre Commandes',
icon: ShoppingCart,
color: 'purple',
getValue: (commandes) => commandes.length,
getSubtitle: (commandes) => {
const avgCommande = commandes.length > 0
? commandes.reduce((sum, item) => sum + item.total_ht, 0) / commandes.length
: 0;
return `${avgCommande.toLocaleString('fr-FR', { maximumFractionDigits: 0 })}€ moyen`;
},
getChange: (commandes, value, period, allCommandes) => {
const previousPeriodCommandes = getPreviousPeriodItems(allCommandes, period);
const countChange = value - previousPeriodCommandes.length;
return countChange !== 0 ? `${countChange > 0 ? '+' : ''}${countChange}` : '0';
},
getTrend: (commandes, value, period, allCommandes) => {
const previousPeriodCommandes = getPreviousPeriodItems(allCommandes, period);
return value >= previousPeriodCommandes.length ? 'up' : 'down';
},
filter: (commandes) => commandes,
},
{
id: 'delivered',
title: 'Commandes à préparer',
icon: CheckSquare,
color: 'green',
getValue: (commandes) => commandes.filter(c => c.statut === 2).length,
getSubtitle: (commandes) => {
const delivered = commandes.filter(c => c.statut === 2);
const deliveredAmount = delivered.reduce((sum, item) => sum + item.total_ht, 0);
return `${deliveredAmount.toLocaleString('fr-FR', { maximumFractionDigits: 0 })}€`;
},
getChange: (commandes, value, period, allCommandes) => {
const previousPeriodCommandes = getPreviousPeriodItems(allCommandes, period);
const previousDelivered = previousPeriodCommandes.filter(c => c.statut === 2);
const deliveredChange = value - previousDelivered.length;
return deliveredChange !== 0 ? `${deliveredChange > 0 ? '+' : ''}${deliveredChange}` : '';
},
getTrend: (commandes, value, period, allCommandes) => {
const previousPeriodCommandes = getPreviousPeriodItems(allCommandes, period);
const previousDelivered = previousPeriodCommandes.filter(c => c.statut === 2);
return value >= previousDelivered.length ? 'up' : 'down';
},
filter: (commandes) => commandes.filter(c => c.statut === 2),
},
{
id: 'pending',
title: 'Commandes à confirmer',
icon: AlertCircle,
color: 'orange',
getValue: (commandes) => commandes.filter(c => c.statut === 1).length,
getSubtitle: (commandes) => {
const pending = commandes.filter(c => c.statut === 1);
const pendingAmount = pending.reduce((sum, item) => sum + item.total_ht, 0);
return `${pendingAmount.toLocaleString('fr-FR', { maximumFractionDigits: 0 })}€`;
},
getChange: (commandes, value, period, allCommandes) => {
const previousPeriodCommandes = getPreviousPeriodItems(allCommandes, period);
const previousPending = previousPeriodCommandes.filter(c => c.statut === 1);
const pendingChange = value - previousPending.length;
return pendingChange !== 0 ? `${pendingChange > 0 ? '+' : ''}${pendingChange}` : '';
},
getTrend: (commandes, value, period, allCommandes) => {
const previousPeriodCommandes = getPreviousPeriodItems(allCommandes, period);
const previousPending = previousPeriodCommandes.filter(c => c.statut === 1);
// Pour les en cours, moins c'est mieux
return value <= previousPending.length ? 'up' : 'down';
},
filter: (commandes) => commandes.filter(c => c.statut === 1),
},
{
id: 'deliveredAmount',
title: 'Montant Livré',
icon: CheckSquare,
color: 'green',
getValue: (commandes) => {
const delivered = commandes.filter(c => c.statut === 2);
return Math.round(delivered.reduce((sum, item) => sum + item.total_ht, 0));
},
getSubtitle: (commandes) => {
const delivered = commandes.filter(c => c.statut === 2);
return `${delivered.length} commande${delivered.length > 1 ? 's' : ''}`;
},
getChange: (commandes, value, period, allCommandes) => {
const previousPeriodCommandes = getPreviousPeriodItems(allCommandes, period);
const previousDelivered = previousPeriodCommandes.filter(c => c.statut === 2);
const previousDeliveredValue = previousDelivered.reduce((sum, item) => sum + item.total_ht, 0);
return previousDeliveredValue > 0 ? (((value - previousDeliveredValue) / previousDeliveredValue) * 100).toFixed(1) : '0';
},
getTrend: (commandes, value, period, allCommandes) => {
const previousPeriodCommandes = getPreviousPeriodItems(allCommandes, period);
const previousDelivered = previousPeriodCommandes.filter(c => c.statut === 2);
const previousDeliveredValue = previousDelivered.reduce((sum, item) => sum + item.total_ht, 0);
return value >= previousDeliveredValue ? 'up' : 'down';
},
filter: (commandes) => commandes.filter(c => c.statut === 2),
tooltip: { content: "Total des commandes livrées sur la période.", source: "Ventes > Commandes" }
},
];
// ============================================
// COMPOSANT PRINCIPAL
// ============================================
const OrdersPage = () => {
const navigate = useNavigate();
const dispatch = useAppDispatch();
@ -64,8 +217,8 @@ const OrdersPage = () => {
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [openStatus, setOpenStatus] = useState(false);
const [activeFilters, setActiveFilters] = useState<Record<string, string[] | undefined>>({});
const [activeFilter, setActiveFilter] = useState<FilterType>('all');
// État des colonnes visibles
const [columnConfig, setColumnConfig] = useState<ColumnConfig[]>(DEFAULT_COLUMNS);
const commandes = useAppSelector(getAllcommandes) as Commande[];
@ -80,18 +233,14 @@ const OrdersPage = () => {
const clients = useAppSelector(getAllClients) as Client[];
const statusClient = useAppSelector(clientStatus);
const statusArticle = useAppSelector(articleStatus);
const statusCommercial = useAppSelector(commercialsStatus) ;
const statusCommercial = useAppSelector(commercialsStatus);
useEffect(() => {
const load = async () => {
try {
if (statusArticle === 'idle') {
await dispatch(getArticles()).unwrap();
}
if (statusClient === 'idle' || statusClient === 'failed') {
await dispatch(getClients()).unwrap();
}
if (statusCommercial === "idle") await dispatch(getCommercials()).unwrap();
if (statusArticle === 'idle') await dispatch(getArticles()).unwrap();
if (statusClient === 'idle' || statusClient === 'failed') await dispatch(getClients()).unwrap();
if (statusCommercial === 'idle') await dispatch(getCommercials()).unwrap();
} catch (error) {
console.error(error);
}
@ -108,8 +257,7 @@ const OrdersPage = () => {
load();
}, [statusCommande, dispatch]);
const { refresh } = useDashboardData();
const { refresh } = useDashboardData();
const commercialOptions = useMemo(() => {
return commercials.map(c => ({
@ -137,29 +285,61 @@ const OrdersPage = () => {
},
];
// ============================================
// 1. CRÉER UNE MAP CLIENT -> COMMERCIAL (dans le composant)
// ============================================
// Map pour retrouver le commercial d'un client rapidement
const clientCommercialMap = useMemo(() => {
const map = new Map<string, string>();
clients.forEach(client => {
if (client.commercial?.numero) {
map.set(client.numero, client.commercial.numero.toString());
}
});
return map;
}, [clients]);
const clientCommercialMap = useMemo(() => {
const map = new Map<string, string>();
clients.forEach(client => {
if (client.commercial?.numero) {
map.set(client.numero, client.commercial.numero.toString());
}
});
return map;
}, [clients]);
// ============================================
// KPIs avec onClick
// ============================================
const kpis = useMemo(() => {
const periodFilteredCommandes = filterItemByPeriod(commandes, period, 'date');
return KPI_CONFIG.map(config => {
const value = config.getValue(periodFilteredCommandes);
return {
id: config.id,
title: config.title,
value: config.id === 'all' || config.id === 'deliveredAmount' ? `${value.toLocaleString('fr-FR')}` : value,
change: config.getChange(periodFilteredCommandes, value, period, commandes),
trend: config.getTrend(periodFilteredCommandes, value, period, commandes),
icon: config.icon,
subtitle: config.getSubtitle(periodFilteredCommandes, value),
color: config.color,
tooltip: config.tooltip,
isActive: activeFilter === config.id,
onClick: () => setActiveFilter(prev => (prev === config.id ? 'all' : config.id)),
};
});
}, [commandes, period, activeFilter]);
// ============================================
// Filtrage combiné : Période + KPI + Filtres avancés
// ============================================
const filteredCommandes = useMemo(() => {
// 1. Filtrer par période
let result = filterItemByPeriod(commandes, period, 'date');
// 2. Filtrer par KPI actif
const kpiConfig = KPI_CONFIG.find(k => k.id === activeFilter);
if (kpiConfig && activeFilter !== 'all') {
result = kpiConfig.filter(result);
}
// 3. Filtres avancés (statut)
if (activeFilters.status && activeFilters.status.length > 0) {
result = result.filter(item => activeFilters.status!.includes(item.statut.toString()));
}
// 4. Filtre par commercial
if (activeFilters.rep && activeFilters.rep.length > 0) {
result = result.filter(item => {
const commercialCode = clientCommercialMap.get(item.client_code);
@ -167,82 +347,21 @@ const OrdersPage = () => {
});
}
// 5. Tri par date décroissante
return [...result].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
}, [commandes, period, activeFilters, clientCommercialMap]);
const kpis = useMemo(() => {
const totalHT = Math.round(filteredCommandes.reduce((sum, item) => sum + item.total_ht, 0))
const validated = filteredCommandes.filter((item: Commande) => item.statut === 2);
const pending = filteredCommandes.filter((item: Commande) => item.statut === 1);
const validatedValue = Math.round(validated.reduce((sum, item) => sum + item.total_ht, 0))
const previousPeriodCommandes = getPreviousPeriodItems(commandes, period);
const previousTotalHT = previousPeriodCommandes.reduce((sum, item) => sum + item.total_ht, 0);
const previousValidated = previousPeriodCommandes.filter((item: Commande) => item.statut === 2);
const previousValidatedValue = previousValidated.reduce((sum, item) => sum + item.total_ht, 0);
const htChange = previousTotalHT > 0 ? (((totalHT - previousTotalHT) / previousTotalHT) * 100).toFixed(1) : '0';
const htTrend = totalHT >= previousTotalHT ? 'up' : 'down';
const countChange = filteredCommandes.length - previousPeriodCommandes.length;
const countTrend = countChange >= 0 ? 'up' : 'down';
const validatedChange = previousValidated.length > 0 ? validated.length - previousValidated.length : 0;
const validatedTrend = validatedChange >= 0 ? 'up' : 'down';
const pendingChange = previousPeriodCommandes.filter((item: Commande) => item.statut === 1).length;
const pendingDiff = pending.length - pendingChange;
const pendingTrend = pendingDiff >= 0 ? 'up' : 'down';
const validatedValueChange =
previousValidatedValue > 0 ? (((validatedValue - previousValidatedValue) / previousValidatedValue) * 100).toFixed(1) : '0';
const validatedValueTrend = validatedValue >= previousValidatedValue ? 'up' : 'down';
return [
{
title: 'Total HT',
value: `${totalHT.toLocaleString('fr-FR')}`,
change: `${htTrend === 'up' ? '+' : ''}${htChange}%`,
trend: htTrend,
icon: Euro,
tooltip: { content: "Total des commandes sur la période.", source: "Ventes > Commandes" }
},
{
title: 'Nombre Commandes',
value: filteredCommandes.length,
change: countChange !== 0 ? `${countChange > 0 ? '+' : ''}${countChange}` : '0',
trend: countTrend,
icon: ShoppingCart,
},
{
title: 'Livrées',
value: validated.length,
change: validatedChange !== 0 ? `${validatedChange > 0 ? '+' : ''}${validatedChange}` : '',
trend: validatedTrend,
icon: CheckSquare,
},
{
title: 'En Cours',
value: pending.length,
change: pendingDiff !== 0 ? `${pendingDiff > 0 ? '+' : ''}${pendingDiff}` : '',
trend: pendingTrend,
icon: AlertCircle,
},
{
title: 'Montant Livré',
value: `${validatedValue.toLocaleString('fr-FR')}`,
change: `${validatedValueTrend === 'up' ? '+' : ''}${validatedValueChange}%`,
trend: validatedValueTrend,
icon: CheckSquare,
tooltip: { content: "Total des commandes livrées sur la période.", source: "Ventes > Commandes" }
},
];
}, [filteredCommandes, commandes, period]);
}, [commandes, period, activeFilter, activeFilters, clientCommercialMap]);
// ============================================
// COLONNES DYNAMIQUES
// Label du filtre actif
// ============================================
const activeFilterLabel = useMemo(() => {
const config = KPI_CONFIG.find(k => k.id === activeFilter);
return config?.title || 'Tous';
}, [activeFilter]);
// ... reste du code inchangé (allColumnsDefinition, visibleColumns, columnFormatters, onDuplicate, actions, etc.)
const allColumnsDefinition = useMemo(() => {
const clientsMap = new Map(clients.map(c => [c.numero, c]));
@ -302,7 +421,6 @@ const OrdersPage = () => {
};
}, [clients]);
// Colonnes filtrées selon la config
const visibleColumns = useMemo(() => {
return columnConfig
.filter(col => col.visible)
@ -310,44 +428,18 @@ const OrdersPage = () => {
.filter(Boolean);
}, [columnConfig, allColumnsDefinition]);
// ============================================
// DÉFINIR LES FORMATEURS POUR L'EXPORT
// ============================================
// Formateurs personnalisés pour transformer les valeurs à l'export
const columnFormatters: Record<string, (value: any, row: any) => string> = {
date: (value) => {
if (!value) return '';
return formatDateFRCourt(value);
},
total_ht_calcule: (value) => {
if (value === null || value === undefined) return '0 €';
return `${value.toLocaleString('fr-FR', { minimumFractionDigits: 2 })} €`;
},
total_taxes_calcule: (value) => {
if (value === null || value === undefined) return '0 €';
return `${value.toLocaleString('fr-FR', { minimumFractionDigits: 2 })} €`;
},
total_ttc_calcule: (value) => {
if (value === null || value === undefined) return '0 €';
return `${value.toLocaleString('fr-FR', { minimumFractionDigits: 2 })} €`;
},
statut: (value) => {
return STATUS_LABELS_COMMANDE[value as StatusCodeCommande]?.label || 'Inconnu';
},
client_intitule: (value, row) => {
// Si client_intitule n'existe pas, on peut le récupérer depuis la map des clients
return value || row.client_code || '';
},
date: value => (value ? formatDateFRCourt(value) : ''),
total_ht_calcule: value => (value === null || value === undefined ? '0 €' : `${value.toLocaleString('fr-FR', { minimumFractionDigits: 2 })} €`),
total_taxes_calcule: value => (value === null || value === undefined ? '0 €' : `${value.toLocaleString('fr-FR', { minimumFractionDigits: 2 })} €`),
total_ttc_calcule: value => (value === null || value === undefined ? '0 €' : `${value.toLocaleString('fr-FR', { minimumFractionDigits: 2 })} €`),
statut: value => STATUS_LABELS_COMMANDE[value as StatusCodeCommande]?.label || 'Inconnu',
client_intitule: (value, row) => value || row.client_code || '',
};
const onDuplicate = async () => {
try {
setLoading(true);
const payloadCreate: CommandeRequest = {
client_id: commandeSelected.client_code,
date_commande: (() => {
@ -361,16 +453,12 @@ const OrdersPage = () => {
})),
reference: commandeSelected.reference,
};
const result = (await dispatch(createCommande(payloadCreate)).unwrap()) as CommandeResponse;
const data = result.data;
await new Promise(resolve => setTimeout(resolve, 1500));
const itemCreated = (await dispatch(getCommande(data.numero_commande)).unwrap()) as any;
const res = itemCreated as Commande;
dispatch(selectcommande(res));
toast({
title: 'Commande dupliquée avec succès !',
description: `La commande ${commandeSelected.numero} a été dupliquée avec succès.`,
@ -441,28 +529,37 @@ const OrdersPage = () => {
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex items-center gap-4">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Commandes</h1>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">{filteredCommandes.length} commandes</p>
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Commandes</h1>
{/* Badge filtre actif */}
{activeFilter !== 'all' && (
<button
onClick={() => setActiveFilter('all')}
className={cn(
'inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium transition-colors',
'bg-[#007E45]/10 text-[#007E45] hover:bg-[#007E45]/20'
)}
>
{activeFilterLabel}
<X className="w-3 h-3" />
</button>
)}
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{filteredCommandes.length} commande{filteredCommandes.length > 1 ? 's' : ''}
{activeFilter !== 'all' && ` (${activeFilterLabel.toLowerCase()})`}
</p>
</div>
<PeriodSelector value={period} onChange={setPeriod} />
</div>
<div className="flex gap-3 flex-wrap items-center">
{/* Sélecteur de colonnes */}
<ColumnSelector columns={columnConfig} onChange={setColumnConfig} />
<ExportDropdown
data={filteredCommandes}
columns={columnConfig}
columnFormatters={columnFormatters}
filename="commande"
/>
<ExportDropdown data={filteredCommandes} columns={columnConfig} columnFormatters={columnFormatters} filename="commande" />
<AdvancedFilters
filters={filterDefinitions}
activeFilters={activeFilters}
onFilterChange={(key, values) => {
setActiveFilters(prev => ({
...prev,
[key]: values,
}));
setActiveFilters(prev => ({ ...prev, [key]: values }));
}}
onReset={() => setActiveFilters({})}
/>

View file

@ -137,10 +137,20 @@ const KPI_CONFIG: KPIConfig[] = [
},
{
id: 'pending',
title: 'Nombre de Devis',
title: 'Nombre de confirmé',
icon: FileText,
color: 'orange',
getValue: (devis) => devis.length,
// getValue: (devis) => {
// const avgDevis = devis.length > 0
// ? devis.reduce((sum, item) => sum + item.total_ht, 0) / devis.length
// : 0;
// return avgDevis.toString().length;
// },
getValue: (devis) => {
const devisConfirmed = devis.filter(d => d.statut === 1);
return devisConfirmed.length
},
getSubtitle: (devis) => {
const avgDevis = devis.length > 0
? devis.reduce((sum, item) => sum + item.total_ht, 0) / devis.length
@ -158,34 +168,41 @@ const KPI_CONFIG: KPIConfig[] = [
},
filter: (devis) => devis.filter(d => d.statut === 1),
},
{
id: 'signed',
title: 'Devis Signés',
icon: Percent,
color: 'purple',
getValue: (devis, universign) => {
return universign.filter(u => u.local_status === "SIGNE").length;
},
getSubtitle: (devis, value, universign) => {
const pending = devis.filter(d => d.statut === 1).length;
return `${pending} en attente`;
},
getChange: (devis, value) => {
return `${value}/${devis.length}`;
},
getTrend: (devis, value) => {
const acceptanceRate = devis.length > 0 ? (value / devis.length) * 100 : 0;
return acceptanceRate >= 50 ? 'up' : 'down';
},
filter: (devis, universign) => {
const signedDocIds = new Set(
universign
.filter(u => u.local_status === "SIGNE")
.map(u => u.sage_document_id)
);
return devis.filter(d => signedDocIds.has(d.numero));
},
{
id: 'signed',
title: 'Devis Signés',
icon: Percent,
color: 'purple',
getValue: (devis, universign) => {
// Créer un Set des IDs de documents signés
const signedDocIds = new Set(
universign
.filter(u => u.local_status === "SIGNE")
.map(u => u.sage_document_id)
);
// Compter uniquement les devis de la période qui sont signés
return devis.filter(d => signedDocIds.has(d.numero)).length;
},
getSubtitle: (devis, value, universign) => {
const pending = devis.filter(d => d.statut === 1).length;
return `${pending} en attente`;
},
getChange: (devis, value) => {
return `${value}/${devis.length}`;
},
getTrend: (devis, value) => {
const acceptanceRate = devis.length > 0 ? (value / devis.length) * 100 : 0;
return acceptanceRate >= 50 ? 'up' : 'down';
},
filter: (devis, universign) => {
const signedDocIds = new Set(
universign
.filter(u => u.local_status === "SIGNE")
.map(u => u.sage_document_id)
);
return devis.filter(d => signedDocIds.has(d.numero));
},
},
];
// ============================================
@ -318,12 +335,13 @@ const QuotesPage = () => {
let result = filterItemByPeriod(devis, period, 'date');
result = result.filter(item => item.numero !== 'DE00126');
// 2. Filtrer par KPI actif
const kpiConfig = KPI_CONFIG.find(k => k.id === activeFilter);
if (kpiConfig && activeFilter !== 'all') {
result = kpiConfig.filter(result, universign);
}
// 3. Filtres avancés (statut)
if (activeFilters.status && activeFilters.status.length > 0) {
result = result.filter(item =>
@ -345,6 +363,7 @@ const QuotesPage = () => {
);
}, [devis, period, activeFilter, activeFilters, clientCommercialMap, universign]);
// ============================================
// Label du filtre actif
// ============================================

View file

@ -14,6 +14,7 @@ const apiService = axios.create({
baseURL: BackUrl,
headers: {
Authorization: access_token ? `Bearer ${access_token}` : "dsqd",
"X-API-Key": "sdk_live_TeeZGyvg34H-nIR7tBhyiemAfIV0MbT2HQ1BzG9bofE"
}
})