366 lines
12 KiB
JavaScript
366 lines
12 KiB
JavaScript
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 { hasArticleLines, manifestConfig, partyDisplayName } from './manifestConfig';
|
|
import { notifyErrorMessage } from '../../utils/notify';
|
|
|
|
class ManifestHeader extends PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
this.fileInputRef = createRef();
|
|
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);
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
kind,
|
|
headerColor,
|
|
dateLabel,
|
|
partyLabel,
|
|
parties,
|
|
partiesLoading,
|
|
partyId,
|
|
date,
|
|
number,
|
|
externalNumber,
|
|
saving,
|
|
pdfUploading,
|
|
attachments,
|
|
onPdfUpload,
|
|
onPdfRescan,
|
|
onPdfDelete,
|
|
onPdfClick,
|
|
onAttachmentClick,
|
|
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);
|
|
const useExternalNumber = Boolean(cfg.externalNumber);
|
|
|
|
const selected =
|
|
parties.find((row) => row.id === partyId) ?? null;
|
|
|
|
const isDateEmpty = !date || String(date).trim() === '';
|
|
const isPartyEmpty = partyId == null || partyId === '';
|
|
|
|
return (
|
|
<>
|
|
{/* 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',
|
|
}}
|
|
>
|
|
<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}
|
|
/>
|
|
</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={{
|
|
height: 40,
|
|
fontWeight: 700,
|
|
fontSize: '0.875rem',
|
|
whiteSpace: 'nowrap',
|
|
}}
|
|
>
|
|
{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}
|
|
type="file"
|
|
accept="application/pdf,.pdf,text/plain,.txt,text/csv,.csv"
|
|
style={{ display: 'none' }}
|
|
onChange={this.handleFileChange}
|
|
/>
|
|
) : null}
|
|
</>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default ManifestHeader;
|
|
|