Files
bellsystems-cp/frontend/src/pages/crm/customers/tabs/FinancialsTab.jsx
T

442 lines
25 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 }) => (
<svg width={size} height={size} viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.5 6.99542H4V7.99542H4.5V6.99542ZM10.5 7.99542H11V6.99542H10.5V7.99542ZM4.5 9.5H4V10H4.5V9.5ZM10.5 9.5V10H11V9.5H10.5ZM4.5 5.5V5H4V5.5H4.5ZM10.5 5.5H11V5H10.5V5.5ZM13.5 3.5H14V3.29289L13.8536 3.14645L13.5 3.5ZM10.5 0.5L10.8536 0.146447L10.7071 0H10.5V0.5ZM4.5 7.99542H10.5V6.99542H4.5V7.99542ZM4.5 10H10.5V9H4.5V10ZM4.5 6H10.5V5H4.5V6ZM12.5 14H2.5V15H12.5V14ZM2 13.5V1.5H1V13.5H2ZM13 3.5V13.5H14V3.5H13ZM2.5 1H10.5V0H2.5V1ZM10.1464 0.853553L13.1464 3.85355L13.8536 3.14645L10.8536 0.146447L10.1464 0.853553ZM2.5 14C2.22386 14 2 13.7761 2 13.5H1C1 14.3284 1.67157 15 2.5 15V14ZM12.5 15C13.3284 15 14 14.3284 14 13.5H13C13 13.7761 12.7761 14 12.5 14V15ZM2 1.5C2 1.22386 2.22386 1 2.5 1V0C1.67157 0 1 0.671574 1 1.5H2ZM4 5.5V9.5H5V5.5H4ZM7 5.5V9.5H8V5.5H7ZM10 5.5V9.5H11V5.5H10ZM4 4H7V3H4V4ZM8 12H11V11H8V12Z" fill="currentColor"/>
</svg>
)
const MoneyIcon = ({ size = 20 }) => (
<svg width={size} height={size} viewBox="0 0 512 512" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M432,7.938H80c-44.172,0-80,35.813-80,80c0,44.172,35.828,80,80,80h24v-48c-17.641,0-32-14.359-32-32c0-17.656,14.359-32,32-32h304c17.656,0,32,14.344,32,32c0,17.641-14.344,32-32,32v48h24c44.188,0,80-35.828,80-80C512,43.75,476.188,7.938,432,7.938z"/>
<path d="M120,504.063h272v-432H120V504.063z M152,147.25c23.453-2.344,42.297-20.219,46.188-43.188h115.656c3.875,22.969,22.703,40.844,46.156,43.188v92.938c-20.375-30.016-59.25-50.313-104-50.313c-44.734,0-83.625,20.281-104,50.297V147.25z M273.844,266.531v53.5l-15.969-3.313v-54.516c-0.906-0.063-1.844-0.109-2.75-0.109c-2.297,0-4.609,0.203-6.953,0.594v52.063l-15.969-3.281v-42.563c-12.063,7.75-19.375,20.969-19.375,35.469c0,4.906,0.906,9.859,2.688,14.656l1.438,3.844h-22.469l-0.563-2.109c-1.453-5.391-2.188-10.922-2.188-16.391c0-26.094,16.188-49.563,40.469-59.016v-13.5h15.969v9.578c2.453-0.297,4.719-0.438,6.953-0.438c0.906,0,1.844,0.031,2.75,0.063v-9.203h15.969v12.047c26.391,8.188,44.641,32.734,44.641,60.469c0,7.375-1.297,14.594-3.828,21.531l-0.828,2.297l-21.156-4.313l1.359-3.219c2.219-5.266,3.344-10.75,3.344-16.297C297.375,288.344,288.078,273.625,273.844,266.531z M152,335.953c20.375,30.016,59.266,50.297,104,50.297c44.75,0,83.625-20.281,104-50.313v92.906c-23.453,2.344-42.281,20.25-46.156,43.219H198.172c-3.875-22.969-22.719-40.875-46.172-43.219V335.953z"/>
<circle cx="256" cy="157.188" r="13.75"/>
<circle cx="256" cy="418.938" r="13.75"/>
</svg>
)
const BalanceDueIcon = ({ size = 20 }) => (
<svg width={size} height={size} viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="m 7 0 c -0.554688 0 -1 0.445312 -1 1 h -2 c -1.644531 0 -3 1.355469 -3 3 v 9 c 0 1.644531 1.355469 3 3 3 h 2 c 0.550781 0 1 -0.449219 1 -1 s -0.449219 -1 -1 -1 h -2 c -0.570312 0 -1 -0.429688 -1 -1 v -9 c 0 -0.570312 0.429688 -1 1 -1 h 1 v 1 c 0 0.554688 0.445312 1 1 1 h 4 c 0.554688 0 1 -0.445312 1 -1 v -1 h 1 c 0.570312 0 1 0.429688 1 1 v 2 c 0 0.550781 0.449219 1 1 1 s 1 -0.449219 1 -1 v -2 c 0 -1.644531 -1.355469 -3 -3 -3 h -2 c 0 -0.554688 -0.445312 -1 -1 -1 z m 0 0"/>
<path d="m 8.875 8 c -0.492188 0 -0.875 0.382812 -0.875 0.875 v 6.25 c 0 0.492188 0.382812 0.875 0.875 0.875 h 6.25 c 0.492188 0 0.875 -0.382812 0.875 -0.875 v -6.25 c 0 -0.492188 -0.382812 -0.875 -0.875 -0.875 z m 2.125 1 h 2 v 2.5 s 0 0.5 -0.5 0.5 h -1 c -0.5 0 -0.5 -0.5 -0.5 -0.5 z m 0.5 4 h 1 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 h -1 c -0.277344 0 -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0"/>
</svg>
)
const TotalOrdersIcon = ({ size = 20 }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" xmlns="http://www.w3.org/2000/svg">
<path d="M12 13H15M12 16H20M12 20H20M12 24H20M21 7V2C21 1.447 20.553 1 20 1H2C1.447 1 1 1.447 1 2V24C1 24 1 25 2 25H3M26 27H30C30.553 27 31 26.553 31 26V4C31 3.447 30.553 3 30 3H24M26 30C26 30.553 25.553 31 25 31H7C6.447 31 6 30.553 6 30V8C6 7.447 6.447 7 7 7H25C25.553 7 26 7.447 26 8V30Z"/>
</svg>
)
const PaymentStatusIcon = ({ size = 20 }) => (
<svg width={size} height={size} viewBox="0 0 512 512" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M432.891,288.344c-5.469,3.219-11.828,5.125-18.641,5.125h-59.375c-20.266,0-36.766-16.5-36.766-36.781c0-5.672,1.328-11.172,3.766-16.109c-2.438-4.938-3.766-10.438-3.766-16.125c0-5.672,1.328-11.172,3.766-16.109c-2.438-4.938-3.766-10.438-3.766-16.109c0-5.688,1.328-11.188,3.766-16.125c-2.422-4.938-3.766-10.438-3.766-16.125c0-3.969,0.641-7.781,1.813-11.359h-36.75c-1.703,18.797-7.984,47.141-28.594,71.813c-22.453,26.844-55.891,42.531-99.313,46.813c-14.125,27.375-24.203,56.891-24.203,86.625c0,87.328,70.797,158.125,158.109,158.125c87.328,0,158.125-70.797,158.125-158.125C447.297,331.625,441.625,309.531,432.891,288.344z M322.828,346.844l-3.641-1.516c-5.969-2.531-12.188-3.797-18.469-3.797c-18.188,0-34.891,10.531-42.891,26.688h60.609l-3.734,18.094h-61.797c-0.078,1.031-0.125,2.078-0.125,3.125c0,2.594,0.219,5.219,0.672,7.875h59.016l-3.719,18.094H260.5c8.797,13.688,23.766,21.969,40.219,21.969c5.563,0,11.156-1.016,16.609-3.063l4.328-1.609v25.484l-2.375,0.625c-6.109,1.656-12.344,2.484-18.563,2.484c-29.594,0-56.219-18.344-66.922-45.891H218.5v-18.094h10.844c-0.328-2.781-0.484-5.359-0.484-7.875c0-1.031,0.031-2.094,0.078-3.125H218.5v-18.094h13.641c9.281-29.938,37.125-50.625,68.578-50.625c8.344,0,16.547,1.469,24.391,4.328l2.609,0.953L322.828,346.844z"/>
<path d="M335.859,68.875c0,0,4.656-12.469,29.734-28.297c24.063-15.172,5.594-36.141-29.734-28.422C311.469,17.469,319.75,0,285.547,0s-25.922,17.469-50.313,12.156c-35.328-7.719-53.797,13.25-29.734,28.422c25.078,15.828,29.734,28.297,29.734,28.297H335.859z"/>
<path d="M432.063,224.453c0-7.141-4.234-13.281-10.328-16.109c6.094-2.844,10.344-8.969,10.344-16.109c0-7.156-4.25-13.297-10.344-16.125c6.078-2.828,10.328-8.953,10.328-16.125c0-9.828-7.969-17.797-17.813-17.797h-59.375c-9.828,0-17.813,7.969-17.813,17.797c0,7.172,4.25,13.297,10.344,16.125c-6.094,2.828-10.344,8.969-10.344,16.125c0,7.141,4.25,13.266,10.344,16.109c-6.094,2.828-10.344,8.969-10.344,16.109c0,7.156,4.25,13.297,10.344,16.109c-6.094,2.844-10.344,8.969-10.344,16.125c0,9.844,7.984,17.813,17.813,17.813h59.375c9.828,0,17.813-7.969,17.813-17.813c0-7.156-4.25-13.281-10.328-16.109C427.813,237.75,432.063,231.609,432.063,224.453z"/>
<path d="M346.203,89.641c-20,0-113.25,0-113.25,0c-48.969,0-62.656,38.328-100.578,38.328v121.641l16.578-0.766c126.688-9.813,115.625-119.188,115.625-119.188s31.063,0,71.094,0C375.688,129.656,377.797,89.641,346.203,89.641z"/>
<rect x="64.703" y="113.75" width="50.563" height="164.281"/>
</svg>
)
// ─── Stat Box ─────────────────────────────────────────────────────────────────
function StatBox({ label, value, accentColor, note, icon: IconComponent }) {
return (
<div style={{
backgroundColor: 'var(--color-bg-elevated)',
border: '1px solid var(--color-border)',
borderRadius: 'var(--radius-lg)',
padding: 'var(--space-4)',
paddingLeft: 'var(--space-5)',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
gap: 'var(--space-4)',
}}>
{IconComponent && (
<div style={{
color: accentColor || 'var(--color-text-muted)',
flexShrink: 0,
opacity: 0.8,
display: 'flex',
alignItems: 'center',
}}>
<IconComponent size={40} />
</div>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-1)', minWidth: 0 }}>
<span className="card-field-label">{label}</span>
<span style={{
fontSize: 'var(--font-size-xl)',
fontWeight: 'var(--font-weight-bold)',
color: accentColor || 'var(--color-text-primary)',
fontFamily: 'var(--font-family-mono)',
lineHeight: 1.2,
}}>
{value}
</span>
{note && (
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)' }}>{note}</span>
)}
</div>
</div>
)
}
// ─── 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 (
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-6)' }}>
{/* ── Overall Financial Status ──────────────────────────────────────── */}
<Card
title="Overall Financial Status"
style={{
background: 'linear-gradient(135deg, rgba(128, 131, 255, 0.18) 0%, rgba(192, 193, 255, 0.07) 100%)',
}}
>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(170px, 1fr))', gap: 'var(--space-4)' }}>
<StatBox label="Total Invoiced" value={fmtEuro(totalInvoiced)} accentColor="var(--color-text-primary)" icon={InvoiceIcon} />
<StatBox
label="Total Received"
value={fmtEuro(totalPaid)}
accentColor="var(--color-success)"
icon={MoneyIcon}
/>
<StatBox
label="Balance Due"
value={fmtEuro(outstanding)}
accentColor={outstanding > 0 ? 'var(--color-warning)' : 'var(--color-success)'}
icon={BalanceDueIcon}
/>
<StatBox
label="Total Orders"
value={(orders || []).length}
accentColor="var(--color-text-primary)"
icon={TotalOrdersIcon}
/>
</div>
</Card>
{/* ── Active Orders Payment Status ──────────────────────────────────── */}
{activeOrders.length > 0 && (
<Card title="Active Orders — Payment Status">
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-6)' }}>
{activeOrders.map((order, orderIdx) => {
const ps = order.payment_status || {}
return (
<div key={order.id}>
{/* Order label row */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: 'var(--space-3)',
marginBottom: 'var(--space-3)',
flexWrap: 'wrap',
}}>
<span style={{
fontFamily: 'var(--font-family-mono)',
fontSize: 'var(--font-size-sm)',
color: 'var(--color-text-primary)',
fontWeight: 'var(--font-weight-semibold)',
}}>
{order.order_number}
</span>
{order.title && (
<span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)' }}>
— {order.title}
</span>
)}
<span style={{
fontSize: 'var(--font-size-xs)',
color: 'var(--color-text-muted)',
backgroundColor: 'var(--color-bg-elevated)',
border: '1px solid var(--color-border)',
borderRadius: 'var(--radius-full)',
padding: '2px var(--space-2)',
}}>
{ORDER_STATUS_LABELS[order.status] || order.status}
</span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: 'var(--space-3)' }}>
<StatBox label="Required" value={fmtEuro(ps.required_amount)} accentColor="var(--color-text-primary)" icon={InvoiceIcon} />
<StatBox label="Received" value={fmtEuro(ps.received_amount)} accentColor="var(--color-success)" icon={MoneyIcon} />
<StatBox
label="Balance Due"
value={fmtEuro(ps.balance_due)}
accentColor={(ps.balance_due || 0) > 0 ? 'var(--color-warning)' : 'var(--color-success)'}
icon={BalanceDueIcon}
/>
<StatBox
label="Payment"
value={ps.payment_complete ? 'Complete' : (ps.required_amount || 0) === 0 ? 'No Invoice' : 'Pending'}
accentColor={ps.payment_complete ? 'var(--color-success)' : 'var(--color-text-muted)'}
icon={PaymentStatusIcon}
/>
</div>
{orderIdx < activeOrders.length - 1 && (
<div style={{ borderBottom: '1px solid var(--color-border)', marginTop: 'var(--space-6)' }} />
)}
</div>
)
})}
</div>
</Card>
)}
{/* ── Transaction History ───────────────────────────────────────────── */}
<Card
title="Transaction History"
subtitle={txns.length > 0 ? `${txns.length} record${txns.length !== 1 ? 's' : ''}` : undefined}
>
{/* Header action */}
{canEdit && (
<div style={{ marginBottom: 'var(--space-4)', display: 'flex', justifyContent: 'flex-end' }}>
<Button variant="primary" size="sm" onClick={openAdd}>
+ Add Transaction
</Button>
</div>
)}
{txns.length === 0 ? (
<div style={{
padding: 'var(--space-10)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 'var(--space-2)',
color: 'var(--color-text-muted)',
}}>
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="5" width="20" height="14" rx="2" />
<path d="M2 10h20" />
</svg>
<span style={{ fontSize: 'var(--font-size-sm)' }}>No transactions recorded yet.</span>
</div>
) : (
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 700 }}>
<thead>
<tr style={{ borderBottom: '1px solid var(--color-border)' }}>
{['Date', 'Flow', 'Amount', 'Method', 'Category', 'Order', 'Note', 'By', ''].map(h => (
<th key={h} style={{
padding: 'var(--space-2) var(--space-3)',
textAlign: 'left',
fontSize: 'var(--font-size-xs)',
fontWeight: 'var(--font-weight-semibold)',
color: 'var(--color-text-muted)',
textTransform: 'uppercase',
letterSpacing: 'var(--tracking-wide)',
whiteSpace: 'nowrap',
}}>
{h}
</th>
))}
</tr>
</thead>
<tbody>
{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 (
<tr
key={i}
style={{ borderBottom: '1px solid var(--color-border)', transition: 'background 0.1s' }}
onMouseEnter={e => e.currentTarget.style.backgroundColor = 'var(--color-bg-elevated)'}
onMouseLeave={e => e.currentTarget.style.backgroundColor = 'transparent'}
>
<td style={{ padding: 'var(--space-2) var(--space-3)', color: 'var(--color-text-muted)', whiteSpace: 'nowrap', fontSize: 'var(--font-size-sm)' }}>
{fmtDate(t.date)}
</td>
<td style={{ padding: 'var(--space-2) var(--space-3)' }}>
<StatusBadge variant={FLOW_VARIANT[t.flow] || 'neutral'}>
{FLOW_LABELS[t.flow] || t.flow}
</StatusBadge>
</td>
<td style={{
padding: 'var(--space-2) var(--space-3)',
fontWeight: 'var(--font-weight-semibold)',
fontFamily: 'var(--font-family-mono)',
fontSize: 'var(--font-size-sm)',
color: t.flow === 'payment' ? 'var(--color-success)'
: t.flow === 'invoice' ? 'var(--color-text-primary)'
: 'var(--color-text-muted)',
whiteSpace: 'nowrap',
}}>
{t.flow === 'payment' ? '+' : t.flow === 'invoice' ? '−' : ''}{fmtEuro(t.amount, t.currency)}
</td>
<td style={{ padding: 'var(--space-2) var(--space-3)', color: 'var(--color-text-muted)', fontSize: 'var(--font-size-sm)' }}>
{PAYMENT_TYPE_LABELS[t.payment_type] || '—'}
</td>
<td style={{ padding: 'var(--space-2) var(--space-3)', color: 'var(--color-text-muted)', fontSize: 'var(--font-size-sm)' }}>
{t.flow === 'invoice' ? '—' : (CATEGORY_LABELS[t.category] || t.category || '—')}
</td>
<td style={{ padding: 'var(--space-2) var(--space-3)' }}>
{linkedOrder ? (
<span style={{
fontFamily: 'var(--font-family-mono)',
fontSize: 'var(--font-size-xs)',
color: 'var(--color-text-accent)',
}}>
{linkedOrder.order_number}
</span>
) : t.order_ref ? (
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', fontStyle: 'italic' }}>
{t.order_ref.slice(0, 8)}
</span>
) : (
<span style={{ color: 'var(--color-text-muted)' }}>—</span>
)}
</td>
<td style={{
padding: 'var(--space-2) var(--space-3)',
color: 'var(--color-text-muted)',
fontSize: 'var(--font-size-sm)',
maxWidth: 160,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}>
{t.note || '—'}
</td>
<td style={{ padding: 'var(--space-2) var(--space-3)', color: 'var(--color-text-muted)', fontSize: 'var(--font-size-sm)', whiteSpace: 'nowrap' }}>
{t.recorded_by || '—'}
</td>
{canEdit && (
<td style={{ padding: 'var(--space-2) var(--space-3)', whiteSpace: 'nowrap' }}>
<div style={{ display: 'flex', gap: 'var(--space-2)' }}>
<Button variant="table-actions" size="sm" onClick={() => openEdit(origIdx)}>
Edit
</Button>
<Button variant="danger" size="sm" onClick={() => setDeleteTxnIndex(origIdx)}>
Delete
</Button>
</div>
</td>
)}
</tr>
)
})}
</tbody>
</table>
</div>
)}
</Card>
{/* ── Modals ─────────────────────────────────────────────────────────── */}
<TransactionModal
open={showTxnModal}
customerId={customer.id}
orders={orders || []}
editIndex={editTxnIndex}
initialData={
editTxnIndex !== null && editTxnIndex !== undefined
? (customer.transaction_history || [])[editTxnIndex]
: undefined
}
outstandingBalance={outstanding}
onClose={() => { setShowTxnModal(false); setEditTxnIndex(null) }}
onSaved={updated => { onCustomerUpdated?.(updated); onReloadOrders?.() }}
/>
<ConfirmDialog
open={deleteTxnIndex !== null}
variant="danger"
title="Delete Transaction"
message="This transaction will be permanently removed. This cannot be undone."
confirmLabel={deleting ? 'Deleting…' : 'Delete'}
onConfirm={handleDelete}
onCancel={() => setDeleteTxnIndex(null)}
/>
</div>
)
}