Files
dashfront/src/components/manifest/OrderDocumentsTiles.jsx
2026-10-17 04:11:42 +02:00

300 lines
11 KiB
JavaScript
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.

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 { PanelTile } from 'dashgui/panels';
import { formatDate } from './manifestConfig';
import { formatPrice } from '../../utils/price';
import { typeLabel, typeIcon } from './OrderPaymentList';
import ShipmentDetailDialog from './ShipmentDetailDialog';
import {
GREEN_PANEL,
YELLOW_PANEL,
WHITE,
} from '../../colors';
/**
* Renders 1-wide associated document tiles for an order in edit mode:
* 1. Lieferscheine / Auslieferungen (Shipments)
* 2. Zahlungen (Payments)
*/
export class OrderDocumentsTiles extends PureComponent {
constructor(props) {
super(props);
this.state = {
selectedShipment: null,
};
this.handleOpenShipment = this.handleOpenShipment.bind(this);
this.handleCloseShipment = this.handleCloseShipment.bind(this);
this.handleOpenPayments = this.handleOpenPayments.bind(this);
}
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 (
<PanelTile wide={1} minHeight={220} color={GREEN_PANEL} title="Lieferscheine">
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1.5 }}>
<Stack direction="row" spacing={0.75} sx={{ alignItems: 'center' }}>
<LocalShippingOutlinedIcon sx={{ fontSize: 20, color: 'text.secondary' }} />
<Chip size="small" label={statusLabel} color={statusColor} sx={{ fontWeight: 600, height: 22 }} />
</Stack>
<Button
size="small"
variant="outlined"
startIcon={<AddIcon />}
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
</Button>
</Box>
{shipments.length > 0 ? (
<Stack spacing={1}>
{shipments.map((sh) => {
const carrier = sh.shippings?.[0]?.carrier;
const tracking = sh.shippings?.[0]?.trackingCode;
const posCount = sh.lineCount ?? sh.lines?.length ?? 0;
return (
<Card
key={sh.id ?? sh.number}
variant="outlined"
onClick={() => 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',
},
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 0.25 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, fontSize: '0.85rem' }}>
{sh.number}
</Typography>
<Stack direction="row" spacing={0.5} sx={{ alignItems: 'center' }}>
<Typography variant="caption" color="text.secondary">
{sh.date ? formatDate(sh.date) : '–'}
</Typography>
{navigate && (
<IconButton
size="small"
title="In Auslieferungen öffnen"
onClick={(e) => {
e.stopPropagation();
navigate(`/auftrag/ausliefern?shipmentId=${sh.id}`);
}}
sx={{ p: 0.25, ml: 0.5, color: 'text.secondary', '&:hover': { color: 'primary.main' } }}
>
<OpenInNewIcon sx={{ fontSize: 13 }} />
</IconButton>
)}
</Stack>
</Box>
{sh.externalNumber ? (
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.25 }}>
Ref: {sh.externalNumber}
</Typography>
) : null}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 0.5 }}>
<Typography variant="caption" color="text.secondary">
{posCount} Pos.
</Typography>
{carrier || tracking ? (
<Chip
size="small"
color="primary"
variant="outlined"
label={carrier || 'Paket'}
sx={{ height: 18, fontSize: '0.65rem' }}
/>
) : null}
</Box>
</Card>
);
})}
</Stack>
) : null}
</PanelTile>
);
}
renderPaymentsTile() {
const { payments = [], paid, paidTotal, orderTotal } = this.props;
const tot = Number(orderTotal) || 0;
const pd = Number(paidTotal) || 0;
const openAmount = Math.max(0, tot - pd);
const statusLabel = paid ? 'Bezahlt' : pd > 0 ? 'Teilzahlung' : 'Offen';
const statusColor = paid ? 'success' : pd > 0 ? 'warning' : 'error';
return (
<PanelTile wide={1} minHeight={220} color={YELLOW_PANEL} title="Zahlungen">
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1.5 }}>
<Stack direction="row" spacing={0.75} sx={{ alignItems: 'center' }}>
<PaymentsOutlinedIcon sx={{ fontSize: 20, color: 'text.secondary' }} />
<Chip size="small" label={statusLabel} color={statusColor} sx={{ fontWeight: 600, height: 22 }} />
</Stack>
<Button
size="small"
variant="outlined"
onClick={this.handleOpenPayments}
sx={{ fontWeight: 600, textTransform: 'none', height: 26, fontSize: '0.75rem', px: 1 }}
>
Zahlungen
</Button>
</Box>
{/* Financial KPI Card */}
<Box
sx={{
p: 1,
mb: 1.5,
bgcolor: WHITE,
borderRadius: 1,
border: '1px solid',
borderColor: 'divider',
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.25 }}>
<Typography variant="caption" color="text.secondary">
Summe:
</Typography>
<Typography variant="caption" sx={{ fontWeight: 700 }}>
{formatPrice(tot)}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.25 }}>
<Typography variant="caption" color="text.secondary">
Bezahlt:
</Typography>
<Typography variant="caption" sx={{ fontWeight: 700, color: 'success.main' }}>
{formatPrice(pd)}
</Typography>
</Box>
<Divider sx={{ my: 0.5 }} />
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="caption" color="text.secondary">
Offen:
</Typography>
<Typography variant="caption" sx={{ fontWeight: 700, color: openAmount > 0 ? 'error.main' : 'text.secondary' }}>
{formatPrice(openAmount)}
</Typography>
</Box>
</Box>
{payments.length > 0 ? (
<Stack spacing={1}>
{payments.map((pay) => {
const pType = pay.type || pay.paymentType || pay.payment_type;
return (
<Card
key={pay.id}
variant="outlined"
onClick={this.handleOpenPayments}
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',
},
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Stack direction="row" spacing={0.75} sx={{ alignItems: 'center', minWidth: 0, overflow: 'hidden' }}>
<Box sx={{ display: 'flex', alignItems: 'center', color: 'text.secondary', flexShrink: 0 }}>
{typeIcon(pType, { fontSize: 16 })}
</Box>
<Typography variant="body2" sx={{ fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{typeLabel(pType)}
</Typography>
</Stack>
<Stack direction="row" spacing={1} sx={{ alignItems: 'center', flexShrink: 0, ml: 1 }}>
{pay.date ? (
<Typography variant="caption" color="text.secondary">
{formatDate(pay.date)}
</Typography>
) : null}
<Typography
variant="subtitle2"
sx={{ fontWeight: 700, color: 'success.dark', fontVariantNumeric: 'tabular-nums' }}
>
+{formatPrice(pay.amount)}
</Typography>
</Stack>
</Box>
</Card>
);
})}
</Stack>
) : null}
</PanelTile>
);
}
render() {
return (
<>
{this.renderShipmentsTile()}
{this.renderPaymentsTile()}
<ShipmentDetailDialog
open={Boolean(this.state.selectedShipment)}
shipment={this.state.selectedShipment}
onClose={this.handleCloseShipment}
onNavigate={this.props.navigate}
onChanged={this.props.onShipmentChanged}
/>
</>
);
}
}
export default OrderDocumentsTiles;