Compare commits
2 commits
78e75ce41c
...
1fc87ac59e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fc87ac59e | ||
|
|
a2421c86ee |
6 changed files with 409 additions and 273 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export type StatusCode = keyof typeof FACTURE_STATUS_LABELS;
|
|||
// TYPES
|
||||
// ============================================
|
||||
|
||||
type FilterType = 'all' | 'paid' | 'pending' | 'validated';
|
||||
type FilterType = 'all' | 'paid' | 'pending' | 'validated' | 'partial';
|
||||
|
||||
interface FactureWithReglement extends Facture {
|
||||
statut_reglement: string;
|
||||
|
|
@ -143,63 +143,100 @@ const KPI_CONFIG: KPIConfig[] = [
|
|||
filter: (factures) => factures,
|
||||
tooltip: { content: "Total des factures sur la période.", source: "Ventes > Factures" }
|
||||
},
|
||||
{
|
||||
id: 'pending',
|
||||
title: 'Nombre de Factures',
|
||||
icon: FileText,
|
||||
color: 'orange',
|
||||
getValue: (factures) => factures.length,
|
||||
getSubtitle: (factures) => {
|
||||
const paid = factures.filter(f => f.statut === 4);
|
||||
const paymentRate = factures.length > 0 ? ((paid.length / factures.length) * 100).toFixed(1) : '0';
|
||||
return `${paymentRate}% payées`;
|
||||
},
|
||||
getChange: (factures, value, period, allFactures) => {
|
||||
const previousPeriodFactures = getPreviousPeriodItems(allFactures, period);
|
||||
const countChange = Number(value) - previousPeriodFactures.length;
|
||||
return countChange !== 0 ? `${countChange > 0 ? '+' : ''}${countChange}` : '0';
|
||||
},
|
||||
getTrend: (factures, value, period, allFactures) => {
|
||||
const previousPeriodFactures = getPreviousPeriodItems(allFactures, period);
|
||||
return Number(value) >= previousPeriodFactures.length ? 'up' : 'down';
|
||||
},
|
||||
filter: (factures) => factures.filter(f => f.statut === 1),
|
||||
},
|
||||
{
|
||||
id: 'paid',
|
||||
title: 'Montant Payé',
|
||||
title: 'Payés',
|
||||
icon: CheckCircle,
|
||||
color: 'green',
|
||||
getValue: (factures) => {
|
||||
const paid = factures.filter(f => f.statut === 4);
|
||||
return Math.round(paid.reduce((sum, item) => sum + item.total_ht, 0));
|
||||
const soldees = factures.filter(f => (f as any).statut_display === 6);
|
||||
return Math.round(soldees.reduce((sum, item) => sum + item.total_ttc, 0));
|
||||
},
|
||||
getSubtitle: (factures) => {
|
||||
const paid = factures.filter(f => f.statut === 4);
|
||||
return `${paid.length} facture${paid.length > 1 ? 's' : ''}`;
|
||||
const soldees = factures.filter(f => (f as any).statut_display === 6);
|
||||
return `${soldees.length} facture${soldees.length > 1 ? 's' : ''}`;
|
||||
},
|
||||
getChange: (factures) => {
|
||||
const paid = factures.filter(f => f.statut === 4);
|
||||
return `${paid.length}/${factures.length}`;
|
||||
getChange: (factures, value, period, allFactures) => {
|
||||
const soldees = factures.filter(f => (f as any).statut_display === 6);
|
||||
const totalFacture = factures.length;
|
||||
return totalFacture > 0 ? `${soldees.length}/${totalFacture}` : '0';
|
||||
},
|
||||
getTrend: (factures, value, period, allFactures) => {
|
||||
const previousPeriodFactures = getPreviousPeriodItems(allFactures, period);
|
||||
const previousPaid = previousPeriodFactures.filter(f => f.statut === 4);
|
||||
const previousPaidAmount = previousPaid.reduce((sum, item) => sum + item.total_ht, 0);
|
||||
return Number(value) >= previousPaidAmount ? 'up' : 'down';
|
||||
const previousSoldees = previousPeriodFactures.filter(f => (f as any).statut_display === 6);
|
||||
const previousTotal = previousSoldees.reduce((sum, item) => sum + item.total_ttc, 0);
|
||||
return Number(value) >= previousTotal ? 'up' : 'down';
|
||||
},
|
||||
filter: (factures) => factures.filter(f => f.statut === 4),
|
||||
tooltip: { content: "Montant des factures payées sur la période.", source: "Ventes > Factures" }
|
||||
filter: (factures) => factures.filter(f => (f as any).statut_display === 6),
|
||||
tooltip: { content: "Montant des factures entièrement réglées.", source: "Ventes > Factures" }
|
||||
},
|
||||
{
|
||||
id: 'partial',
|
||||
title: 'Partiellement réglées',
|
||||
icon: AlertTriangle,
|
||||
color: 'orange',
|
||||
getValue: (factures) => {
|
||||
const partielles = factures.filter(f => (f as any).statut_display === 7);
|
||||
return Math.round(partielles.reduce((sum, item) => sum + ((item as any).reste_a_regler || item.total_ttc), 0));
|
||||
},
|
||||
getSubtitle: (factures) => {
|
||||
const partielles = factures.filter(f => (f as any).statut_display === 7);
|
||||
return `${partielles.length} facture${partielles.length > 1 ? 's' : ''}`;
|
||||
},
|
||||
getChange: (factures, value, period, allFactures) => {
|
||||
const partielles = factures.filter(f => (f as any).statut_display === 7);
|
||||
const totalFacture = factures.length;
|
||||
return totalFacture > 0 ? `${partielles.length}/${totalFacture}` : '0';
|
||||
},
|
||||
getTrend: (factures, value, period, allFactures) => {
|
||||
const previousPeriodFactures = getPreviousPeriodItems(allFactures, period);
|
||||
const previousPartielles = previousPeriodFactures.filter(f => (f as any).statut_display === 7);
|
||||
const previousTotal = previousPartielles.reduce((sum, item) => sum + ((item as any).reste_a_regler || item.total_ttc), 0);
|
||||
return Number(value) <= previousTotal ? 'up' : 'down';
|
||||
},
|
||||
filter: (factures) => factures.filter(f => (f as any).statut_display === 7),
|
||||
tooltip: { content: "Montant restant à régler sur les factures partiellement payées.", source: "Ventes > Factures" }
|
||||
},
|
||||
{
|
||||
id: 'pending',
|
||||
title: 'Non réglées',
|
||||
icon: FileText,
|
||||
color: 'red',
|
||||
getValue: (factures) => {
|
||||
// Factures ni soldées (6) ni partiellement payées (7)
|
||||
const nonReglees = factures.filter(f => (f as any).statut_display !== 6 && (f as any).statut_display !== 7);
|
||||
return nonReglees.length;
|
||||
},
|
||||
getSubtitle: (factures) => {
|
||||
const nonReglees = factures.filter(f => (f as any).statut_display !== 6 && (f as any).statut_display !== 7);
|
||||
const totalRestant = nonReglees.reduce((sum, item) => sum + ((item as any).reste_a_regler || item.total_ttc), 0);
|
||||
return `${totalRestant.toLocaleString('fr-FR')}€ à régler`;
|
||||
},
|
||||
getChange: (factures, value, period, allFactures) => {
|
||||
const previousPeriodFactures = getPreviousPeriodItems(allFactures, period);
|
||||
const previousNonReglees = previousPeriodFactures.filter(f => (f as any).statut_display !== 6 && (f as any).statut_display !== 7);
|
||||
return previousNonReglees.length > 0
|
||||
? (((Number(value) - previousNonReglees.length) / previousNonReglees.length) * 100).toFixed(1)
|
||||
: '0';
|
||||
},
|
||||
getTrend: (factures, value, period, allFactures) => {
|
||||
const previousPeriodFactures = getPreviousPeriodItems(allFactures, period);
|
||||
const previousNonReglees = previousPeriodFactures.filter(f => (f as any).statut_display !== 6 && (f as any).statut_display !== 7);
|
||||
// Moins de non réglées = mieux
|
||||
return Number(value) <= previousNonReglees.length ? 'up' : 'down';
|
||||
},
|
||||
filter: (factures) => factures.filter(f => (f as any).statut_display !== 6 && (f as any).statut_display !== 7),
|
||||
tooltip: { content: "Nombre de factures non réglées.", source: "Ventes > Factures" }
|
||||
},
|
||||
{
|
||||
id: 'validated',
|
||||
title: 'Factures Validées',
|
||||
title: 'Validées',
|
||||
icon: CheckCircle,
|
||||
color: 'purple',
|
||||
getValue: (factures) => factures.filter(f => f.valide === 1).length,
|
||||
getSubtitle: (factures) => {
|
||||
const pending = factures.filter(f => f.statut === 1);
|
||||
return `${pending.length} en attente`;
|
||||
const nonValidees = factures.filter(f => f.valide !== 1);
|
||||
return `${nonValidees.length} non validée${nonValidees.length > 1 ? 's' : ''}`;
|
||||
},
|
||||
getChange: (factures) => {
|
||||
const validated = factures.filter(f => f.valide === 1);
|
||||
|
|
@ -211,6 +248,7 @@ const KPI_CONFIG: KPIConfig[] = [
|
|||
return Number(value) >= previousValidated.length ? 'up' : 'down';
|
||||
},
|
||||
filter: (factures) => factures.filter(f => f.valide === 1),
|
||||
tooltip: { content: "Nombre de factures validées.", source: "Ventes > Factures" }
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -356,6 +394,7 @@ const InvoicesPage = () => {
|
|||
});
|
||||
}, [factures, reglementsMap]);
|
||||
|
||||
|
||||
// ============================================
|
||||
// Client actuellement sélectionné
|
||||
// ============================================
|
||||
|
|
@ -376,25 +415,26 @@ const InvoicesPage = () => {
|
|||
// ============================================
|
||||
|
||||
const kpis = useMemo(() => {
|
||||
const periodFilteredFactures = filterItemByPeriod(factures, period, 'date');
|
||||
// Utiliser facturesWithReglement au lieu de factures
|
||||
const periodFilteredFactures = filterItemByPeriod(facturesWithReglement, period, 'date');
|
||||
|
||||
return KPI_CONFIG.map(config => {
|
||||
const value = config.getValue(periodFilteredFactures);
|
||||
return {
|
||||
id: config.id,
|
||||
title: config.title,
|
||||
value: config.id === 'all' || config.id === 'paid' ? `${value.toLocaleString('fr-FR')}€` : value,
|
||||
change: config.getChange(periodFilteredFactures, value, period, factures),
|
||||
trend: config.getTrend(periodFilteredFactures, value, period, factures),
|
||||
icon: config.icon,
|
||||
subtitle: config.getSubtitle(periodFilteredFactures, value),
|
||||
color: config.color,
|
||||
tooltip: config.tooltip,
|
||||
isActive: activeFilter === config.id,
|
||||
onClick: () => setActiveFilter(prev => (prev === config.id ? 'all' : config.id)),
|
||||
};
|
||||
});
|
||||
}, [factures, period, activeFilter]);
|
||||
return KPI_CONFIG.map(config => {
|
||||
const value = config.getValue(periodFilteredFactures);
|
||||
return {
|
||||
id: config.id,
|
||||
title: config.title,
|
||||
value: config.id === 'all' || config.id === 'paid' || config.id === 'partial' ? `${value.toLocaleString('fr-FR')}€` : value,
|
||||
change: config.getChange(periodFilteredFactures, value, period, facturesWithReglement), // aussi ici
|
||||
trend: config.getTrend(periodFilteredFactures, value, period, facturesWithReglement), // et ici
|
||||
icon: config.icon,
|
||||
subtitle: config.getSubtitle(periodFilteredFactures, value),
|
||||
color: config.color,
|
||||
tooltip: config.tooltip,
|
||||
isActive: activeFilter === config.id,
|
||||
onClick: () => setActiveFilter(prev => (prev === config.id ? 'all' : config.id)),
|
||||
};
|
||||
});
|
||||
}, [facturesWithReglement, period, activeFilter]);
|
||||
|
||||
// ============================================
|
||||
// Filtrage combiné : Période + KPI + Filtres avancés
|
||||
|
|
@ -429,6 +469,7 @@ const InvoicesPage = () => {
|
|||
return [...result].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
}, [facturesWithReglement, period, activeFilter, activeFilters, clientCommercialMap]);
|
||||
|
||||
|
||||
// ============================================
|
||||
// Factures sélectionnables
|
||||
// ============================================
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
// ============================================
|
||||
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]);
|
||||
|
||||
// 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]);
|
||||
// ============================================
|
||||
// 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({})}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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,6 +335,7 @@ 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') {
|
||||
|
|
@ -345,6 +363,7 @@ const QuotesPage = () => {
|
|||
);
|
||||
}, [devis, period, activeFilter, activeFilters, clientCommercialMap, universign]);
|
||||
|
||||
|
||||
// ============================================
|
||||
// Label du filtre actif
|
||||
// ============================================
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue