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 (
}
disabled={deliverDisabled}
onClick={this.props.onOpenCreateShipment || (() => navigate?.('/auftrag/ausliefern'))}
title={deliverDisabled ? 'Alle Positionen bereits ausgeliefert' : 'Lieferschein erstellen'}
sx={{ fontWeight: 600, textTransform: 'none', height: 26, fontSize: '0.75rem', px: 1 }}
>
Liefern
{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) => (
))}
this.setState({ paymentDate: e.target.value })}
slotProps={{ inputLabel: { shrink: true } }}
fullWidth
/>
>
);
}
}
export default OrderDocumentsTiles;