add payement detail page

This commit is contained in:
mickael 2026-01-20 22:07:08 +03:00
parent ac06585c0e
commit 0b8c217a91
4 changed files with 614 additions and 516 deletions

View file

@ -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 <div className="min-h-screen flex items-center justify-center"><div className="animate-spin w-8 h-8 border-4 border-[#941403] border-t-transparent rounded-full"></div></div>;
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 (
<>
<Helmet>
<title>{payment.number} - Règlement - Bijou ERP</title>
</Helmet>
<div className="flex flex-col min-h-screen bg-gray-50/50 dark:bg-black/20 pb-20">
{/* TOP HEADER */}
<div className="bg-white dark:bg-gray-950 border-b border-gray-200 dark:border-gray-800 sticky top-0 z-30 shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<div className="flex items-center gap-4">
<button onClick={() => navigate('/reglements')} className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors text-gray-500">
<ArrowLeft className="w-5 h-5" />
</button>
<div>
<div className="flex items-center gap-3">
<h1 className="text-xl font-bold text-gray-900 dark:text-white">{payment.number}</h1>
<StatusBadge status={payment.status} />
</div>
<div className="flex items-center gap-2 text-sm text-gray-500">
<span className="font-medium text-gray-900 dark:text-gray-300">{payment.clientName || 'Client inconnu'}</span>
<span></span>
<span>{new Date(payment.date).toLocaleDateString()}</span>
<span className="text-[#941403] font-bold ml-2">{formatCurrency(payment.amount)}</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setIsPDFPreviewOpen(true)}
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-200 rounded-xl hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-700 transition-colors"
>
<Eye className="w-4 h-4" />
<span className="hidden sm:inline">Aperçu</span>
</button>
{isDraft && (
<PrimaryButton onClick={handleValidate} className="bg-blue-600 hover:bg-blue-700">
<Lock className="w-4 h-4 mr-2" /> Valider
</PrimaryButton>
)}
{!isDraft && !isNew && (
<button className="p-2 border border-gray-200 dark:border-gray-800 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-900 text-gray-600 dark:text-gray-400" title="Télécharger">
<Download className="w-5 h-5"/>
</button>
)}
{isDraft && !isEditing && (
<button
onClick={() => setIsEditing(true)}
className="px-4 py-2 text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white border border-gray-200 dark:border-gray-700 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
Modifier
</button>
)}
{isEditing && (
<>
<button
onClick={() => isNew ? navigate('/reglements') : setIsEditing(false)}
className="px-4 py-2 text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors"
>
Annuler
</button>
<PrimaryButton onClick={handleSave}>Enregistrer</PrimaryButton>
</>
)}
</div>
</div>
</div>
</div>
{/* LOCKED BANNER */}
{isValidated && (
<div className="bg-blue-50 dark:bg-blue-900/20 border-b border-blue-100 dark:border-blue-800 px-4 py-3 text-center">
<p className="text-sm text-blue-800 dark:text-blue-200 font-medium flex items-center justify-center gap-2">
<Lock className="w-4 h-4" />
Ce règlement est validé et n'est plus modifiable.
</p>
</div>
)}
{/* CONTENT */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full mt-6">
<Tabs tabs={tabs} active={activeTab} onChange={setActiveTab} />
<div className="mt-6">
{/* --- IDENTIFICATION TAB --- */}
{activeTab === 'identification' && (
<div className="space-y-6">
<div className="bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 rounded-2xl p-6 relative overflow-hidden">
<h3 className="text-lg font-bold text-gray-900 dark:text-white mb-4">Détails du paiement</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<FormField label="Client *" required fullWidth>
<ClientSearchInput
value={selectedClient}
onChange={handleClientSelect}
disabled={fieldsDisabled}
placeholder="Rechercher un client..."
/>
</FormField>
<div className="grid grid-cols-2 gap-4">
<FormField label="Date de règlement" required>
<Input type="date" value={payment.date} onChange={(e) => !fieldsDisabled && setPayment({...payment, date: e.target.value})} disabled={fieldsDisabled} />
</FormField>
<FormField label="Mode de paiement">
<Select value={payment.mode} onChange={(e) => !fieldsDisabled && setPayment({...payment, mode: e.target.value})} disabled={fieldsDisabled}>
<option value="Virement">Virement</option>
<option value="CB">Carte Bancaire</option>
<option value="Prélèvement">Prélèvement</option>
<option value="Chèque">Chèque</option>
<option value="Espèces">Espèces</option>
<option value="Autre">Autre</option>
</Select>
</FormField>
</div>
<FormField label="Référence bancaire / Libellé">
<Input value={payment.reference} onChange={(e) => !fieldsDisabled && setPayment({...payment, reference: e.target.value})} disabled={fieldsDisabled} placeholder="Ex: VIR 12345" />
</FormField>
<FormField label="Montant reçu (€) *" required>
<Input
type="number"
min="0"
step="0.01"
value={payment.amount}
onChange={(e) => !fieldsDisabled && handleAmountChange(e.target.value)}
disabled={fieldsDisabled}
className="font-bold text-lg"
/>
</FormField>
<FormField label="Notes internes" fullWidth>
<Textarea value={payment.notes} onChange={(e) => !fieldsDisabled && setPayment({...payment, notes: e.target.value})} disabled={fieldsDisabled} rows={2} />
</FormField>
</div>
</div>
</div>
)}
{/* --- ALLOCATIONS TAB --- */}
{activeTab === 'allocations' && (
<div className="bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 rounded-2xl p-6 shadow-sm">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-6">
<div>
<h3 className="text-lg font-bold text-gray-900 dark:text-white">Imputations</h3>
<p className="text-sm text-gray-500">Répartissez le montant reçu sur les factures en attente.</p>
</div>
{!fieldsDisabled && (
<button
onClick={handleAutoAllocate}
className="flex items-center gap-2 px-4 py-2 bg-blue-50 text-blue-700 hover:bg-blue-100 rounded-lg text-sm font-medium transition-colors"
>
<RefreshCw className="w-4 h-4" /> Imputation automatique
</button>
)}
</div>
{/* Summary Bar */}
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="p-4 bg-gray-50 dark:bg-gray-900 rounded-xl border border-gray-100 dark:border-gray-800">
<p className="text-xs text-gray-500 uppercase font-bold">Montant Reçu</p>
<p className="text-xl font-bold text-gray-900 dark:text-white">{formatCurrency(payment.amount)}</p>
</div>
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-xl border border-green-100 dark:border-green-800">
<p className="text-xs text-green-700 uppercase font-bold">Montant Imputé</p>
<p className="text-xl font-bold text-green-700">{formatCurrency(payment.allocatedAmount)}</p>
</div>
<div className={cn(
"p-4 rounded-xl border",
payment.remainingBalance > 0 ? "bg-purple-50 dark:bg-purple-900/20 border-purple-100 dark:border-purple-800" : "bg-gray-50 dark:bg-gray-900 border-gray-100 dark:border-gray-800"
)}>
<p className={cn("text-xs uppercase font-bold", payment.remainingBalance > 0 ? "text-purple-700" : "text-gray-500")}>
{payment.remainingBalance > 0 ? "Solde (Crédit)" : "Solde"}
</p>
<p className={cn("text-xl font-bold", payment.remainingBalance > 0 ? "text-purple-700" : "text-gray-500")}>
{formatCurrency(payment.remainingBalance)}
</p>
</div>
</div>
{clientInvoices.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-gray-50 dark:bg-gray-900 text-xs uppercase text-gray-500 font-semibold border-b border-gray-200 dark:border-gray-800">
<tr>
<th className="px-4 py-3 text-left">Facture</th>
<th className="px-4 py-3 text-left">Date</th>
<th className="px-4 py-3 text-left text-red-600"> le</th>
<th className="px-4 py-3 text-right">Montant TTC</th>
<th className="px-4 py-3 text-right">Reste à payer</th>
<th className="px-4 py-3 text-right w-40">Imputation</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
{clientInvoices.map(inv => {
const allocatedHere = payment.allocations.find(a => a.invoiceId === inv.id)?.amount || 0;
return (
<tr key={inv.id} className="hover:bg-gray-50 dark:hover:bg-gray-900/50">
<td className="px-4 py-3 font-medium">{inv.number}</td>
<td className="px-4 py-3 text-gray-500">{new Date(inv.date).toLocaleDateString()}</td>
<td className="px-4 py-3 text-red-600 font-medium">{new Date(inv.dueDate).toLocaleDateString()}</td>
<td className="px-4 py-3 text-right text-gray-600">{formatCurrency(inv.amountTTC)}</td>
<td className="px-4 py-3 text-right font-bold">{formatCurrency(inv.balanceDue)}</td>
<td className="px-4 py-3 text-right">
<Input
type="number"
min="0"
max={inv.balanceDue}
value={allocatedHere}
onChange={(e) => handleManualAllocation(inv.id, e.target.value)}
disabled={fieldsDisabled}
className={cn(
"text-right h-8 py-1",
allocatedHere > 0 && "border-green-500 bg-green-50 text-green-900 font-bold"
)}
/>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
) : (
<div className="text-center py-12 text-gray-500">
<CheckCircle className="w-12 h-12 mx-auto mb-2 text-gray-300" />
<p>Aucune facture en attente de paiement pour ce client.</p>
</div>
)}
</div>
)}
{/* --- DOCUMENTS TAB --- */}
{activeTab === 'documents' && (
<div className="space-y-6">
{/* Reuse simplified doc view */}
<div className="bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 rounded-2xl p-8 text-center text-gray-500">
<p>Module Documents identique aux autres pages...</p>
</div>
</div>
)}
{/* --- TIMELINE TAB --- */}
{activeTab === 'timeline' && (
<div className="bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 rounded-2xl p-6">
<Timeline events={payment.timeline} />
</div>
)}
</div>
</div>
</div>
<PDFPreviewPanel
isOpen={isPDFPreviewOpen}
onClose={() => setIsPDFPreviewOpen(false)}
title={`Recu - ${payment.number}`}
/>
</>
);
};
export default PaymentDetailPage;

View file

@ -0,0 +1,607 @@
import { useEffect, useMemo, useState } from "react";
import { Helmet } from "react-helmet";
import { useNavigate, useParams } from "react-router-dom";
import {
ArrowLeft,
Calendar,
CheckCircle,
Clock,
CreditCard,
Download,
Euro,
FileText,
Hash,
AlertTriangle,
Building,
Receipt,
Wallet,
User,
MapPin,
Mail,
Phone,
ExternalLink,
} from "lucide-react";
import { toast } from "@/components/ui/use-toast";
import { useAppDispatch, useAppSelector } from "@/store/hooks";
import { cn, formatDateFR, formatDateFRCourt } from "@/lib/utils";
import { ModalLoading } from "@/components/modal/ModalLoading";
import Tabs from "@/components/Tabs";
import { getAllReglements, getreglementSelected, reglementStatus } from "@/store/features/reglement/selectors";
import { loadAllReglementData } from "@/store/features/reglement/thunk";
import { FacturesReglement, echeances, ReglementDetail } from "@/types/reglementType";
import { selectClient } from "@/store/features/client/slice";
// ============================================
// CONSTANTES
// ============================================
const STATUT_REGLEMENT = {
SOLDE: 'Soldé',
PARTIEL: 'Partiellement réglé',
NON_REGLE: 'Non réglé',
} as const;
// ============================================
// COMPOSANTS UTILITAIRES
// ============================================
const ReglementStatusBadge = ({ status }: { status: string }) => {
const config: Record<string, { label: string; className: string; icon: React.ElementType }> = {
[STATUT_REGLEMENT.SOLDE]: {
label: 'Payé',
className: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
icon: CheckCircle
},
[STATUT_REGLEMENT.PARTIEL]: {
label: 'Partiel',
className: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400',
icon: Clock
},
[STATUT_REGLEMENT.NON_REGLE]: {
label: 'Non réglé',
className: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400',
icon: AlertTriangle
},
};
const statusConfig = config[status] || config[STATUT_REGLEMENT.NON_REGLE];
const Icon = statusConfig.icon;
return (
<span className={cn(
"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium",
statusConfig.className
)}>
<Icon className="w-4 h-4" />
{statusConfig.label}
</span>
);
};
const InfoCard = ({ icon: Icon, label, value, className }: { icon: React.ElementType; label: string; value: string | number; className?: string }) => (
<div className="flex items-start gap-3">
<div className="p-2 bg-gray-100 dark:bg-gray-800 rounded-lg">
<Icon className="w-4 h-4 text-gray-500" />
</div>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400">{label}</p>
<p className={cn("text-sm font-medium text-gray-900 dark:text-white", className)}>{value}</p>
</div>
</div>
);
const formatCurrency = (amount: number) => {
return `${amount.toLocaleString('fr-FR', { minimumFractionDigits: 2 })}€`;
};
// ============================================
// COMPOSANT PRINCIPAL
// ============================================
const PaymentDetailPage = () => {
const navigate = useNavigate();
const dispatch = useAppDispatch();
const [activeTab, setActiveTab] = useState("overview");
const reglements = useAppSelector(getAllReglements) as FacturesReglement[];
const status = useAppSelector(reglementStatus);
const isLoading = status === 'loading' && reglements.length === 0;
useEffect(() => {
const load = async () => {
if (status === 'idle' || status === 'failed') {
await dispatch(loadAllReglementData()).unwrap();
}
};
load();
}, [status, dispatch]);
// Trouver le règlement correspondant
const reglement = useAppSelector(getreglementSelected)
// Calculs
const totalEcheances = useMemo(() => {
return reglement?.echeances?.length || 0;
}, [reglement]);
const totalReglements = useMemo(() => {
return reglement?.echeances?.reduce((total, ech) => total + (ech.reglements?.length || 0), 0) || 0;
}, [reglement]);
const echeancesReglees = useMemo(() => {
return reglement?.echeances?.filter(ech => ech.est_regle).length || 0;
}, [reglement]);
const prochainEcheance = useMemo(() => {
const echeancesNonReglees = reglement?.echeances?.filter(ech => !ech.est_regle) || [];
if (echeancesNonReglees.length === 0) return null;
return echeancesNonReglees.sort((a, b) => new Date(a.date_echeance).getTime() - new Date(b.date_echeance).getTime())[0];
}, [reglement]);
if (isLoading) {
return <ModalLoading />;
}
if (!reglement) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] space-y-4">
<AlertTriangle className="w-12 h-12 text-gray-400" />
<p className="text-gray-500">Règlement introuvable</p>
<button
onClick={() => navigate('/home/reglements')}
className="text-[#007E45] hover:underline"
>
Retour à la liste
</button>
</div>
);
}
const tabs = [
{ id: "overview", label: "Vue d'ensemble" },
{ id: "echeances", label: "Échéances", count: totalEcheances },
{ id: "reglements", label: "Règlements", count: totalReglements },
{ id: "client", label: "Client" },
];
const client = reglement.client;
const firstReglement = reglement.echeances?.[0]?.reglements?.[0];
const reglementNo = firstReglement?.rg_no;
return (
<>
<Helmet>
<title>{reglement.numero} - Règlement</title>
</Helmet>
<div className="space-y-6">
{/* HEADER */}
<div className="sticky top-0 z-30 bg-white border-b border-gray-200 shadow-sm dark:bg-gray-950 dark:border-gray-800">
<div className="px-4 mx-auto max-w-7xl sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="flex gap-4 items-center">
<button
onClick={() => navigate("/home/reglements")}
title="Retour à la liste"
className="p-2 text-gray-500 rounded-full transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
>
<ArrowLeft className="w-5 h-5" />
</button>
<div>
<div className="flex gap-3 items-center">
<h1 className="text-xl font-bold text-gray-900 dark:text-white">
{reglementNo ? `REG_${reglementNo}` : '-'}
</h1>
<ReglementStatusBadge status={reglement.statut_reglement} />
</div>
<div className="flex gap-2 items-center text-sm text-gray-500">
<span className="font-medium text-gray-900 dark:text-gray-300">
{client?.intitule || 'Client inconnu'}
</span>
<span></span>
<span>{formatDateFR(reglement.date)}</span>
<span></span>
<span className="text-gray-400">{reglement.type_libelle}</span>
</div>
</div>
</div>
{/* ACTIONS */}
<div className="flex gap-2 items-center">
<button
onClick={() => navigate(`/home/factures/${reglement.numero}`)}
className="flex gap-2 items-center px-3 py-2 text-sm font-medium text-gray-700 bg-white rounded-xl border border-gray-200 transition-colors hover:bg-gray-50"
>
<FileText className="w-4 h-4" />
Voir la facture
</button>
<button
onClick={() => toast({ title: 'Téléchargement', description: 'Génération du PDF...' })}
className="flex gap-2 items-center px-3 py-2 text-sm font-medium text-gray-700 bg-white rounded-xl border border-gray-200 transition-colors hover:bg-gray-50"
>
<Download className="w-4 h-4" />
Exporter
</button>
</div>
</div>
</div>
</div>
{/* CONTENT */}
<div className="px-4 mx-auto mt-6 w-full max-w-7xl sm:px-6 lg:px-8">
<Tabs tabs={tabs} active={activeTab} onChange={setActiveTab} />
<div className="mt-6">
{/* --- OVERVIEW TAB --- */}
{activeTab === "overview" && (
<div className="space-y-6">
{/* Résumé financier */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="p-5 bg-white rounded-2xl border border-gray-200 dark:bg-gray-950 dark:border-gray-800">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
<Euro className="w-5 h-5 text-blue-600 dark:text-blue-400" />
</div>
<span className="text-sm text-gray-500">Total TTC</span>
</div>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{formatCurrency(reglement.montants?.total_ttc || 0)}
</p>
</div>
<div className="p-5 bg-white rounded-2xl border border-gray-200 dark:bg-gray-950 dark:border-gray-800">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-green-100 dark:bg-green-900/30 rounded-lg">
<CheckCircle className="w-5 h-5 text-green-600 dark:text-green-400" />
</div>
<span className="text-sm text-gray-500">Montant réglé</span>
</div>
<p className="text-2xl font-bold text-green-600 dark:text-green-400">
{formatCurrency(reglement.montants?.montant_regle || 0)}
</p>
</div>
<div className="p-5 bg-white rounded-2xl border border-gray-200 dark:bg-gray-950 dark:border-gray-800">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-red-100 dark:bg-red-900/30 rounded-lg">
<AlertTriangle className="w-5 h-5 text-red-600 dark:text-red-400" />
</div>
<span className="text-sm text-gray-500">Reste à régler</span>
</div>
<p className="text-2xl font-bold text-red-600 dark:text-red-400">
{formatCurrency(reglement.montants?.reste_a_regler || 0)}
</p>
</div>
<div className="p-5 bg-white rounded-2xl border border-gray-200 dark:bg-gray-950 dark:border-gray-800">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
<Receipt className="w-5 h-5 text-purple-600 dark:text-purple-400" />
</div>
<span className="text-sm text-gray-500">Progression</span>
</div>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{reglement.montants?.total_ttc > 0
? Math.round((reglement.montants.montant_regle / reglement.montants.total_ttc) * 100)
: 0}%
</p>
<div className="mt-2 h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
<div
className="h-full bg-[#007E45] rounded-full transition-all"
style={{
width: `${reglement.montants?.total_ttc > 0
? (reglement.montants.montant_regle / reglement.montants.total_ttc) * 100
: 0}%`
}}
/>
</div>
</div>
</div>
{/* Informations générales */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="p-6 bg-white rounded-2xl border border-gray-200 dark:bg-gray-950 dark:border-gray-800">
<h3 className="mb-4 text-lg font-bold text-gray-900 dark:text-white flex items-center gap-2">
<FileText className="w-5 h-5 text-gray-400" />
Informations du document
</h3>
<div className="grid grid-cols-2 gap-4">
<InfoCard icon={Hash} label="Numéro" value={reglement.numero} />
<InfoCard icon={FileText} label="Type" value={reglement.type_libelle} />
<InfoCard icon={Calendar} label="Date" value={formatDateFRCourt(reglement.date)} />
<InfoCard icon={Calendar} label="Date livraison" value={reglement.date_livraison ? formatDateFRCourt(reglement.date_livraison) : '-'} />
<InfoCard icon={Hash} label="Référence" value={reglement.reference || '-'} />
<InfoCard icon={Calendar} label="Création" value={formatDateFRCourt(reglement.date_creation)} />
</div>
</div>
<div className="p-6 bg-white rounded-2xl border border-gray-200 dark:bg-gray-950 dark:border-gray-800">
<h3 className="mb-4 text-lg font-bold text-gray-900 dark:text-white flex items-center gap-2">
<Wallet className="w-5 h-5 text-gray-400" />
Suivi des paiements
</h3>
<div className="grid grid-cols-2 gap-4">
<InfoCard icon={Calendar} label="Échéances" value={`${echeancesReglees}/${totalEcheances} réglées`} />
<InfoCard icon={Receipt} label="Règlements" value={totalReglements} />
{prochainEcheance && (
<>
<InfoCard
icon={Clock}
label="Prochaine échéance"
value={formatDateFRCourt(prochainEcheance.date_echeance)}
className="text-amber-600"
/>
<InfoCard
icon={Euro}
label="Montant dû"
value={formatCurrency(prochainEcheance.reste_a_regler)}
className="text-red-600"
/>
</>
)}
</div>
</div>
</div>
{/* Derniers règlements */}
{totalReglements > 0 && (
<div className="p-6 bg-white rounded-2xl border border-gray-200 dark:bg-gray-950 dark:border-gray-800">
<h3 className="mb-4 text-lg font-bold text-gray-900 dark:text-white flex items-center gap-2">
<CreditCard className="w-5 h-5 text-gray-400" />
Derniers règlements
</h3>
<div className="space-y-3">
{reglement.echeances?.flatMap(ech => ech.reglements || []).slice(0, 3).map((reg, i) => (
<div key={i} className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-900 rounded-xl">
<div className="flex items-center gap-4">
<div className="p-2 bg-green-100 dark:bg-green-900/30 rounded-lg">
<CheckCircle className="w-5 h-5 text-green-600 dark:text-green-400" />
</div>
<div>
<p className="font-medium text-gray-900 dark:text-white">
REG_{reg.rg_no}
</p>
<p className="text-sm text-gray-500">
{formatDateFRCourt(reg.date)} {reg.mode_reglement?.libelle}
</p>
</div>
</div>
<div className="text-right">
<p className="font-bold text-green-600 dark:text-green-400">
+{formatCurrency(reg.montant)}
</p>
<p className="text-xs text-gray-500">{reg.reference || reg.libelle}</p>
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
{/* --- ECHEANCES TAB --- */}
{activeTab === "echeances" && (
<div className="p-6 bg-white rounded-2xl border border-gray-200 shadow-sm dark:bg-gray-950 dark:border-gray-800">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="text-xs font-semibold tracking-wider text-left text-gray-500 uppercase bg-gray-50 dark:bg-gray-900/50">
<tr>
<th className="px-4 py-3 rounded-l-lg">N° Échéance</th>
<th className="px-4 py-3">Date échéance</th>
<th className="px-4 py-3 text-right">Montant</th>
<th className="px-4 py-3 text-right">Réglé</th>
<th className="px-4 py-3 text-right">Reste</th>
<th className="px-4 py-3">Mode</th>
<th className="px-4 py-3 text-center">Statut</th>
<th className="px-4 py-3 rounded-r-lg text-center">Règlements</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
{reglement.echeances && reglement.echeances.length > 0 ? (
reglement.echeances.map((ech, index) => (
<tr key={ech.dr_no || index} className="hover:bg-gray-50/50 dark:hover:bg-gray-900/20">
<td className="px-4 py-4">
<span className="font-mono font-bold text-gray-900 dark:text-white">
ECH_{ech.dr_no}
</span>
</td>
<td className="px-4 py-4">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-gray-400" />
<span>{formatDateFRCourt(ech.date_echeance)}</span>
</div>
</td>
<td className="px-4 py-4 text-right font-medium">
{formatCurrency(ech.montant)}
</td>
<td className="px-4 py-4 text-right font-medium text-green-600 dark:text-green-400">
{formatCurrency(ech.montant_regle)}
</td>
<td className="px-4 py-4 text-right font-bold text-red-600 dark:text-red-400">
{formatCurrency(ech.reste_a_regler)}
</td>
<td className="px-4 py-4">
<span className="text-sm text-gray-600 dark:text-gray-300">
{ech.mode_reglement?.libelle || '-'}
</span>
</td>
<td className="px-4 py-4 text-center">
{ech.est_regle ? (
<span className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 rounded-full">
<CheckCircle className="w-3 h-3" />
Réglé
</span>
) : (
<span className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400 rounded-full">
<Clock className="w-3 h-3" />
En attente
</span>
)}
</td>
<td className="px-4 py-4 text-center">
<span className="inline-flex items-center justify-center w-6 h-6 text-xs font-bold bg-gray-100 dark:bg-gray-800 rounded-full">
{ech.nb_reglements || 0}
</span>
</td>
</tr>
))
) : (
<tr>
<td colSpan={8} className="px-4 py-12 text-center text-gray-500">
<Calendar className="w-12 h-12 mx-auto mb-3 text-gray-300" />
Aucune échéance
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
)}
{/* --- REGLEMENTS TAB --- */}
{activeTab === "reglements" && (
<div className="p-6 bg-white rounded-2xl border border-gray-200 shadow-sm dark:bg-gray-950 dark:border-gray-800">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="text-xs font-semibold tracking-wider text-left text-gray-500 uppercase bg-gray-50 dark:bg-gray-900/50">
<tr>
<th className="px-4 py-3 rounded-l-lg">N° Règlement</th>
<th className="px-4 py-3">Date</th>
<th className="px-4 py-3">Référence</th>
<th className="px-4 py-3">Mode</th>
<th className="px-4 py-3">Journal</th>
<th className="px-4 py-3 text-right">Montant</th>
<th className="px-4 py-3 rounded-r-lg text-center">Statut</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
{reglement.echeances?.flatMap(ech => ech.reglements || []).length > 0 ? (
reglement.echeances?.flatMap(ech => ech.reglements || []).map((reg, index) => (
<tr key={reg.rg_no || index} className="hover:bg-gray-50/50 dark:hover:bg-gray-900/20">
<td className="px-4 py-4">
<span className="font-mono font-bold text-gray-900 dark:text-white">
REG_{reg.rg_no}
</span>
<div className="text-xs text-gray-500">{reg.numero_piece}</div>
</td>
<td className="px-4 py-4">
<div className="flex flex-col">
<span>{formatDateFRCourt(reg.date)}</span>
<span className="text-xs text-gray-500">{reg.heure}</span>
</div>
</td>
<td className="px-4 py-4">
<span className="text-sm">{reg.reference || '-'}</span>
<div className="text-xs text-gray-500">{reg.libelle}</div>
</td>
<td className="px-4 py-4">
<div className="flex items-center gap-2">
<CreditCard className="w-4 h-4 text-gray-400" />
<span className="text-sm">{reg.mode_reglement?.libelle}</span>
</div>
</td>
<td className="px-4 py-4">
<div className="flex items-center gap-2">
<Building className="w-4 h-4 text-gray-400" />
<div>
<span className="text-sm font-medium">{reg.journal?.code}</span>
<div className="text-xs text-gray-500">{reg.journal?.intitule}</div>
</div>
</div>
</td>
<td className="px-4 py-4 text-right">
<span className="font-bold text-green-600 dark:text-green-400">
+{formatCurrency(reg.montant)}
</span>
</td>
<td className="px-4 py-4 text-center">
{reg.est_impaye ? (
<span className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400 rounded-full">
<AlertTriangle className="w-3 h-3" />
Impayé
</span>
) : reg.est_valide ? (
<span className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 rounded-full">
<CheckCircle className="w-3 h-3" />
Validé
</span>
) : (
<span className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-400 rounded-full">
<Clock className="w-3 h-3" />
En cours
</span>
)}
</td>
</tr>
))
) : (
<tr>
<td colSpan={7} className="px-4 py-12 text-center text-gray-500">
<Receipt className="w-12 h-12 mx-auto mb-3 text-gray-300" />
Aucun règlement enregistré
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
)}
{/* --- CLIENT TAB --- */}
{activeTab === "client" && client && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="p-6 bg-white rounded-2xl border border-gray-200 dark:bg-gray-950 dark:border-gray-800">
<div className="flex items-center gap-4 mb-6">
<div className="w-16 h-16 rounded-full bg-[#007E45]/10 flex items-center justify-center text-2xl font-bold text-[#007E45]">
{client.intitule?.charAt(0).toUpperCase() || '?'}
</div>
<div>
<h3 className="text-lg font-bold text-gray-900 dark:text-white">
{client.intitule}
</h3>
<p className="text-sm text-gray-500">{client.numero}</p>
</div>
</div>
<div className="space-y-4">
<InfoCard icon={User} label="Code client" value={client.numero} />
{client.email && <InfoCard icon={Mail} label="Email" value={client.email} />}
{client.telephone && <InfoCard icon={Phone} label="Téléphone" value={client.telephone} />}
</div>
{/* <button
onClick={() => {
dispatch(selectClient(client as any));
navigate(`/home/clients/${client.numero}`);
}}
className="mt-6 w-full flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium text-[#007E45] bg-[#007E45]/10 rounded-xl hover:bg-[#007E45]/20 transition-colors"
>
<ExternalLink className="w-4 h-4" />
Voir la fiche client
</button> */}
</div>
<div className="p-6 bg-white rounded-2xl border border-gray-200 dark:bg-gray-950 dark:border-gray-800">
<h3 className="mb-4 text-lg font-bold text-gray-900 dark:text-white flex items-center gap-2">
<MapPin className="w-5 h-5 text-gray-400" />
Adresse
</h3>
<div className="p-4 bg-gray-50 dark:bg-gray-900 rounded-xl">
<p className="text-gray-900 dark:text-white">{client.adresse || '-'}</p>
<p className="text-gray-600 dark:text-gray-400">
{client.code_postal} {client.ville}
</p>
</div>
</div>
</div>
)}
</div>
</div>
</div>
</>
);
};
export default PaymentDetailPage;

View file

@ -20,6 +20,7 @@ import { getAllReglements, getReglementStats, reglementStatus } from '@/store/fe
import { loadAllReglementData } from '@/store/features/reglement/thunk'; import { loadAllReglementData } from '@/store/features/reglement/thunk';
import ModalPaymentPanel from '@/components/modal/ModalPaymentPanel'; import ModalPaymentPanel from '@/components/modal/ModalPaymentPanel';
import { FacturesReglement, Statistique } from '@/types/reglementType'; import { FacturesReglement, Statistique } from '@/types/reglementType';
import { selectReglement } from '@/store/features/reglement/slice';
// ============================================ // ============================================
// CONSTANTES - STATUTS DE RÈGLEMENT // CONSTANTES - STATUTS DE RÈGLEMENT
@ -214,6 +215,7 @@ const PaymentsPage = () => {
const isLoading = statusReglement === 'loading' && reglements.length === 0; const isLoading = statusReglement === 'loading' && reglements.length === 0;
useEffect(() => { useEffect(() => {
const load = async () => { const load = async () => {
try { try {
@ -556,8 +558,10 @@ const PaymentsPage = () => {
// selectedIds={selectedInvoiceIds} // selectedIds={selectedInvoiceIds}
// onSelectRow={handleSelectInvoice} // onSelectRow={handleSelectInvoice}
// onSelectAll={handleSelectAll} // onSelectAll={handleSelectAll}
onRowClick={(row: FacturesReglement) => { onRowClick={async(row: FacturesReglement) => {
console.log("Row clicked:", row.numero); console.log("Row clicked:", row.numero);
dispatch(selectReglement(row))
navigate(`/home/reglements/${row.numero}`);
}} }}
/> />
</div> </div>

View file

@ -74,6 +74,7 @@ import QuoteCreatePage from '@/pages/sales/QuoteCreate';
import InvoiceCreatePage from '@/pages/sales/InvoiceCreatePage'; import InvoiceCreatePage from '@/pages/sales/InvoiceCreatePage';
import SageBuilderPage from '@/pages/SageBuilderPage'; import SageBuilderPage from '@/pages/SageBuilderPage';
import PaymentsPage from '@/pages/sales/PaymentsPage'; import PaymentsPage from '@/pages/sales/PaymentsPage';
import PaymentDetailPage from '@/pages/sales/PaymentDetailPage';
const DatavenRoute = () => { const DatavenRoute = () => {
return ( return (
@ -99,7 +100,7 @@ const DatavenRoute = () => {
<Route path="/commercial" element={<CommercialPage />} /> <Route path="/commercial" element={<CommercialPage />} />
<Route path="/reglements" element={<PaymentsPage />} /> <Route path="/reglements" element={<PaymentsPage />} />
{/* <Route path="/reglements/:id" element={<PaymentDetailPage />} /> */} <Route path="/reglements/:id" element={<PaymentDetailPage />} />
{/* Tiers - Articles & Familles */} {/* Tiers - Articles & Familles */}
<Route path="/articles" element={<ArticlesPage />} /> <Route path="/articles" element={<ArticlesPage />} />