import { useState, useMemo, useEffect, useCallback } from 'react'; import { Helmet } from 'react-helmet'; import { useNavigate } from 'react-router-dom'; import { Plus, Eye, Download, CheckCircle, FileText, Euro, AlertTriangle, X, Wallet, Info } from 'lucide-react'; import KPIBar, { PeriodType } from '@/components/KPIBar'; import { toast } from '@/components/ui/use-toast'; import { cn, formatDateFRCourt } from '@/lib/utils'; import { Facture, FactureAction } from '@/types/factureType'; import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { factureStatus, getAllfacture, getfactureSelected } from '@/store/features/factures/selectors'; import { getFactures, selectFactureAsync, validerFacture, getFacture } from '@/store/features/factures/thunk'; import PrimaryButton_v2 from '@/components/PrimaryButton_v2'; import { filterItemByPeriod, getPreviousPeriodItems } from '@/components/filter/ItemsFilter'; import { ModalFacture } from '@/components/modal/ModalFacture'; import { DropdownMenuTable } from '@/components/DropdownMenu'; import { clientStatus, getAllClients } from '@/store/features/client/selectors'; import { articleStatus } from '@/store/features/article/selectors'; import { getArticles } from '@/store/features/article/thunk'; import { getClients } from '@/store/features/client/thunk'; import { Client } from '@/types/clientType'; import ModalStatus from '@/components/modal/ModalStatus'; import StatusBadge, { STATUS_LABELS } from '@/components/ui/StatusBadge'; import { SageDocumentType } from '@/types/sageTypes'; import ColumnSelector, { ColumnConfig } from '@/components/common/ColumnSelector'; import PeriodSelector from '@/components/common/PeriodSelector'; import { CompanyInfo } from '@/data/mockData'; import PDFPreview, { DocumentData } from '@/components/modal/PDFPreview'; import { usePDFPreview } from '@/components/ui/PDFActionButtons'; import ExportDropdown from '@/components/common/ExportDropdown'; import { useDashboardData } from '@/store/hooks/useAppData'; import DataTable from '@/components/ui/DataTable'; import { getAllReglements, reglementStatus } from '@/store/features/reglement/selectors'; import { loadAllReglementData } from '@/store/features/reglement/thunk'; import ModalPaymentPanel from '@/components/modal/ModalPaymentPanel'; import { FacturesReglement } from '@/types/reglementType'; import { selectfacture } from '@/store/features/factures/slice'; import FormModal from '@/components/ui/FormModal'; import ModalValidationWarningCard from '@/components/modal/ModalValidationWarningCard'; import { ModalLoading } from '@/components/modal/ModalLoading'; import AdvancedFilters from '@/components/common/AdvancedFilters'; import { Commercial } from '@/types/commercialType'; import { commercialsStatus, getAllcommercials } from '@/store/features/commercial/selectors'; import { getCommercials } from '@/store/features/commercial/thunk'; // ============================================ // CONSTANTES // ============================================ const STATUT_REGLEMENT = { SOLDE: 'Soldé', PARTIEL: 'Partiellement réglé', NON_REGLE: 'Non réglé', } as const; const COLORS = { gray: 'bg-gray-400', yellow: 'bg-yellow-400', orange: 'bg-orange-400', green: 'bg-green-400', purple: 'bg-purple-400', blue: 'bg-blue-400', }; // Labels de statut pour les factures const FACTURE_STATUS_LABELS = { 0: { label: 'Saisi', color: COLORS.gray }, 1: { label: 'Confirmé', color: COLORS.yellow }, 2: { label: 'A comptabiliser', color: COLORS.orange }, 3: { label: 'Comptabilisé', color: COLORS.green }, // 4: { label: 'Payé', color: COLORS.purple }, 5: { label: 'Validé', color: COLORS.blue }, 6: { label: 'Payé', color: COLORS.green }, 7: { label: 'Partiellement payé', color: COLORS.orange }, } as const; export type StatusCode = keyof typeof FACTURE_STATUS_LABELS; // ============================================ // TYPES // ============================================ type FilterType = 'all' | 'paid' | 'pending' | 'validated'; interface FactureWithReglement extends Facture { statut_reglement: string; montant_regle: number; reste_a_regler: number; statut_display: number; // Statut à afficher (peut être 6 ou 7 selon règlement) } interface KPIConfig { id: FilterType; title: string; icon: React.ElementType; color: string; getValue: (factures: Facture[]) => number | string; getSubtitle: (factures: Facture[], value: number | string) => string; getChange: (factures: Facture[], value: number | string, period: PeriodType, allFactures: Facture[]) => string; getTrend: (factures: Facture[], value: number | string, period: PeriodType, allFactures: Facture[]) => 'up' | 'down' | 'neutral'; filter: (factures: Facture[]) => Facture[]; tooltip?: { content: string; source: string }; } // ============================================ // CONFIGURATION DES COLONNES // ============================================ const DEFAULT_COLUMNS: ColumnConfig[] = [ { key: 'numero', label: 'N° Pièce', visible: true, locked: true }, { key: 'client_code', label: 'Client', visible: true }, { key: 'date', label: 'Date', visible: true }, { key: 'total_ht_calcule', label: 'Montant HT', visible: false }, { key: 'total_ttc_calcule', label: 'Montant TTC', visible: true }, { key: 'reste_a_regler', label: 'Solde dû', 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: (factures) => Math.round(factures.reduce((sum, item) => sum + item.total_ht, 0)), getSubtitle: (factures) => { const totalTTC = factures.reduce((sum, item) => sum + item.total_ttc, 0); return `${totalTTC.toLocaleString('fr-FR')}€ TTC`; }, getChange: (factures, value, period, allFactures) => { const previousPeriodFactures = getPreviousPeriodItems(allFactures, period); const previousTotalHT = previousPeriodFactures.reduce((sum, item) => sum + item.total_ht, 0); return previousTotalHT > 0 ? (((Number(value) - previousTotalHT) / previousTotalHT) * 100).toFixed(1) : '0'; }, getTrend: (factures, value, period, allFactures) => { const previousPeriodFactures = getPreviousPeriodItems(allFactures, period); const previousTotalHT = previousPeriodFactures.reduce((sum, item) => sum + item.total_ht, 0); return Number(value) >= previousTotalHT ? 'up' : 'down'; }, filter: (factures) => factures, tooltip: { content: "Total des factures sur la période.", source: "Ventes > Factures" } }, { id: 'pending', title: 'Nombre de Factures', icon: FileText, color: 'orange', getValue: (factures) => factures.length, getSubtitle: (factures) => { const paid = factures.filter(f => f.statut === 4); const paymentRate = factures.length > 0 ? ((paid.length / factures.length) * 100).toFixed(1) : '0'; return `${paymentRate}% payées`; }, getChange: (factures, value, period, allFactures) => { const previousPeriodFactures = getPreviousPeriodItems(allFactures, period); const countChange = Number(value) - previousPeriodFactures.length; return countChange !== 0 ? `${countChange > 0 ? '+' : ''}${countChange}` : '0'; }, getTrend: (factures, value, period, allFactures) => { const previousPeriodFactures = getPreviousPeriodItems(allFactures, period); return Number(value) >= previousPeriodFactures.length ? 'up' : 'down'; }, filter: (factures) => factures.filter(f => f.statut === 1), }, { id: 'paid', title: 'Montant Payé', icon: CheckCircle, color: 'green', getValue: (factures) => { const paid = factures.filter(f => f.statut === 4); return Math.round(paid.reduce((sum, item) => sum + item.total_ht, 0)); }, getSubtitle: (factures) => { const paid = factures.filter(f => f.statut === 4); return `${paid.length} facture${paid.length > 1 ? 's' : ''}`; }, getChange: (factures) => { const paid = factures.filter(f => f.statut === 4); return `${paid.length}/${factures.length}`; }, getTrend: (factures, value, period, allFactures) => { const previousPeriodFactures = getPreviousPeriodItems(allFactures, period); const previousPaid = previousPeriodFactures.filter(f => f.statut === 4); const previousPaidAmount = previousPaid.reduce((sum, item) => sum + item.total_ht, 0); return Number(value) >= previousPaidAmount ? 'up' : 'down'; }, filter: (factures) => factures.filter(f => f.statut === 4), tooltip: { content: "Montant des factures payées sur la période.", source: "Ventes > Factures" } }, { id: 'validated', title: 'Factures Validées', icon: CheckCircle, color: 'purple', getValue: (factures) => factures.filter(f => f.valide === 1).length, getSubtitle: (factures) => { const pending = factures.filter(f => f.statut === 1); return `${pending.length} en attente`; }, getChange: (factures) => { const validated = factures.filter(f => f.valide === 1); return `${validated.length}/${factures.length}`; }, getTrend: (factures, value, period, allFactures) => { const previousPeriodFactures = getPreviousPeriodItems(allFactures, period); const previousValidated = previousPeriodFactures.filter(f => f.valide === 1); return Number(value) >= previousValidated.length ? 'up' : 'down'; }, filter: (factures) => factures.filter(f => f.valide === 1), }, ]; // ============================================ // COMPOSANT PRINCIPAL // ============================================ const InvoicesPage = () => { const navigate = useNavigate(); const dispatch = useAppDispatch(); const [period, setPeriod] = useState('all'); const [activeFilter, setActiveFilter] = useState('all'); const [columnConfig, setColumnConfig] = useState(DEFAULT_COLUMNS); const [selectedInvoiceIds, setSelectedInvoiceIds] = useState([]); const [isPaymentPanelOpen, setIsPaymentPanelOpen] = useState(false); const [isValidationDialogOpen, setIsValidationDialogOpen] = useState(false); const [isValidating, setIsValidating] = useState(false); const [isSaving, setIsSaving] = useState(false); const [facturesToValidate, setFacturesToValidate] = useState([]); const [activeFilters, setActiveFilters] = useState>({}); const { showPreview, openPreview, closePreview } = usePDFPreview(); const clients = useAppSelector(getAllClients) as Client[]; const commercials = useAppSelector(getAllcommercials) as Commercial[]; const factures = useAppSelector(getAllfacture) as Facture[]; const facturesSelected = useAppSelector(getfactureSelected) as Facture; const statusFacture = useAppSelector(factureStatus); const reglements = useAppSelector(getAllReglements) as FacturesReglement[]; const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [openStatus, setOpenStatus] = useState(false); const [editing, setEditing] = useState(null); const isLoading = statusFacture === 'loading' && factures.length === 0; const statusClient = useAppSelector(clientStatus); const statusArticle = useAppSelector(articleStatus); const statusReglement = useAppSelector(reglementStatus); const statusCommercial = useAppSelector(commercialsStatus); useEffect(() => { const load = async () => { try { if (statusArticle === 'idle') await dispatch(getArticles()).unwrap(); if (statusClient === 'idle' || statusClient === 'failed') await dispatch(getClients()).unwrap(); if (statusFacture === 'idle' || statusFacture === 'failed') await dispatch(getFactures()).unwrap(); if (statusReglement === 'idle' || statusReglement === 'failed') await dispatch(loadAllReglementData()).unwrap(); if (statusCommercial === 'idle') await dispatch(getCommercials()).unwrap(); } catch (error) { console.error(error); } }; load(); }, [statusArticle, statusClient, statusFacture, statusReglement, statusCommercial, dispatch]); const { refresh } = useDashboardData(); // ============================================ // OPTIONS POUR LES FILTRES // ============================================ const commercialOptions = useMemo(() => { return commercials.map(c => ({ value: c.numero.toString(), label: `${c.prenom || ''} ${c.nom || ''}`.trim() || `Commercial ${c.numero}`, })); }, [commercials]); // Map pour retrouver le commercial d'un client rapidement const clientCommercialMap = useMemo(() => { const map = new Map(); clients.forEach(client => { if (client.commercial?.numero) { map.set(client.numero, client.commercial.numero.toString()); } }); return map; }, [clients]); const filterDefinitions = [ { key: 'status', label: 'Statut', options: (Object.entries(FACTURE_STATUS_LABELS) as [string, typeof FACTURE_STATUS_LABELS[StatusCode]][]).map( ([value, { label, color }]) => ({ value: value.toString(), label, color, }) ), }, { key: 'rep', label: 'Commercial', options: commercialOptions, }, ]; // ============================================ // Map des règlements par numéro de facture // ============================================ const reglementsMap = useMemo(() => { return new Map(reglements.map(r => [r.numero, r])); }, [reglements]); // ============================================ // Factures enrichies avec statut règlement // ============================================ const facturesWithReglement = useMemo((): FactureWithReglement[] => { return factures.map(f => { const reglement = reglementsMap.get(f.numero); let resteARegler = f.total_ttc_calcule; let montantRegle = 0; let statutReglement: string = STATUT_REGLEMENT.NON_REGLE; let statutDisplay = f.statut; // Par défaut, le statut original if (reglement) { statutReglement = reglement.statut_reglement || STATUT_REGLEMENT.NON_REGLE; montantRegle = reglement.montants?.montant_regle || 0; // Déterminer le statut_display selon le règlement if (statutReglement === STATUT_REGLEMENT.SOLDE) { resteARegler = 0; statutDisplay = 6; // Payé (via règlement) } else if (statutReglement === STATUT_REGLEMENT.PARTIEL) { resteARegler = reglement.montants?.reste_a_regler || 0; statutDisplay = 7; // Partiellement payé } else { resteARegler = reglement.montants?.reste_a_regler || f.total_ttc_calcule; } } return { ...f, statut_reglement: statutReglement, montant_regle: montantRegle, reste_a_regler: resteARegler, statut_display: statutDisplay, }; }); }, [factures, reglementsMap]); // ============================================ // Client actuellement sélectionné // ============================================ const selectedClientCode = useMemo(() => { if (selectedInvoiceIds.length === 0) return null; const firstSelectedFacture = facturesWithReglement.find(f => f.numero === selectedInvoiceIds[0]); return firstSelectedFacture?.client_code || null; }, [selectedInvoiceIds, facturesWithReglement]); const selectedClient = useMemo(() => { if (!selectedClientCode) return null; return clients.find(c => c.numero === selectedClientCode); }, [selectedClientCode, clients]); // ============================================ // KPIs // ============================================ const kpis = useMemo(() => { const periodFilteredFactures = filterItemByPeriod(factures, period, 'date'); return KPI_CONFIG.map(config => { const value = config.getValue(periodFilteredFactures); return { id: config.id, title: config.title, value: config.id === 'all' || config.id === 'paid' ? `${value.toLocaleString('fr-FR')}€` : value, change: config.getChange(periodFilteredFactures, value, period, factures), trend: config.getTrend(periodFilteredFactures, value, period, factures), icon: config.icon, subtitle: config.getSubtitle(periodFilteredFactures, value), color: config.color, tooltip: config.tooltip, isActive: activeFilter === config.id, onClick: () => setActiveFilter(prev => (prev === config.id ? 'all' : config.id)), }; }); }, [factures, period, activeFilter]); // ============================================ // Filtrage combiné : Période + KPI + Filtres avancés // ============================================ const filteredFactures = useMemo(() => { // 1. Filtrer par période let result: FactureWithReglement[] = filterItemByPeriod(facturesWithReglement, period, 'date'); // 2. Filtrer par KPI actif const kpiConfig = KPI_CONFIG.find(k => k.id === activeFilter); if (kpiConfig && activeFilter !== 'all') { result = kpiConfig.filter(result) as FactureWithReglement[]; } // 3. Filtres avancés (statut) if (activeFilters.status && activeFilters.status.length > 0) { result = result.filter(item => activeFilters.status!.includes(item.statut_display.toString()) ); } // 4. Filtre par commercial if (activeFilters.rep && activeFilters.rep.length > 0) { result = result.filter(item => { const commercialCode = clientCommercialMap.get(item.client_code); return commercialCode && activeFilters.rep!.includes(commercialCode); }); } // 5. Tri par date décroissante return [...result].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); }, [facturesWithReglement, period, activeFilter, activeFilters, clientCommercialMap]); // ============================================ // Factures sélectionnables // ============================================ const selectableFactures = useMemo(() => { return filteredFactures.filter(f => { const isNotSolde = f.statut_reglement !== STATUT_REGLEMENT.SOLDE; const isSameClient = !selectedClientCode || f.client_code === selectedClientCode; return isNotSolde && isSameClient; }); }, [filteredFactures, selectedClientCode]); // ============================================ // Factures sélectionnées (avec données de règlement) // ============================================ const selectedFactures = useMemo(() => { return facturesWithReglement.filter(f => selectedInvoiceIds.includes(f.numero)); }, [selectedInvoiceIds, facturesWithReglement]); // ============================================ // Total sélectionné = somme des reste_a_regler // ============================================ const totalSelectedBalance = useMemo(() => { return selectedFactures.reduce((sum, f) => sum + f.reste_a_regler, 0); }, [selectedFactures]); const formatCurrency = (amount: number) => { return `${amount.toLocaleString('fr-FR', { minimumFractionDigits: 2 })}€`; }; // ============================================ // Label du filtre actif // ============================================ const activeFilterLabel = useMemo(() => { const config = KPI_CONFIG.find(k => k.id === activeFilter); return config?.title || 'Tous'; }, [activeFilter]); // ============================================ // Handlers // ============================================ const handleSelectInvoice = (numero: string) => { const facture = facturesWithReglement.find(f => f.numero === numero); if (!facture) return; if (facture.statut_reglement === STATUT_REGLEMENT.SOLDE) { toast({ title: 'Facture déjà réglée', description: 'Cette facture est déjà soldée.', variant: 'destructive', }); return; } if (selectedClientCode && facture.client_code !== selectedClientCode) { toast({ title: 'Client différent', description: 'Vous ne pouvez sélectionner que des factures du même client.', variant: 'destructive', }); return; } setSelectedInvoiceIds(prev => (prev.includes(numero) ? prev.filter(id => id !== numero) : [...prev, numero])); }; const handleSelectAll = (checked: boolean) => { if (checked && selectableFactures.length > 0) { const firstClientCode = selectableFactures[0].client_code; const sameClientFactures = selectableFactures.filter(f => f.client_code === firstClientCode); setSelectedInvoiceIds(sameClientFactures.map(f => f.numero)); } else { setSelectedInvoiceIds([]); } }; // ============================================ // Validation automatique des factures // ============================================ const validateFacturesSequentially = useCallback( async (facturesToValidate: FactureWithReglement[]) => { setIsValidating(true); try { for (const facture of facturesToValidate) { await dispatch(validerFacture(facture.numero)).unwrap(); const itemUpdated = (await dispatch(getFacture(facture.numero)).unwrap()) as Facture; dispatch(selectfacture(itemUpdated)); } toast({ title: 'Factures validées', description: `${facturesToValidate.length} facture(s) validée(s) avec succès.`, className: 'bg-green-500 text-white border-green-600', }); await dispatch(getFactures()).unwrap(); return true; } catch (err: any) { toast({ title: 'Erreur de validation', description: err.message || 'Impossible de valider les factures.', variant: 'destructive', }); return false; } finally { setIsValidating(false); } }, [dispatch] ); const handlePayment = useCallback(() => { if (selectedInvoiceIds.length === 0) return; const nonValidatedFactures = selectedFactures.filter(f => f.valide !== 1); if (nonValidatedFactures.length > 0) { setFacturesToValidate(nonValidatedFactures); setIsValidationDialogOpen(true); } else { setIsPaymentPanelOpen(true); } }, [selectedInvoiceIds, selectedFactures]); const handleConfirmValidationAndPayment = useCallback(async () => { setIsValidationDialogOpen(false); setIsSaving(true); const success = await validateFacturesSequentially(facturesToValidate); if (success) { setIsPaymentPanelOpen(true); } setIsSaving(false); setFacturesToValidate([]); }, [facturesToValidate, validateFacturesSequentially]); const handleCancelValidation = useCallback(() => { setIsValidationDialogOpen(false); setFacturesToValidate([]); }, []); const handlePaymentSuccess = useCallback(() => { setSelectedInvoiceIds([]); setIsPaymentPanelOpen(false); toast({ title: 'Règlement effectué', description: 'Les factures ont été réglées avec succès.', className: 'bg-green-500 text-white border-green-600', }); dispatch(loadAllReglementData()); dispatch(getFactures()); }, [dispatch]); // ============================================ // PDF Data // ============================================ const pdfData = useMemo(() => { if (!facturesSelected) return null; return { numero: facturesSelected.numero, type: 'facture' as const, date: facturesSelected.date, client: { code: facturesSelected.client_code, nom: facturesSelected.client_intitule, adresse: facturesSelected.client_adresse, code_postal: facturesSelected.client_code_postal, ville: facturesSelected.client_ville, email: facturesSelected.client_email, telephone: facturesSelected.client_telephone, }, reference_externe: facturesSelected.reference, lignes: (facturesSelected.lignes ?? []).map(l => ({ article: l.article_code, designation: l.designation, quantite: l.quantite, prix_unitaire: l.prix_unitaire_ht ?? 0, tva: 20, total_ht: l.quantite * (l.prix_unitaire_ht ?? 0), })), total_ht: facturesSelected.total_ht_calcule, total_tva: facturesSelected.total_taxes_calcule, total_ttc: facturesSelected.total_ttc_calcule, }; }, [facturesSelected]) as DocumentData; const handleCreate = () => navigate('/home/factures/nouveau'); const handleEdit = (row: Facture) => { setEditing(row); setIsCreateModalOpen(true); }; const handleAction = (action: FactureAction, row: Facture) => { const messages: Record = { send: { title: 'Facture envoyée', description: `Email envoyé à ${row.client_intitule}` }, download: { title: 'Téléchargement', description: 'Génération du PDF en cours...' }, remind: { title: 'Relance envoyée', description: 'Le client a été notifié.' }, paid: { title: 'Facture payée', description: 'Statut mis à jour avec succès.' }, }; toast(messages[action]); }; const openPDF = async (row: Facture) => { await dispatch(selectFactureAsync(row)).unwrap(); openPreview(); }; // ============================================ // COLONNES // ============================================ const allColumnsDefinition = useMemo(() => { const clientsMap = new Map(clients.map(c => [c.numero, c])); return { numero: { key: 'numero', label: 'N° Pièce', sortable: true, render: (value: string) => {value}, }, client_code: { key: 'client_code', label: 'Client', sortable: true, render: (clientCode: string) => { const client = clientsMap.get(clientCode); if (!client) return {clientCode}; const avatar = client.intitule?.charAt(0).toUpperCase() || '?'; return (
{avatar}

{client.intitule}

{client.email || client.telephone || clientCode}

); }, }, date: { key: 'date', label: 'Date', sortable: true, render: (date: string) => {formatDateFRCourt(date)}, }, total_ht_calcule: { key: 'total_ht_calcule', label: 'Montant HT', sortable: true, render: (v: number) => `${v.toLocaleString()}€`, }, total_ttc_calcule: { key: 'total_ttc_calcule', label: 'Montant TTC', sortable: true, render: (v: number) => {formatCurrency(v)}, }, reste_a_regler: { key: 'reste_a_regler', label: 'Solde dû', sortable: true, render: (value: number, row: FactureWithReglement) => { const isSolde = row.statut_reglement === STATUT_REGLEMENT.SOLDE; const isPartiel = row.statut_reglement === STATUT_REGLEMENT.PARTIEL; return (
{formatCurrency(value)} {isPartiel && row.montant_regle > 0 && ( (déjà réglé: {formatCurrency(row.montant_regle)}) )}
); }, }, statut: { key: 'statut', label: 'Statut', sortable: true, render: (v: number, row: FactureWithReglement) => ( ), }, }; }, [clients]); const visibleColumns = useMemo(() => { return columnConfig .filter(col => col.visible) .map(col => allColumnsDefinition[col.key as keyof typeof allColumnsDefinition]) .filter(Boolean); }, [columnConfig, allColumnsDefinition]); const columnFormatters: Record string> = { date: value => (value ? formatDateFRCourt(value) : ''), total_ht_calcule: value => formatCurrency(value || 0), total_ttc_calcule: value => formatCurrency(value || 0), reste_a_regler: value => formatCurrency(value || 0), statut: (value, row) => row.statut_reglement || FACTURE_STATUS_LABELS[value as StatusCode]?.label || 'Inconnu', client_intitule: (value, row) => value || row.client_code || '', }; const actions = (row: FactureWithReglement) => { const handleStatus = row.statut !== 2 && row.statut !== 3 && row.statut !== 4 ? async () => { await dispatch(selectFactureAsync(row)).unwrap(); setOpenStatus(true); } : undefined; return ( <> { const rep = (await dispatch(selectFactureAsync(row)).unwrap()) as Facture; handleEdit(rep); }} onDownload={() => openPDF(row)} /> ); }; // ============================================ // Conversion pour le modal de paiement // ============================================ const selectedFacturesForModal = useMemo((): Facture[] => { return selectedFactures.map(f => ({ ...f, total_ttc_calcule: f.reste_a_regler, })); }, [selectedFactures]); return ( <> Factures - Dataven

Factures

{activeFilter !== 'all' && ( )}

{filteredFactures.length} facture{filteredFactures.length > 1 ? 's' : ''} {activeFilter !== 'all' && ` (${activeFilterLabel.toLowerCase()})`}

{ setActiveFilters(prev => ({ ...prev, [key]: values, })); }} onReset={() => setActiveFilters({})} /> {selectedInvoiceIds.length > 0 && (
{selectedClient && ( {selectedClient.intitule} )} Régler ({selectedInvoiceIds.length}) · {formatCurrency(totalSelectedBalance)}
)} Nouvelle facture
{selectedClientCode && (
Sélection limitée au client {selectedClient?.intitule}.
)} { await dispatch(selectFactureAsync(row)).unwrap(); navigate(`/home/factures/${row.numero}`); }} />
setOpenStatus(false)} type_doc={SageDocumentType.FACTURE} /> setIsCreateModalOpen(false)} title={editing ? `Mettre à jour la facture ${editing.numero}` : 'Créer une facture'} editing={editing} /> setIsValidationDialogOpen(false)} title="Valider la facture" >
setIsPaymentPanelOpen(false)} selectedInvoices={selectedFacturesForModal} totalAmount={totalSelectedBalance} /> {isSaving && } ); }; export default InvoicesPage;