802 lines
28 KiB
JavaScript
802 lines
28 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 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 */}
|
||
<PanelTile wide={3} minHeight={160} color={headerColor}>
|
||
<Box
|
||
sx={{
|
||
display: 'flex',
|
||
flexWrap: 'wrap',
|
||
gap: 1.5,
|
||
alignItems: 'center',
|
||
}}
|
||
>
|
||
<TextField
|
||
size="small"
|
||
type="date"
|
||
label={dirtyFields?.date ? `${dateLabel} *` : dateLabel}
|
||
value={date}
|
||
error={isDateEmpty}
|
||
disabled={saving || pdfUploading}
|
||
onChange={(e) => 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,
|
||
},
|
||
}),
|
||
}}
|
||
/>
|
||
<Autocomplete
|
||
size="small"
|
||
autoHighlight
|
||
options={parties || []}
|
||
value={selected}
|
||
loading={Boolean(partiesLoading) || this.state.creatingParty}
|
||
loadingText={this.state.creatingParty ? 'Wird angelegt…' : 'Lädt…'}
|
||
noOptionsText={
|
||
this.state.creatingParty ? (
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 0.5 }}>
|
||
<CircularProgress size={16} />
|
||
<span>Wird angelegt…</span>
|
||
</Box>
|
||
) : (
|
||
<Box
|
||
component="span"
|
||
onMouseDown={(e) => {
|
||
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
|
||
</Box>
|
||
)
|
||
}
|
||
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 }) => (
|
||
<TextField
|
||
{...params}
|
||
inputRef={this.partyInputRef}
|
||
label={partyLabel}
|
||
error={isPartyEmpty}
|
||
onKeyDown={this.handlePartyKeyDown}
|
||
slotProps={{
|
||
...params.slotProps,
|
||
htmlInput: {
|
||
...params.slotProps?.htmlInput,
|
||
autoComplete: 'off',
|
||
},
|
||
}}
|
||
sx={{
|
||
...(dirtyFields?.partyId && {
|
||
'& .MuiOutlinedInput-notchedOutline': {
|
||
borderColor: 'warning.main',
|
||
borderWidth: 2,
|
||
},
|
||
}),
|
||
}}
|
||
/>
|
||
)}
|
||
/>
|
||
<SingleLineTextEdit
|
||
size="small"
|
||
label="Nummer"
|
||
value={number || previewNumber || ''}
|
||
disabled
|
||
placeholder="–"
|
||
title={
|
||
!number && previewNumber
|
||
? 'Vorschau der nächsten Nummer (wird beim Speichern vergeben)'
|
||
: undefined
|
||
}
|
||
slotProps={{
|
||
inputLabel: { shrink: true },
|
||
}}
|
||
sx={{
|
||
flex: { xs: '1 1 100%', sm: '1 1 120px' },
|
||
minWidth: { sm: 100 },
|
||
'& .MuiInputBase-input': {
|
||
fontFamily: 'monospace',
|
||
},
|
||
...(!number && previewNumber && {
|
||
'& .MuiInputBase-input.Mui-disabled': {
|
||
fontStyle: 'italic',
|
||
fontFamily: 'monospace',
|
||
},
|
||
}),
|
||
}}
|
||
/>
|
||
{useExternalNumber ? (
|
||
<SingleLineTextEdit
|
||
size="small"
|
||
label={dirtyFields?.externalNumber ? 'Referenz *' : 'Referenz'}
|
||
value={externalNumber ?? ''}
|
||
disabled={saving || pdfUploading}
|
||
onChange={onExternalNumberChange}
|
||
sx={{
|
||
flex: { xs: '1 1 100%', sm: '1 1 120px' },
|
||
minWidth: { sm: 100 },
|
||
'& .MuiInputBase-input': {
|
||
fontFamily: 'monospace',
|
||
},
|
||
...(dirtyFields?.externalNumber && {
|
||
'& .MuiOutlinedInput-notchedOutline': {
|
||
borderColor: 'warning.main',
|
||
borderWidth: 2,
|
||
},
|
||
}),
|
||
}}
|
||
/>
|
||
) : null}
|
||
{isEdit && createdAt ? (
|
||
<SingleLineTextEdit
|
||
size="small"
|
||
label="Erstellt am"
|
||
value={formatDateTime(createdAt)}
|
||
disabled
|
||
slotProps={{
|
||
inputLabel: { shrink: true },
|
||
}}
|
||
sx={{
|
||
flex: { xs: '1 1 100%', sm: '1 1 150px' },
|
||
minWidth: { sm: 130 },
|
||
'& .MuiInputBase-input': {
|
||
fontVariantNumeric: 'tabular-nums',
|
||
},
|
||
}}
|
||
/>
|
||
) : null}
|
||
</Box>
|
||
<AttachmentList
|
||
kind={kind}
|
||
docId={docId}
|
||
attachments={attachments}
|
||
onRescan={onPdfRescan}
|
||
onDelete={onPdfDelete}
|
||
onClick={onAttachmentClick ?? onPdfClick}
|
||
onMentionInChat={onMentionInChat}
|
||
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 ?? SURFACE_RAISED}
|
||
sx={{
|
||
gridColumn: { xs: '1 / -1', sm: '1 / -1', md: 'span 1' },
|
||
}}
|
||
>
|
||
<Stack spacing={1.5} sx={{ pt: 0.5 }}>
|
||
<Button
|
||
variant="contained"
|
||
color={canSave ? 'success' : 'inherit'}
|
||
disabled={Boolean(saving || orderCreating || !canSave)}
|
||
onClick={onSave}
|
||
sx={{
|
||
height: 40,
|
||
fontWeight: 700,
|
||
fontSize: '0.875rem',
|
||
whiteSpace: 'nowrap',
|
||
...(canSave && {
|
||
bgcolor: 'success.main',
|
||
color: '#fff',
|
||
'&:hover': {
|
||
bgcolor: 'success.dark',
|
||
},
|
||
}),
|
||
...(!canSave && {
|
||
bgcolor: 'action.disabledBackground',
|
||
color: 'text.disabled',
|
||
boxShadow: 'none',
|
||
'&.Mui-disabled': {
|
||
bgcolor: 'action.disabledBackground',
|
||
color: 'text.disabled',
|
||
},
|
||
}),
|
||
}}
|
||
>
|
||
{saving ? 'Speichern…' : 'Speichern'}
|
||
</Button>
|
||
{(() => {
|
||
const canRestore = Boolean(onRestore && isDirty && !saving && !orderCreating);
|
||
return (
|
||
<Button
|
||
variant="outlined"
|
||
color={canRestore ? 'warning' : 'inherit'}
|
||
size="small"
|
||
startIcon={<RestoreIcon />}
|
||
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
|
||
</Button>
|
||
);
|
||
})()}
|
||
{docId != null && kind === 'delivery' && onOpenBookDialog && Boolean(ledgerSummary && (ledgerSummary.totalPositions > 0 || ledgerSummary.lines?.length > 0 || ledgerSummary.hasTrackStock)) && (
|
||
<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' && {
|
||
color: '#ffffff',
|
||
'& .MuiButton-startIcon': { color: '#ffffff' },
|
||
}),
|
||
}}
|
||
>
|
||
{ledgerSummary?.status === 'booked'
|
||
? 'Vollständig gebucht'
|
||
: ledgerSummary?.status === 'partial'
|
||
? 'Rest verbuchen'
|
||
: 'Lieferung verbuchen'}
|
||
</Button>
|
||
)}
|
||
{kind === 'quote' && docId != null && onCreateOrder && (
|
||
<Button
|
||
type="button"
|
||
variant="outlined"
|
||
color={partyError && !partyId ? 'warning' : 'primary'}
|
||
startIcon={orderCreating ? <CircularProgress size={16} color="inherit" /> : <ShoppingCartIcon />}
|
||
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'}
|
||
</Button>
|
||
)}
|
||
{(kind === 'quote' || kind === 'order' || kind === 'delivery') && (() => {
|
||
const canCreatePdf = Boolean(
|
||
onPdfCreate &&
|
||
docId != null &&
|
||
Boolean(number) &&
|
||
!saving &&
|
||
!pdfCreating &&
|
||
!orderCreating
|
||
);
|
||
return (
|
||
<Button
|
||
type="button"
|
||
variant="outlined"
|
||
color={canCreatePdf ? 'primary' : 'inherit'}
|
||
startIcon={pdfCreating ? <CircularProgress size={16} color="inherit" /> : <PictureAsPdfIcon />}
|
||
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'}
|
||
</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;
|
||
|