From c79ece307c891a62142d01297a6e9671fe9655e4 Mon Sep 17 00:00:00 2001 From: seb Date: Sat, 3 Oct 2026 07:32:29 +0200 Subject: [PATCH] u --- package-lock.json | 71 ++++++++ package.json | 3 + src/api/index.js | 45 ++++++ src/appRouter.jsx | 17 +- src/automats/articleHistoryAutomat.js | 34 ++++ src/automats/globalHistoryAutomat.js | 34 ++++ src/automats/index.js | 3 + src/components/article/ArticlePhoto.jsx | 42 ++--- .../manifest/ArticleSearchDialog.jsx | 16 +- src/components/manifest/ManifestFormLine.jsx | 40 +++-- src/components/tiles/CategoryTile.jsx | 12 +- src/index.css | 3 + src/nav.js | 31 ++++ src/routes/infos.jsx | 10 -- src/routes/infos/ArticleHistoryPage.jsx | 152 ++++++++++++++++++ src/routes/infos/GlobalHistoryPage.jsx | 137 ++++++++++++++++ src/routes/infos/StatisticsPage.jsx | 36 +++++ src/routes/infos/index.js | 3 + vite.config.js | 2 + 19 files changed, 622 insertions(+), 69 deletions(-) create mode 100644 src/automats/articleHistoryAutomat.js create mode 100644 src/automats/globalHistoryAutomat.js delete mode 100644 src/routes/infos.jsx create mode 100644 src/routes/infos/ArticleHistoryPage.jsx create mode 100644 src/routes/infos/GlobalHistoryPage.jsx create mode 100644 src/routes/infos/StatisticsPage.jsx create mode 100644 src/routes/infos/index.js diff --git a/package-lock.json b/package-lock.json index 1836879..f8ced51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,8 @@ "@mui/icons-material": "^9.4.0", "@mui/material": "^9.4.0", "@shopify/draggable": "^1.2.1", + "ag-grid-community": "^36.2.0", + "ag-grid-react": "^36.2.0", "dashgui": "file:../dashgui", "jsbarcode": "^3.12.3", "notistack": "^3.0.2", @@ -24,6 +26,7 @@ "react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor", "react-dom": "^19.3.0", "react-router-dom": "^7.18.4", + "react-virtual-aggridautomat": "file:../react-virtual-aggridautomat", "sortablejs": "^1.15.7", "thumbhash": "^0.1.1" }, @@ -79,6 +82,34 @@ "react-dom": "^19.0.0" } }, + "../react-virtual-aggridautomat": { + "version": "0.0.0", + "license": "0BSD", + "dependencies": { + "@types/prismjs": "^1.26.6", + "ag-grid-community": "^36.2.0", + "ag-grid-react": "^36.2.0", + "marked": "^18.0.14", + "prismjs": "^1.30.0", + "react": "^19.2.8", + "react-automat": "github:sebseb7/react-automat", + "react-dom": "^19.2.8" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.13.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.7", + "@vitejs/plugin-react": "^6.1.1", + "eslint": "^10.10.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.6", + "globals": "^17.12.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.69.0", + "vite": "^8.3.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -3848,6 +3879,42 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/ag-charts-types": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-14.2.0.tgz", + "integrity": "sha512-4pDV6/4zbzPN7t607Gr1o6DO9RhmPafQGr+hIpTJBaerbaMoC+WOO3v/wkxFbvCNrHKOexEKSfNTH7PaAR4UzA==", + "license": "MIT" + }, + "node_modules/ag-grid-community": { + "version": "36.2.0", + "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-36.2.0.tgz", + "integrity": "sha512-tK0Toj7t0fV/OMLPnDc0xlYKAqDbnCT3iJodYSn2TJ541caU8jPdYwWhMdW+AP2+pUNwFiJVgc04eJNfvBPsWw==", + "license": "MIT", + "dependencies": { + "ag-charts-types": "14.2.0", + "ag-stack": "36.2.0" + } + }, + "node_modules/ag-grid-react": { + "version": "36.2.0", + "resolved": "https://registry.npmjs.org/ag-grid-react/-/ag-grid-react-36.2.0.tgz", + "integrity": "sha512-7dsaR4Ga8/bJAOov7XJqxsDc1JAjUU5Bmh9+s4U9+/TKIdSsaVo53SuZb2yEFSB1elv3eh6aZP8a1fqGZehXwA==", + "license": "MIT", + "dependencies": { + "ag-grid-community": "36.2.0", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/ag-stack": { + "version": "36.2.0", + "resolved": "https://registry.npmjs.org/ag-stack/-/ag-stack-36.2.0.tgz", + "integrity": "sha512-f3XT2htFs9dUPcHni2z9ak69QNta5ODEpooHuuNvbjN98JkokpEqRQ9YLQHQJFdCcgi9Ybs/j3zkkvmNRamIPg==", + "license": "MIT" + }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -7412,6 +7479,10 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-virtual-aggridautomat": { + "resolved": "../react-virtual-aggridautomat", + "link": true + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", diff --git a/package.json b/package.json index e246e56..4286774 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "@mui/icons-material": "^9.4.0", "@mui/material": "^9.4.0", "@shopify/draggable": "^1.2.1", + "ag-grid-community": "^36.2.0", + "ag-grid-react": "^36.2.0", "dashgui": "file:../dashgui", "jsbarcode": "^3.12.3", "notistack": "^3.0.2", @@ -31,6 +33,7 @@ "react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor", "react-dom": "^19.3.0", "react-router-dom": "^7.18.4", + "react-virtual-aggridautomat": "file:../react-virtual-aggridautomat", "sortablejs": "^1.15.7", "thumbhash": "^0.1.1" }, diff --git a/src/api/index.js b/src/api/index.js index 7b73a52..5531e57 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -215,6 +215,32 @@ export const article = { return http(`/api/article/${encodeURIComponent(sku)}`); }, + /** + * GET /api/article/history + * History of all articles with paging/sorting/filtering + */ + getHistory({ startRow, endRow, offset, limit, sortModel, filterModel } = {}) { + const params = new URLSearchParams(); + if (startRow != null) params.set('startRow', String(startRow)); + if (endRow != null) params.set('endRow', String(endRow)); + if (offset != null) params.set('offset', String(offset)); + if (limit != null) params.set('limit', String(limit)); + if (sortModel && sortModel.length) params.set('sort', JSON.stringify(sortModel)); + if (filterModel && Object.keys(filterModel).length) params.set('filter', JSON.stringify(filterModel)); + const qs = params.toString(); + return http(`/api/article/history${qs ? `?${qs}` : ''}`); + }, + + /** + * GET /api/article/:sku/history + */ + getBySkuHistory(sku, { limit } = {}) { + const params = new URLSearchParams(); + if (limit != null) params.set('limit', String(limit)); + const qs = params.toString(); + return http(`/api/article/${encodeURIComponent(sku)}/history${qs ? `?${qs}` : ''}`); + }, + /** POST /api/article → 201 { article } */ create({ parentId, name, track_stock }) { return http('/api/article', { @@ -1797,6 +1823,24 @@ export const labeltemplate = { // thumb is returned inline as a base64 string in list/get responses }; +export const globalHistory = { + /** + * GET /api/history + * Global system/entity history with paging/sorting/filtering + */ + list({ startRow, endRow, offset, limit, sortModel, filterModel } = {}) { + const params = new URLSearchParams(); + if (startRow != null) params.set('startRow', String(startRow)); + if (endRow != null) params.set('endRow', String(endRow)); + if (offset != null) params.set('offset', String(offset)); + if (limit != null) params.set('limit', String(limit)); + if (sortModel && sortModel.length) params.set('sort', JSON.stringify(sortModel)); + if (filterModel && Object.keys(filterModel).length) params.set('filter', JSON.stringify(filterModel)); + const qs = params.toString(); + return http(`/api/history${qs ? `?${qs}` : ''}`); + }, +}; + export default { auth, article, @@ -1822,4 +1866,5 @@ export default { unit, warehouse, stock, + globalHistory, }; diff --git a/src/appRouter.jsx b/src/appRouter.jsx index dc7727b..71785dc 100644 --- a/src/appRouter.jsx +++ b/src/appRouter.jsx @@ -24,7 +24,11 @@ import KundeListePage from './routes/kunde'; import KundeNeuPage from './routes/kunde/neu'; import KundeGruppenPage from './routes/kunde/gruppen'; import KundeEditPage from './routes/kunde/_entityId'; -import InfosPage from './routes/infos'; +import { + ArticleHistoryPage, + GlobalHistoryPage, + StatisticsPage, +} from './routes/infos'; const routes = [ { @@ -42,7 +46,16 @@ const routes = [ { path: ':deliveryId', element: }, ], }, - { path: 'infos', element: }, + { + path: 'infos', + element: , + children: [ + { index: true, element: }, + { path: 'articlehistory', element: }, + { path: 'globalhistory', element: }, + { path: 'statistics', element: }, + ], + }, { path: 'kategorien/*', element: , diff --git a/src/automats/articleHistoryAutomat.js b/src/automats/articleHistoryAutomat.js new file mode 100644 index 0000000..a51c956 --- /dev/null +++ b/src/automats/articleHistoryAutomat.js @@ -0,0 +1,34 @@ +import { createGridAutomat } from 'react-virtual-aggridautomat'; +import { article } from '../api'; +import authAutomat from './authAutomat'; + +export const articleHistoryGridAutomat = createGridAutomat({ + name: 'articleHistory', + blockSize: 50, + fetchRows: async ({ startRow, endRow, sortModel, filterModel }) => { + try { + const res = await article.getHistory({ + startRow, + endRow, + sortModel, + filterModel, + }); + const rows = res?.rows ?? res?.history ?? res?.entries ?? (Array.isArray(res) ? res : []); + const total = res?.total ?? res?.totalRows ?? res?.totalCount; + const lastRow = total != null ? total : (rows.length < (endRow - startRow) ? startRow + rows.length : undefined); + return { rows, lastRow }; + } catch (err) { + console.warn('Failed to fetch article history (backend route pending):', err); + return { rows: [], lastRow: 0 }; + } + }, +}); + +// Reload on company switch or auth change +authAutomat.subscribe((auth) => { + if (auth.status === 'authed' && auth.user?.selectedCompanyId) { + articleHistoryGridAutomat.actions.reload(); + } +}); + +export default articleHistoryGridAutomat; diff --git a/src/automats/globalHistoryAutomat.js b/src/automats/globalHistoryAutomat.js new file mode 100644 index 0000000..d1b716c --- /dev/null +++ b/src/automats/globalHistoryAutomat.js @@ -0,0 +1,34 @@ +import { createGridAutomat } from 'react-virtual-aggridautomat'; +import { globalHistory } from '../api'; +import authAutomat from './authAutomat'; + +export const globalHistoryGridAutomat = createGridAutomat({ + name: 'globalHistory', + blockSize: 50, + fetchRows: async ({ startRow, endRow, sortModel, filterModel }) => { + try { + const res = await globalHistory.list({ + startRow, + endRow, + sortModel, + filterModel, + }); + const rows = res?.rows ?? res?.history ?? res?.entries ?? (Array.isArray(res) ? res : []); + const total = res?.total ?? res?.totalRows ?? res?.totalCount; + const lastRow = total != null ? total : (rows.length < (endRow - startRow) ? startRow + rows.length : undefined); + return { rows, lastRow }; + } catch (err) { + console.warn('Failed to fetch global history (backend route pending):', err); + return { rows: [], lastRow: 0 }; + } + }, +}); + +// Reload on company switch or auth change +authAutomat.subscribe((auth) => { + if (auth.status === 'authed' && auth.user?.selectedCompanyId) { + globalHistoryGridAutomat.actions.reload(); + } +}); + +export default globalHistoryGridAutomat; diff --git a/src/automats/index.js b/src/automats/index.js index 248ee70..e69455e 100644 --- a/src/automats/index.js +++ b/src/automats/index.js @@ -70,3 +70,6 @@ export { invalidateAllCategoryViews, } from './categoryAutomat'; +export { default as articleHistoryGridAutomat } from './articleHistoryAutomat'; +export { default as globalHistoryGridAutomat } from './globalHistoryAutomat'; + diff --git a/src/components/article/ArticlePhoto.jsx b/src/components/article/ArticlePhoto.jsx index 3f9be99..187a813 100644 --- a/src/components/article/ArticlePhoto.jsx +++ b/src/components/article/ArticlePhoto.jsx @@ -65,13 +65,13 @@ class ArticlePhoto extends Component { fill || objectFit === 'cover' ? { position: 'absolute', inset: 0 } : { - position: 'absolute', - maxWidth: '100%', - maxHeight: '100%', - aspectRatio, - width: photo.thumbW >= photo.thumbH ? '100%' : 'auto', - height: photo.thumbH > photo.thumbW ? '100%' : 'auto', - } + position: 'absolute', + maxWidth: '100%', + maxHeight: '100%', + aspectRatio, + width: photo.thumbW >= photo.thumbH ? '100%' : 'auto', + height: photo.thumbH > photo.thumbW ? '100%' : 'auto', + } } > @@ -87,21 +87,21 @@ class ArticlePhoto extends Component { sx={ fill || objectFit === 'cover' ? { - display: 'block', - width: '100%', - height: '100%', - objectFit: 'cover', - opacity: thumbLoaded ? 1 : 0, - } + display: 'block', + width: '100%', + height: '100%', + objectFit: 'cover', + opacity: thumbLoaded ? 1 : 0, + } : { - display: 'block', - maxWidth: '100%', - maxHeight: '100%', - width: 'auto', - height: 'auto', - objectFit: 'contain', - opacity: thumbLoaded ? 1 : 0, - } + display: 'block', + maxWidth: '100%', + maxHeight: '100%', + width: 'auto', + height: 'auto', + objectFit: 'contain', + opacity: thumbLoaded ? 1 : 0, + } } /> ) : null} diff --git a/src/components/manifest/ArticleSearchDialog.jsx b/src/components/manifest/ArticleSearchDialog.jsx index e9f3628..f3b65a2 100644 --- a/src/components/manifest/ArticleSearchDialog.jsx +++ b/src/components/manifest/ArticleSearchDialog.jsx @@ -121,13 +121,13 @@ export default class ArticleSearchDialog extends PureComponent { const trimmed = query.trim().toLowerCase(); const filtered = trimmed ? bookableArticles.filter((a) => { - const skuMatch = a.sku?.toLowerCase().includes(trimmed); - const nameMatch = a.name?.toLowerCase().includes(trimmed); - const eanMatch = Array.isArray(a.eans) - ? a.eans.some((e) => String(e).toLowerCase().includes(trimmed)) - : false; - return skuMatch || nameMatch || eanMatch; - }) + const skuMatch = a.sku?.toLowerCase().includes(trimmed); + const nameMatch = a.name?.toLowerCase().includes(trimmed); + const eanMatch = Array.isArray(a.eans) + ? a.eans.some((e) => String(e).toLowerCase().includes(trimmed)) + : false; + return skuMatch || nameMatch || eanMatch; + }) : bookableArticles.slice(0, 50); const rawSku = line?.sku != null ? String(line.sku).trim() : ''; @@ -270,8 +270,6 @@ export default class ArticleSearchDialog extends PureComponent { ) : ( diff --git a/src/components/manifest/ManifestFormLine.jsx b/src/components/manifest/ManifestFormLine.jsx index fd120f6..61d41f4 100644 --- a/src/components/manifest/ManifestFormLine.jsx +++ b/src/components/manifest/ManifestFormLine.jsx @@ -67,7 +67,7 @@ class ManifestFormLine extends PureComponent { const current = getArticle(sku); this.setState({ cachedArticle: current }); if (!current) { - artikelAutomat.actions.loadArticle(sku).catch(() => {}); + artikelAutomat.actions.loadArticle(sku).catch(() => { }); } this.unsubArticle = artikelAutomat.subscribe(this, (state) => ({ cachedArticle: createArticleSelector(sku)(state), @@ -118,8 +118,8 @@ class ManifestFormLine extends PureComponent { isBooked ? `Vollständig im Lager gebucht: ${ledgerLine.bookedAmount} Stück (Klicken für Details)` : isPartial - ? `Teilweise im Lager gebucht: ${ledgerLine.bookedAmount} von ${ledgerLine.targetAmount} Stück (Klicken zum Einbuchen dieser Position)` - : `Noch nicht im Lager gebucht (${ledgerLine.targetAmount} Stück – Klicken zum Einbuchen dieser Position)` + ? `Teilweise im Lager gebucht: ${ledgerLine.bookedAmount} von ${ledgerLine.targetAmount} Stück (Klicken zum Einbuchen dieser Position)` + : `Noch nicht im Lager gebucht (${ledgerLine.targetAmount} Stück – Klicken zum Einbuchen dieser Position)` } > { - e.stopPropagation(); - onBookLine(ledgerLine); - } + e.stopPropagation(); + onBookLine(ledgerLine); + } : undefined } icon={ @@ -144,8 +144,8 @@ class ManifestFormLine extends PureComponent { isBooked ? `Lager: ${ledgerLine.bookedAmount} Stk. gebucht` : isPartial - ? `Lager: ${ledgerLine.bookedAmount}/${ledgerLine.targetAmount} Stk.` - : 'Lager: offen' + ? `Lager: ${ledgerLine.bookedAmount}/${ledgerLine.targetAmount} Stk.` + : 'Lager: offen' } color={isBooked ? 'success' : isPartial ? 'warning' : 'default'} variant="outlined" @@ -157,12 +157,12 @@ class ManifestFormLine extends PureComponent { bgcolor: isBooked ? 'success.50' : undefined, '&:hover': isClickable ? { - bgcolor: isBooked - ? 'success.100' - : isPartial + bgcolor: isBooked + ? 'success.100' + : isPartial ? 'warning.100' : 'action.hover', - } + } : undefined, }} /> @@ -239,10 +239,10 @@ class ManifestFormLine extends PureComponent { MozAppearance: 'textfield', }, '& input[type=number]::-webkit-outer-spin-button, & input[type=number]::-webkit-inner-spin-button': - { - WebkitAppearance: 'none', - margin: 0, - }, + { + WebkitAppearance: 'none', + margin: 0, + }, }} /> ); @@ -282,9 +282,9 @@ class ManifestFormLine extends PureComponent { const vkNetto = vkPrice?.net; const vkBrutto = vkPrice ? priceBrutto(vkPrice) ?? - (vkPrice.net != null && vkPrice.taxrate != null - ? vkPrice.net * (1 + vkPrice.taxrate / 100) - : null) + (vkPrice.net != null && vkPrice.taxrate != null + ? vkPrice.net * (1 + vkPrice.taxrate / 100) + : null) : null; return ( @@ -335,8 +335,6 @@ class ManifestFormLine extends PureComponent { ) : ( diff --git a/src/components/tiles/CategoryTile.jsx b/src/components/tiles/CategoryTile.jsx index 846255b..04a03f1 100644 --- a/src/components/tiles/CategoryTile.jsx +++ b/src/components/tiles/CategoryTile.jsx @@ -66,8 +66,8 @@ function TopBand({ onSettingsClick, settingsTab, terminalCount, onPlacementClick color: isDeleteMode ? 'error.main' : isTerminalMode || isPhotoMode - ? 'primary.main' - : 'common.white', + ? 'primary.main' + : 'common.white', }} > {isTerminalMode ? ( @@ -114,10 +114,10 @@ function MiddleBand({ settingsTab, nameInput, terminals, categoryPosterminalIds, px: 1, ...(isActionTab ? { - '&:hover': { - bgcolor: settingsTab === -3 ? 'rgba(211, 47, 47, 0.12)' : 'grey.400', - }, - } + '&:hover': { + bgcolor: settingsTab === -3 ? 'rgba(211, 47, 47, 0.12)' : 'grey.400', + }, + } : {}), }; diff --git a/src/index.css b/src/index.css index edb809c..213716a 100644 --- a/src/index.css +++ b/src/index.css @@ -1,3 +1,6 @@ +@import 'ag-grid-community/styles/ag-grid.css'; +@import 'ag-grid-community/styles/ag-theme-alpine.css'; + html, body, #root { diff --git a/src/nav.js b/src/nav.js index 7626d4d..c8ddd6b 100644 --- a/src/nav.js +++ b/src/nav.js @@ -17,6 +17,9 @@ import RequestQuoteIcon from '@mui/icons-material/RequestQuote'; import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; import LabelIcon from '@mui/icons-material/Label'; import TuneIcon from '@mui/icons-material/Tune'; +import HistoryIcon from '@mui/icons-material/History'; +import PublicIcon from '@mui/icons-material/Public'; +import BarChartIcon from '@mui/icons-material/BarChart'; import { isKategorienPath } from './components/category/categoryRoute'; import { deliveryEditorAutomat } from './automats/deliveryEditorAutomat'; @@ -229,6 +232,29 @@ export const SUB_NAV = { help: 'Kundengruppen für Staffelpreise und Rabatte verwalten', }, ], + '/infos': [ + { + segment: '', + label: 'ArticleHistory', + title: 'ArticleHistory', + icon: HistoryIcon, + help: 'Historie aller Artikeländerungen und Ereignisse', + }, + { + segment: 'globalhistory', + label: 'GlobalHistory', + title: 'GlobalHistory', + icon: PublicIcon, + help: 'Globale System- und Änderungshistorie', + }, + { + segment: 'statistics', + label: 'Statistics', + title: 'Statistics', + icon: BarChartIcon, + help: 'Statistische Auswertungen und Kennzahlen', + }, + ], }; export function isNavItemActive(pathname, navPath) { @@ -281,6 +307,11 @@ export function resolveActiveSub(pathname, sectionPath) { return rest; } + // Treat /infos/articlehistory as segment '' + if (sectionPath === '/infos' && rest === 'articlehistory') { + return ''; + } + const active = subNav.find((item) => { if (item.segment === 'kategorien') { return isKategorienPath(pathname); diff --git a/src/routes/infos.jsx b/src/routes/infos.jsx deleted file mode 100644 index 490a315..0000000 --- a/src/routes/infos.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import Typography from '@mui/material/Typography'; -import PageLayout from '../components/PageLayout'; - -export default function InfosPage() { - return ( - - Infos — Inhalt folgt. - - ); -} diff --git a/src/routes/infos/ArticleHistoryPage.jsx b/src/routes/infos/ArticleHistoryPage.jsx new file mode 100644 index 0000000..34e2ce3 --- /dev/null +++ b/src/routes/infos/ArticleHistoryPage.jsx @@ -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 ( + { + e.preventDefault(); + void router.navigate(`/artikel/${encodeURIComponent(p.value)}`); + }} + style={{ color: '#2563eb', fontWeight: 600, textDecoration: 'none' }} + > + {p.value} + + ); + }, + }, + { + 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 = ( + + {loading && } + {totalRows >= 0 && ( + + {totalRows} Einträge + + )} + + + ); + + return ( + + + + + + ); + } +} diff --git a/src/routes/infos/GlobalHistoryPage.jsx b/src/routes/infos/GlobalHistoryPage.jsx new file mode 100644 index 0000000..2798eea --- /dev/null +++ b/src/routes/infos/GlobalHistoryPage.jsx @@ -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 = ( + + {loading && } + {totalRows >= 0 && ( + + {totalRows} Einträge + + )} + + + ); + + return ( + + + + + + ); + } +} diff --git a/src/routes/infos/StatisticsPage.jsx b/src/routes/infos/StatisticsPage.jsx new file mode 100644 index 0000000..474dd36 --- /dev/null +++ b/src/routes/infos/StatisticsPage.jsx @@ -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 ( + + + + + Statistik & Auswertungen + + + Hier entstehen demnächst grafische Auswertungen, Umsatzstatistiken und + Bestandskennzahlen. + + + + ); + } +} diff --git a/src/routes/infos/index.js b/src/routes/infos/index.js new file mode 100644 index 0000000..04a5fba --- /dev/null +++ b/src/routes/infos/index.js @@ -0,0 +1,3 @@ +export { default as ArticleHistoryPage } from './ArticleHistoryPage'; +export { default as GlobalHistoryPage } from './GlobalHistoryPage'; +export { default as StatisticsPage } from './StatisticsPage'; diff --git a/vite.config.js b/vite.config.js index 02dc563..20cc135 100644 --- a/vite.config.js +++ b/vite.config.js @@ -79,12 +79,14 @@ const sharedDeps = [ '@emotion/styled', '@mui/material', '@mui/icons-material', + 'react-automat', ]; const sharedResolve = { dedupe: sharedDeps, alias: { 'node:module': path.resolve(__dirname, 'src/utils/emptyNodeModule.js'), + 'react-automat': path.resolve(__dirname, '../automat'), }, };