Compare commits

..

No commits in common. "1fc87ac59ed55598085f3c81dc1db6b2a5e48859" and "78e75ce41ceaa6ba74646af75007ae38a48803fd" have entirely different histories.

6 changed files with 274 additions and 410 deletions

View file

@ -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: (avoirs) => avoirs.filter(a => a.statut === 1),
}, },
{ {
id: 'validated', id: 'validated',
title: 'Avoirs à Facturer', title: 'Avoirs Validés',
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,

View file

@ -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 Facturer', color: COLORS.blue }, 2: { label: 'A préparer', color: COLORS.blue },
// 3: { label: 'Facturé', color: COLORS.green }, 3: { label: 'Préparé', 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: (bls) => bls.filter(b => b.statut === 1),
}, },
{ {
id: 'delivered', id: 'delivered',
title: 'BL à facturer', title: 'BL Livrés',
icon: CheckCircle, icon: CheckCircle,
color: 'green', color: 'green',
getValue: (bls) => bls.filter(b => b.statut === 2).length, getValue: (bls) => bls.filter(b => b.statut === 3 || b.statut === 4).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,7 +161,29 @@ 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 === 2), 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),
}, },
{ {
id: 'invoiced', id: 'invoiced',
@ -169,16 +191,16 @@ const KPI_CONFIG: KPIConfig[] = [
icon: TrendingUp, icon: TrendingUp,
color: 'blue', color: 'blue',
getValue: (bls) => { getValue: (bls) => {
const invoiced = bls.filter(b => b.statut === 2); const invoiced = bls.filter(b => b.statut === 4);
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 === 2); const invoiced = bls.filter(b => b.statut === 4);
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 === 2); const previousInvoiced = previousPeriodBL.filter(b => b.statut === 4);
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)
@ -186,11 +208,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 === 2); const previousInvoiced = previousPeriodBL.filter(b => b.statut === 4);
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 === 2), filter: (bls) => bls.filter(b => b.statut === 4),
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' },
}, },
]; ];

View file

@ -79,7 +79,7 @@ export type StatusCode = keyof typeof FACTURE_STATUS_LABELS;
// TYPES // TYPES
// ============================================ // ============================================
type FilterType = 'all' | 'paid' | 'pending' | 'validated' | 'partial'; type FilterType = 'all' | 'paid' | 'pending' | 'validated';
interface FactureWithReglement extends Facture { interface FactureWithReglement extends Facture {
statut_reglement: string; statut_reglement: string;
@ -143,100 +143,63 @@ 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: 'Payés', title: 'Montant Payé',
icon: CheckCircle, icon: CheckCircle,
color: 'green', color: 'green',
getValue: (factures) => { getValue: (factures) => {
const soldees = factures.filter(f => (f as any).statut_display === 6); const paid = factures.filter(f => f.statut === 4);
return Math.round(soldees.reduce((sum, item) => sum + item.total_ttc, 0)); return Math.round(paid.reduce((sum, item) => sum + item.total_ht, 0));
}, },
getSubtitle: (factures) => { getSubtitle: (factures) => {
const soldees = factures.filter(f => (f as any).statut_display === 6); const paid = factures.filter(f => f.statut === 4);
return `${soldees.length} facture${soldees.length > 1 ? 's' : ''}`; return `${paid.length} facture${paid.length > 1 ? 's' : ''}`;
}, },
getChange: (factures, value, period, allFactures) => { getChange: (factures) => {
const soldees = factures.filter(f => (f as any).statut_display === 6); const paid = factures.filter(f => f.statut === 4);
const totalFacture = factures.length; return `${paid.length}/${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 previousSoldees = previousPeriodFactures.filter(f => (f as any).statut_display === 6); const previousPaid = previousPeriodFactures.filter(f => f.statut === 4);
const previousTotal = previousSoldees.reduce((sum, item) => sum + item.total_ttc, 0); const previousPaidAmount = previousPaid.reduce((sum, item) => sum + item.total_ht, 0);
return Number(value) >= previousTotal ? 'up' : 'down'; return Number(value) >= previousPaidAmount ? 'up' : 'down';
}, },
filter: (factures) => factures.filter(f => (f as any).statut_display === 6), filter: (factures) => factures.filter(f => f.statut === 4),
tooltip: { content: "Montant des factures entièrement réglées.", source: "Ventes > Factures" } tooltip: { content: "Montant des factures payées sur la période.", 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: 'Validées', title: 'Factures 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 nonValidees = factures.filter(f => f.valide !== 1); const pending = factures.filter(f => f.statut === 1);
return `${nonValidees.length} non validée${nonValidees.length > 1 ? 's' : ''}`; return `${pending.length} en attente`;
}, },
getChange: (factures) => { getChange: (factures) => {
const validated = factures.filter(f => f.valide === 1); const validated = factures.filter(f => f.valide === 1);
@ -248,7 +211,6 @@ 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" }
}, },
]; ];
@ -394,7 +356,6 @@ const InvoicesPage = () => {
}); });
}, [factures, reglementsMap]); }, [factures, reglementsMap]);
// ============================================ // ============================================
// Client actuellement sélectionné // Client actuellement sélectionné
// ============================================ // ============================================
@ -415,26 +376,25 @@ const InvoicesPage = () => {
// ============================================ // ============================================
const kpis = useMemo(() => { const kpis = useMemo(() => {
// Utiliser facturesWithReglement au lieu de factures const periodFilteredFactures = filterItemByPeriod(factures, period, 'date');
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' || config.id === 'partial' ? `${value.toLocaleString('fr-FR')}` : value, value: config.id === 'all' || config.id === 'paid' ? `${value.toLocaleString('fr-FR')}` : value,
change: config.getChange(periodFilteredFactures, value, period, facturesWithReglement), // aussi ici change: config.getChange(periodFilteredFactures, value, period, factures),
trend: config.getTrend(periodFilteredFactures, value, period, facturesWithReglement), // et ici trend: config.getTrend(periodFilteredFactures, value, period, factures),
icon: config.icon, icon: config.icon,
subtitle: config.getSubtitle(periodFilteredFactures, value), subtitle: config.getSubtitle(periodFilteredFactures, value),
color: config.color, color: config.color,
tooltip: config.tooltip, tooltip: config.tooltip,
isActive: activeFilter === config.id, isActive: activeFilter === config.id,
onClick: () => setActiveFilter(prev => (prev === config.id ? 'all' : config.id)), onClick: () => setActiveFilter(prev => (prev === config.id ? 'all' : config.id)),
}; };
}); });
}, [facturesWithReglement, period, activeFilter]); }, [factures, period, activeFilter]);
// ============================================ // ============================================
// Filtrage combiné : Période + KPI + Filtres avancés // Filtrage combiné : Période + KPI + Filtres avancés
@ -469,7 +429,6 @@ 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
// ============================================ // ============================================

View file

@ -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, X } from 'lucide-react'; import { Plus, ShoppingCart, CheckSquare, AlertCircle, Euro, Eye, Copy } 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,25 +43,6 @@ 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
// ============================================ // ============================================
@ -76,140 +57,6 @@ 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();
@ -217,8 +64,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[];
@ -233,14 +80,18 @@ 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') await dispatch(getArticles()).unwrap(); if (statusArticle === 'idle') {
if (statusClient === 'idle' || statusClient === 'failed') await dispatch(getClients()).unwrap(); await dispatch(getArticles()).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);
} }
@ -257,7 +108,8 @@ const OrdersPage = () => {
load(); load();
}, [statusCommande, dispatch]); }, [statusCommande, dispatch]);
const { refresh } = useDashboardData(); const { refresh } = useDashboardData();
const commercialOptions = useMemo(() => { const commercialOptions = useMemo(() => {
return commercials.map(c => ({ return commercials.map(c => ({
@ -285,61 +137,29 @@ const OrdersPage = () => {
}, },
]; ];
const clientCommercialMap = useMemo(() => { // ============================================
const map = new Map<string, string>(); // 1. CRÉER UNE MAP CLIENT -> COMMERCIAL (dans le composant)
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
// KPIs avec onClick const clientCommercialMap = useMemo(() => {
// ============================================ const map = new Map<string, string>();
clients.forEach(client => {
if (client.commercial?.numero) {
map.set(client.numero, client.commercial.numero.toString());
}
});
return map;
}, [clients]);
const 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);
@ -347,21 +167,82 @@ 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, activeFilter, activeFilters, clientCommercialMap]); }, [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]);
// ============================================ // ============================================
// Label du filtre actif // COLONNES DYNAMIQUES
// ============================================ // ============================================
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]));
@ -421,6 +302,7 @@ 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)
@ -428,18 +310,44 @@ 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 => (value ? formatDateFRCourt(value) : ''), date: (value) => {
total_ht_calcule: value => (value === null || value === undefined ? '0 €' : `${value.toLocaleString('fr-FR', { minimumFractionDigits: 2 })} €`), if (!value) return '';
total_taxes_calcule: value => (value === null || value === undefined ? '0 €' : `${value.toLocaleString('fr-FR', { minimumFractionDigits: 2 })} €`), return formatDateFRCourt(value);
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', total_ht_calcule: (value) => {
client_intitule: (value, row) => value || row.client_code || '', 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 || '';
},
}; };
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: (() => {
@ -453,12 +361,16 @@ 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.`,
@ -529,37 +441,28 @@ 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 data={filteredCommandes} columns={columnConfig} columnFormatters={columnFormatters} filename="commande" /> <ExportDropdown
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 => ({ ...prev, [key]: values })); setActiveFilters(prev => ({
...prev,
[key]: values,
}));
}} }}
onReset={() => setActiveFilters({})} onReset={() => setActiveFilters({})}
/> />

View file

@ -137,20 +137,10 @@ const KPI_CONFIG: KPIConfig[] = [
}, },
{ {
id: 'pending', id: 'pending',
title: 'Nombre de confirmé', title: 'Nombre de Devis',
icon: FileText, icon: FileText,
color: 'orange', color: 'orange',
// getValue: (devis) => { getValue: (devis) => devis.length,
// 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
@ -168,41 +158,34 @@ const KPI_CONFIG: KPIConfig[] = [
}, },
filter: (devis) => devis.filter(d => d.statut === 1), filter: (devis) => devis.filter(d => d.statut === 1),
}, },
{ {
id: 'signed', id: 'signed',
title: 'Devis Signés', title: 'Devis Signés',
icon: Percent, icon: Percent,
color: 'purple', color: 'purple',
getValue: (devis, universign) => { getValue: (devis, universign) => {
// Créer un Set des IDs de documents signés return universign.filter(u => u.local_status === "SIGNE").length;
const signedDocIds = new Set( },
universign getSubtitle: (devis, value, universign) => {
.filter(u => u.local_status === "SIGNE") const pending = devis.filter(d => d.statut === 1).length;
.map(u => u.sage_document_id) return `${pending} en attente`;
); },
// Compter uniquement les devis de la période qui sont signés getChange: (devis, value) => {
return devis.filter(d => signedDocIds.has(d.numero)).length; 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));
},
}, },
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));
},
},
]; ];
// ============================================ // ============================================
@ -335,7 +318,6 @@ 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') {
@ -363,7 +345,6 @@ const QuotesPage = () => {
); );
}, [devis, period, activeFilter, activeFilters, clientCommercialMap, universign]); }, [devis, period, activeFilter, activeFilters, clientCommercialMap, universign]);
// ============================================ // ============================================
// Label du filtre actif // Label du filtre actif
// ============================================ // ============================================

View file

@ -14,7 +14,6 @@ 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"
} }
}) })