Files
dashfront/src/components/manifest/ManifestList.jsx

233 lines
7.1 KiB
JavaScript

import { PureComponent } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import IconButton from '@mui/material/IconButton';
import Stack from '@mui/material/Stack';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Typography from '@mui/material/Typography';
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import { enqueueSnackbar } from 'notistack';
import BackendError from '../BackendError';
import { PanelTile, PanelTileGrid } from '../panels';
import { StorageManagerContext } from '../../api/storageManagerContext';
import { backendErrorFrom } from '../../utils/backendError';
import { notifyError } from '../../utils/notify';
import withNavigate from '../withNavigate';
import {
formatDate,
loadParties,
manifestConfig,
numberDisplay,
partyDisplayName,
sortDocs,
} from './manifestConfig';
class ManifestList extends PureComponent {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.cfg = manifestConfig(props.kind);
this.state = {
status: 'loading',
error: null,
docs: [],
partiesById: {},
busyId: null,
};
this._cancelled = false;
this.handleRetry = this.handleRetry.bind(this);
this.handleCreate = this.handleCreate.bind(this);
}
componentDidMount() {
this._cancelled = false;
void this.load();
}
componentWillUnmount() {
this._cancelled = true;
}
async load() {
const cfg = this.cfg;
this.setState({ status: 'loading', error: null });
try {
const [docs, parties] = await Promise.all([
cfg.listPromise(this.context),
loadParties(this.props.kind, this.context),
]);
if (this._cancelled) return;
this.setState({
status: 'ready',
docs: sortDocs(docs),
partiesById: cfg.partiesByIdFromList(parties),
});
} catch (err) {
if (this._cancelled) return;
this.setState({ status: 'failed', error: backendErrorFrom(err) });
}
}
handleRetry() {
void this.load();
}
/** Same path as AppShell sub-nav: live useNavigate via withNavigate. */
go(to) {
const { navigate } = this.props;
if (typeof navigate === 'function') {
navigate(to);
return;
}
window.location.assign(to);
}
handleCreate() {
this.go(this.cfg.createPath);
}
handleEdit(doc) {
this.go(this.cfg.editPath(doc.id));
}
async handleDelete(doc) {
if (this.state.busyId != null) return;
if (!window.confirm(this.cfg.confirmDelete(doc.number))) return;
const cfg = this.cfg;
this.setState({ busyId: doc.id });
try {
await this.context[cfg.api].delete(doc.id);
const list = await this.context[cfg.updateCache]();
if (this._cancelled) return;
this.setState({
docs: sortDocs(list),
busyId: null,
});
enqueueSnackbar(cfg.deletedMsg, { variant: 'success' });
} catch (err) {
if (this._cancelled) return;
notifyError(err);
this.setState({ busyId: null });
}
}
partyName(partyId) {
if (partyId == null) return null;
return partyDisplayName(
this.props.kind,
this.state.partiesById[partyId],
);
}
render() {
const { status, error, docs, busyId } = this.state;
const cfg = this.cfg;
const partyField = cfg.partyField;
if (status === 'loading') {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
<CircularProgress />
</Box>
);
}
if (status === 'failed') {
return <BackendError error={error} onRetry={this.handleRetry} />;
}
return (
<PanelTileGrid>
<PanelTile wide={3} minHeight={240} color={cfg.listColor}>
<Stack spacing={2}>
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button
type="button"
variant="contained"
size="small"
onClick={this.handleCreate}
>
{cfg.createButton}
</Button>
</Box>
{docs.length === 0 ? (
<Typography variant="body2" color="text.secondary">
{cfg.emptyList}
</Typography>
) : (
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Nummer</TableCell>
{cfg.externalNumber ? (
<TableCell>Referenz</TableCell>
) : null}
<TableCell>{cfg.dateLabel}</TableCell>
<TableCell>{cfg.partyLabel}</TableCell>
<TableCell padding="checkbox" />
</TableRow>
</TableHead>
<TableBody>
{docs.map((row) => (
<TableRow
key={row.id}
hover
sx={{ cursor: 'pointer' }}
onClick={() => this.handleEdit(row)}
>
<TableCell>{row.number}</TableCell>
{cfg.externalNumber ? (
<TableCell>
{numberDisplay(row.externalNumber)}
</TableCell>
) : null}
<TableCell>{formatDate(row.date)}</TableCell>
<TableCell>{this.partyName(row[partyField])}</TableCell>
<TableCell
padding="checkbox"
onClick={(e) => e.stopPropagation()}
>
<IconButton
type="button"
size="small"
aria-label={`„${row.number}“ bearbeiten`}
onClick={() => this.handleEdit(row)}
>
<EditOutlinedIcon fontSize="small" />
</IconButton>
<IconButton
type="button"
size="small"
aria-label={`„${row.number}“ löschen`}
disabled={busyId != null}
onClick={() => void this.handleDelete(row)}
>
<DeleteOutlinedIcon fontSize="small" />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</Stack>
</PanelTile>
</PanelTileGrid>
);
}
}
export default withNavigate(ManifestList);