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 UploadFileIcon from '@mui/icons-material/UploadFile';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
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 { enqueueSnackbar } from 'notistack';
import RestoreIcon from '@mui/icons-material/Restore';
import FileDropzone from '../FileDropzone';
import AttachmentList from './AttachmentList';
import {
hasArticleLines,
manifestConfig,
partyDisplayName,
getLowestSortNonSupplierCustomerGroup,
formatDateTime,
} from './manifestConfig';
import { notifyError, notifyErrorMessage } from '../../utils/notify';
import entityAutomat from '../../automats/entityAutomat';
import companyAutomat from '../../automats/companyAutomat';
import quoteAutomat from '../../automats/quoteAutomat';
import orderAutomat from '../../automats/orderAutomat';
import deliveryAutomat from '../../automats/deliveryAutomat';
import { getNextDocumentNumber, ensureDocumentNumber } from '../../utils/documentNumber';
import {
SURFACE_RAISED,
} from '../../colors';
class ManifestHeader extends PureComponent {
constructor(props) {
super(props);
const selected =
(props.parties || []).find((row) => row.id === props.partyId) ??
(props.kind === 'delivery'
? (companyAutomat.state.suppliers || []).find((row) => row.id === props.partyId)
: (entityAutomat.state.items || []).find((row) => row.id === props.partyId)) ??
null;
this.state = {
partyInput: selected ? partyDisplayName(props.kind, selected) : '',
creatingParty: false,
createdParty: null,
};
this.fileInputRef = createRef();
this.partyInputRef = createRef();
this.focusPartyInput = this.focusPartyInput.bind(this);
this.handlePartyChange = this.handlePartyChange.bind(this);
this.handlePartyInputChange = this.handlePartyInputChange.bind(this);
this.handlePartyKeyDown = this.handlePartyKeyDown.bind(this);
this.handleCreateParty = this.handleCreateParty.bind(this);
this.handleUploadClick = this.handleUploadClick.bind(this);
this.handleFileChange = this.handleFileChange.bind(this);
this.handleFileDrop = this.handleFileDrop.bind(this);
}
componentDidUpdate(prevProps) {
if (prevProps.partyId !== this.props.partyId || prevProps.parties !== this.props.parties) {
const selected =
(this.props.parties || []).find((row) => row.id === this.props.partyId) ??
(this.state.createdParty?.id === this.props.partyId ? this.state.createdParty : null) ??
(this.props.kind === 'delivery'
? (companyAutomat.state.suppliers || []).find((row) => row.id === this.props.partyId)
: (entityAutomat.state.items || []).find((row) => row.id === this.props.partyId));
if (selected) {
this.setState({ partyInput: partyDisplayName(this.props.kind, selected) });
} else if (this.props.partyId == null) {
this.setState({ partyInput: '', createdParty: null });
}
}
if (!prevProps.partyError && this.props.partyError) {
this.partyInputRef.current?.focus();
}
}
focusPartyInput() {
this.partyInputRef.current?.focus();
}
handlePartyChange(_event, option) {
this.setState({
createdParty: null,
partyInput: option ? partyDisplayName(this.props.kind, option) : '',
});
this.props.onPartyChange(option?.id ?? null);
}
handlePartyInputChange(_event, value, reason) {
if (reason === 'reset') {
const selected =
(this.props.parties || []).find((row) => row.id === this.props.partyId) ??
this.state.createdParty;
this.setState({
partyInput: selected ? partyDisplayName(this.props.kind, selected) : value,
});
return;
}
this.setState({ partyInput: value });
}
handlePartyKeyDown(event) {
if (event.key !== 'Enter') return;
if (event.defaultPrevented) return;
const activeDescendant = event.target?.getAttribute?.('aria-activedescendant');
if (activeDescendant) return;
const raw = this.state.partyInput?.trim();
if (!raw || this.state.creatingParty) return;
const { parties, kind } = this.props;
const lower = raw.toLowerCase();
const existing = (parties || []).find((p) => {
const label = partyDisplayName(kind, p).toLowerCase();
return label === lower || String(p.name ?? '').toLowerCase() === lower;
});
if (existing) {
event.preventDefault();
event.stopPropagation();
this.setState({
createdParty: null,
partyInput: partyDisplayName(kind, existing),
});
this.props.onPartyChange(existing.id);
event.target?.blur?.();
} else {
event.preventDefault();
event.stopPropagation();
void this.handleCreateParty(raw, event.target);
}
}
async handleCreateParty(rawInput, targetEl) {
const raw = String(rawInput ?? this.state.partyInput ?? '').trim();
if (!raw || this.state.creatingParty) return;
this.setState({ creatingParty: true });
try {
const { kind, onPartyChange } = this.props;
if (kind === 'delivery') {
const row = await companyAutomat.actions.createSupplier({ name: raw });
const displayName = row?.name || raw;
enqueueSnackbar(`Lieferant „${displayName}“ angelegt.`, { variant: 'success' });
this.setState({ creatingParty: false, createdParty: row, partyInput: displayName });
onPartyChange?.(row.id);
} else {
await companyAutomat.actions.ensureCustomergroupsLoaded();
const groups = companyAutomat.state.customergroups ?? [];
const targetGroup = getLowestSortNonSupplierCustomerGroup(groups) ?? groups[0];
if (!targetGroup) {
throw new Error('Keine Kundengruppe vorhanden.');
}
let name = raw;
let firstName = null;
if (raw.includes(',')) {
const parts = raw.split(',').map((p) => p.trim());
name = parts[0];
firstName = parts.slice(1).join(' ') || null;
} else if (raw.includes(' ') && !/gmbh|ag|kg|ug|inc|ltd|co\./i.test(raw)) {
const parts = raw.split(' ');
firstName = parts[0];
name = parts.slice(1).join(' ');
}
const body = {
customergroupId: targetGroup.id,
name,
};
if (firstName) body.firstName = firstName;
const row = await entityAutomat.actions.create(body);
const displayName = [row.firstName, row.name].filter(Boolean).join(' ') || row.name || raw;
enqueueSnackbar(`Kunde „${displayName}“ angelegt.`, { variant: 'success' });
this.setState({ creatingParty: false, createdParty: row, partyInput: displayName });
onPartyChange?.(row.id);
}
if (targetEl?.blur) {
targetEl.blur();
} else if (document.activeElement?.blur) {
document.activeElement.blur();
}
} catch (err) {
this.setState({ creatingParty: false });
notifyError(err);
}
}
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);
}
subscribeListAutomat(kind) {
this.unsubList?.();
this.unsubList = null;
const automat =
kind === 'quote'
? quoteAutomat
: kind === 'order'
? orderAutomat
: kind === 'delivery'
? deliveryAutomat
: null;
if (automat) {
this.unsubList = automat.subscribe(this, () => {
if (!this._cancelled) this.forceUpdate();
});
}
}
componentDidMount() {
this._cancelled = false;
this.unsubNumber = companyAutomat.subscribe(
this,
() => {
this.forceUpdate();
},
['numbers'],
);
this.subscribeListAutomat(this.props.kind);
void companyAutomat.actions.ensureNumbersLoaded().catch(() => {});
if (!this.props.number && this.props.docId == null) {
void ensureDocumentNumber(this.props.kind)
.then(() => {
if (!this._cancelled) this.forceUpdate();
})
.catch(() => {});
}
}
componentDidUpdate(prevProps) {
if (prevProps.kind !== this.props.kind) {
this.subscribeListAutomat(this.props.kind);
}
if (
!this.props.number &&
this.props.docId == null &&
(prevProps.kind !== this.props.kind || prevProps.docId !== this.props.docId)
) {
void ensureDocumentNumber(this.props.kind)
.then(() => {
if (!this._cancelled) this.forceUpdate();
})
.catch(() => {});
}
}
componentWillUnmount() {
this._cancelled = true;
this.unsubNumber?.();
this.unsubList?.();
}
render() {
const {
kind,
headerColor,
dateLabel,
partyLabel,
parties,
partiesLoading,
partyId,
date,
createdAt,
number,
externalNumber,
saving,
pdfUploading,
attachments,
onPdfUpload,
onPdfCreate,
pdfCreating,
onPdfRescan,
onPdfDelete,
onPdfClick,
onAttachmentClick,
onMentionInChat,
onDateChange,
onNumberChange,
onExternalNumberChange,
onSave,
onRestore,
orderCreating,
onCreateOrder,
actionsColor,
partyError,
docId,
ledgerSummary,
onOpenBookDialog,
isDirty,
dirtyFields,
dirtySummary,
} = this.props;
const hasArticles = this.hasArticles();
const isEdit = docId != null;
const previewNumber =
this.props.previewNumber ??
(!number && !isEdit ? getNextDocumentNumber(kind) : null);
const hasAttachments = Array.isArray(attachments) && attachments.length > 0;
const hasParty = partyId != null && partyId !== '';
const partyValid = kind !== 'order' || hasParty;
const canSave =
partyValid &&
(isEdit ? Boolean(isDirty) : Boolean(hasArticles || hasAttachments));
// 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) ??
(this.state.createdParty?.id === partyId ? this.state.createdParty : null);
const isDateEmpty = !date || String(date).trim() === '';
const isPartyEmpty =
(kind !== 'quote' || Boolean(partyError)) && (partyId == null || partyId === '');
return (
<>
{/* Tile 1: 3-wide Tile */}
onDateChange(e.target.value)}
autoComplete="off"
slotProps={{
inputLabel: { shrink: true },
}}
sx={{
flex: { xs: '1 1 100%', sm: '1 1 150px' },
minWidth: { sm: 130 },
...(dirtyFields?.date && {
'& .MuiOutlinedInput-notchedOutline': {
borderColor: 'warning.main',
borderWidth: 2,
},
}),
}}
/>
Wird angelegt…
) : (
{
e.preventDefault();
const raw = this.state.partyInput?.trim();
if (raw) void this.handleCreateParty(raw);
}}
sx={{
cursor: 'pointer',
display: 'block',
width: '100%',
userSelect: 'none',
fontWeight: 600,
color: 'primary.main',
}}
>
Enter zum anlegen
)
}
disabled={saving || pdfUploading}
getOptionLabel={(option) => partyDisplayName(kind, option)}
isOptionEqualToValue={(option, value) => option?.id === value?.id}
onChange={this.handlePartyChange}
onInputChange={this.handlePartyInputChange}
onKeyDown={this.handlePartyKeyDown}
sx={{
flex: { xs: '1 1 100%', sm: '2 1 200px' },
minWidth: { sm: 160 },
}}
renderInput={({ inputProps: _unusedInputProps, InputProps: _unusedInputProps2, InputLabelProps: _unusedInputLabelProps, ...params }) => (
)}
/>
{useExternalNumber ? (
) : null}
{isEdit && createdAt ? (
) : null}
{/* Tile 2: 1-wide Action Tile (4th tile in row) with Speichern & Datei hochladen */}
{(() => {
const canRestore = Boolean(onRestore && isDirty && !saving && !orderCreating);
return (
}
disabled={!canRestore}
onClick={canRestore ? onRestore : undefined}
title={
canRestore
? 'Änderungen verwerfen und ursprünglichen Stand wiederherstellen'
: 'Keine Änderungen zum Wiederherstellen'
}
sx={{
height: 34,
width: '100%',
fontWeight: 700,
fontSize: '0.8rem',
whiteSpace: 'nowrap',
textTransform: 'none',
borderWidth: 1.5,
'&:hover': { borderWidth: 1.5 },
...(!canRestore && {
borderColor: 'action.disabled',
color: 'text.disabled',
'&.Mui-disabled': {
borderColor: 'action.disabled',
color: 'text.disabled',
borderWidth: 1.5,
},
}),
}}
>
Wiederherstellen
);
})()}
{docId != null && kind === 'delivery' && onOpenBookDialog && Boolean(ledgerSummary && (ledgerSummary.totalPositions > 0 || ledgerSummary.lines?.length > 0 || ledgerSummary.hasTrackStock)) && (
) : (
)
}
disabled={saving || pdfUploading}
onClick={onOpenBookDialog}
sx={{
height: 40,
fontWeight: 700,
fontSize: '0.8125rem',
whiteSpace: 'nowrap',
...(ledgerSummary?.status !== 'booked' && {
color: '#ffffff',
'& .MuiButton-startIcon': { color: '#ffffff' },
}),
}}
>
{ledgerSummary?.status === 'booked'
? 'Vollständig gebucht'
: ledgerSummary?.status === 'partial'
? 'Rest verbuchen'
: 'Lieferung verbuchen'}
)}
{kind === 'quote' && docId != null && onCreateOrder && (
: }
disabled={saving || orderCreating || pdfCreating}
onClick={onCreateOrder}
title={!partyId ? 'Kunde für Bestellung erforderlich' : 'Bestellung aus diesem Angebot erstellen'}
sx={{
whiteSpace: 'nowrap',
height: 40,
width: '100%',
px: 1.5,
boxSizing: 'border-box',
fontWeight: 600,
fontSize: '0.875rem',
}}
>
{orderCreating ? 'Wird erstellt…' : 'Bestellung erstellen'}
)}
{(kind === 'quote' || kind === 'order' || kind === 'delivery') && (() => {
const canCreatePdf = Boolean(
onPdfCreate &&
docId != null &&
Boolean(number) &&
!saving &&
!pdfCreating &&
!orderCreating
);
return (
: }
disabled={!canCreatePdf}
onClick={canCreatePdf ? onPdfCreate : undefined}
title={
pdfCreating
? 'PDF wird erstellt…'
: canCreatePdf
? 'PDF erstellen'
: docId == null
? 'PDF kann erst nach dem Speichern erstellt werden'
: !number
? 'Dokumentnummer erforderlich für PDF'
: 'PDF erstellen'
}
sx={{
whiteSpace: 'nowrap',
height: 40,
width: '100%',
px: 1.5,
boxSizing: 'border-box',
fontWeight: 600,
fontSize: '0.875rem',
...(!canCreatePdf && {
borderColor: 'action.disabled',
color: 'text.disabled',
'&.Mui-disabled': {
borderColor: 'action.disabled',
color: 'text.disabled',
},
}),
}}
>
{pdfCreating ? 'Wird erstellt…' : 'PDF erstellen'}
);
})()}
{onPdfUpload ? (
{({ isDragging }) => (
: }
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'}
)}
) : null}
{onPdfUpload ? (
) : null}
>
);
}
}
export default ManifestHeader;