This commit is contained in:
seb
2026-09-25 07:27:37 +02:00
parent b331f8f535
commit a3db1742e7
17 changed files with 2273 additions and 371 deletions

View File

@@ -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>
</>
);
}
}