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);
|
const previousPeriodAvoirs = getPreviousPeriodItems(allAvoirs, period);
|
||||||
return Number(value) <= previousPeriodAvoirs.length ? 'up' : 'down';
|
return Number(value) <= previousPeriodAvoirs.length ? 'up' : 'down';
|
||||||
},
|
},
|
||||||
filter: (avoirs) => avoirs.filter(a => a.statut === 1),
|
filter: (avoirs) => avoirs
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'validated',
|
id: 'validated',
|
||||||
title: 'Avoirs Validés',
|
title: 'Avoirs à Facturer',
|
||||||
icon: TrendingDown,
|
icon: TrendingDown,
|
||||||
color: 'green',
|
color: 'green',
|
||||||
getValue: (avoirs) => avoirs.filter(a => a.statut === 2).length,
|
getValue: (avoirs) => avoirs.filter(a => a.statut === 2).length,
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,9 @@ const COLORS = {
|
||||||
const BL_STATUS_LABELS = {
|
const BL_STATUS_LABELS = {
|
||||||
0: { label: 'Saisi', color: COLORS.gray },
|
0: { label: 'Saisi', color: COLORS.gray },
|
||||||
1: { label: 'Confirmé', color: COLORS.yellow },
|
1: { label: 'Confirmé', color: COLORS.yellow },
|
||||||
2: { label: 'A préparer', color: COLORS.blue },
|
2: { label: 'A Facturer', color: COLORS.blue },
|
||||||
3: { label: 'Préparé', color: COLORS.green },
|
// 3: { label: 'Facturé', color: COLORS.green },
|
||||||
4: { label: 'Soldé', color: COLORS.purple },
|
// 4: { label: 'Soldé', color: COLORS.purple },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type StatusCode = keyof typeof BL_STATUS_LABELS;
|
export type StatusCode = keyof typeof BL_STATUS_LABELS;
|
||||||
|
|
@ -137,14 +137,14 @@ const KPI_CONFIG: KPIConfig[] = [
|
||||||
const previousPeriodBL = getPreviousPeriodItems(allBLs, period);
|
const previousPeriodBL = getPreviousPeriodItems(allBLs, period);
|
||||||
return Number(value) >= previousPeriodBL.length ? 'up' : 'down';
|
return Number(value) >= previousPeriodBL.length ? 'up' : 'down';
|
||||||
},
|
},
|
||||||
filter: (bls) => bls.filter(b => b.statut === 1),
|
filter: (bls) => bls,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'delivered',
|
id: 'delivered',
|
||||||
title: 'BL Livrés',
|
title: 'BL à facturer',
|
||||||
icon: CheckCircle,
|
icon: CheckCircle,
|
||||||
color: 'green',
|
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) => {
|
getSubtitle: (bls) => {
|
||||||
const delivered = bls.filter(b => b.statut === 3 || b.statut === 4);
|
const delivered = bls.filter(b => b.statut === 3 || b.statut === 4);
|
||||||
const deliveredAmount = delivered.reduce((sum, item) => sum + item.total_ht, 0);
|
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);
|
const previousDelivered = previousPeriodBL.filter(b => b.statut === 3 || b.statut === 4);
|
||||||
return Number(value) >= previousDelivered.length ? 'up' : 'down';
|
return Number(value) >= previousDelivered.length ? 'up' : 'down';
|
||||||
},
|
},
|
||||||
filter: (bls) => bls.filter(b => b.statut === 3 || b.statut === 4),
|
filter: (bls) => bls.filter(b => b.statut === 2),
|
||||||
},
|
|
||||||
{
|
|
||||||
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),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'invoiced',
|
id: 'invoiced',
|
||||||
|
|
@ -191,16 +169,16 @@ const KPI_CONFIG: KPIConfig[] = [
|
||||||
icon: TrendingUp,
|
icon: TrendingUp,
|
||||||
color: 'blue',
|
color: 'blue',
|
||||||
getValue: (bls) => {
|
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));
|
return Math.round(invoiced.reduce((sum, item) => sum + item.total_ht, 0));
|
||||||
},
|
},
|
||||||
getSubtitle: (bls) => {
|
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' : ''}`;
|
return `${invoiced.length} BL facturé${invoiced.length > 1 ? 's' : ''}`;
|
||||||
},
|
},
|
||||||
getChange: (bls, value, period, allBLs) => {
|
getChange: (bls, value, period, allBLs) => {
|
||||||
const previousPeriodBL = getPreviousPeriodItems(allBLs, period);
|
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);
|
const previousInvoicedAmount = previousInvoiced.reduce((sum, item) => sum + item.total_ht, 0);
|
||||||
return previousInvoicedAmount > 0
|
return previousInvoicedAmount > 0
|
||||||
? (((Number(value) - previousInvoicedAmount) / previousInvoicedAmount) * 100).toFixed(1)
|
? (((Number(value) - previousInvoicedAmount) / previousInvoicedAmount) * 100).toFixed(1)
|
||||||
|
|
@ -208,11 +186,11 @@ const KPI_CONFIG: KPIConfig[] = [
|
||||||
},
|
},
|
||||||
getTrend: (bls, value, period, allBLs) => {
|
getTrend: (bls, value, period, allBLs) => {
|
||||||
const previousPeriodBL = getPreviousPeriodItems(allBLs, period);
|
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);
|
const previousInvoicedAmount = previousInvoiced.reduce((sum, item) => sum + item.total_ht, 0);
|
||||||
return Number(value) >= previousInvoicedAmount ? 'up' : 'down';
|
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' },
|
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
|
// TYPES
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
type FilterType = 'all' | 'paid' | 'pending' | 'validated';
|
type FilterType = 'all' | 'paid' | 'pending' | 'validated' | 'partial';
|
||||||
|
|
||||||
interface FactureWithReglement extends Facture {
|
interface FactureWithReglement extends Facture {
|
||||||
statut_reglement: string;
|
statut_reglement: string;
|
||||||
|
|
@ -143,63 +143,100 @@ const KPI_CONFIG: KPIConfig[] = [
|
||||||
filter: (factures) => factures,
|
filter: (factures) => factures,
|
||||||
tooltip: { content: "Total des factures sur la période.", source: "Ventes > 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',
|
id: 'paid',
|
||||||
title: 'Montant Payé',
|
title: 'Payés',
|
||||||
icon: CheckCircle,
|
icon: CheckCircle,
|
||||||
color: 'green',
|
color: 'green',
|
||||||
getValue: (factures) => {
|
getValue: (factures) => {
|
||||||
const paid = factures.filter(f => f.statut === 4);
|
const soldees = factures.filter(f => (f as any).statut_display === 6);
|
||||||
return Math.round(paid.reduce((sum, item) => sum + item.total_ht, 0));
|
return Math.round(soldees.reduce((sum, item) => sum + item.total_ttc, 0));
|
||||||
},
|
},
|
||||||
getSubtitle: (factures) => {
|
getSubtitle: (factures) => {
|
||||||
const paid = factures.filter(f => f.statut === 4);
|
const soldees = factures.filter(f => (f as any).statut_display === 6);
|
||||||
return `${paid.length} facture${paid.length > 1 ? 's' : ''}`;
|
return `${soldees.length} facture${soldees.length > 1 ? 's' : ''}`;
|
||||||
},
|
},
|
||||||
getChange: (factures) => {
|
getChange: (factures, value, period, allFactures) => {
|
||||||
const paid = factures.filter(f => f.statut === 4);
|
const soldees = factures.filter(f => (f as any).statut_display === 6);
|
||||||
return `${paid.length}/${factures.length}`;
|
const totalFacture = factures.length;
|
||||||
|
return totalFacture > 0 ? `${soldees.length}/${totalFacture}` : '0';
|
||||||
},
|
},
|
||||||
getTrend: (factures, value, period, allFactures) => {
|
getTrend: (factures, value, period, allFactures) => {
|
||||||
const previousPeriodFactures = getPreviousPeriodItems(allFactures, period);
|
const previousPeriodFactures = getPreviousPeriodItems(allFactures, period);
|
||||||
const previousPaid = previousPeriodFactures.filter(f => f.statut === 4);
|
const previousSoldees = previousPeriodFactures.filter(f => (f as any).statut_display === 6);
|
||||||
const previousPaidAmount = previousPaid.reduce((sum, item) => sum + item.total_ht, 0);
|
const previousTotal = previousSoldees.reduce((sum, item) => sum + item.total_ttc, 0);
|
||||||
return Number(value) >= previousPaidAmount ? 'up' : 'down';
|
return Number(value) >= previousTotal ? 'up' : 'down';
|
||||||
},
|
},
|
||||||
filter: (factures) => factures.filter(f => f.statut === 4),
|
filter: (factures) => factures.filter(f => (f as any).statut_display === 6),
|
||||||
tooltip: { content: "Montant des factures payées sur la période.", source: "Ventes > Factures" }
|
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',
|
id: 'validated',
|
||||||
title: 'Factures Validées',
|
title: 'Validées',
|
||||||
icon: CheckCircle,
|
icon: CheckCircle,
|
||||||
color: 'purple',
|
color: 'purple',
|
||||||
getValue: (factures) => factures.filter(f => f.valide === 1).length,
|
getValue: (factures) => factures.filter(f => f.valide === 1).length,
|
||||||
getSubtitle: (factures) => {
|
getSubtitle: (factures) => {
|
||||||
const pending = factures.filter(f => f.statut === 1);
|
const nonValidees = factures.filter(f => f.valide !== 1);
|
||||||
return `${pending.length} en attente`;
|
return `${nonValidees.length} non validée${nonValidees.length > 1 ? 's' : ''}`;
|
||||||
},
|
},
|
||||||
getChange: (factures) => {
|
getChange: (factures) => {
|
||||||
const validated = factures.filter(f => f.valide === 1);
|
const validated = factures.filter(f => f.valide === 1);
|
||||||
|
|
@ -211,6 +248,7 @@ const KPI_CONFIG: KPIConfig[] = [
|
||||||
return Number(value) >= previousValidated.length ? 'up' : 'down';
|
return Number(value) >= previousValidated.length ? 'up' : 'down';
|
||||||
},
|
},
|
||||||
filter: (factures) => factures.filter(f => f.valide === 1),
|
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]);
|
}, [factures, reglementsMap]);
|
||||||
|
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Client actuellement sélectionné
|
// Client actuellement sélectionné
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
@ -376,16 +415,17 @@ const InvoicesPage = () => {
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
const kpis = useMemo(() => {
|
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 => {
|
return KPI_CONFIG.map(config => {
|
||||||
const value = config.getValue(periodFilteredFactures);
|
const value = config.getValue(periodFilteredFactures);
|
||||||
return {
|
return {
|
||||||
id: config.id,
|
id: config.id,
|
||||||
title: config.title,
|
title: config.title,
|
||||||
value: config.id === 'all' || config.id === 'paid' ? `${value.toLocaleString('fr-FR')}€` : value,
|
value: config.id === 'all' || config.id === 'paid' || config.id === 'partial' ? `${value.toLocaleString('fr-FR')}€` : value,
|
||||||
change: config.getChange(periodFilteredFactures, value, period, factures),
|
change: config.getChange(periodFilteredFactures, value, period, facturesWithReglement), // aussi ici
|
||||||
trend: config.getTrend(periodFilteredFactures, value, period, factures),
|
trend: config.getTrend(periodFilteredFactures, value, period, facturesWithReglement), // et ici
|
||||||
icon: config.icon,
|
icon: config.icon,
|
||||||
subtitle: config.getSubtitle(periodFilteredFactures, value),
|
subtitle: config.getSubtitle(periodFilteredFactures, value),
|
||||||
color: config.color,
|
color: config.color,
|
||||||
|
|
@ -394,7 +434,7 @@ const InvoicesPage = () => {
|
||||||
onClick: () => setActiveFilter(prev => (prev === config.id ? 'all' : config.id)),
|
onClick: () => setActiveFilter(prev => (prev === config.id ? 'all' : config.id)),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [factures, period, activeFilter]);
|
}, [facturesWithReglement, period, activeFilter]);
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Filtrage combiné : Période + KPI + Filtres avancés
|
// 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());
|
return [...result].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||||
}, [facturesWithReglement, period, activeFilter, activeFilters, clientCommercialMap]);
|
}, [facturesWithReglement, period, activeFilter, activeFilters, clientCommercialMap]);
|
||||||
|
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Factures sélectionnables
|
// Factures sélectionnables
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { ReactNode, useEffect, useMemo, useState } from 'react';
|
import React, { ReactNode, useEffect, useMemo, useState } from 'react';
|
||||||
import { Helmet } from 'react-helmet';
|
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 KPIBar, { PeriodType } from '@/components/KPIBar';
|
||||||
import PrimaryButton_v2 from '@/components/PrimaryButton_v2';
|
import PrimaryButton_v2 from '@/components/PrimaryButton_v2';
|
||||||
import DataTable from '@/components/DataTable';
|
import DataTable from '@/components/DataTable';
|
||||||
|
|
@ -43,6 +43,25 @@ export const STATUS_LABELS_COMMANDE = {
|
||||||
|
|
||||||
export type StatusCodeCommande = keyof typeof 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
|
// CONFIGURATION DES COLONNES
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
@ -57,6 +76,140 @@ const DEFAULT_COLUMNS: ColumnConfig[] = [
|
||||||
{ key: 'statut', label: 'Statut', visible: true },
|
{ 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 OrdersPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
@ -64,8 +217,8 @@ const OrdersPage = () => {
|
||||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||||
const [openStatus, setOpenStatus] = useState(false);
|
const [openStatus, setOpenStatus] = useState(false);
|
||||||
const [activeFilters, setActiveFilters] = useState<Record<string, string[] | undefined>>({});
|
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 [columnConfig, setColumnConfig] = useState<ColumnConfig[]>(DEFAULT_COLUMNS);
|
||||||
|
|
||||||
const commandes = useAppSelector(getAllcommandes) as Commande[];
|
const commandes = useAppSelector(getAllcommandes) as Commande[];
|
||||||
|
|
@ -80,18 +233,14 @@ const OrdersPage = () => {
|
||||||
const clients = useAppSelector(getAllClients) as Client[];
|
const clients = useAppSelector(getAllClients) as Client[];
|
||||||
const statusClient = useAppSelector(clientStatus);
|
const statusClient = useAppSelector(clientStatus);
|
||||||
const statusArticle = useAppSelector(articleStatus);
|
const statusArticle = useAppSelector(articleStatus);
|
||||||
const statusCommercial = useAppSelector(commercialsStatus) ;
|
const statusCommercial = useAppSelector(commercialsStatus);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
try {
|
try {
|
||||||
if (statusArticle === 'idle') {
|
if (statusArticle === 'idle') await dispatch(getArticles()).unwrap();
|
||||||
await dispatch(getArticles()).unwrap();
|
if (statusClient === 'idle' || statusClient === 'failed') await dispatch(getClients()).unwrap();
|
||||||
}
|
if (statusCommercial === 'idle') await dispatch(getCommercials()).unwrap();
|
||||||
if (statusClient === 'idle' || statusClient === 'failed') {
|
|
||||||
await dispatch(getClients()).unwrap();
|
|
||||||
}
|
|
||||||
if (statusCommercial === "idle") await dispatch(getCommercials()).unwrap();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|
@ -110,7 +259,6 @@ const OrdersPage = () => {
|
||||||
|
|
||||||
const { refresh } = useDashboardData();
|
const { refresh } = useDashboardData();
|
||||||
|
|
||||||
|
|
||||||
const commercialOptions = useMemo(() => {
|
const commercialOptions = useMemo(() => {
|
||||||
return commercials.map(c => ({
|
return commercials.map(c => ({
|
||||||
value: c.numero.toString(),
|
value: c.numero.toString(),
|
||||||
|
|
@ -137,11 +285,6 @@ 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 clientCommercialMap = useMemo(() => {
|
||||||
const map = new Map<string, string>();
|
const map = new Map<string, string>();
|
||||||
clients.forEach(client => {
|
clients.forEach(client => {
|
||||||
|
|
@ -152,14 +295,51 @@ const OrdersPage = () => {
|
||||||
return map;
|
return map;
|
||||||
}, [clients]);
|
}, [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(() => {
|
const filteredCommandes = useMemo(() => {
|
||||||
|
// 1. Filtrer par période
|
||||||
let result = filterItemByPeriod(commandes, period, 'date');
|
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) {
|
if (activeFilters.status && activeFilters.status.length > 0) {
|
||||||
result = result.filter(item => activeFilters.status!.includes(item.statut.toString()));
|
result = result.filter(item => activeFilters.status!.includes(item.statut.toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. Filtre par commercial
|
||||||
if (activeFilters.rep && activeFilters.rep.length > 0) {
|
if (activeFilters.rep && activeFilters.rep.length > 0) {
|
||||||
result = result.filter(item => {
|
result = result.filter(item => {
|
||||||
const commercialCode = clientCommercialMap.get(item.client_code);
|
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());
|
return [...result].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||||
}, [commandes, period, activeFilters, clientCommercialMap]);
|
}, [commandes, period, activeFilter, 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]);
|
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 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 allColumnsDefinition = useMemo(() => {
|
||||||
const clientsMap = new Map(clients.map(c => [c.numero, c]));
|
const clientsMap = new Map(clients.map(c => [c.numero, c]));
|
||||||
|
|
||||||
|
|
@ -302,7 +421,6 @@ const OrdersPage = () => {
|
||||||
};
|
};
|
||||||
}, [clients]);
|
}, [clients]);
|
||||||
|
|
||||||
// Colonnes filtrées selon la config
|
|
||||||
const visibleColumns = useMemo(() => {
|
const visibleColumns = useMemo(() => {
|
||||||
return columnConfig
|
return columnConfig
|
||||||
.filter(col => col.visible)
|
.filter(col => col.visible)
|
||||||
|
|
@ -310,44 +428,18 @@ const OrdersPage = () => {
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}, [columnConfig, allColumnsDefinition]);
|
}, [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> = {
|
const columnFormatters: Record<string, (value: any, row: any) => string> = {
|
||||||
date: (value) => {
|
date: value => (value ? formatDateFRCourt(value) : ''),
|
||||||
if (!value) return '';
|
total_ht_calcule: value => (value === null || value === undefined ? '0 €' : `${value.toLocaleString('fr-FR', { minimumFractionDigits: 2 })} €`),
|
||||||
return formatDateFRCourt(value);
|
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 })} €`),
|
||||||
total_ht_calcule: (value) => {
|
statut: value => STATUS_LABELS_COMMANDE[value as StatusCodeCommande]?.label || 'Inconnu',
|
||||||
if (value === null || value === undefined) return '0 €';
|
client_intitule: (value, row) => value || row.client_code || '',
|
||||||
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 || '';
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const onDuplicate = async () => {
|
const onDuplicate = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const payloadCreate: CommandeRequest = {
|
const payloadCreate: CommandeRequest = {
|
||||||
client_id: commandeSelected.client_code,
|
client_id: commandeSelected.client_code,
|
||||||
date_commande: (() => {
|
date_commande: (() => {
|
||||||
|
|
@ -361,16 +453,12 @@ const OrdersPage = () => {
|
||||||
})),
|
})),
|
||||||
reference: commandeSelected.reference,
|
reference: commandeSelected.reference,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = (await dispatch(createCommande(payloadCreate)).unwrap()) as CommandeResponse;
|
const result = (await dispatch(createCommande(payloadCreate)).unwrap()) as CommandeResponse;
|
||||||
const data = result.data;
|
const data = result.data;
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
|
|
||||||
const itemCreated = (await dispatch(getCommande(data.numero_commande)).unwrap()) as any;
|
const itemCreated = (await dispatch(getCommande(data.numero_commande)).unwrap()) as any;
|
||||||
const res = itemCreated as Commande;
|
const res = itemCreated as Commande;
|
||||||
dispatch(selectcommande(res));
|
dispatch(selectcommande(res));
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: 'Commande dupliquée avec succès !',
|
title: 'Commande dupliquée avec succès !',
|
||||||
description: `La commande ${commandeSelected.numero} a été 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 flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Commandes</h1>
|
<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>
|
{/* 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>
|
</div>
|
||||||
<PeriodSelector value={period} onChange={setPeriod} />
|
<PeriodSelector value={period} onChange={setPeriod} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3 flex-wrap items-center">
|
<div className="flex gap-3 flex-wrap items-center">
|
||||||
{/* Sélecteur de colonnes */}
|
|
||||||
<ColumnSelector columns={columnConfig} onChange={setColumnConfig} />
|
<ColumnSelector columns={columnConfig} onChange={setColumnConfig} />
|
||||||
<ExportDropdown
|
<ExportDropdown data={filteredCommandes} columns={columnConfig} columnFormatters={columnFormatters} filename="commande" />
|
||||||
data={filteredCommandes}
|
|
||||||
columns={columnConfig}
|
|
||||||
columnFormatters={columnFormatters}
|
|
||||||
filename="commande"
|
|
||||||
/>
|
|
||||||
<AdvancedFilters
|
<AdvancedFilters
|
||||||
filters={filterDefinitions}
|
filters={filterDefinitions}
|
||||||
activeFilters={activeFilters}
|
activeFilters={activeFilters}
|
||||||
onFilterChange={(key, values) => {
|
onFilterChange={(key, values) => {
|
||||||
setActiveFilters(prev => ({
|
setActiveFilters(prev => ({ ...prev, [key]: values }));
|
||||||
...prev,
|
|
||||||
[key]: values,
|
|
||||||
}));
|
|
||||||
}}
|
}}
|
||||||
onReset={() => setActiveFilters({})}
|
onReset={() => setActiveFilters({})}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -137,10 +137,20 @@ const KPI_CONFIG: KPIConfig[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'pending',
|
id: 'pending',
|
||||||
title: 'Nombre de Devis',
|
title: 'Nombre de confirmé',
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
color: 'orange',
|
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) => {
|
getSubtitle: (devis) => {
|
||||||
const avgDevis = devis.length > 0
|
const avgDevis = devis.length > 0
|
||||||
? devis.reduce((sum, item) => sum + item.total_ht, 0) / devis.length
|
? devis.reduce((sum, item) => sum + item.total_ht, 0) / devis.length
|
||||||
|
|
@ -164,7 +174,14 @@ const KPI_CONFIG: KPIConfig[] = [
|
||||||
icon: Percent,
|
icon: Percent,
|
||||||
color: 'purple',
|
color: 'purple',
|
||||||
getValue: (devis, universign) => {
|
getValue: (devis, universign) => {
|
||||||
return universign.filter(u => u.local_status === "SIGNE").length;
|
// 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) => {
|
getSubtitle: (devis, value, universign) => {
|
||||||
const pending = devis.filter(d => d.statut === 1).length;
|
const pending = devis.filter(d => d.statut === 1).length;
|
||||||
|
|
@ -185,7 +202,7 @@ const KPI_CONFIG: KPIConfig[] = [
|
||||||
);
|
);
|
||||||
return devis.filter(d => signedDocIds.has(d.numero));
|
return devis.filter(d => signedDocIds.has(d.numero));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
@ -318,6 +335,7 @@ const QuotesPage = () => {
|
||||||
let result = filterItemByPeriod(devis, period, 'date');
|
let result = filterItemByPeriod(devis, period, 'date');
|
||||||
result = result.filter(item => item.numero !== 'DE00126');
|
result = result.filter(item => item.numero !== 'DE00126');
|
||||||
|
|
||||||
|
|
||||||
// 2. Filtrer par KPI actif
|
// 2. Filtrer par KPI actif
|
||||||
const kpiConfig = KPI_CONFIG.find(k => k.id === activeFilter);
|
const kpiConfig = KPI_CONFIG.find(k => k.id === activeFilter);
|
||||||
if (kpiConfig && activeFilter !== 'all') {
|
if (kpiConfig && activeFilter !== 'all') {
|
||||||
|
|
@ -345,6 +363,7 @@ const QuotesPage = () => {
|
||||||
);
|
);
|
||||||
}, [devis, period, activeFilter, activeFilters, clientCommercialMap, universign]);
|
}, [devis, period, activeFilter, activeFilters, clientCommercialMap, universign]);
|
||||||
|
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// Label du filtre actif
|
// Label du filtre actif
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ const apiService = axios.create({
|
||||||
baseURL: BackUrl,
|
baseURL: BackUrl,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: access_token ? `Bearer ${access_token}` : "dsqd",
|
Authorization: access_token ? `Bearer ${access_token}` : "dsqd",
|
||||||
|
"X-API-Key": "sdk_live_TeeZGyvg34H-nIR7tBhyiemAfIV0MbT2HQ1BzG9bofE"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue