// 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 => (
|
{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'}
>
|
{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 || '—'}
|
{canEdit && (
|
)}
)
})}
)}
{/* ── Modals ─────────────────────────────────────────────────────────── */}
{ setShowTxnModal(false); setEditTxnIndex(null) }}
onSaved={updated => { onCustomerUpdated?.(updated); onReloadOrders?.() }}
/>
setDeleteTxnIndex(null)}
/>
)
}