import { PureComponent } from 'react'; import Box from '@mui/material/Box'; import Chip from '@mui/material/Chip'; import CircularProgress from '@mui/material/CircularProgress'; import Divider from '@mui/material/Divider'; import IconButton from '@mui/material/IconButton'; import ListItemButton from '@mui/material/ListItemButton'; import Typography from '@mui/material/Typography'; import PaymentsIcon from '@mui/icons-material/Payments'; import AccountBalanceIcon from '@mui/icons-material/AccountBalance'; import PointOfSaleIcon from '@mui/icons-material/PointOfSale'; import CreditCardIcon from '@mui/icons-material/CreditCard'; import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; import { enqueueSnackbar } from 'notistack'; import SingleLineTextEdit from 'dashgui/SingleLineTextEdit'; import FillSpace from 'dashgui/FillSpace'; import { PanelTile, PanelTileGrid } from 'dashgui/panels'; import BackendError from '../BackendError'; import orderAutomat from '../../automats/orderAutomat'; import companyAutomat from '../../automats/companyAutomat'; import { payment as paymentApi } from '../../api'; import { backendErrorFrom } from '../../utils/backendError'; import { notifyError } from '../../utils/notify'; import { formatPrice } from '../../utils/price'; import withNavigate from '../withNavigate'; import { loadParties, manifestConfig, partyDisplayName, } from './manifestConfig'; import { YELLOW_PANEL, SURFACE_RAISED, } from '../../colors'; const cfg = manifestConfig('order'); export const PAYMENT_TYPES = [ { value: 'transfer', label: 'Banküberweisung', icon: AccountBalanceIcon }, { value: 'cash', label: 'Barzahlung', icon: PointOfSaleIcon }, { value: 'card', label: 'EC- / Kreditkarte', icon: CreditCardIcon }, { value: 'paypal', label: 'PayPal', icon: PaymentsIcon }, { value: 'sepa', label: 'SEPA-Lastschrift', icon: AccountBalanceIcon }, { value: 'invoice', label: 'Auf Rechnung', icon: PaymentsIcon }, { value: 'other', label: 'Sonstige', icon: PaymentsIcon }, ]; export function typeLabel(val) { const match = PAYMENT_TYPES.find((t) => t.value === val); return match?.label || val || 'Zahlung'; } export function typeIcon(val, sx = { fontSize: 18 }) { const match = PAYMENT_TYPES.find((t) => t.value === val); const IconComp = match?.icon || PaymentsIcon; return ; } const rowSx = { alignItems: 'center', height: 56, minHeight: 56, maxHeight: 56, boxSizing: 'border-box', py: 0, gap: 1.5, }; const ellipsisSx = { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }; const paymentRowContext = { partiesById: {}, accountsById: {}, busyId: null, onDelete: null, onNavigateOrder: null, }; // MARK: class PaymentTableRow extends PureComponent { render() { const { item: payment } = this.props; if (!payment) { return (   ); } const { partiesById, accountsById, busyId, onDelete, onNavigateOrder } = paymentRowContext; const party = partiesById[payment.entityId]; const customerName = partyDisplayName('order', party) || (payment.entityId ? `Kunde #${payment.entityId}` : '–'); const accountName = accountsById[payment.accountId] || `Konto #${payment.accountId}`; const isBusy = busyId === payment.id; return ( onNavigateOrder?.(payment.orderId)}> {/* Payment ID from payment table */} {/* Amount */} +{formatPrice(payment.amount)} {/* Payment Type */} {typeIcon(payment.type)} {typeLabel(payment.type)} {/* Account from account table */} {accountName} {/* Linked Manifest (Auftrag) */} {payment.orderNumber} {/* Customer Name */} {customerName} {/* Action: Delete payment */} { e.stopPropagation(); onDelete?.(payment); }} aria-label={`Zahlung #${payment.id} löschen`} sx={{ width: 36, height: 36, flexShrink: 0 }} > ); } } // MARK: export class OrderPaymentList extends PureComponent { constructor(props) { super(props); const s = orderAutomat.state; this.state = { status: s.status === 'ready' ? 'ready' : 'loading', error: s.error ? backendErrorFrom(s.error) : null, items: s.items ?? [], filter: '', partiesById: {}, accountsById: {}, busyId: null, }; this._cancelled = false; this.handleRetry = this.handleRetry.bind(this); this.handleDeletePayment = this.handleDeletePayment.bind(this); this.handleNavigateOrder = this.handleNavigateOrder.bind(this); } componentDidMount() { this._cancelled = false; this.unsub = orderAutomat.subscribe(this, (state) => ({ items: state.items ?? [], status: state.status, error: state.error ? backendErrorFrom(state.error) : null, })); this.unsubAccount = companyAutomat.subscribe( this, (state) => ({ accountsById: Object.fromEntries((state.accounts || []).map((a) => [a.id, a.name])), }), ['accounts'], ); if (orderAutomat.state.status === 'idle') { void orderAutomat.actions.load().catch(() => {}); } void companyAutomat.actions.ensureAccountsLoaded().catch(() => {}); void this.loadPartiesData(); } componentWillUnmount() { this._cancelled = true; this.unsub?.(); this.unsubAccount?.(); } async loadPartiesData() { try { const parties = await loadParties('order'); if (!this._cancelled) { this.setState({ partiesById: cfg.partiesByIdFromList(parties) }); } } catch { // Non-fatal } } handleRetry() { void orderAutomat.actions.load().catch(() => {}); } extractAllPayments(orders = this.state.items) { const list = []; for (const order of orders) { if (!order || !Array.isArray(order.payments)) continue; for (const pay of order.payments) { list.push({ ...pay, orderId: order.id, orderNumber: order.number, orderDate: order.date, entityId: order.entityId, orderTotal: order.orderTotal, paidTotal: order.paidTotal, }); } } // Sort descending by payment id return list.sort((a, b) => Number(b.id || 0) - Number(a.id || 0)); } async handleDeletePayment(payment) { if (!window.confirm(`Zahlung #${payment.id} über ${formatPrice(payment.amount)} wirklich löschen?`)) return; this.setState({ busyId: payment.id }); try { await paymentApi.delete(payment.id); enqueueSnackbar(`Zahlung #${payment.id} gelöscht.`, { variant: 'success' }); await orderAutomat.actions.reload(); } catch (err) { notifyError(err); } finally { if (!this._cancelled) { this.setState({ busyId: null }); } } } handleNavigateOrder(orderId) { if (orderId) { this.props.navigate(`/auftrag/bestellung/${orderId}`); } } getFilteredPayments() { const all = this.extractAllPayments(); const q = this.state.filter.trim().toLowerCase(); if (!q) return all; const { partiesById, accountsById } = this.state; return all.filter((pay) => { const party = partiesById[pay.entityId]; const customerName = partyDisplayName('order', party); const accountName = accountsById[pay.accountId]; const typeStr = typeLabel(pay.type); return [ String(pay.id), pay.orderNumber, customerName, accountName, typeStr, pay.type, ].some((val) => String(val || '').toLowerCase().includes(q)); }); } render() { const { status, error, filter, partiesById, accountsById, busyId } = this.state; if (status === 'loading' || status === 'idle') { return ( ); } if (status === 'failed') { return ; } const filtered = this.getFilteredPayments(); const allPayments = this.extractAllPayments(); const totalAmount = allPayments.reduce((sum, p) => sum + (Number(p.amount) || 0), 0); paymentRowContext.partiesById = partiesById; paymentRowContext.accountsById = accountsById; paymentRowContext.busyId = busyId; paymentRowContext.onDelete = this.handleDeletePayment; paymentRowContext.onNavigateOrder = this.handleNavigateOrder; return ( <> {/* Metrics Banner */} Gesamtsumme aller Zahlungen {formatPrice(totalAmount)} Anzahl Zahlungseinträge (Tabelle payment) {allPayments.length} {/* Search Bar */} this.setState({ filter: next })} /> {/* Payments Table (connected directly to payment records) */} ID Betrag Zahlungsart Konto Auftrag Kunde } /> ); } } export default withNavigate(OrderPaymentList);