This commit is contained in:
seb
2026-10-03 07:32:29 +02:00
parent bc4a1e0468
commit c79ece307c
19 changed files with 622 additions and 69 deletions

View File

@@ -0,0 +1,152 @@
import { PureComponent } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import RefreshIcon from '@mui/icons-material/Refresh';
import CircularProgress from '@mui/material/CircularProgress';
import { PanelTile } from 'dashgui/panels';
import { AutomatGrid } from 'react-virtual-aggridautomat';
import { articleHistoryGridAutomat } from '../../automats';
import { router } from '../../appRouter';
const COLUMN_DEFS = [
{
headerName: 'ID',
field: 'id',
maxWidth: 90,
sortable: true,
filter: 'agNumberColumnFilter',
cellStyle: { fontVariantNumeric: 'tabular-nums' },
},
{
headerName: 'Datum',
field: 'date',
minWidth: 170,
maxWidth: 200,
sortable: true,
valueFormatter: (p) => (p.value ? new Date(p.value).toLocaleString('de-DE') : ''),
cellStyle: { fontVariantNumeric: 'tabular-nums' },
},
{
headerName: 'Artikel-Nr.',
field: 'sku',
minWidth: 130,
maxWidth: 160,
sortable: true,
filter: 'agTextColumnFilter',
cellRenderer: (p) => {
if (!p.value) return '';
return (
<a
href={`/artikel/${encodeURIComponent(p.value)}`}
onClick={(e) => {
e.preventDefault();
void router.navigate(`/artikel/${encodeURIComponent(p.value)}`);
}}
style={{ color: '#2563eb', fontWeight: 600, textDecoration: 'none' }}
>
{p.value}
</a>
);
},
},
{
headerName: 'Artikelname',
field: 'name',
minWidth: 180,
flex: 1,
sortable: true,
filter: 'agTextColumnFilter',
},
{
headerName: 'Änderung / Ereignis',
field: 'text',
minWidth: 300,
flex: 2,
sortable: true,
filter: 'agTextColumnFilter',
cellStyle: { whiteSpace: 'pre-wrap', lineHeight: '1.4' },
autoHeight: true,
},
{
headerName: 'Benutzer',
field: 'user',
maxWidth: 160,
sortable: true,
filter: 'agTextColumnFilter',
valueGetter: (p) =>
p.data?.user ||
p.data?.employee ||
p.data?.companyuser ||
(p.data?.companyuserId ? `User #${p.data.companyuserId}` : '—'),
},
];
export default class ArticleHistoryPage extends PureComponent {
constructor(props) {
super(props);
const s = articleHistoryGridAutomat.state;
this.state = {
loading: s.loading,
error: s.error,
totalRows: s.totalRows,
};
this.handleReload = this.handleReload.bind(this);
}
componentDidMount() {
this.unsub = articleHistoryGridAutomat.subscribe(this, (state) => ({
loading: state.loading,
error: state.error,
totalRows: state.totalRows,
}));
}
componentWillUnmount() {
this.unsub?.();
}
handleReload() {
articleHistoryGridAutomat.actions.reload();
}
render() {
const { loading, totalRows } = this.state;
const actions = (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
{loading && <CircularProgress size={18} thickness={5} />}
{totalRows >= 0 && (
<Typography variant="body2" color="text.secondary">
{totalRows} Einträge
</Typography>
)}
<Button
size="small"
variant="outlined"
startIcon={<RefreshIcon />}
onClick={this.handleReload}
disabled={loading}
>
Aktualisieren
</Button>
</Box>
);
return (
<PanelTile title="Artikel-Historie" actions={actions} sx={{ p: 0, height: 'calc(100vh - 140px)', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ flex: 1, minHeight: 0, width: '100%' }}>
<AutomatGrid
automat={articleHistoryGridAutomat}
columnDefs={COLUMN_DEFS}
rowIdField="id"
cacheBlockSize={50}
height="100%"
width="100%"
noRowsOverlayText="Keine Historieneinträge vorhanden"
/>
</Box>
</PanelTile>
);
}
}

View File

@@ -0,0 +1,137 @@
import { PureComponent } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import RefreshIcon from '@mui/icons-material/Refresh';
import CircularProgress from '@mui/material/CircularProgress';
import { PanelTile } from 'dashgui/panels';
import { AutomatGrid } from 'react-virtual-aggridautomat';
import { globalHistoryGridAutomat } from '../../automats';
const COLUMN_DEFS = [
{
headerName: 'ID',
field: 'id',
maxWidth: 90,
sortable: true,
filter: 'agNumberColumnFilter',
cellStyle: { fontVariantNumeric: 'tabular-nums' },
},
{
headerName: 'Datum',
field: 'date',
minWidth: 170,
maxWidth: 200,
sortable: true,
valueFormatter: (p) => (p.value ? new Date(p.value).toLocaleString('de-DE') : ''),
cellStyle: { fontVariantNumeric: 'tabular-nums' },
},
{
headerName: 'Typ',
field: 'type',
minWidth: 120,
maxWidth: 160,
sortable: true,
filter: 'agTextColumnFilter',
},
{
headerName: 'Referenz / Key',
field: 'reference',
minWidth: 140,
maxWidth: 200,
sortable: true,
filter: 'agTextColumnFilter',
valueGetter: (p) => p.data?.reference || p.data?.p1 || '',
},
{
headerName: 'Meldung / Beschreibung',
field: 'text',
minWidth: 300,
flex: 2,
sortable: true,
filter: 'agTextColumnFilter',
valueGetter: (p) => p.data?.text || p.data?.value || '',
cellStyle: { whiteSpace: 'pre-wrap', lineHeight: '1.4' },
autoHeight: true,
},
{
headerName: 'Benutzer',
field: 'user',
maxWidth: 160,
sortable: true,
filter: 'agTextColumnFilter',
valueGetter: (p) =>
p.data?.user ||
p.data?.employee ||
(p.data?.companyuserId ? `User #${p.data.companyuserId}` : '—'),
},
];
export default class GlobalHistoryPage extends PureComponent {
constructor(props) {
super(props);
const s = globalHistoryGridAutomat.state;
this.state = {
loading: s.loading,
error: s.error,
totalRows: s.totalRows,
};
this.handleReload = this.handleReload.bind(this);
}
componentDidMount() {
this.unsub = globalHistoryGridAutomat.subscribe(this, (state) => ({
loading: state.loading,
error: state.error,
totalRows: state.totalRows,
}));
}
componentWillUnmount() {
this.unsub?.();
}
handleReload() {
globalHistoryGridAutomat.actions.reload();
}
render() {
const { loading, totalRows } = this.state;
const actions = (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
{loading && <CircularProgress size={18} thickness={5} />}
{totalRows >= 0 && (
<Typography variant="body2" color="text.secondary">
{totalRows} Einträge
</Typography>
)}
<Button
size="small"
variant="outlined"
startIcon={<RefreshIcon />}
onClick={this.handleReload}
disabled={loading}
>
Aktualisieren
</Button>
</Box>
);
return (
<PanelTile title="Globale Historie" actions={actions} sx={{ p: 0, height: 'calc(100vh - 140px)', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ flex: 1, minHeight: 0, width: '100%' }}>
<AutomatGrid
automat={globalHistoryGridAutomat}
columnDefs={COLUMN_DEFS}
rowIdField="id"
cacheBlockSize={50}
height="100%"
width="100%"
noRowsOverlayText="Keine globalen Historieneinträge vorhanden"
/>
</Box>
</PanelTile>
);
}
}

View File

@@ -0,0 +1,36 @@
import { PureComponent } from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import BarChartIcon from '@mui/icons-material/BarChart';
import { PanelTile } from 'dashgui/panels';
export default class StatisticsPage extends PureComponent {
render() {
return (
<PanelTile title="Statistik">
<Box
sx={{
py: 8,
px: 2,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
color: 'text.secondary',
textAlign: 'center',
gap: 1.5,
}}
>
<BarChartIcon sx={{ fontSize: 48, opacity: 0.4 }} />
<Typography variant="h6" color="text.primary" fontWeight={600}>
Statistik & Auswertungen
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ maxWidth: 420 }}>
Hier entstehen demnächst grafische Auswertungen, Umsatzstatistiken und
Bestandskennzahlen.
</Typography>
</Box>
</PanelTile>
);
}
}

View File

@@ -0,0 +1,3 @@
export { default as ArticleHistoryPage } from './ArticleHistoryPage';
export { default as GlobalHistoryPage } from './GlobalHistoryPage';
export { default as StatisticsPage } from './StatisticsPage';