// frontend/src/pages/crm/customers/tabs/FinancialsTab.jsx // Financial overview: overall stats, per-order payment status, transaction history import { useState } from 'react' import api from '@/lib/api' import Card from '@/components/ui/Card' import Button from '@/components/ui/Button' import StatusBadge from '@/components/ui/StatusBadge' import ConfirmDialog from '@/components/ui/ConfirmDialog' import { useToast } from '@/components/ui/Toast' import TransactionModal from '@/modals/crm/customers/TransactionModal' import { fmtDateMedium } from '@/lib/formatters' // ─── Constants ──────────────────────────────────────────────────────────────── const FLOW_LABELS = { invoice: 'Invoice', payment: 'Payment', refund: 'Refund', credit: 'Credit' } const PAYMENT_TYPE_LABELS = { cash: 'Cash', bank_transfer: 'Bank Transfer', card: 'Card', paypal: 'PayPal' } const CATEGORY_LABELS = { full_payment: 'Full Payment', advance: 'Advance', installment: 'Installment' } const ORDER_STATUS_LABELS = { negotiating: 'Negotiating', awaiting_quotation: 'Awaiting Quotation', awaiting_customer_confirmation: 'Awaiting Confirmation', awaiting_fulfilment: 'Accepted - Waiting', awaiting_payment: 'Awaiting Payment', manufacturing: 'Manufacturing', shipped: 'Shipped', installed: 'Installed', declined: 'Declined', complete: 'Complete', } const TERMINAL = new Set(['declined', 'complete']) const FLOW_VARIANT = { payment: 'success', invoice: 'info', refund: 'warning', credit: 'neutral' } // ─── Helpers ───────────────────────────────────────────────────────────────── // Format as European currency: 12500800.70 → "€12.500.800,70" function fmtEuro(amount, currency = 'EUR') { const n = Number(amount || 0) const [intPart, decPart] = n.toFixed(2).split('.') const intFormatted = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, '.') const suffix = currency && currency !== 'EUR' ? ` ${currency}` : '' return `€${intFormatted},${decPart}${suffix}` } function fmtDate(iso) { return fmtDateMedium(iso) } // ─── Stat Box Icons ─────────────────────────────────────────────────────────── const InvoiceIcon = ({ size = 20 }) => ( ) const MoneyIcon = ({ size = 20 }) => ( ) const BalanceDueIcon = ({ size = 20 }) => ( ) const TotalOrdersIcon = ({ size = 20 }) => ( ) const PaymentStatusIcon = ({ size = 20 }) => ( ) // ─── Stat Box ───────────────────────────────────────────────────────────────── function StatBox({ label, value, accentColor, note, icon: IconComponent }) { return (
{IconComponent && (
)}
{label} {value} {note && ( {note} )}
) } // ─── Main Component ─────────────────────────────────────────────────────────── export default function FinancialsTab({ customer, orders, canEdit, onCustomerUpdated, onReloadOrders }) { const { toast } = useToast() const [showTxnModal, setShowTxnModal] = useState(false) const [editTxnIndex, setEditTxnIndex] = useState(null) const [deleteTxnIndex, setDeleteTxnIndex] = useState(null) const [deleting, setDeleting] = useState(false) const txns = [...(customer.transaction_history || [])].sort( (a, b) => (b.date || '').localeCompare(a.date || '') ) // Overall financial totals const totalInvoiced = (customer.transaction_history || []) .filter(t => t.flow === 'invoice') .reduce((s, t) => s + (t.amount || 0), 0) const totalPaid = (customer.transaction_history || []) .filter(t => t.flow === 'payment') .reduce((s, t) => s + (t.amount || 0), 0) const outstanding = totalInvoiced - totalPaid const activeOrders = (orders || []).filter(o => !TERMINAL.has(o.status)) const handleDelete = async () => { setDeleting(true) try { const updated = await api.delete(`/crm/customers/${customer.id}/transactions/${deleteTxnIndex}`) onCustomerUpdated?.(updated) setDeleteTxnIndex(null) toast.success('Deleted', 'Transaction removed.') } catch (err) { toast.danger('Error', err.message || 'Failed to delete transaction.') } finally { setDeleting(false) } } const openAdd = () => { setEditTxnIndex(null); setShowTxnModal(true) } const openEdit = (origIdx) => { setEditTxnIndex(origIdx); setShowTxnModal(true) } return (
{/* ── Overall Financial Status ──────────────────────────────────────── */}
0 ? 'var(--color-warning)' : 'var(--color-success)'} icon={BalanceDueIcon} />
{/* ── Active Orders Payment Status ──────────────────────────────────── */} {activeOrders.length > 0 && (
{activeOrders.map((order, orderIdx) => { const ps = order.payment_status || {} return (
{/* Order label row */}
{order.order_number} {order.title && ( — {order.title} )} {ORDER_STATUS_LABELS[order.status] || order.status}
0 ? 'var(--color-warning)' : 'var(--color-success)'} icon={BalanceDueIcon} />
{orderIdx < activeOrders.length - 1 && (
)}
) })}
)} {/* ── Transaction History ───────────────────────────────────────────── */} 0 ? `${txns.length} record${txns.length !== 1 ? 's' : ''}` : undefined} > {/* Header action */} {canEdit && (
)} {txns.length === 0 ? (
No transactions recorded yet.
) : (
{['Date', 'Flow', 'Amount', 'Method', 'Category', 'Order', 'Note', 'By', ''].map(h => ( ))} {txns.map((t, i) => { const origIdx = (customer.transaction_history || []).findIndex( x => x.date === t.date && x.amount === t.amount && x.note === t.note && x.recorded_by === t.recorded_by ) const linkedOrder = t.order_ref ? (orders || []).find(o => o.id === t.order_ref) : null return ( e.currentTarget.style.backgroundColor = 'var(--color-bg-elevated)'} onMouseLeave={e => e.currentTarget.style.backgroundColor = 'transparent'} > {canEdit && ( )} ) })}
{h}
{fmtDate(t.date)} {FLOW_LABELS[t.flow] || t.flow} {t.flow === 'payment' ? '+' : t.flow === 'invoice' ? '−' : ''}{fmtEuro(t.amount, t.currency)} {PAYMENT_TYPE_LABELS[t.payment_type] || '—'} {t.flow === 'invoice' ? '—' : (CATEGORY_LABELS[t.category] || t.category || '—')} {linkedOrder ? ( {linkedOrder.order_number} ) : t.order_ref ? ( {t.order_ref.slice(0, 8)} ) : ( — )} {t.note || '—'} {t.recorded_by || '—'}
)}
{/* ── Modals ─────────────────────────────────────────────────────────── */} { setShowTxnModal(false); setEditTxnIndex(null) }} onSaved={updated => { onCustomerUpdated?.(updated); onReloadOrders?.() }} /> setDeleteTxnIndex(null)} />
) }