From 0b8c217a9196c7e09afac0549120cf6d897cdf58 Mon Sep 17 00:00:00 2001 From: mickael Date: Tue, 20 Jan 2026 22:07:08 +0300 Subject: [PATCH] add payement detail page --- src/pages/sales/PaymentDetailPage.jsx | 514 ---------------------- src/pages/sales/PaymentDetailPage.tsx | 607 ++++++++++++++++++++++++++ src/pages/sales/PaymentsPage.tsx | 6 +- src/routes/DatavenRoute.jsx | 3 +- 4 files changed, 614 insertions(+), 516 deletions(-) delete mode 100644 src/pages/sales/PaymentDetailPage.jsx create mode 100644 src/pages/sales/PaymentDetailPage.tsx diff --git a/src/pages/sales/PaymentDetailPage.jsx b/src/pages/sales/PaymentDetailPage.jsx deleted file mode 100644 index 4570aa6..0000000 --- a/src/pages/sales/PaymentDetailPage.jsx +++ /dev/null @@ -1,514 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Helmet } from 'react-helmet'; -import { useParams, useNavigate, useLocation } from 'react-router-dom'; -import { - ArrowLeft, Lock, Download, MoreVertical, Trash2, - CheckCircle, Plus, RefreshCw, Eye -} from 'lucide-react'; -import StatusBadge from '@/components/StatusBadge'; -import PrimaryButton from '@/components/PrimaryButton'; -import Tabs from '@/components/Tabs'; -import Timeline from '@/components/Timeline'; -import { Input, Select, Textarea, FormField } from '@/components/FormModal'; -import ClientSearchInput from '@/components/inputs/ClientSearchInput'; -import PDFPreviewPanel from '@/components/panels/PDFPreviewPanel'; -import { toast } from '@/components/ui/use-toast'; -import { mockPayments, mockInvoices, mockClients, currentUser } from '@/data/mockData'; -import { cn } from '@/lib/utils'; - -const formatCurrency = (val) => new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(val); - -const emptyPayment = { - id: 'new', - number: 'REG-PROVISOIRE', - clientId: null, - clientName: '', - date: new Date().toISOString().split('T')[0], - mode: 'Virement', - reference: '', - amount: 0, - allocatedAmount: 0, - remainingBalance: 0, - status: 'Brouillon', - notes: '', - allocations: [], - timeline: [] -}; - -const PaymentDetailPage = () => { - const { id } = useParams(); - const navigate = useNavigate(); - const location = useLocation(); - const isNew = id === 'new'; - - const [activeTab, setActiveTab] = useState('identification'); - const [isEditing, setIsEditing] = useState(isNew); - const [isPDFPreviewOpen, setIsPDFPreviewOpen] = useState(false); - - const [payment, setPayment] = useState(null); - const [selectedClient, setSelectedClient] = useState(null); - const [clientInvoices, setClientInvoices] = useState([]); - - useEffect(() => { - if (isNew) { - // Check for initial client from state (e.g., coming from Client Detail) - const initialClient = location.state?.client || null; - const initialInvoice = location.state?.invoice || null; - - let newPayment = { ...emptyPayment }; - if (initialClient) { - newPayment.clientId = initialClient.id; - newPayment.clientName = initialClient.company; - setSelectedClient(initialClient); - } - - if (initialInvoice) { - // Pre-fill amount from invoice balance - newPayment.amount = initialInvoice.balanceDue; - newPayment.reference = `Regl. ${initialInvoice.number}`; - } - - setPayment(newPayment); - setIsEditing(true); - setActiveTab('identification'); - } else { - const found = mockPayments.find(p => p.id === parseInt(id)); - if (found) { - setPayment({ - ...found, - // Ensure fields exist - notes: found.notes || '', - timeline: found.timeline || [] - }); - const fullClient = mockClients.find(c => c.id === found.clientId); - setSelectedClient(fullClient || { id: found.clientId, company: found.clientName }); - } - } - }, [id, isNew, location.state]); - - // Load Invoices when client changes - useEffect(() => { - if (payment?.clientId) { - // Find unpaid invoices for this client - const openInvoices = mockInvoices.filter(inv => - inv.clientId === payment.clientId && - inv.status !== 'Brouillon' && - inv.balanceDue > 0 - ).sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate)); // Sort by Due Date Ascending - - // Augment invoices with allocation data from current payment - const invoicesWithAllocation = openInvoices.map(inv => { - const existingAlloc = payment.allocations.find(a => a.invoiceId === inv.id); - return { - ...inv, - currentAllocation: existingAlloc ? existingAlloc.amount : 0 - }; - }); - - setClientInvoices(invoicesWithAllocation); - } else { - setClientInvoices([]); - } - }, [payment?.clientId, payment?.allocations]); // Re-run if allocations change to keep sync, though simple state update below handles input - - // Calculations - useEffect(() => { - if (!payment) return; - // Only recalc if editing - if (isEditing) { - const totalAllocated = payment.allocations.reduce((sum, a) => sum + a.amount, 0); - setPayment(prev => ({ - ...prev, - allocatedAmount: totalAllocated, - remainingBalance: Math.max(0, prev.amount - totalAllocated) - })); - } - }, [payment?.allocations, payment?.amount, isEditing]); - - if (!payment) return
; - - const isDraft = payment.status === 'Brouillon'; - const isValidated = payment.status === 'Validé' || payment.status === 'Imputé' || payment.status === 'Crédit client'; - const fieldsDisabled = !isEditing || isValidated; - - // --- HANDLERS --- - const handleClientSelect = (client) => { - if (fieldsDisabled) return; - setSelectedClient(client); - setPayment(prev => ({ - ...prev, - clientName: client ? (client.company || `${client.firstName} ${client.lastName}`) : '', - clientId: client ? client.id : null, - allocations: [] // Clear allocations on client change - })); - }; - - const handleAmountChange = (val) => { - const newAmount = parseFloat(val) || 0; - setPayment(prev => ({ ...prev, amount: newAmount })); - }; - - const handleAutoAllocate = () => { - let remaining = payment.amount; - const newAllocations = []; - - const updatedInvoices = [...clientInvoices]; // These are sorted by due date - - for (const inv of updatedInvoices) { - if (remaining <= 0) break; - - const amountToAllocate = Math.min(remaining, inv.balanceDue); - if (amountToAllocate > 0) { - newAllocations.push({ - invoiceId: inv.id, - invoiceNumber: inv.number, - amount: amountToAllocate - }); - remaining -= amountToAllocate; - } - } - - setPayment(prev => ({ - ...prev, - allocations: newAllocations - })); - - toast({ title: "Imputation automatique", description: "Les montants ont été répartis par échéance." }); - }; - - const handleManualAllocation = (invoiceId, val) => { - const amount = parseFloat(val) || 0; - // Validate max amount - const invoice = clientInvoices.find(i => i.id === invoiceId); - if (!invoice) return; - - if (amount > invoice.balanceDue) { - // Prevent over allocation on invoice - return; - } - - // Check global limit? Maybe soft check or just visual - - setPayment(prev => { - const filtered = prev.allocations.filter(a => a.invoiceId !== invoiceId); - if (amount > 0) { - filtered.push({ - invoiceId: invoiceId, - invoiceNumber: invoice.number, - amount: amount - }); - } - return { ...prev, allocations: filtered }; - }); - }; - - const handleSave = () => { - if (!payment.clientId) { - toast({ title: "Client manquant", description: "Veuillez sélectionner un client.", variant: "destructive" }); - return; - } - if (payment.amount <= 0) { - toast({ title: "Montant invalide", description: "Le montant doit être supérieur à 0.", variant: "destructive" }); - return; - } - - setIsEditing(false); - if (isNew) { - toast({ title: "Règlement créé", description: "Le brouillon a été enregistré.", variant: "success" }); - navigate('/reglements'); - } else { - setPayment(prev => ({ - ...prev, - timeline: [{ date: new Date().toISOString(), type: 'update', title: 'Règlement modifié', user: currentUser.name }, ...prev.timeline] - })); - toast({ title: "Sauvegardé", description: "Les modifications ont été enregistrées.", variant: "success" }); - } - }; - - const handleValidate = () => { - let finalStatus = 'Validé'; - if (payment.remainingBalance === 0 && payment.amount > 0) finalStatus = 'Imputé'; - if (payment.remainingBalance > 0) finalStatus = 'Crédit client'; - - setPayment(prev => ({ - ...prev, - status: finalStatus, - number: prev.number.replace('PROVISOIRE', `REG-2025-${Math.floor(Math.random()*1000)}`), - timeline: [{ date: new Date().toISOString(), type: 'validation', title: 'Règlement validé', description: `Statut: ${finalStatus}`, user: currentUser.name }, ...prev.timeline] - })); - setIsEditing(false); - toast({ title: "Règlement validé", description: "Le paiement est verrouillé et les écritures sont passées.", variant: "success" }); - }; - - const tabs = [ - { id: 'identification', label: 'Identification' }, - { id: 'allocations', label: 'Imputations', count: payment.allocations.length }, - { id: 'documents', label: 'Documents', count: payment.linkedDocuments?.length || 0 }, - { id: 'timeline', label: 'Historique' }, - ]; - - return ( - <> - - {payment.number} - Règlement - Bijou ERP - - -
- - {/* TOP HEADER */} -
-
-
-
- -
-
-

{payment.number}

- -
-
- {payment.clientName || 'Client inconnu'} - - {new Date(payment.date).toLocaleDateString()} - {formatCurrency(payment.amount)} -
-
-
- -
- - - {isDraft && ( - - Valider - - )} - - {!isDraft && !isNew && ( - - )} - - {isDraft && !isEditing && ( - - )} - - {isEditing && ( - <> - - Enregistrer - - )} -
-
-
-
- - {/* LOCKED BANNER */} - {isValidated && ( -
-

- - Ce règlement est validé et n'est plus modifiable. -

-
- )} - - {/* CONTENT */} -
- - -
- {/* --- IDENTIFICATION TAB --- */} - {activeTab === 'identification' && ( -
-
-

Détails du paiement

-
- - - -
- - !fieldsDisabled && setPayment({...payment, date: e.target.value})} disabled={fieldsDisabled} /> - - - - -
- - !fieldsDisabled && setPayment({...payment, reference: e.target.value})} disabled={fieldsDisabled} placeholder="Ex: VIR 12345" /> - - - !fieldsDisabled && handleAmountChange(e.target.value)} - disabled={fieldsDisabled} - className="font-bold text-lg" - /> - - -