import { PureComponent } from 'react'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Card from '@mui/material/Card'; import Chip from '@mui/material/Chip'; import Divider from '@mui/material/Divider'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; import IconButton from '@mui/material/IconButton'; import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined'; import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined'; import AddIcon from '@mui/icons-material/Add'; import OpenInNewIcon from '@mui/icons-material/OpenInNew'; import DialogContent from '@mui/material/DialogContent'; import DialogActions from '@mui/material/DialogActions'; import DialogTitle from '@mui/material/DialogTitle'; import TextField from '@mui/material/TextField'; import MenuItem from '@mui/material/MenuItem'; import { enqueueSnackbar } from 'notistack'; import { PanelTile } from 'dashgui/panels'; import AppDialog from '../AppDialog'; import { payment as paymentApi } from '../../api'; import { notifyError } from '../../utils/notify'; import { formatDate, formatDateTime, todayInputValue } from './manifestConfig'; import { formatPrice } from '../../utils/price'; import { typeLabel, typeIcon, PAYMENT_TYPES } from './OrderPaymentList'; import ShipmentDetailDialog from './ShipmentDetailDialog'; import { GREEN_PANEL, YELLOW_PANEL, WHITE, } from '../../colors'; /** * Renders 1-wide associated document tiles for an order or retoure in edit mode: * 1. Lieferscheine / Auslieferungen (Shipments) * 2. Zahlungen / Erstattungen (Payments) */ export class OrderDocumentsTiles extends PureComponent { constructor(props) { super(props); this.state = { selectedShipment: null, paymentDialogOpen: false, paymentAmount: '', paymentType: 'cash', paymentDate: todayInputValue(), paymentSaving: false, }; this.handleOpenShipment = this.handleOpenShipment.bind(this); this.handleCloseShipment = this.handleCloseShipment.bind(this); this.handleOpenPayments = this.handleOpenPayments.bind(this); this.handleOpenPaymentDialog = this.handleOpenPaymentDialog.bind(this); this.handleClosePaymentDialog = this.handleClosePaymentDialog.bind(this); this.handleSubmitPayment = this.handleSubmitPayment.bind(this); } handleOpenPaymentDialog() { const { orderTotal, paidTotal, kind } = this.props; const tot = Number(orderTotal) || 0; const pd = Number(paidTotal) || 0; const absTot = Math.abs(tot); const absPd = Math.abs(pd); const isPaid = Boolean(this.props.paid || (absTot > 0 && absPd >= absTot - 0.005)); const remaining = isPaid ? 0 : Math.max(0, absTot - absPd); const defaultAmount = remaining > 0 ? (kind === 'retoure' || tot < 0 ? -remaining : remaining) : ''; this.setState({ paymentDialogOpen: true, paymentAmount: defaultAmount !== '' ? String(Math.round(defaultAmount * 100) / 100) : '', paymentType: 'cash', paymentDate: todayInputValue(), }); } handleClosePaymentDialog() { this.setState({ paymentDialogOpen: false, paymentSaving: false }); } async handleSubmitPayment(e) { e?.preventDefault?.(); const { orderId, onShipmentChanged } = this.props; const { paymentAmount, paymentType, paymentDate, paymentSaving } = this.state; if (paymentSaving || !orderId) return; const amt = Number(String(paymentAmount).replace(',', '.')); if (Number.isNaN(amt) || amt === 0) { enqueueSnackbar('Bitte einen gültigen Betrag ungleich 0 eingeben.', { variant: 'warning' }); return; } this.setState({ paymentSaving: true }); try { await paymentApi.create({ manifestId: orderId, amount: amt, type: paymentType, date: paymentDate || undefined, }); enqueueSnackbar(amt < 0 ? 'Erstattung erfolgreich gebucht!' : 'Zahlung erfolgreich erfasst!', { variant: 'success', }); this.handleClosePaymentDialog(); onShipmentChanged?.(); } catch (err) { notifyError(err); this.setState({ paymentSaving: false }); } } handleOpenShipment(sh) { this.setState({ selectedShipment: sh }); } handleCloseShipment() { this.setState({ selectedShipment: null }); } handleOpenPayments() { if (this.props.navigate) { this.props.navigate('/auftrag/zahlungen'); } } renderShipmentsTile() { const { shipments = [], shipped, delivered, navigate, deliverDisabled } = this.props; const statusLabel = delivered ? 'Geliefert' : shipped ? 'Versendet' : shipments.length > 0 ? 'Teilweise' : 'Offen'; const statusColor = delivered || shipped ? 'success' : shipments.length > 0 ? 'warning' : 'default'; return ( {shipments.length > 0 ? ( {shipments.map((sh) => { const carrier = sh.shippings?.[0]?.carrier; const tracking = sh.shippings?.[0]?.trackingCode; const posCount = sh.lineCount ?? sh.lines?.length ?? 0; const shDate = sh.date || sh.createdAt || sh.created_at; return ( this.handleOpenShipment(sh)} sx={{ p: 1.25, bgcolor: WHITE, cursor: 'pointer', transition: 'all 0.15s ease', '&:hover': { boxShadow: '0 2px 8px rgba(0,0,0,0.1)', borderColor: 'primary.main', }, }} > {sh.number} {shDate ? formatDateTime(shDate) : '–'} {navigate && ( { e.stopPropagation(); navigate(`/auftrag/ausliefern?shipmentId=${sh.id}`); }} sx={{ p: 0.25, ml: 0.5, color: 'text.secondary', '&:hover': { color: 'primary.main' } }} > )} {sh.externalNumber ? ( Ref: {sh.externalNumber} ) : null} {posCount} Pos. {carrier || tracking ? ( ) : null} ); })} ) : null} ); } renderPaymentsTile() { const { payments = [], paid, paidTotal, orderTotal, kind } = this.props; const isRetoure = kind === 'retoure'; const tot = Number(orderTotal) || 0; const pd = Number(paidTotal) || 0; const absTot = Math.abs(tot); const absPd = Math.abs(pd); const isPaid = Boolean(paid || (absTot > 0 && absPd >= absTot - 0.005)); let openAmount = 0; if (!isPaid) { const remaining = Math.max(0, absTot - absPd); openAmount = tot < 0 ? -remaining : remaining; } const statusLabel = isPaid ? (isRetoure ? 'Erstattet' : 'Bezahlt') : Math.abs(pd) > 0 ? 'Teilzahlung' : 'Offen'; const statusColor = isPaid ? 'success' : Math.abs(pd) > 0 ? 'warning' : 'error'; return ( {/* Financial KPI Card */} {isRetoure ? 'Gutschrift / Summe:' : 'Summe:'} {formatPrice(tot)} {isRetoure ? 'Erstattet:' : 'Bezahlt:'} {formatPrice(pd)} Offen: {formatPrice(openAmount)} {payments.length > 0 ? ( {payments.map((pay) => { const pType = pay.type || pay.paymentType || pay.payment_type; const payDate = pay.date || pay.createdAt || pay.created_at || pay.timestamp; const pAmt = Number(pay.amount) || 0; const isRefund = pAmt < 0; return ( {typeIcon(pType, { fontSize: 16 })} {typeLabel(pType)} {isRefund ? formatPrice(pAmt) : `+${formatPrice(pAmt)}`} {payDate ? ( {pay.id != null ? ( #{pay.id} ) : } {formatDateTime(payDate)} ) : null} ); })} ) : null} ); } render() { const { kind } = this.props; const isRetoure = kind === 'retoure'; return ( <> {this.renderShipmentsTile()} {this.renderPaymentsTile()} this.setState({ paymentAmount: e.target.value })} helperText={isRetoure ? 'Negative Beträge für Gutschrift/Erstattung (z.B. -50.00)' : 'Zahlungsbetrag'} fullWidth required /> this.setState({ paymentType: e.target.value })} fullWidth > {PAYMENT_TYPES.map((t) => ( {typeIcon(t.value, { fontSize: 18 })} {t.label} ))} this.setState({ paymentDate: e.target.value })} slotProps={{ inputLabel: { shrink: true } }} fullWidth /> ); } } export default OrderDocumentsTiles;