import { PureComponent } from 'react'; import Box from '@mui/material/Box'; import CircularProgress from '@mui/material/CircularProgress'; import Typography from '@mui/material/Typography'; import { enqueueSnackbar } from 'notistack'; import BackendError from '../BackendError'; import { PanelTile, PanelTileGrid } from 'dashgui/panels'; import { StorageManagerContext } from '../../api/storageManagerContext'; import { requireArray, requireDefined, requireObject } from '../../utils/apiShape'; import { backendErrorFrom } from '../../utils/backendError'; import { notifyError } from '../../utils/notify'; import withNavigate from '../withNavigate'; import ManifestHeader from './ManifestHeader'; import ManifestLines from './ManifestLines'; import { buildManifestBody, dateInputValue, emptyLine, lineFromApi, loadParties, manifestConfig, } from './manifestConfig'; class ManifestForm extends PureComponent { static contextType = StorageManagerContext; constructor(props) { super(props); this.cfg = manifestConfig(props.kind); this.state = { status: props.mode === 'edit' ? 'loading' : 'loadingParties', error: null, parties: [], partiesLoading: true, date: '', partyId: null, number: '', externalNumber: '', lines: [emptyLine()], saving: false, }; this._cancelled = false; this.handleRetry = this.handleRetry.bind(this); this.handlePartyChange = this.handlePartyChange.bind(this); this.handleDateChange = this.handleDateChange.bind(this); this.handleNumberChange = this.handleNumberChange.bind(this); this.handleExternalNumberChange = this.handleExternalNumberChange.bind(this); this.handleLineChange = this.handleLineChange.bind(this); this.handleAddLine = this.handleAddLine.bind(this); this.handleRemoveLine = this.handleRemoveLine.bind(this); this.handleSave = this.handleSave.bind(this); } componentDidMount() { this._cancelled = false; if (this.props.mode === 'edit') void this.loadEdit(); else void this.loadPartiesOnly(); } componentDidUpdate(prevProps) { if ( this.props.mode === 'edit' && prevProps.docId !== this.props.docId ) { void this.loadEdit(); } } componentWillUnmount() { this._cancelled = true; } async loadPartiesOnly() { this.setState({ status: 'loadingParties', error: null, partiesLoading: true }); try { const parties = await loadParties(this.props.kind, this.context); if (this._cancelled) return; this.setState({ status: 'ready', parties, partiesLoading: false, }); } catch (err) { if (this._cancelled) return; this.setState({ status: 'failed', error: backendErrorFrom(err), partiesLoading: false, }); notifyError(err); } } async loadEdit() { const { docId, kind } = this.props; const cfg = this.cfg; if (docId == null) { this.setState({ status: 'failed', error: backendErrorFrom(new Error(cfg.invalidId)), }); return; } this.setState({ status: 'loading', error: null, partiesLoading: true }); try { const [docRes, parties] = await Promise.all([ this.context[cfg.api].get(docId), loadParties(kind, this.context), ]); if (this._cancelled) return; requireObject(docRes, cfg.getPathLabel); requireObject(docRes[cfg.docField], cfg.docField); const doc = docRes[cfg.docField]; const apiLines = requireArray(doc.lines, `${cfg.docField}.lines`); const lines = apiLines.length > 0 ? apiLines.map(lineFromApi) : [emptyLine()]; this.setState({ status: 'ready', parties, partiesLoading: false, date: dateInputValue(doc.date), partyId: doc[cfg.partyField], number: doc.number ?? '', externalNumber: doc.externalNumber != null ? String(doc.externalNumber) : '', lines, }); } catch (err) { if (this._cancelled) return; this.setState({ status: 'failed', error: backendErrorFrom(err), partiesLoading: false, }); } } handleRetry() { if (this.props.mode === 'edit') void this.loadEdit(); else void this.loadPartiesOnly(); } handlePartyChange(partyId) { const { mode } = this.props; const { requirePartyBeforeLines } = this.cfg; if (mode === 'create' && requirePartyBeforeLines && partyId == null) { this.setState({ partyId: null, lines: [emptyLine()] }); return; } this.setState({ partyId }); } handleDateChange(date) { this.setState({ date }); } handleNumberChange(number) { this.setState({ number }); } handleExternalNumberChange(externalNumber) { this.setState({ externalNumber }); } handleLineChange(key, field, value) { this.setState((prev) => ({ lines: prev.lines.map((line) => line.key === key ? { ...line, [field]: value } : line, ), })); } handleAddLine() { this.setState((prev) => ({ lines: [...prev.lines, emptyLine()] })); } handleRemoveLine(key) { this.setState((prev) => { const next = prev.lines.filter((line) => line.key !== key); return { lines: next.length > 0 ? next : [emptyLine()] }; }); } async handleSave() { const { mode, docId } = this.props; const cfg = this.cfg; if (this.state.saving) return; if (mode === 'edit' && docId == null) return; const requireAtLeastOne = mode === 'create'; let body; try { body = buildManifestBody( { number: this.state.number, externalNumber: this.state.externalNumber, date: this.state.date, partyId: this.state.partyId, lines: this.state.lines, }, { partyField: cfg.partyField, requireAtLeastOne, externalNumber: cfg.externalNumber, }, ); } catch (err) { notifyError(err); return; } this.setState({ saving: true }); try { if (mode === 'create') { const data = await this.context[cfg.api].create(body); requireObject(data, cfg.postPathLabel); requireObject(data[cfg.docField], cfg.docField); requireDefined(data[cfg.docField].id, `${cfg.docField}.id`); await this.context[cfg.updateCache](); if (this._cancelled) return; enqueueSnackbar(cfg.createdMsg, { variant: 'success' }); this.props.navigate(cfg.editPath(data[cfg.docField].id)); return; } const data = await this.context[cfg.api].update(docId, body); requireObject(data, cfg.patchPathLabel); requireObject(data[cfg.docField], cfg.docField); requireDefined(data[cfg.docField].id, `${cfg.docField}.id`); await this.context[cfg.updateCache](); if (this._cancelled) return; const doc = data[cfg.docField]; const apiLines = requireArray(doc.lines, `${cfg.docField}.lines`); this.setState({ date: dateInputValue(doc.date), partyId: doc[cfg.partyField], number: doc.number ?? '', externalNumber: doc.externalNumber != null ? String(doc.externalNumber) : '', lines: apiLines.length > 0 ? apiLines.map(lineFromApi) : [emptyLine()], saving: false, }); enqueueSnackbar(cfg.savedMsg, { variant: 'success' }); } catch (err) { if (this._cancelled) return; notifyError(err); this.setState({ saving: false }); } } renderArticles() { const { mode } = this.props; const cfg = this.cfg; const { partyId, lines, saving } = this.state; if ( cfg.requirePartyBeforeLines && mode === 'create' && partyId == null ) { return ( {cfg.partyGateMessage} ); } return ( void this.handleSave()} /> ); } render() { const { kind, mode } = this.props; const cfg = this.cfg; const { status, error, parties, partiesLoading, date, partyId, number, externalNumber, saving, } = this.state; if (status === 'loading') { return ( ); } if (status === 'failed') { return ; } return ( {status === 'ready' ? this.renderArticles() : null} ); } } export default withNavigate(ManifestForm);