refactor(lists): use shared RecordList from dashgui

Replace duplicated table/list markup in the Kunde list page and Manifest
list with the shared dashgui RecordList component, and document dashgui/lists
in the README.
This commit is contained in:
seb
2026-08-08 17:00:42 +02:00
parent 156c53ecc8
commit 5a7574f69a
3 changed files with 73 additions and 193 deletions

View File

@@ -34,6 +34,8 @@ Reusable custom GUI class components live in the **`dashgui`** package
so source imports are reflected instantly. so source imports are reflected instantly.
- `dashgui/panels` — `PanelTile`, `PanelTileGrid`, `EditablePanelTile`, layout helpers - `dashgui/panels` — `PanelTile`, `PanelTileGrid`, `EditablePanelTile`, layout helpers
- `dashgui/lists` — `RecordList`, the generic presentational list used by the
Lieferungen / Angebote / Bestellungen / Kunden list pages
- `dashgui/tiles` — base `Tile` (extended by `src/components/tiles/*`) - `dashgui/tiles` — base `Tile` (extended by `src/components/tiles/*`)
- `dashgui/SingleLineTextEdit`, `dashgui/ElevatedPill` - `dashgui/SingleLineTextEdit`, `dashgui/ElevatedPill`

View File

@@ -1,21 +1,7 @@
import { PureComponent } from 'react'; 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 { enqueueSnackbar } from 'notistack';
import BackendError from '../BackendError'; import BackendError from '../BackendError';
import { PanelTile, PanelTileGrid } from 'dashgui/panels'; import { RecordList } from 'dashgui/lists';
import { StorageManagerContext } from '../../api/storageManagerContext'; import { StorageManagerContext } from '../../api/storageManagerContext';
import { backendErrorFrom } from '../../utils/backendError'; import { backendErrorFrom } from '../../utils/backendError';
import { notifyError } from '../../utils/notify'; import { notifyError } from '../../utils/notify';
@@ -133,98 +119,43 @@ class ManifestList extends PureComponent {
const cfg = this.cfg; const cfg = this.cfg;
const partyField = cfg.partyField; const partyField = cfg.partyField;
if (status === 'loading') { const columns = [{ key: 'number', label: 'Nummer' }];
return ( if (cfg.externalNumber) {
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}> columns.push({
<CircularProgress /> key: 'externalNumber',
</Box> label: 'Referenz',
); render: (row) => numberDisplay(row.externalNumber),
} });
if (status === 'failed') {
return <BackendError error={error} onRetry={this.handleRetry} />;
} }
columns.push(
{
key: 'date',
label: cfg.dateLabel,
render: (row) => formatDate(row.date),
},
{
key: partyField,
label: cfg.partyLabel,
render: (row) => this.partyName(row[partyField]),
},
);
return ( return (
<PanelTileGrid> <RecordList
<PanelTile wide={3} minHeight={240} color={cfg.listColor}> status={status}
<Stack spacing={2}> error={<BackendError error={error} onRetry={this.handleRetry} />}
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}> rows={docs}
<Button columns={columns}
type="button" createLabel={cfg.createButton}
variant="contained" onCreate={this.handleCreate}
size="small" onEdit={this.handleEdit}
onClick={this.handleCreate} onDelete={this.handleDelete}
> deleteDisabled={busyId != null}
{cfg.createButton} emptyMessage={cfg.emptyList}
</Button> color={cfg.listColor}
</Box> getEditLabel={(row) => `„${row.number}“ bearbeiten`}
getDeleteLabel={(row) => `„${row.number}“ löschen`}
{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>
); );
} }
} }

View File

@@ -1,21 +1,7 @@
import { PureComponent } from 'react'; 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 { enqueueSnackbar } from 'notistack';
import BackendError from '../../components/BackendError'; import BackendError from '../../components/BackendError';
import { PanelTile, PanelTileGrid } from 'dashgui/panels'; import { RecordList } from 'dashgui/lists';
import { StorageManagerContext } from '../../api/storageManagerContext'; import { StorageManagerContext } from '../../api/storageManagerContext';
import { router } from '../../appRouter'; import { router } from '../../appRouter';
import { backendErrorFrom } from '../../utils/backendError'; import { backendErrorFrom } from '../../utils/backendError';
@@ -119,86 +105,47 @@ class KundeListePage extends PureComponent {
} }
render() { render() {
const { status, error, entities, busyId } = this.state; const { status, error, entities, groupsById, busyId } = this.state;
if (status === 'loading') {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
<CircularProgress />
</Box>
);
}
if (status === 'failed') { if (status === 'failed') {
return <BackendError error={error} onRetry={this.handleRetry} />; return <BackendError error={error} onRetry={this.handleRetry} />;
} }
return ( const columns = [
<PanelTileGrid> { key: 'number', label: 'Kundennummer' },
<PanelTile wide={3} minHeight={240} color="#bfdbfe"> { key: 'name', label: 'Name' },
<Stack spacing={2}> { key: 'firstName', label: 'Vorname' },
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}> {
<Button variant="contained" size="small" onClick={this.handleCreate}> key: 'customergroupId',
Neuer Kunde label: 'Gruppe',
</Button> render: (row) => this.groupName(row.customergroupId),
</Box> },
{
key: 'createdAt',
label: 'Erstellt am',
render: (row) => formatDate(row.createdAt),
},
];
{entities.length === 0 ? ( const label = (row) =>
<Typography variant="body2" color="text.secondary"> [row.firstName, row.name].filter(Boolean).join(' ') || String(row.number);
Noch keine Kunden.
</Typography> return (
) : ( <RecordList
<TableContainer> status={status}
<Table size="small"> error={<BackendError error={error} onRetry={this.handleRetry} />}
<TableHead> rows={entities}
<TableRow> columns={columns}
<TableCell>Kundennummer</TableCell> createLabel="Neuer Kunde"
<TableCell>Name</TableCell> onCreate={this.handleCreate}
<TableCell>Vorname</TableCell> onEdit={this.handleEdit}
<TableCell>Gruppe</TableCell> onDelete={this.handleDelete}
<TableCell>Erstellt am</TableCell> deleteDisabled={busyId != null}
<TableCell padding="checkbox" /> emptyMessage="Noch keine Kunden."
</TableRow> color="#bfdbfe"
</TableHead> getEditLabel={(row) => `„${label(row)}“ bearbeiten`}
<TableBody> getDeleteLabel={(row) => `„${label(row)}“ löschen`}
{entities.map((row) => ( />
<TableRow
key={row.id}
hover
sx={{ cursor: 'pointer' }}
onClick={() => this.handleEdit(row)}
>
<TableCell>{row.number}</TableCell>
<TableCell>{row.name}</TableCell>
<TableCell>{row.firstName}</TableCell>
<TableCell>{this.groupName(row.customergroupId)}</TableCell>
<TableCell>{formatDate(row.createdAt)}</TableCell>
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
<IconButton
size="small"
aria-label={`„${row.name}“ bearbeiten`}
onClick={() => this.handleEdit(row)}
>
<EditOutlinedIcon fontSize="small" />
</IconButton>
<IconButton
size="small"
aria-label={`„${row.name}“ löschen`}
disabled={busyId != null}
onClick={() => void this.handleDelete(row)}
>
<DeleteOutlinedIcon fontSize="small" />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</Stack>
</PanelTile>
</PanelTileGrid>
); );
} }
} }