@@ -1,4 +1,4 @@
import { PureComponent } from 'react' ;
import { PureComponent , createRef } from 'react' ;
import Box from '@mui/material/Box' ;
import Button from '@mui/material/Button' ;
import Chip from '@mui/material/Chip' ;
@@ -9,9 +9,8 @@ import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography' ;
import MenuItem from '@mui/material/MenuItem' ;
import Stack from '@mui/material/Stack' ;
import ContentCopyIc on from '@mui/icons- material/ContentCopy ' ;
import Skelet on from '@mui/material/Skeleton ' ;
import LocalShippingIcon from '@mui/icons-material/LocalShipping' ;
import AddIcon from '@mui/icons-material/Add' ;
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined' ;
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined' ;
import CloseIcon from '@mui/icons-material/Close' ;
@@ -19,14 +18,17 @@ import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import Menu from '@mui/material/Menu' ;
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 AppDialog from '../AppDialog' ;
import shipmentAutomat from '../../automats/shipmentAutomat' ;
import orderAutomat from '../../automats/orderAutomat' ;
import numberAutomat from '../../automats/numberAutomat' ;
import { shipment as shipmentApi } from '../../api' ;
import { backendErrorFrom } from '../../utils/backendError' ;
import { notifyError } from '../../utils/notify' ;
import { getMaxDocumentNumber } from '../../utils/documentNumber' ;
import withNavigate from '../withNavigate' ;
import ShipmentDetailDialog from './ShipmentDetailDialog' ;
import {
@@ -38,9 +40,7 @@ import {
} from './manifestConfig' ;
import {
GREEN _PANEL ,
SURFACE _BASE ,
SURFACE _RAISED ,
WHITE ,
} from '../../colors' ;
const cfg = manifestConfig ( 'order' ) ;
@@ -119,63 +119,46 @@ class NewShipmentModal extends PureComponent {
}
async handleSubmit ( ) {
const { orders = [ ] , onClose , onSuccess } = this . props ;
const { orderId , date , externalNumber , lineAmounts } = this . state ;
const order = orders . find ( ( o ) => o . id === Number ( orderId ) ) ;
if ( ! order ) return ;
const linesToShip = [ ] ;
for ( const [ sku , amtStr ] of Object . entries ( lineAmounts ) ) {
const amt = Number ( amtStr ) ;
if ( amt > 0 ) {
const orderLine = order . lines ? . find ( ( l ) => l . sku === sku ) ;
linesToShip . push ( {
sku ,
amount : amt ,
name : orderLine ? . name ? ? '' ,
pu : orderLine ? . pu ? ? 1 ,
net : orderLine ? . net ? ? 0 ,
rebate : orderLine ? . rebate ? ? 0 ,
taxrate : orderLine ? . taxrate ? ? 0 ,
} ) ;
}
if ( ! orderId ) {
enqueueSnackbar ( 'Bitte wählen Sie einen Auftrag aus.' , { variant : 'warning' } ) ;
return ;
}
if ( linesToShip . l ength === 0 ) {
enqueueSnackbar ( 'Mindestens ein Artikel muss eine Liefermenge > 0 haben.' , { variant : 'warning' } ) ;
const lines = Object . entries ( lineAmounts )
. filter ( ( [ , amt ] ) => Number ( amt ) > 0 )
. map ( ( [ sku , amt ] ) => ( { sku , amount : Number ( amt ) } ) ) ;
if ( lines . length === 0 ) {
enqueueSnackbar ( 'Mindestens eine Position muss mit Menge > 0 geliefert werden.' , { variant : 'warning' } ) ;
return ;
}
this . setState ( { saving : true } ) ;
try {
const res = await shipmentApi . create ( order. i d , {
const res = await shipmentApi . create ( orderI d , {
date ,
externalNumber : externalNumber . trim ( ) || undefined ,
lines : linesToShip ,
lines ,
} ) ;
enqueueSnackbar ( ` Lieferschein ${ res . shipment ? . number || '' } erfolgreich angelegt! ` , { variant : 'success' } ) ;
await Promise . all ( [
orderAutomat . actions . reload ( ) ,
shipmentAutomat . actions . reload ( ) ,
] ) ;
onSuccess ? . ( res . shipment ) ;
onClose ( ) ;
enqueueSnackbar ( res . message || 'Lieferschein erfolgreich erstellt.' , { variant : 'success' } ) ;
void orderAutomat . actions . reload ( ) . catch ( ( ) => { } ) ;
void shipmentAutomat . actions . reload ( ) . catch ( ( ) => { } ) ;
this . props . onSuccess ? . ( res . shipment ) ;
this . props . onClose ? . ( ) ;
} catch ( err ) {
notifyError ( err ) ;
} finally {
this . setState ( { saving : false } ) ;
}
}
render ( ) {
const { open , onClose , orders , partiesById } = this . props ;
const { open , onClose , orders = [ ] , partiesById = { } } = this . props ;
const { orderId , date , externalNumber , lineAmounts , saving } = this . state ;
const openOrders = orders . filter ( ( o ) => {
const ordQty = Number ( o . orderedQuantity ) || 0 ;
const shpQty = Number ( o . shippedQuantity ) || 0 ;
return ordQty === 0 || shpQty < ordQty ;
} ) ;
const currentOrder = orders . find ( ( o ) => o . id === Number ( orderId ) ) ;
const lines = Array . isArray ( currentOrder ? . lines ) ? currentOrder . lines : [ ] ;
@@ -187,32 +170,29 @@ class NewShipmentModal extends PureComponent {
}
return (
< AppDialog open = { open } onClose = { onClose } maxWidth = "md" fullWidth >
< AppDialog
open = { open }
onClose = { saving ? undefined : onClose }
title = "Neuen Lieferschein erstellen"
maxWidth = "md"
fullWidth
>
< Box sx = { { p : 2.5 } } >
< Box sx = { { display : 'flex' , alignItems : 'center' , justifyContent : 'space-betwee n', pb : 2 } } >
< Typography variant = "h6" sx = { { fontWeight : 600 } } >
Neuen Lieferschein erstellen ( Shipment Manifest )
< / Typography >
< IconButton onClick = { onClose } size = "small" >
< CloseIcon / >
< / IconButton >
< / Box >
< Box sx = { { display : 'flex' , flexDirection : 'column' , gap : 2 , mb : 3 } } >
< Box sx = { { mb : 2.5 , display: 'flex' , flexDirection : 'colum n', ga p: 2 } } >
< TextField
select
label = "A uftrag auswählen"
label = "Bezugsa uftrag auswählen"
size = "small"
value = { orderId }
onChange = { ( e ) => this . handleOrderChange ( e . target . value ) }
fullWidth
>
{ openO rders . map ( ( o ) => {
const party = partiesById [ o . entityId ] ;
const party Name = partyDisplayName ( 'order' , party ) || ` Kunde # ${ o . entityId } ` ;
{ orders . map ( ( o ) => {
const customer = partiesById [ o . entityId ] ;
const cust Name = partyDisplayName ( 'order' , customer ) || ( o . entityId ? ` Kunde # ${ o . entityId } ` : '' ) ;
return (
< MenuItem key = { o . id } value = { o . id } >
{ o . number } — { party Name } ( { o . shippedQuantity || 0 } / { o . orderedQuantity || 0 } geliefert )
{ o . number } { custName ? ` — ${ cust Name }` : '' } { o . date ? ` ( ${ formatDate ( o . date ) } ) ` : '' }
< / MenuItem >
) ;
} ) }
@@ -389,56 +369,182 @@ class DeliveryStateMenu extends PureComponent {
}
}
// MARK: <ShipmentRow/>
// Dummy shipment generator with max digits for layout testing and probe measurement
export function createDummyTestShipment ( ) {
const maxOrderNumber = getMaxDocumentNumber ( 'order' ) ;
return {
id : 999999 ,
number : ` ${ maxOrderNumber } -L-99 ` ,
externalNumber : 'REF-999999' ,
date : '2030-10-30T12:00:00Z' ,
orderNumber : maxOrderNumber ,
customerName : 'Kunde' ,
positions : 999 ,
totalQuantity : 999 ,
packageCount : 9 ,
carrier : 'DHL' ,
trackingCode : '00340434161094042578' ,
state : 'delivered' ,
} ;
}
const rowGridSx = {
display : 'grid' ,
gridTemplateColumns : '170px 105px 130px 1.4fr 120px 160px 110px 48px ' ,
alignItem s: 'cente r' ,
gridTemplateColumns : 'max-content max-content max-content 1fr max-content max-content max-content auto ' ,
gridTemplateRow s: '0px 1f r' ,
rowGap : 0 ,
columnGap : 1.5 ,
alignItems : 'center' ,
width : '100%' ,
px : 2 ,
py : 1.1 ,
boxSizing : 'border-box' ,
} ;
const measuringCellSx = {
visibility : 'hidden' ,
height : 0 ,
boxSizing : 'border-box' ,
pointerEvents : 'none' ,
textTransform : 'none' ,
letterSpacing : 'normal' ,
} ;
const headerCellSx = {
color : 'text.secondary' ,
typography : 'caption' ,
fontWeight : 600 ,
textTransform : 'uppercase' ,
letterSpacing : '0.04em' ,
userSelect : 'none' ,
} ;
function renderInvisibleDummyRow ( dummy ) {
const d = dummy || createDummyTestShipment ( ) ;
return (
< >
< Box aria - hidden sx = { { gridColumn : 1 , gridRow : 1 , whiteSpace : 'nowrap' , ... measuringCellSx } } >
< Typography variant = "body1" sx = { { fontWeight : 600 } } > { d . number } < / Typography >
< Typography variant = "body2" > { d . externalNumber || 'Nr. / Ref.' } < / Typography >
< / Box >
< Box aria - hidden sx = { { gridColumn : 2 , gridRow : 1 , whiteSpace : 'nowrap' , ... measuringCellSx } } >
< Typography variant = "body2" > { d . date ? formatDate ( d . date ) : '– ' } < / Typography >
< / Box >
< Box aria - hidden sx = { { gridColumn : 3 , gridRow : 1 , whiteSpace : 'nowrap' , ... measuringCellSx } } >
< Typography variant = "body2" sx = { { fontWeight : 600 } } > { d . orderNumber } < / Typography >
< / Box >
< Box aria - hidden sx = { { gridColumn : 4 , gridRow : 1 , whiteSpace : 'nowrap' , ... measuringCellSx } } >
< Typography variant = "body1" > { d . customerName } < / Typography >
< / Box >
< Box aria - hidden sx = { { gridColumn : 5 , gridRow : 1 , display : 'flex' , justifyContent : 'center' , ... measuringCellSx } } >
< Chip size = "small" label = { d . positions } sx = { { height : 24 , '& .MuiChip-label' : { px : 0.5 } } } / >
< / Box >
< Box aria - hidden sx = { { gridColumn : 6 , gridRow : 1 , whiteSpace : 'nowrap' , ... measuringCellSx } } >
< Box sx = { { display : 'flex' , alignItems : 'center' , gap : 0.5 } } >
< Chip size = "small" variant = "outlined" label = "DHL" sx = { { height : 20 , fontSize : '0.7rem' } } / >
< Typography variant = "caption" sx = { { fontWeight : 600 , fontVariantNumeric : 'tabular-nums' } } >
{ d . trackingCode }
< / Typography >
< / Box >
< / Box >
< Box aria - hidden sx = { { gridColumn : 7 , gridRow : 1 , display : 'flex' , justifyContent : 'center' , ... measuringCellSx } } >
< Chip
size = "small"
color = "success"
label = {
< Box sx = { { display : 'flex' , alignItems : 'center' , gap : 0.25 } } >
< span > Geliefert < / span >
< ArrowDropDownIcon sx = { { fontSize : 16 } } / >
< / Box >
}
sx = { { fontWeight : 600 , height : 24 } }
/ >
< / Box >
< Box aria - hidden sx = { { gridColumn : 8 , gridRow : 1 , display : 'flex' , justifyContent : 'center' , ... measuringCellSx } } >
< IconButton sx = { { width : 40 , height : 40 } } / >
< / Box >
< / >
) ;
}
/**
* Ambient context for `ShipmentRow`: FillSpace renders rows as bare elements
* (`<Row item index />`), so the list publishes callbacks and party lookup here.
*/
const rowContext = {
partiesById : { } ,
busyId : null ,
onOpen : null ,
onDelete : null ,
headerItem : null ,
} ;
// MARK: <ShipmentRow/>
class ShipmentRow extends PureComponent {
partyName ( partyId ) {
if ( partyId == null ) return '– ' ;
const party = rowContext . partiesById [ partyId ] ;
return partyDisplayName ( 'order' , party ) || ` Kunde # ${ partyId } ` ;
}
render ( ) {
const { shipment , onOpen , onDelete , disabled } = this . props ;
if ( ! shipment ) return null ;
const { item : shipment , disabled } = this . props ;
const dummy = this . props . headerItem || rowContext . headerItem ;
// Data-free probe (FillSpace measures the row height with `item` undefined).
if ( ! shipment ) {
if ( disabled ) {
return (
< ListItemButton disabled divider sx = { { ... rowGridSx , height : 64 , minHeight : 64 , maxHeight : 64 , px : 2 , py : 0 , opacity : 0 , pointerEvents : 'none' } } >
< Typography variant = "body2" > & nbsp ; < / Typography >
< / ListItemButton >
) ;
}
return (
< ListItemButton disabled divider sx = { { ... rowGridSx , height : 64 , minHeight : 64 , maxHeight : 64 , px : 2 , py : 0 } } >
{ renderInvisibleDummyRow ( dummy ) }
< Box sx = { { gridColumn : 1 , gridRow : 2 } } > < Skeleton variant = "text" width = "90%" height = { 20 } / > < / Box >
< Box sx = { { gridColumn : 2 , gridRow : 2 } } > < Skeleton variant = "text" width = "90%" height = { 20 } / > < / Box >
< Box sx = { { gridColumn : 3 , gridRow : 2 } } > < Skeleton variant = "text" width = "90%" height = { 20 } / > < / Box >
< Box sx = { { gridColumn : 4 , gridRow : 2 } } > < Skeleton variant = "text" width = "60%" height = { 20 } / > < / Box >
< Box sx = { { gridColumn : 5 , gridRow : 2 , display : 'flex' , justifyContent : 'center' } } > < Skeleton variant = "rounded" width = { 28 } height = { 20 } / > < / Box >
< Box sx = { { gridColumn : 6 , gridRow : 2 } } > < Skeleton variant = "text" width = { 100 } height = { 20 } / > < / Box >
< Box sx = { { gridColumn : 7 , gridRow : 2 , display : 'flex' , justifyContent : 'center' } } > < Skeleton variant = "rounded" width = { 70 } height = { 24 } / > < / Box >
< Box sx = { { gridColumn : 8 , gridRow : 2 } } / >
< / ListItemButton >
) ;
}
const { busyId , onOpen , onDelete } = rowContext ;
const isBusy = busyId === shipment . id ;
const canDelete = shipment . state === 'new' ;
return (
< ListItemButton
onClick = { ( ) => onOpen ( shipment ) }
sx = { {
p : 0 ,
borderBottom : '1px solid' ,
borderColor : 'divider' ,
bgcolor : WHITE ,
'&:hover' : {
bgcolor : 'action.hover' ,
} ,
} }
divider
sx = { { ... rowGridSx , height : 64 , minHeight : 64 , maxHeight : 64 , px : 2 , py : 0 } }
onClick = { disabled ? undefined : ( ) => onOpen ? . ( shipment ) }
disabled = { disabled }
>
< Box sx = { rowGridSx } >
{ renderInvisibleDummyRow ( dummy ) }
{ /* 1. Lieferschein-Nr & Ref */ }
< Box sx = { { minWidth : 0 , overflow : 'hidden ' } } >
< Typography variant = "body2 " sx = { { fontWeight : 7 00, ... ellipsisSx } } >
< Box sx = { { gridColumn : 1 , gridRow : 2 , minWidth : 0 , overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap ' } } >
< Typography variant = "body1 " sx = { { fontWeight : 6 00, ... ellipsisSx } } >
{ shipment . number }
< / Typography >
{ shipment . externalNumber ? (
< Typography variant = "caption" color = "text.secondary" sx = { ellipsisSx } >
< Typography variant = "body2" color = "text.secondary" sx = { ellipsisSx } >
{ numberDisplay ( shipment . externalNumber ) }
< / Typography >
) : null }
< / Box >
{ /* 2. Datum */ }
< Box sx = { { gridColumn : 2 , gridRow : 2 , minWidth : 0 , overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap' } } >
< Typography variant = "body2" color = "text.secondary" sx = { ellipsisSx } >
{ shipment . date ? formatDate ( shipment . date ) : '– ' }
< / Typography >
< / Box >
{ /* 3. Bezugsauftrag */ }
< Box sx = { { minWidth : 0 , overflow : 'hidden ' } } >
< Box sx = { { gridColumn : 3 , gridRow : 2 , minWidth : 0 , overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap ' } } >
{ shipment . orderNumber ? (
< Typography variant = "body2" sx = { { fontWeight : 600 , color : 'primary.main' , ... ellipsisSx } } >
{ shipment . orderNumber }
@@ -451,80 +557,68 @@ class ShipmentRow extends PureComponent {
< / Box >
{ /* 4. Kunde */ }
< Typography variant = "body2" sx = { { fontWeight : 50 0, ... ellipsisSx } } >
{ shipment . customerName || ( shipment . entityId ? ` Kunde # ${ shipment . entityId } ` : '– ' ) }
< / Typography >
{ /* 5. Positionen & Menge */ }
< Box sx = { { display : 'flex' , flexDirection : 'column' } } >
< Typography variant = "caption" sx = { { fontWeight : 700 } } >
{ shipment . positions || 0 } Pos .
< / Typography >
< Typography variant = "caption" color = "text.secondary" >
{ shipment . totalQuantity || 0 } Stk
< Box sx = { { gridColumn : 4 , gridRow : 2 , minWidth : 0 , overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap' } } >
< Typography variant = "body1" sx = { { fontWeight : 500 , ... ellipsisSx } } >
{ shipment . customerName || this . partyName ( shipment . entityId ) }
< / Typography >
< / Box >
{ /* 6 . Paket & Tracking */ }
< Box sx = { { minWidth : 0 , overflow : 'hidden ' } } >
{ shipment . carrier || shipment . trackingCode ? (
< Box sx = { { display : 'flex' , alignItems : 'center' , gap : 0.5 } } >
{ shipment . carrier && (
{ /* 5 . Positionen */ }
< Box sx = { { gridColumn : 5 , gridRow : 2 , display : 'flex' , justifyContent : 'center ' } } >
< Chip
size = "small"
variant = "outlined"
color = "primary"
label = { shipment . carrier }
sx = { { height : 20 , fontSize : '0.65rem' , fontWeight : 600 } }
label = { shipment . positions || 0 }
title = { ` ${ shipment . positions || 0 } Positionen ( ${ shipment . totalQuantity || 0 } Stk.) ` }
sx = { { height : 24 , '& .MuiChip- label' : { px : 0.5 } } }
/ >
< / Box >
{ /* 6. Paket / Tracking */ }
< Box sx = { { gridColumn : 6 , gridRow : 2 , minWidth : 0 , overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap' } } >
{ shipment . trackingCode ? (
< Box sx = { { display : 'flex' , alignItems : 'center' , gap : 0.5 } } >
{ shipment . carrier && (
< Chip size = "small" variant = "outlined" label = { shipment . carrier } sx = { { height : 20 , fontSize : '0.7rem' } } / >
) }
{ shipment . trackingCode && (
< Box
sx = { { display : 'flex' , alignItems : 'center' , gap : 0.25 , cursor : 'pointer' } }
onClick = { ( e ) => {
e . stopPropagation ( ) ;
navigator . clipboard . writeText ( shipment . trackingCode ) ;
enqueueSnackbar ( 'Tracking-Code kopiert!' , { variant : 'info' } ) ;
} }
title = "Tracking-Code kopieren"
>
< Typography variant = "caption" sx = { { fontFamily : 'monospace' , fontWeight : 600 , color : 'text.secondary' , ... ellipsisSx } } >
< Typography variant = "caption" sx = { { fontWeight : 600 , fontVariantNumeric : 'tabular-nums' , ... ellipsisSx } } >
{ shipment . trackingCode }
< / Typography >
< ContentCopyIcon sx = { { fontSize : 13 , color : 'text.secondary' } } / >
< / Box >
) }
< / Box >
) : shipment . packageCount > 0 ? (
< Typography variant = "caption" color = "text.secondary" >
{ shipment . packageCount } { shipment . packageCount === 1 ? 'Paket' : 'Pakete' }
< / Typography >
) : (
< Typography variant = "caption" color = "text.secondary" >
{ shipment . packageCount ? ` ${ shipment . packageCount } Paket(e) ` : 'Keine Pakete' }
–
< / Typography >
) }
< / Box >
{ /* 7. Status */ }
< Box >
< Box sx = { { gridColumn : 7 , gridRow : 2 , display : 'flex' , justifyContent : 'center' } } >
< DeliveryStateMenu shipment = { shipment } / >
< / Box >
{ /* 8. Aktionen */ }
< Box sx = { { display : 'flex' , justifyContent : 'center' } } >
{ shipment . state === 'new' && (
{ /* 8. Aktionen (Löschen) */ }
< Box sx = { { gridColumn : 8 , gridRow : 2 , display : 'flex' , justifyContent : 'center' } } >
{ canDelete ? (
< IconButton
size = "small"
tit le = { ` Lieferschein „ ${ shipment . number } “ löschen ` }
disabled = { disabled }
aria - label = { ` „ ${ shipment . number } “ löschen ` }
disab led = { isBusy || disabled }
onClick = { ( e ) => {
e . stopPropagation ( ) ;
onDelete ( shipment ) ;
onDelete ? . ( shipment ) ;
} }
sx = { { p : 0.5 , color : 'text.secondary' , '&:hover' : { color : 'error.main' } } }
onMouseDown = { ( e ) => e . stopPropagation ( ) }
sx = { { width : 40 , height : 40 } }
>
< DeleteOutlinedIcon sx = { { fontSize : 18 } } / >
< DeleteOutlinedIcon sx = { { fontSize : 24 } } / >
< / IconButton >
) : (
< Box sx = { { width : 40 , height : 40 } } / >
) }
< / Box >
< / Box >
< / ListItemButton >
) ;
}
@@ -532,12 +626,42 @@ class ShipmentRow extends PureComponent {
// MARK: <ShipmentSearchField/>
class ShipmentSearchField extends PureComponent {
constructor ( props ) {
super ( props ) ;
const s = shipmentAutomat . state ;
this . state = {
input : s . filter ? ? '' ,
stateFilter : s . stateFilter ? ? '' ,
seekMonths : s . seekMonths ? ? 0 ,
seekYears : s . seekYears ? ? 0 ,
} ;
}
componentDidMount ( ) {
this . unsub = shipmentAutomat . subscribe ( this , ( state ) => ( {
input : state . filter ? ? '' ,
stateFilter : state . stateFilter ? ? '' ,
seekMonths : state . seekMonths ? ? 0 ,
seekYears : state . seekYears ? ? 0 ,
} ) ) ;
}
componentWillUnmount ( ) {
this . unsub ? . ( ) ;
}
render ( ) {
const { input, stateFilter , onInputChange , onStateFilterChange , onSeekChange , onCreate , seekMonths } = this . props ;
const { inputRef , onCreate } = this . props ;
const { input , stateFilter , seekMonths , seekYears } = this . state ;
const monthActive = seekMonths > 0 ;
const monthLabel = ! monthActive ? '-1 Monat' : ` - ${ seekMonths } Monat ${ seekMonths > 1 ? 'e' : '' } ` ;
const yearActive = seekYears > 0 ;
const yearLabel = ! yearActive ? '-1 Jahr' : ` - ${ seekYears } Jahr ${ seekYears > 1 ? 'e' : '' } ` ;
return (
< PanelTileGrid >
< Box
ref = { inputRef }
sx = { {
gridColumn : { xs : '1 / -1' , sm : 'span 2' , md : 'span 2' } ,
display : 'flex' ,
@@ -546,103 +670,165 @@ class ShipmentSearchField extends PureComponent {
pb : 2 ,
} }
>
{ /* Top row: search & create */ }
< Box sx = { { display : 'flex' , alignItems : 'center' , gap : 1 } } >
{ /* Top row: search field & create button */ }
< Box
sx = { {
display : 'flex' ,
alignItems : 'center' ,
gap : 1 ,
} }
>
< SingleLineTextEdit
sx = { { flex : 1 } }
size = "small"
search
clear
placeholder = "Lieferschein, Auftrag, Kunde, Paket suchen…"
placeholder = "Lieferschein- , Auftrags-, Tracking-Nr. oder Kunde suchen…"
value = { input }
onChange = { onInputChange }
onChange = { ( next ) => shipmentAutomat . actions . setFilter ( next ) }
/ >
< Button variant = "contained" startIcon = { < AddIcon / > } onClick= { onCreate } >
< Button variant = "contained" onClick = { onCreate } >
Lieferschein erstellen
< / Button >
< / Box >
{ /* Filter Bar */ }
< Box sx = { { display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' , flexWrap : 'wrap' , gap : 1.5 } } >
{ /* Filter Bar: Status chips & Negativeseek (-1 Month, -1 Year, Reset) */ }
< Box
sx = { {
display : 'flex' ,
alignItems : 'center' ,
justifyContent : 'space-between' ,
flexWrap : 'wrap' ,
gap : 1.5 ,
} }
>
{ /* Left: Status chips */ }
< Box
sx = { {
display : 'inline-flex' ,
alignItems : 'center' ,
gap : 1 ,
gap : 0.75 ,
px : 1.25 ,
py : 0.5 ,
py : 0.2 5 ,
borderRadius : 2 ,
bgcolor : 'background.paper' ,
boxShadow : 1 ,
} }
>
< Typography variant = "caption" sx = { { fontWeight : 600 , color : 'text.secondary' , mr : 0.5 } } >
Status :
< / Typography >
< Chip
size = "small"
label = "Alle"
color = { ! stateFilter ? 'primary' : 'default' }
variant = { ! stateFilter ? 'filled' : 'outlined' }
onClick = { ( ) => on StateFilterChange ( '' ) }
sx = { { fontWeight : 600 , cursor : 'pointer' } }
onClick = { ( ) => shipmentAutomat . actions . set StateFilter( '' ) }
sx = { { fontWeight : 600 , cursor : 'pointer' , height : 26 } }
/ >
< Chip
size = "small"
label = "Neu"
color = { stateFilter === 'new' ? 'info' : 'default' }
variant = { stateFilter === 'new' ? 'filled' : 'outlined' }
onClick = { ( ) => on StateFilterChange ( 'new' ) }
sx = { { fontWeight : 600 , cursor : 'pointer' } }
onClick = { ( ) => shipmentAutomat . actions . set StateFilter( 'new' ) }
sx = { { fontWeight : 600 , cursor : 'pointer' , height : 26 } }
/ >
< Chip
size = "small"
label = "Geliefert"
color = { stateFilter === 'delivered' ? 'success' : 'default' }
variant = { stateFilter === 'delivered' ? 'filled' : 'outlined' }
onClick = { ( ) => on StateFilterChange ( 'delivered' ) }
sx = { { fontWeight : 600 , cursor : 'pointer' } }
onClick = { ( ) => shipmentAutomat . actions . set StateFilter( 'delivered' ) }
sx = { { fontWeight : 600 , cursor : 'pointer' , height : 26 } }
/ >
< Chip
size = "small"
label = "Retoure"
color = { stateFilter === 'returned' ? 'error' : 'default' }
variant = { stateFilter === 'returned' ? 'filled' : 'outlined' }
onClick = { ( ) => on StateFilterChange ( 'returned' ) }
sx = { { fontWeight : 600 , cursor : 'pointer' } }
onClick = { ( ) => shipmentAutomat . actions . set StateFilter( 'returned' ) }
sx = { { fontWeight : 600 , cursor : 'pointer' , height : 26 } }
/ >
< / Box >
{ /* Date Seek Cutoff */ }
< Box sx = { { display : 'inline- flex' , alignItems : 'center' , gap : 0.75 , px : 1 , py : 0.25 , borderRadius : 2 , bgcolor : 'background.paper' , boxShadow : 1 } } >
{ /* Right: Negativeseek controls (-1 Month, -1 Year, Reset) */ }
< Box sx = { { display : 'flex' , alignItems : 'center' , gap : 1 , flexWrap : 'wrap' } } >
< Box
sx = { {
display : 'inline-flex' ,
alignItems : 'center' ,
gap : 0.75 ,
px : 1.25 ,
py : 0.25 ,
borderRadius : 2 ,
bgcolor : 'background.paper' ,
boxShadow : 1 ,
} }
>
< Button
size = "small"
variant = { seekMonths === 1 ? 'contained' : 'text ' }
onClick = { ( ) => onSeekChange ( seekMonths === 1 ? 0 : 1 ) }
sx = { { minWidth : 'auto' , px : 1 , py : 0.25 , height : 28 , fontSize : '0.75rem' , fontWeight : 600 , textTransform : 'none' } }
variant = { monthActive ? 'contained' : 'outlined ' }
color = { monthActive ? 'primary' : 'inherit' }
onClick = { ( ) => shipmentAutomat . actions . addSeekMonth ( ) }
sx = { {
minWidth : 88 ,
px : 1.25 ,
py : 0.25 ,
height : 28 ,
fontSize : '0.75rem' ,
fontWeight : 600 ,
textTransform : 'none' ,
boxSizing : 'border-box' ,
border : '1px solid' ,
borderColor : monthActive ? 'primary.main' : 'divider' ,
whiteSpace : 'nowrap' ,
} }
>
- 1 Monat
{ monthLabel }
< / Button >
< Button
size = "small"
variant = { seekMonths === 12 ? 'contained' : 'text ' }
onClick = { ( ) => onSeekChange ( seekMonths === 12 ? 0 : 12 ) }
sx = { { minWidth : 'auto' , px : 1 , py : 0.25 , height : 28 , fontSize : '0.75rem' , fontWeight : 600 , textTransform : 'none' } }
variant = { yearActive ? 'contained' : 'outlined ' }
color = { yearActive ? 'primary' : 'inherit' }
onClick = { ( ) => shipmentAutomat . actions . addSeekYear ( ) }
sx = { {
minWidth : 78 ,
px : 1.25 ,
py : 0.25 ,
height : 28 ,
fontSize : '0.75rem' ,
fontWeight : 600 ,
textTransform : 'none' ,
boxSizing : 'border-box' ,
border : '1px solid' ,
borderColor : yearActive ? 'primary.main' : 'divider' ,
whiteSpace : 'nowrap' ,
} }
>
- 1 Jahr
{ yearLabel }
< / Button >
< Button
size = "small"
variant = "text"
disabled = { seekMonths === 0 }
onClick = { ( ) => onSeekChange ( 0 ) }
sx = { { minWidth : 'auto' , px : 1 , py : 0.25 , height : 28 , fontSize : '0.75rem' , fontWeight : 600 , textTransform : 'none' } }
color = "inherit"
disabled = { ! monthActive && ! yearActive }
onClick = { ( ) => shipmentAutomat . actions . resetNegativeSeek ( ) }
sx = { {
minWidth : 'auto' ,
px : 1 ,
py : 0.25 ,
height : 28 ,
fontSize : '0.75rem' ,
fontWeight : 600 ,
textTransform : 'none' ,
} }
>
Reset
< / Button >
< / Box >
< / Box >
< / Box >
< / Box >
< / PanelTileGrid >
) ;
}
@@ -652,22 +838,18 @@ class ShipmentSearchField extends PureComponent {
export class ShipmentList extends PureComponent {
constructor ( props ) {
super ( props ) ;
const s = shipmentAutomat . state ;
this . headerItem = createDummyTestShipment ( ) ;
this . state = {
items : s. items ,
total : s . total ,
un filteredTotal : s . unf ilteredTotal ,
filter : s . filter ,
stateFilter : s . stateFilter ,
seekMonths : s . seekMonths ,
status : s . status ,
error : s . error ? backendErrorFrom ( s . error ) : null ,
items : shipmentAutomat . getItems ( ) ,
unfilteredTotal : shipmentAutomat . state . unfilteredTotal ? ? 0 ,
filterKey : shipmentAutomat . getF ilterKey ? . ( ) ? ? '' ,
orders : orderAutomat . state ? . items || [ ] ,
partiesById : { } ,
selectedShipment : null ,
newShipmentModalOpen : false ,
busyId : null ,
} ;
this . searchRef = createRef ( ) ;
this . _cancelled = false ;
this . handleRetry = this . handleRetry . bind ( this ) ;
this . handleOpenShipment = this . handleOpenShipment . bind ( this ) ;
@@ -679,19 +861,20 @@ export class ShipmentList extends PureComponent {
this . _cancelled = false ;
this . unsub = shipmentAutomat . subscribe ( this , ( state ) => ( {
items : state . items ,
t otal: state . total ,
un filteredTotal : st ate . unf ilteredTotal ,
filter : state . filter ,
stateFilter : state . stateFilter ,
seekMonths : state . seekMonths ,
status : state . status ,
error : state . error ? backendErrorFrom ( state . error ) : null ,
unfilteredT otal: state . unfilteredTotal ? ? 0 ,
filterKey : shipmentAutom at . getF ilterKey ( state ) ,
} ) ) ;
this . unsubOrder = orderAutomat . subscribe ( this , ( state ) => ( {
orders : state . items || [ ] ,
} ) ) ;
this . unsubNumber = numberAutomat . subscribe ( this , ( ) => {
this . headerItem = createDummyTestShipment ( ) ;
this . forceUpdate ( ) ;
} ) ;
void numberAutomat . actions . ensureLoaded ( ) . catch ( ( ) => { } ) ;
void shipmentAutomat . actions . load ( ) . catch ( ( ) => { } ) ;
void orderAutomat . actions . load ( ) . catch ( ( ) => { } ) ;
void this . loadParties ( ) ;
@@ -720,6 +903,7 @@ export class ShipmentList extends PureComponent {
this . _cancelled = true ;
this . unsub ? . ( ) ;
this . unsubOrder ? . ( ) ;
this . unsubNumber ? . ( ) ;
}
async loadParties ( ) {
@@ -746,6 +930,7 @@ export class ShipmentList extends PureComponent {
}
async handleDeleteShipment ( sh ) {
if ( this . state . busyId != null ) return ;
if ( ! window . confirm ( ` Lieferschein „ ${ sh . number } “ wirklich löschen? ` ) ) return ;
this . setState ( { busyId : sh . id } ) ;
try {
@@ -761,13 +946,8 @@ export class ShipmentList extends PureComponent {
render ( ) {
const {
items ,
total ,
unfilteredTotal ,
filter ,
stateFilter ,
seekMonths ,
status ,
error ,
filterKey ,
orders ,
partiesById ,
selectedShipment ,
@@ -775,113 +955,87 @@ export class ShipmentList extends PureComponent {
busyId ,
} = this . state ;
if ( items === null && status === 'failed' ) {
rowContext . partiesById = partiesById ;
rowContext . busyId = busyId ;
rowContext . onOpen = this . handleOpenShipment ;
rowContext . onDelete = this . handleDeleteShipment ;
rowContext . headerItem = this . headerItem ;
if ( items === null && shipmentAutomat . state . status === 'failed' ) {
return (
< >
< ShipmentSearchField inputRef = { this . searchRef } onCreate = { ( ) => this . setState ( { newShipmentModalOpen : true } ) } / >
< PanelTileGrid >
< PanelTile wide = { 3 } minHeight = { 0 } color = { GREEN _PANEL } sx = { { p : 2 } } >
< BackendError error = { error } onRetry = { this . handleRetry } / >
< BackendError error = { backendErrorFrom ( shipmentAutomat . state . error ) } onRetry = { this . handleRetry } / >
< / PanelTile >
< / PanelTileGrid >
< / >
) ;
}
const shipmentList = Array . isArray ( items ) ? items : [ ] ;
return (
< >
< ShipmentSearchField
input = { filter }
stateFilter = { stateFilter }
seekMonths = { seekMonths }
onInputChange = { ( val ) => shipmentAutomat . actions . setFilter ( val ) }
onStateFilterChange = { ( st ) => shipmentAutomat . actions . setStateFilter ( st ) }
onSeekChange = { ( m ) => shipmentAutomat . actions . setSeekMonths ( m ) }
inputRef = { this . searchRef }
onCreate = { ( ) => this . setState ( { newShipmentModalOpen : true } ) }
/ >
< PanelTileGrid >
< PanelTile wide = { 3 } minHeight = { 48 0} color = { GREEN _PANEL } sx = { { p : 0 , position : 'relative' } } >
{ /* Table Header */ }
< PanelTile wide = { 3 } minHeight = { 0 } color = { GREEN _PANEL } sx = { { pb : 0 , position : 'relative' } } >
< FillSpace
aboveRef = { [ this . searchRef ] }
reserveBottom = { 32 }
minPageSize = { 8 }
minHeight = { 0 }
resetKey = { filterKey }
items = { items || [ ] }
totalCount = { items ? items . length : 0 }
unfilteredTotal = { unfilteredTotal }
headerItem = { this . headerItem }
renderItem = { ShipmentRow }
virtualScrollbar
header = {
< Box
sx = { {
... rowGridSx ,
height : 38 ,
bgcolor : SURFACE _BASE ,
height : 36 ,
minHeight : 36 ,
px : 2 ,
borderBottom : '1px solid' ,
borderColor : 'divider' ,
} }
>
< Typography variant = "caption" sx = { { fontWeight : 700 , color : 'text.secondary' , textTransform : 'uppercase' } } >
Lieferschein / Ref
< / Typography >
< Typography variant = "caption" sx = { { fontWeight : 700 , color : 'text.secondary' , textTransform : 'uppercase' } } >
Datum
< / Typography >
< Typography variant = "caption" sx = { { fontWeight : 700 , color : 'text.secondary' , textTransform : 'uppercase' } } >
Auftrag
< / Typography >
< Typography variant = "caption" sx = { { fontWeight : 700 , color : 'text.secondary' , textTransform : 'uppercase' } } >
Kunde
< / Typography >
< Typography variant = "caption" sx = { { fontWeight : 700 , color : 'text.secondary' , textTransform : 'uppercase' } } >
Positionen
< / Typography >
< Typography variant = "caption" sx = { { fontWeight : 700 , color : 'text.secondary' , textTransform : 'uppercase' } } >
Paket / Tracking
< / Typography >
< Typography variant = "caption" sx = { { fontWeight : 700 , color : 'text.secondary' , textTransform : 'uppercase' } } >
Status
< / Typography >
< Box / >
{ renderInvisibleDummyRow ( this . headerItem ) }
< Box sx = { { gridColumn : 1 , gridRow : 2 , ... headerCellSx } } > Lieferschein / Ref < / Box >
< Box sx = { { gridColumn : 2 , gridRow : 2 , ... headerCellSx } } > Datum < / Box >
< Box sx = { { gridColumn : 3 , gridRow : 2 , ... headerCellSx } } > Auftrag < / Box >
< Box sx = { { gridColumn : 4 , gridRow : 2 , ... headerCellSx } } > Kunde < / Box >
< Box sx = { { gridColumn : 5 , gridRow : 2 , display : 'flex' , justifyContent : 'center' , textAlign : 'center' , ... headerCellSx } } > Pos . < / Box >
< Box sx = { { gridColumn : 6 , gridRow : 2 , ... headerCellSx } } > Paket / Tracking < / Box >
< Box sx = { { gridColumn : 7 , gridRow : 2 , display : 'flex' , justifyContent : 'center' , textAlign : 'center' , ... headerCellSx } } > Status < / Box >
< Box sx = { { gridColumn : 8 , gridRow : 2 , ... headerCellSx } } / >
< / Box >
{ /* Rows */ }
{ items === null && status === 'loading' ? (
< Box sx = { { display : 'flex' , justifyContent : 'center' , py : 8 } } >
< CircularProgress size = { 36 } / >
< / Box >
) : shipmentList . length === 0 ? (
< Box sx = { { p : 4 , textAlign : 'center' } } >
< LocalShippingIcon sx = { { fontSize : 40 , color : 'text.secondary' , mb : 1 } } / >
< Typography variant = "body1" sx = { { fontWeight : 600 } } >
Keine Lieferscheine gefunden .
< / Typography >
< Typography variant = "body2" color = "text.secondary" sx = { { mt : 0.5 } } >
{ filter || stateFilter ? 'Passen Sie die Filter an oder erstellen Sie einen neuen Lieferschein.' : 'Erstellen Sie den ersten Lieferschein aus einem Auftrag.' }
< / Typography >
< / Box >
) : (
< Box sx = { { maxHeight : 'calc(100vh - 280px)' , overflowY : 'auto' } } >
{ shipmentList . map ( ( sh ) => (
< ShipmentRow
key = { sh . id }
shipment = { sh }
onOpen = { this . handleOpenShipment }
onDelete = { this . handleDeleteShipment }
disabled = { busyId === sh . id }
}
/ >
) ) }
< / Box >
) }
{ /* Footer Summary */ }
{ items === null && (
< Box
sx = { {
position : 'absolute' ,
top : 52 ,
left : 0 ,
right : 0 ,
bottom : 32 ,
display : 'flex' ,
alignItems : 'center' ,
justifyContent : 'space-between ' ,
px : 2 ,
py : 1 ,
bgcolor : SURFACE _BASE ,
borderTop : '1px solid' ,
borderColor : 'divider' ,
justifyContent : 'center ' ,
pointerEvents : 'none' ,
zIndex : 2 ,
} }
>
< Typography variant = "caption" color = "text.secondary" >
{ shipmentList . length } von { total } { total === 1 ? 'Lieferschein' : 'Lieferscheinen' } angezeigt
{ unfilteredTotal > total ? ` (gefiltert von ${ unfilteredTotal } ) ` : '' }
< / Typography >
< CircularProgress size = { 32 } / >
< / Box >
) }
< / PanelTile >
< / PanelTileGrid >