u
This commit is contained in:
@@ -1,15 +1,20 @@
|
||||
import { PureComponent, createRef } from 'react';
|
||||
import Autocomplete from '@mui/material/Autocomplete';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import UploadFileIcon from '@mui/icons-material/UploadFile';
|
||||
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
|
||||
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
|
||||
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||
import { PanelTile } from 'dashgui/panels';
|
||||
import FileDropzone from '../FileDropzone';
|
||||
import AttachmentList from './AttachmentList';
|
||||
import { manifestConfig, partyDisplayName } from './manifestConfig';
|
||||
import { hasArticleLines, manifestConfig, partyDisplayName } from './manifestConfig';
|
||||
import { notifyErrorMessage } from '../../utils/notify';
|
||||
|
||||
class ManifestHeader extends PureComponent {
|
||||
constructor(props) {
|
||||
@@ -18,20 +23,50 @@ class ManifestHeader extends PureComponent {
|
||||
this.handlePartyChange = this.handlePartyChange.bind(this);
|
||||
this.handleUploadClick = this.handleUploadClick.bind(this);
|
||||
this.handleFileChange = this.handleFileChange.bind(this);
|
||||
this.handleFileDrop = this.handleFileDrop.bind(this);
|
||||
}
|
||||
|
||||
handlePartyChange(_event, option) {
|
||||
this.props.onPartyChange(option?.id ?? null);
|
||||
}
|
||||
|
||||
hasArticles() {
|
||||
return Boolean(
|
||||
this.props.hasArticles ??
|
||||
(Array.isArray(this.props.lines) && hasArticleLines(this.props.lines)),
|
||||
);
|
||||
}
|
||||
|
||||
handleUploadClick() {
|
||||
if (this.hasArticles()) {
|
||||
notifyErrorMessage(
|
||||
'Datei-Upload nicht möglich, solange Artikel vorhanden sind. Bitte entfernen Sie zuerst alle Zeilen.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.fileInputRef.current?.click();
|
||||
}
|
||||
|
||||
handleFileDrop(file) {
|
||||
if (this.hasArticles()) {
|
||||
notifyErrorMessage(
|
||||
'Datei-Upload nicht möglich, solange Artikel vorhanden sind. Bitte entfernen Sie zuerst alle Zeilen.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.props.onPdfUpload?.(file);
|
||||
}
|
||||
|
||||
handleFileChange(event) {
|
||||
const file = event.target.files?.[0];
|
||||
event.target.value = '';
|
||||
if (!file) return;
|
||||
if (this.hasArticles()) {
|
||||
notifyErrorMessage(
|
||||
'Datei-Upload nicht möglich, solange Artikel vorhanden sind. Bitte entfernen Sie zuerst alle Zeilen.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.props.onPdfUpload?.(file);
|
||||
}
|
||||
|
||||
@@ -50,6 +85,7 @@ class ManifestHeader extends PureComponent {
|
||||
saving,
|
||||
pdfUploading,
|
||||
attachments,
|
||||
lines,
|
||||
onPdfUpload,
|
||||
onPdfRescan,
|
||||
onPdfDelete,
|
||||
@@ -58,8 +94,15 @@ class ManifestHeader extends PureComponent {
|
||||
onDateChange,
|
||||
onNumberChange,
|
||||
onExternalNumberChange,
|
||||
onSave,
|
||||
actionsColor,
|
||||
docId,
|
||||
ledgerSummary,
|
||||
onOpenBookDialog,
|
||||
} = this.props;
|
||||
|
||||
const hasArticles = this.hasArticles();
|
||||
|
||||
// Delivery: number is server-allocated/read-only; the caller reference
|
||||
// lives in the editable externalNumber field.
|
||||
const cfg = manifestConfig(kind);
|
||||
@@ -68,117 +111,243 @@ class ManifestHeader extends PureComponent {
|
||||
const selected =
|
||||
parties.find((row) => row.id === partyId) ?? null;
|
||||
|
||||
const isDateEmpty = !date || String(date).trim() === '';
|
||||
const isPartyEmpty = partyId == null || partyId === '';
|
||||
|
||||
return (
|
||||
<PanelTile wide={3} minHeight={160} color={headerColor} title="Kopfdaten">
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
spacing={2}
|
||||
sx={{ alignItems: { xs: 'stretch', sm: 'center' } }}
|
||||
>
|
||||
<TextField
|
||||
size="small"
|
||||
type="date"
|
||||
label={dateLabel}
|
||||
value={date}
|
||||
disabled={saving || pdfUploading}
|
||||
onChange={(e) => onDateChange(e.target.value)}
|
||||
autoComplete="off"
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
htmlInput: { spellCheck: false },
|
||||
<>
|
||||
{/* Tile 1: 3-wide Kopfdaten Tile */}
|
||||
<PanelTile wide={3} minHeight={160} color={headerColor} title="Kopfdaten">
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 1.5,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
sx={{ minWidth: 220 }}
|
||||
/>
|
||||
<Autocomplete
|
||||
size="small"
|
||||
options={parties}
|
||||
value={selected}
|
||||
loading={Boolean(partiesLoading)}
|
||||
disabled={saving || pdfUploading}
|
||||
getOptionLabel={(option) => partyDisplayName(kind, option)}
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
onChange={this.handlePartyChange}
|
||||
sx={{ flex: 1, minWidth: 220 }}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label={partyLabel}
|
||||
slotProps={{
|
||||
...params.slotProps,
|
||||
htmlInput: {
|
||||
...params.slotProps?.htmlInput,
|
||||
spellCheck: false,
|
||||
autoComplete: 'off',
|
||||
},
|
||||
>
|
||||
<TextField
|
||||
size="small"
|
||||
type="date"
|
||||
label={dateLabel}
|
||||
value={date}
|
||||
error={isDateEmpty}
|
||||
disabled={saving || pdfUploading}
|
||||
onChange={(e) => onDateChange(e.target.value)}
|
||||
autoComplete="off"
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
htmlInput: { spellCheck: false },
|
||||
}}
|
||||
sx={{
|
||||
flex: { xs: '1 1 100%', sm: '1 1 150px' },
|
||||
minWidth: { sm: 130 },
|
||||
}}
|
||||
/>
|
||||
<Autocomplete
|
||||
size="small"
|
||||
options={parties}
|
||||
value={selected}
|
||||
loading={Boolean(partiesLoading)}
|
||||
disabled={saving || pdfUploading}
|
||||
getOptionLabel={(option) => partyDisplayName(kind, option)}
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
onChange={this.handlePartyChange}
|
||||
sx={{
|
||||
flex: { xs: '1 1 100%', sm: '2 1 200px' },
|
||||
minWidth: { sm: 160 },
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label={partyLabel}
|
||||
error={isPartyEmpty}
|
||||
slotProps={{
|
||||
...params.slotProps,
|
||||
htmlInput: {
|
||||
...params.slotProps?.htmlInput,
|
||||
spellCheck: false,
|
||||
autoComplete: 'off',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<SingleLineTextEdit
|
||||
size="small"
|
||||
label="Nummer"
|
||||
value={number}
|
||||
disabled={saving || pdfUploading || useExternalNumber}
|
||||
onChange={onNumberChange}
|
||||
sx={{
|
||||
flex: { xs: '1 1 100%', sm: '1 1 120px' },
|
||||
minWidth: { sm: 100 },
|
||||
}}
|
||||
/>
|
||||
{useExternalNumber ? (
|
||||
<SingleLineTextEdit
|
||||
size="small"
|
||||
label="Referenz"
|
||||
value={externalNumber ?? ''}
|
||||
disabled={saving || pdfUploading}
|
||||
onChange={onExternalNumberChange}
|
||||
sx={{
|
||||
flex: { xs: '1 1 100%', sm: '1 1 120px' },
|
||||
minWidth: { sm: 100 },
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{docId != null && kind === 'delivery' && ledgerSummary && (
|
||||
<Chip
|
||||
size="small"
|
||||
icon={
|
||||
ledgerSummary.status === 'booked' ? (
|
||||
<CheckCircleOutlineOutlinedIcon sx={{ fontSize: 16 }} />
|
||||
) : (
|
||||
<Inventory2OutlinedIcon sx={{ fontSize: 16 }} />
|
||||
)
|
||||
}
|
||||
label={
|
||||
ledgerSummary.status === 'booked'
|
||||
? `Lager: Gebucht (${ledgerSummary.bookedPositions}/${ledgerSummary.totalPositions})`
|
||||
: ledgerSummary.status === 'partial'
|
||||
? `Lager: Teilweise (${ledgerSummary.bookedPositions}/${ledgerSummary.totalPositions})`
|
||||
: ledgerSummary.status === 'empty'
|
||||
? 'Lager: Keine Pos.'
|
||||
: `Lager: Offen (${ledgerSummary.totalPositions} Pos.)`
|
||||
}
|
||||
color={
|
||||
ledgerSummary.status === 'booked'
|
||||
? 'success'
|
||||
: ledgerSummary.status === 'partial'
|
||||
? 'warning'
|
||||
: 'default'
|
||||
}
|
||||
variant="outlined"
|
||||
onClick={onOpenBookDialog}
|
||||
sx={{
|
||||
cursor: onOpenBookDialog ? 'pointer' : 'default',
|
||||
fontWeight: 600,
|
||||
height: 32,
|
||||
px: 0.5,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<AttachmentList
|
||||
attachments={attachments}
|
||||
onRescan={onPdfRescan}
|
||||
onDelete={onPdfDelete}
|
||||
onClick={onAttachmentClick ?? onPdfClick}
|
||||
disabled={saving || pdfUploading}
|
||||
hasArticles={hasArticles}
|
||||
/>
|
||||
<SingleLineTextEdit
|
||||
size="small"
|
||||
label="Nummer"
|
||||
value={number}
|
||||
disabled={saving || pdfUploading || useExternalNumber}
|
||||
onChange={onNumberChange}
|
||||
sx={{ minWidth: 180, flex: 1 }}
|
||||
/>
|
||||
{useExternalNumber ? (
|
||||
<SingleLineTextEdit
|
||||
size="small"
|
||||
label="Referenz"
|
||||
value={externalNumber ?? ''}
|
||||
disabled={saving || pdfUploading}
|
||||
onChange={onExternalNumberChange}
|
||||
sx={{ minWidth: 180, flex: 1 }}
|
||||
/>
|
||||
) : null}
|
||||
{onPdfUpload ? (
|
||||
<FileDropzone
|
||||
accept="application/pdf,.pdf,text/plain,.txt,text/csv,.csv"
|
||||
disabled={saving || pdfUploading}
|
||||
onFileDrop={onPdfUpload}
|
||||
</PanelTile>
|
||||
|
||||
{/* Tile 2: 1-wide Action Tile (4th tile in row) with Speichern & Datei hochladen */}
|
||||
<PanelTile
|
||||
wide={1}
|
||||
minHeight={160}
|
||||
color={actionsColor ?? '#f1f5f9'}
|
||||
sx={{
|
||||
gridColumn: { xs: '1 / -1', sm: '1 / -1', md: 'span 1' },
|
||||
}}
|
||||
>
|
||||
<Stack spacing={1.5} sx={{ pt: 0.5 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={saving}
|
||||
onClick={onSave}
|
||||
sx={{
|
||||
alignSelf: { xs: 'stretch', sm: 'center' },
|
||||
flexShrink: 0,
|
||||
width: { xs: '100%', sm: 175 },
|
||||
height: 40,
|
||||
fontWeight: 700,
|
||||
fontSize: '0.875rem',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{({ isDragging }) => (
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={pdfUploading ? <CircularProgress size={16} color="inherit" /> : <UploadFileIcon />}
|
||||
disabled={saving || pdfUploading}
|
||||
onClick={this.handleUploadClick}
|
||||
title="PDF, TXT oder CSV hochladen"
|
||||
sx={{
|
||||
whiteSpace: 'nowrap',
|
||||
height: 40,
|
||||
width: '100%',
|
||||
px: 2,
|
||||
boxSizing: 'border-box',
|
||||
bgcolor: isDragging ? 'action.hover' : 'transparent',
|
||||
borderColor: isDragging ? 'primary.main' : undefined,
|
||||
transition: 'background-color 0.15s, border-color 0.15s',
|
||||
}}
|
||||
>
|
||||
{pdfUploading
|
||||
? 'Wird analysiert…'
|
||||
: isDragging
|
||||
? 'Datei hier ablegen'
|
||||
: 'Datei hochladen'}
|
||||
</Button>
|
||||
)}
|
||||
</FileDropzone>
|
||||
) : null}
|
||||
</Stack>
|
||||
<AttachmentList
|
||||
attachments={attachments}
|
||||
onRescan={onPdfRescan}
|
||||
onDelete={onPdfDelete}
|
||||
onClick={onAttachmentClick ?? onPdfClick}
|
||||
disabled={saving || pdfUploading}
|
||||
/>
|
||||
{saving ? 'Speichern…' : 'Speichern'}
|
||||
</Button>
|
||||
{docId != null && kind === 'delivery' && onOpenBookDialog && (
|
||||
<Button
|
||||
variant={ledgerSummary?.status === 'booked' ? 'outlined' : 'contained'}
|
||||
color={
|
||||
ledgerSummary?.status === 'booked'
|
||||
? 'success'
|
||||
: ledgerSummary?.status === 'partial'
|
||||
? 'warning'
|
||||
: 'primary'
|
||||
}
|
||||
startIcon={
|
||||
ledgerSummary?.status === 'booked' ? (
|
||||
<CheckCircleOutlineOutlinedIcon />
|
||||
) : (
|
||||
<Inventory2OutlinedIcon />
|
||||
)
|
||||
}
|
||||
disabled={saving || pdfUploading}
|
||||
onClick={onOpenBookDialog}
|
||||
sx={{
|
||||
height: 40,
|
||||
fontWeight: 700,
|
||||
fontSize: '0.8125rem',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{ledgerSummary?.status === 'booked'
|
||||
? 'Lager gebucht'
|
||||
: ledgerSummary?.status === 'partial'
|
||||
? 'Rest einbuchen'
|
||||
: 'Ins Lager buchen'}
|
||||
</Button>
|
||||
)}
|
||||
{onPdfUpload ? (
|
||||
<FileDropzone
|
||||
accept="application/pdf,.pdf,text/plain,.txt,text/csv,.csv"
|
||||
disabled={saving || pdfUploading}
|
||||
onFileDrop={this.handleFileDrop}
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
{({ isDragging }) => (
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={pdfUploading ? <CircularProgress size={16} color="inherit" /> : <UploadFileIcon />}
|
||||
disabled={saving || pdfUploading}
|
||||
onClick={this.handleUploadClick}
|
||||
title={
|
||||
hasArticles
|
||||
? 'Datei-Upload nicht möglich, solange Artikel vorhanden sind'
|
||||
: 'PDF, TXT oder CSV hochladen'
|
||||
}
|
||||
sx={{
|
||||
whiteSpace: 'nowrap',
|
||||
height: 40,
|
||||
width: '100%',
|
||||
px: 1.5,
|
||||
boxSizing: 'border-box',
|
||||
bgcolor: isDragging ? 'action.hover' : 'transparent',
|
||||
borderColor: isDragging
|
||||
? 'primary.main'
|
||||
: hasArticles
|
||||
? 'action.disabled'
|
||||
: undefined,
|
||||
color: hasArticles ? 'text.disabled' : undefined,
|
||||
opacity: hasArticles ? 0.6 : 1,
|
||||
cursor: hasArticles ? 'not-allowed' : 'pointer',
|
||||
transition: 'background-color 0.15s, border-color 0.15s, opacity 0.15s',
|
||||
}}
|
||||
>
|
||||
{pdfUploading
|
||||
? 'Wird analysiert…'
|
||||
: isDragging
|
||||
? 'Datei hier ablegen'
|
||||
: 'Datei hochladen'}
|
||||
</Button>
|
||||
)}
|
||||
</FileDropzone>
|
||||
) : null}
|
||||
</Stack>
|
||||
</PanelTile>
|
||||
{onPdfUpload ? (
|
||||
<input
|
||||
ref={this.fileInputRef}
|
||||
@@ -188,7 +357,7 @@ class ManifestHeader extends PureComponent {
|
||||
onChange={this.handleFileChange}
|
||||
/>
|
||||
) : null}
|
||||
</PanelTile>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user