From ab9d5fe4136102f01a50b9d7637e4541f9b683ba Mon Sep 17 00:00:00 2001 From: seb Date: Sun, 18 Oct 2026 03:14:15 +0200 Subject: [PATCH] u --- automat-graph.dot | 80 +-- automat-graph.svg | 465 +++++++------ generate-automat-graph.js | 14 +- src/api/ledger.js | 24 + src/appRouter.jsx | 2 + src/automats/createChunkListAutomat.js | 16 +- src/automats/index.js | 1 + src/automats/ledgerHistoryAutomat.js | 26 + .../manifest/BookDeliveryDialog.jsx | 22 +- src/nav.js | 8 + src/routes/infos/LedgerHistoryPage.jsx | 629 ++++++++++++++++++ src/routes/infos/index.js | 1 + test/dummyBackend.mjs | 1 + 13 files changed, 1018 insertions(+), 271 deletions(-) create mode 100644 src/automats/ledgerHistoryAutomat.js create mode 100644 src/routes/infos/LedgerHistoryPage.jsx diff --git a/automat-graph.dot b/automat-graph.dot index 26afac2..0e07c3c 100644 --- a/automat-graph.dot +++ b/automat-graph.dot @@ -87,6 +87,7 @@ digraph automats { fontcolor="#e2e8f0"; margin=14; articleHistoryGridAutomat [label=<
articleHistoryGridAutomat
createGridAutomat (Article)
STATErows: [] / totalRowsBlock size 50 virtual rows
ACTIONfetchRows(viewport)Infinite scroll query
ACTIONreload()Reload on company switch
APIGET /api/article/historyInfinite audit window
>]; globalHistoryGridAutomat [label=<
globalHistoryGridAutomat
createGridAutomat (Global)
STATErows: [] / totalRowsBlock size 50 virtual rows
ACTIONfetchRows(viewport)Infinite scroll query
ACTIONreload()Reload on company switch
APIGET /api/globalhistoryCompany global audit
>]; + ledgerHistoryGridAutomat [label=<
ledgerHistoryGridAutomat
createGridAutomat (Ledger)
STATErows: [] / totalRowsBlock size 50 virtual rows
ACTIONfetchRows(viewport)Infinite scroll query
ACTIONreload()Reload on company switch
APIGET /api/ledger/historyAppend-only ledger audit
>]; } subgraph cluster_system_ui { label="UI, Settings & System"; labeljust="l"; fontsize=15; @@ -120,43 +121,44 @@ digraph automats { authAutomat:p_auth_status:e -> vfsAutomat:in0:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="authAutomat → vfsAutomat (load company vfs)", label=" load company vfs ", constraint=false, weight=1]; authAutomat:p_auth_status:e -> articleHistoryGridAutomat:in0:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="authAutomat → articleHistoryGridAutomat (reload history on authed)", label=" reload history on authed ", constraint=false, weight=1]; authAutomat:p_auth_status:e -> globalHistoryGridAutomat:in0:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="authAutomat → globalHistoryGridAutomat (reload audit on authed)", label=" reload audit on authed ", constraint=false, weight=1]; - sseAutomat:p_sse_events:w -> categoryAutomat:in6:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="sseAutomat → categoryAutomat ('category' sse trigger)", label=" 'category' sse trigger ", constraint=false, weight=1]; - sseAutomat:p_sse_events:w -> categoryViewAutomat:in1:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="sseAutomat → categoryViewAutomat (invalidate views on sse)", label=" invalidate views on sse ", constraint=false, weight=1]; - sseAutomat:p_sse_events:w -> articleIndexAutomat:in2:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="sseAutomat → articleIndexAutomat ('articles' sse trigger)", label=" 'articles' sse trigger ", constraint=false, weight=1]; - sseAutomat:p_sse_events:w -> artikelAutomat:in4:n [color="#c79bff", fontcolor="#c79bff", tooltip="sseAutomat → artikelAutomat ('article-<sku>' sse reload)", label=" 'article-<sku>' sse reload ", weight=3]; - sseAutomat:p_sse_events:w -> articleDetailAutomat:in0:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="sseAutomat → articleDetailAutomat (invalidateArticle(sku))", label=" invalidateArticle(sku) ", constraint=false, weight=1]; - sseAutomat:p_sse_events:e -> warehouseAutomat:in1:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="sseAutomat → warehouseAutomat ('warehouse' sse trigger)", label=" 'warehouse' sse trigger ", constraint=false, weight=1]; - sseAutomat:p_sse_events:w -> supplierAutomat:in2:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="sseAutomat → supplierAutomat ('supplier' sse trigger)", label=" 'supplier' sse trigger ", constraint=false, weight=1]; - sseAutomat:p_sse_events:w -> brandAutomat:in1:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="sseAutomat → brandAutomat ('brand' sse trigger)", label=" 'brand' sse trigger ", constraint=false, weight=1]; - sseAutomat:p_sse_events:e -> attributeAutomat:in1:n [color="#58d18a", fontcolor="#58d18a", tooltip="sseAutomat → attributeAutomat ('attributename' sse)", label=" 'attributename' sse ", constraint=false, weight=1]; - sseAutomat:p_sse_events:e -> propertyAutomat:in1:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="sseAutomat → propertyAutomat ('propertyname' sse)", label=" 'propertyname' sse ", constraint=false, weight=1]; - sseAutomat:p_sse_events:e -> orderAutomat:in1:n [color="#a98cff", fontcolor="#a98cff", tooltip="sseAutomat → orderAutomat ('order'/'shipment' sse)", label=" 'order'/'shipment' sse ", constraint=false, weight=1]; - sseAutomat:p_sse_events:e -> vfsAutomat:in2:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="sseAutomat → vfsAutomat ('vfs' sse mutation)", label=" 'vfs' sse mutation ", constraint=false, weight=1]; - companyAutomat:p_comp_root:e -> categoryAutomat:in1:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="companyAutomat → categoryAutomat (rootCategoryId)", label=" rootCategoryId ", constraint=false, weight=1]; - categoryAutomat:p_cat_loadTree:e -> categoryViewAutomat:in0:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="categoryAutomat → categoryViewAutomat (attachTree & warm slugs)", label=" attachTree & warm slugs ", weight=3]; - categoryViewAutomat:p_catview_collect:e -> articleIndexAutomat:in1:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="categoryViewAutomat → articleIndexAutomat (derived union index)", label=" derived union index ", weight=3]; - articleIndexAutomat:p_idx_items:e -> articleSearchAutomat:in1:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="articleIndexAutomat → articleSearchAutomat (query index items)", label=" query index items ", weight=3]; - categoryAutomat:p_cat_categories:e -> articleSearchAutomat:in0:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="categoryAutomat → articleSearchAutomat (categoryFacets)", label=" categoryFacets ", constraint=false, weight=1]; - artikelAutomat:p_art_setName:e -> articleIndexAutomat:in0:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="artikelAutomat → articleIndexAutomat (setArticleIndexName (optimistic))", label=" setArticleIndexName (optimistic) ", constraint=false, weight=1]; - artikelAutomat:p_art_set:e -> categoryAutomat:in3:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="artikelAutomat → categoryAutomat (articleCount updates)", label=" articleCount updates ", weight=3]; - articleDetailAutomat:p_artdet_hist:e -> artikelAutomat:in2:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="articleDetailAutomat → artikelAutomat (watch SKU details)", label=" watch SKU details ", weight=3]; - deliveryEditorAutomat:p_deled_supplier:w -> supplierAutomat:in0:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="deliveryEditorAutomat → supplierAutomat (match vendor catalog)", label=" match vendor catalog ", constraint=false, weight=1]; - deliveryEditorAutomat:p_deled_lines:e -> artikelAutomat:in1:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="deliveryEditorAutomat → artikelAutomat (lookup SKU & pack units)", label=" lookup SKU & pack units ", constraint=false, weight=1]; - deliveryEditorAutomat:p_deled_commit:e -> deliveryAutomat:in0:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="deliveryEditorAutomat → deliveryAutomat (commit delivery receipt)", label=" commit delivery receipt ", weight=3]; - quoteAutomat:p_quote_items:w -> orderAutomat:in2:n [color="#c79bff", fontcolor="#c79bff", tooltip="quoteAutomat → orderAutomat (convert quote to order)", label=" convert quote to order ", weight=3]; - orderAutomat:p_order_items:e -> entityAutomat:in1:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="orderAutomat → entityAutomat (customer lookup)", label=" customer lookup ", weight=3]; - companyAutomat:p_comp_curr:e -> customergroupAutomat:in0:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="companyAutomat → customergroupAutomat (poscustomergroup)", label=" poscustomergroup ", constraint=false, weight=1]; - posterminalAutomat:p_pos_term:e -> categoryAutomat:in2:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="posterminalAutomat → categoryAutomat (filter enabled roots)", label=" filter enabled roots ", weight=3]; - companyAutomat:p_comp_curr:e -> posterminalAutomat:in0:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="companyAutomat → posterminalAutomat (company context)", label=" company context ", weight=3]; - chatAutomat:p_chat_msg:e -> vfsAutomat:in1:n [color="#58d18a", fontcolor="#58d18a", tooltip="chatAutomat → vfsAutomat (inspect / edit applets)", label=" inspect / edit applets ", constraint=false, weight=1]; - chatAutomat:p_chat_tools:w -> navigationAutomat:in1:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="chatAutomat → navigationAutomat (set_user_route tool)", label=" set_user_route tool ", constraint=false, weight=1]; - chatAutomat:p_chat_sse:e -> sseAutomat:in1:n [color="#a98cff", fontcolor="#a98cff", tooltip="chatAutomat → sseAutomat (getSseClientId())", label=" getSseClientId() ", weight=3]; - navigationAutomat:p_nav_sku:e -> artikelAutomat:in0:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="navigationAutomat → artikelAutomat (restore last SKU)", label=" restore last SKU ", constraint=false, weight=1]; - navigationAutomat:p_nav_catslug:e -> categoryAutomat:in0:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="navigationAutomat → categoryAutomat (restore last category)", label=" restore last category ", constraint=false, weight=1]; - navigationAutomat:p_nav_delpath:e -> deliveryEditorAutomat:in0:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="navigationAutomat → deliveryEditorAutomat (restore draft path)", label=" restore draft path ", weight=3]; - userSettingsAutomat:p_usr_drawer:e -> chatAutomat:in0:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="userSettingsAutomat → chatAutomat (chatDrawerWidth)", label=" chatDrawerWidth ", weight=3]; - helpAutomat:p_help_active:e -> navigationAutomat:in0:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="helpAutomat → navigationAutomat (route-contextual help)", label=" route-contextual help ", weight=3]; - categoryAutomat:p_cat_categories:ne -> categoryAutomat:in4:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="categoryAutomat → categoryAutomat (nested tree hierarchy)", label=" nested tree hierarchy ", weight=3]; - artikelAutomat:p_art_set:ne -> artikelAutomat:in3:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="artikelAutomat → artikelAutomat (variants & parent SKU)", label=" variants & parent SKU ", weight=3]; - chatAutomat:p_chat_msg:nw -> chatAutomat:in2:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="chatAutomat → chatAutomat (stream delta appends)", label=" stream delta appends ", weight=3]; + authAutomat:p_auth_status:e -> ledgerHistoryGridAutomat:in0:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="authAutomat → ledgerHistoryGridAutomat (reload ledger on authed)", label=" reload ledger on authed ", constraint=false, weight=1]; + sseAutomat:p_sse_events:w -> categoryAutomat:in6:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="sseAutomat → categoryAutomat ('category' sse trigger)", label=" 'category' sse trigger ", constraint=false, weight=1]; + sseAutomat:p_sse_events:w -> categoryViewAutomat:in1:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="sseAutomat → categoryViewAutomat (invalidate views on sse)", label=" invalidate views on sse ", constraint=false, weight=1]; + sseAutomat:p_sse_events:w -> articleIndexAutomat:in2:n [color="#c79bff", fontcolor="#c79bff", tooltip="sseAutomat → articleIndexAutomat ('articles' sse trigger)", label=" 'articles' sse trigger ", constraint=false, weight=1]; + sseAutomat:p_sse_events:w -> artikelAutomat:in4:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="sseAutomat → artikelAutomat ('article-<sku>' sse reload)", label=" 'article-<sku>' sse reload ", weight=3]; + sseAutomat:p_sse_events:w -> articleDetailAutomat:in0:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="sseAutomat → articleDetailAutomat (invalidateArticle(sku))", label=" invalidateArticle(sku) ", constraint=false, weight=1]; + sseAutomat:p_sse_events:e -> warehouseAutomat:in1:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="sseAutomat → warehouseAutomat ('warehouse' sse trigger)", label=" 'warehouse' sse trigger ", constraint=false, weight=1]; + sseAutomat:p_sse_events:w -> supplierAutomat:in2:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="sseAutomat → supplierAutomat ('supplier' sse trigger)", label=" 'supplier' sse trigger ", constraint=false, weight=1]; + sseAutomat:p_sse_events:w -> brandAutomat:in1:n [color="#58d18a", fontcolor="#58d18a", tooltip="sseAutomat → brandAutomat ('brand' sse trigger)", label=" 'brand' sse trigger ", constraint=false, weight=1]; + sseAutomat:p_sse_events:e -> attributeAutomat:in1:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="sseAutomat → attributeAutomat ('attributename' sse)", label=" 'attributename' sse ", constraint=false, weight=1]; + sseAutomat:p_sse_events:e -> propertyAutomat:in1:n [color="#a98cff", fontcolor="#a98cff", tooltip="sseAutomat → propertyAutomat ('propertyname' sse)", label=" 'propertyname' sse ", constraint=false, weight=1]; + sseAutomat:p_sse_events:e -> orderAutomat:in1:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="sseAutomat → orderAutomat ('order'/'shipment' sse)", label=" 'order'/'shipment' sse ", constraint=false, weight=1]; + sseAutomat:p_sse_events:e -> vfsAutomat:in2:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="sseAutomat → vfsAutomat ('vfs' sse mutation)", label=" 'vfs' sse mutation ", constraint=false, weight=1]; + companyAutomat:p_comp_root:e -> categoryAutomat:in1:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="companyAutomat → categoryAutomat (rootCategoryId)", label=" rootCategoryId ", constraint=false, weight=1]; + categoryAutomat:p_cat_loadTree:e -> categoryViewAutomat:in0:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="categoryAutomat → categoryViewAutomat (attachTree & warm slugs)", label=" attachTree & warm slugs ", weight=3]; + categoryViewAutomat:p_catview_collect:e -> articleIndexAutomat:in1:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="categoryViewAutomat → articleIndexAutomat (derived union index)", label=" derived union index ", weight=3]; + articleIndexAutomat:p_idx_items:e -> articleSearchAutomat:in1:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="articleIndexAutomat → articleSearchAutomat (query index items)", label=" query index items ", weight=3]; + categoryAutomat:p_cat_categories:e -> articleSearchAutomat:in0:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="categoryAutomat → articleSearchAutomat (categoryFacets)", label=" categoryFacets ", constraint=false, weight=1]; + artikelAutomat:p_art_setName:e -> articleIndexAutomat:in0:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="artikelAutomat → articleIndexAutomat (setArticleIndexName (optimistic))", label=" setArticleIndexName (optimistic) ", constraint=false, weight=1]; + artikelAutomat:p_art_set:e -> categoryAutomat:in3:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="artikelAutomat → categoryAutomat (articleCount updates)", label=" articleCount updates ", weight=3]; + articleDetailAutomat:p_artdet_hist:e -> artikelAutomat:in2:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="articleDetailAutomat → artikelAutomat (watch SKU details)", label=" watch SKU details ", weight=3]; + deliveryEditorAutomat:p_deled_supplier:w -> supplierAutomat:in0:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="deliveryEditorAutomat → supplierAutomat (match vendor catalog)", label=" match vendor catalog ", constraint=false, weight=1]; + deliveryEditorAutomat:p_deled_lines:e -> artikelAutomat:in1:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="deliveryEditorAutomat → artikelAutomat (lookup SKU & pack units)", label=" lookup SKU & pack units ", constraint=false, weight=1]; + deliveryEditorAutomat:p_deled_commit:e -> deliveryAutomat:in0:n [color="#c79bff", fontcolor="#c79bff", tooltip="deliveryEditorAutomat → deliveryAutomat (commit delivery receipt)", label=" commit delivery receipt ", weight=3]; + quoteAutomat:p_quote_items:w -> orderAutomat:in2:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="quoteAutomat → orderAutomat (convert quote to order)", label=" convert quote to order ", weight=3]; + orderAutomat:p_order_items:e -> entityAutomat:in1:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="orderAutomat → entityAutomat (customer lookup)", label=" customer lookup ", weight=3]; + companyAutomat:p_comp_curr:e -> customergroupAutomat:in0:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="companyAutomat → customergroupAutomat (poscustomergroup)", label=" poscustomergroup ", constraint=false, weight=1]; + posterminalAutomat:p_pos_term:e -> categoryAutomat:in2:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="posterminalAutomat → categoryAutomat (filter enabled roots)", label=" filter enabled roots ", weight=3]; + companyAutomat:p_comp_curr:e -> posterminalAutomat:in0:n [color="#58d18a", fontcolor="#58d18a", tooltip="companyAutomat → posterminalAutomat (company context)", label=" company context ", weight=3]; + chatAutomat:p_chat_msg:e -> vfsAutomat:in1:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="chatAutomat → vfsAutomat (inspect / edit applets)", label=" inspect / edit applets ", constraint=false, weight=1]; + chatAutomat:p_chat_tools:w -> navigationAutomat:in1:n [color="#a98cff", fontcolor="#a98cff", tooltip="chatAutomat → navigationAutomat (set_user_route tool)", label=" set_user_route tool ", constraint=false, weight=1]; + chatAutomat:p_chat_sse:e -> sseAutomat:in1:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="chatAutomat → sseAutomat (getSseClientId())", label=" getSseClientId() ", weight=3]; + navigationAutomat:p_nav_sku:e -> artikelAutomat:in0:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="navigationAutomat → artikelAutomat (restore last SKU)", label=" restore last SKU ", constraint=false, weight=1]; + navigationAutomat:p_nav_catslug:e -> categoryAutomat:in0:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="navigationAutomat → categoryAutomat (restore last category)", label=" restore last category ", constraint=false, weight=1]; + navigationAutomat:p_nav_delpath:e -> deliveryEditorAutomat:in0:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="navigationAutomat → deliveryEditorAutomat (restore draft path)", label=" restore draft path ", weight=3]; + userSettingsAutomat:p_usr_drawer:e -> chatAutomat:in0:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="userSettingsAutomat → chatAutomat (chatDrawerWidth)", label=" chatDrawerWidth ", weight=3]; + helpAutomat:p_help_active:e -> navigationAutomat:in0:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="helpAutomat → navigationAutomat (route-contextual help)", label=" route-contextual help ", weight=3]; + categoryAutomat:p_cat_categories:ne -> categoryAutomat:in4:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="categoryAutomat → categoryAutomat (nested tree hierarchy)", label=" nested tree hierarchy ", weight=3]; + artikelAutomat:p_art_set:ne -> artikelAutomat:in3:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="artikelAutomat → artikelAutomat (variants & parent SKU)", label=" variants & parent SKU ", weight=3]; + chatAutomat:p_chat_msg:nw -> chatAutomat:in2:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="chatAutomat → chatAutomat (stream delta appends)", label=" stream delta appends ", weight=3]; } \ No newline at end of file diff --git a/automat-graph.svg b/automat-graph.svg index a4768a1..9ed5aaf 100644 --- a/automat-graph.svg +++ b/automat-graph.svg @@ -4,11 +4,11 @@ - + automats - + cluster_session_realtime @@ -61,7 +61,7 @@ cluster_audit_grids - + Audit & Virtual Grids @@ -344,7 +344,7 @@ -  sync brands on authed   +  sync brands on authed   @@ -923,7 +923,7 @@ -  load company profile   +  load company profile   @@ -960,7 +960,7 @@ -  sync payment accounts   +  sync payment accounts   @@ -997,7 +997,7 @@ -  sync number series   +  sync number series   @@ -1234,7 +1234,7 @@ -  reload history on authed   +  reload history on authed   @@ -1277,17 +1277,60 @@ -  reload audit on authed   +  reload audit on authed   - + + +ledgerHistoryGridAutomat + + + +ledgerHistoryGridAutomat +createGridAutomat (Ledger) + + + +STATE +rows: [] / totalRows +Block size 50 virtual rows + + + +ACTION +fetchRows(viewport) +Infinite scroll query + + + +ACTION +reload() +Reload on company switch + + + +API +GET /api/ledger/history +Append-only ledger audit + + -sseAutomat:w->categoryAutomat:n - - - +authAutomat:e->ledgerHistoryGridAutomat:n + + + -  'category' sse trigger   +  reload ledger on authed   + + + +sseAutomat:w->categoryAutomat:n + + + + + +  'category' sse trigger   @@ -1342,14 +1385,14 @@ Category article batch - + sseAutomat:w->categoryViewAutomat:n - - - + + + -  invalidate views on sse   +  invalidate views on sse   @@ -1413,14 +1456,14 @@ Full article document - + sseAutomat:w->artikelAutomat:n - - - + + + -  'article-<sku>' sse reload   +  'article-<sku>' sse reload   @@ -1468,24 +1511,24 @@ Vendor links & prices - + sseAutomat:w->articleDetailAutomat:n - - - + + + -  invalidateArticle(sku)   +  invalidateArticle(sku)   - + sseAutomat:w->brandAutomat:n - - - + + + -  'brand' sse trigger   +  'brand' sse trigger   @@ -1541,94 +1584,94 @@ Await warm views - + sseAutomat:w->articleIndexAutomat:n - - - + + + -  'articles' sse trigger   +  'articles' sse trigger   - + sseAutomat:e->propertyAutomat:n - - - + + + -  'propertyname' sse   +  'propertyname' sse   - + sseAutomat:e->attributeAutomat:n - - - + + + -  'attributename' sse   +  'attributename' sse   - + sseAutomat:w->supplierAutomat:n - - - + + + -  'supplier' sse trigger   +  'supplier' sse trigger   - + sseAutomat:e->orderAutomat:n - - - + + + -  'order'/'shipment' sse   +  'order'/'shipment' sse   - + sseAutomat:e->warehouseAutomat:n - - - + + + -  'warehouse' sse trigger   +  'warehouse' sse trigger   - + sseAutomat:e->vfsAutomat:n - - - + + + -  'vfs' sse mutation   +  'vfs' sse mutation   - + categoryAutomat:ne->categoryAutomat:n - - - + + + -  nested tree hierarchy   +  nested tree hierarchy   - + categoryAutomat:e->categoryViewAutomat:n - - - + + + -  attachTree & warm slugs   +  attachTree & warm slugs   @@ -1671,74 +1714,74 @@ Reactive search outcome - + categoryAutomat:e->articleSearchAutomat:n - - - + + + -  categoryFacets   +  categoryFacets   - + categoryViewAutomat:e->articleIndexAutomat:n - - - + + + -  derived union index   +  derived union index   - + artikelAutomat:e->categoryAutomat:n - - - + + + -  articleCount updates   +  articleCount updates   - + artikelAutomat:ne->artikelAutomat:n - - - + + + -  variants & parent SKU   +  variants & parent SKU   - + artikelAutomat:e->articleIndexAutomat:n - - - + + + -  setArticleIndexName (optimistic)   +  setArticleIndexName (optimistic)   - + articleDetailAutomat:e->artikelAutomat:n - - - + + + -  watch SKU details   +  watch SKU details   - + articleIndexAutomat:e->articleSearchAutomat:n - - - + + + -  query index items   +  query index items   @@ -1798,127 +1841,127 @@ Create goods receipt - + deliveryEditorAutomat:e->artikelAutomat:n - - - + + + -  lookup SKU & pack units   +  lookup SKU & pack units   - + deliveryEditorAutomat:w->supplierAutomat:n - - - + + + -  match vendor catalog   +  match vendor catalog   - + deliveryEditorAutomat:e->deliveryAutomat:n - - - + + + -  commit delivery receipt   +  commit delivery receipt   - + quoteAutomat:w->orderAutomat:n - - - + + + -  convert quote to order   +  convert quote to order   - + orderAutomat:e->entityAutomat:n - - - + + + -  customer lookup   +  customer lookup   - + companyAutomat:e->categoryAutomat:n - - - + + + -  rootCategoryId   +  rootCategoryId   - + companyAutomat:e->customergroupAutomat:n - - - + + + -  poscustomergroup   +  poscustomergroup   - + companyAutomat:e->posterminalAutomat:n - - - + + + -  company context   +  company context   - + posterminalAutomat:e->categoryAutomat:n - - - + + + -  filter enabled roots   +  filter enabled roots   - + chatAutomat:e->sseAutomat:n - - - + + + -  getSseClientId()   +  getSseClientId()   - + chatAutomat:nw->chatAutomat:n - - - + + + -  stream delta appends   +  stream delta appends   - + chatAutomat:e->vfsAutomat:n - - - + + + -  inspect / edit applets   +  inspect / edit applets   - + navigationAutomat @@ -1970,47 +2013,47 @@ Reset on company switch - + chatAutomat:w->navigationAutomat:n - - - + + + -  set_user_route tool   +  set_user_route tool   - + navigationAutomat:e->categoryAutomat:n - - - + + + -  restore last category   +  restore last category   - + navigationAutomat:e->artikelAutomat:n - - - + + + -  restore last SKU   +  restore last SKU   - + navigationAutomat:e->deliveryEditorAutomat:n - - - + + + -  restore draft path   +  restore draft path   - + userSettingsAutomat @@ -2048,17 +2091,17 @@ Survives page reloads - + userSettingsAutomat:e->chatAutomat:n - - - + + + -  chatDrawerWidth   +  chatDrawerWidth   - + helpAutomat @@ -2096,17 +2139,17 @@ Survives page reloads - + helpAutomat:e->navigationAutomat:n - - - + + + -  route-contextual help   +  route-contextual help   - + pwaAutomat @@ -2144,7 +2187,7 @@ Modal controls - + Legend diff --git a/generate-automat-graph.js b/generate-automat-graph.js index 798c1c1..ea97163 100644 --- a/generate-automat-graph.js +++ b/generate-automat-graph.js @@ -106,7 +106,7 @@ const CLUSTERS = [ { id: 'audit_grids', label: 'Audit & Virtual Grids', - automats: ['articleHistoryGridAutomat', 'globalHistoryGridAutomat'], + automats: ['articleHistoryGridAutomat', 'globalHistoryGridAutomat', 'ledgerHistoryGridAutomat'], }, { id: 'system_ui', @@ -520,6 +520,17 @@ const AUTOMAT_SPECS = [ { type: 'API', name: 'GET /api/globalhistory', desc: 'Company global audit' }, ], }, + { + id: 'ledgerHistoryGridAutomat', + title: 'ledgerHistoryGridAutomat', + subtitle: 'createGridAutomat (Ledger)', + entries: [ + { type: 'STATE', name: 'rows: [] / totalRows', desc: 'Block size 50 virtual rows' }, + { type: 'ACTION', name: 'fetchRows(viewport)', desc: 'Infinite scroll query' }, + { type: 'ACTION', name: 'reload()', desc: 'Reload on company switch' }, + { type: 'API', name: 'GET /api/ledger/history', desc: 'Append-only ledger audit' }, + ], + }, // 12. UI, Settings & System { @@ -605,6 +616,7 @@ const EDGE_DEFS = [ { from: 'authAutomat', to: 'vfsAutomat', label: 'load company vfs', port: 'p_auth_status' }, { from: 'authAutomat', to: 'articleHistoryGridAutomat', label: 'reload history on authed', port: 'p_auth_status' }, { from: 'authAutomat', to: 'globalHistoryGridAutomat', label: 'reload audit on authed', port: 'p_auth_status' }, + { from: 'authAutomat', to: 'ledgerHistoryGridAutomat', label: 'reload ledger on authed', port: 'p_auth_status' }, // Real-time SSE dispatch { from: 'sseAutomat', to: 'categoryAutomat', label: '\'category\' sse trigger', port: 'p_sse_events' }, diff --git a/src/api/ledger.js b/src/api/ledger.js index a421c61..5ecdfb0 100644 --- a/src/api/ledger.js +++ b/src/api/ledger.js @@ -7,6 +7,30 @@ import { http } from './http.js'; */ export const ledger = { + /** + * GET /api/ledger/history?offset=0&limit=50&search=...&seekMonths=-1 + * Append-only ledger audit history with chunk paging, search, and seek filtering + * @param {{ offset?: number, limit?: number, search?: string, seekMonths?: number, sortModel?: Array, filterModel?: object, sku?: string, transactionType?: string }} [params] + * → { ledger: LedgerEntry[], history?: LedgerEntry[], total: number, unfilteredTotal?: number } + */ + // #region GET /api/ledger/history — getHistory + getHistory({ offset = 0, limit = 50, search, seekMonths, sortModel, filterModel, sku, transactionType } = {}) { + const params = new URLSearchParams(); + params.set('offset', String(Math.max(0, Math.floor(Number(offset) || 0)))); + params.set('limit', String(Math.max(1, Math.floor(Number(limit) || 50)))); + if (search && String(search).trim()) params.set('search', String(search).trim()); + if (seekMonths != null && Number(seekMonths) < 0) { + params.set('seekMonths', String(Math.floor(Number(seekMonths)))); + } + if (sku != null) params.set('sku', String(sku)); + if (transactionType != null) params.set('transaction_type', String(transactionType)); + 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/ledger/history${qs ? `?${qs}` : ''}`); + }, + // #endregion + /** * GET /api/ledger/manifestarticle/:manifestarticleId * Query: ?transaction_type=... diff --git a/src/appRouter.jsx b/src/appRouter.jsx index 35806fa..66e84a7 100644 --- a/src/appRouter.jsx +++ b/src/appRouter.jsx @@ -34,6 +34,7 @@ import OAuthAuthorizePage from './routes/oauth/OAuthAuthorizePage'; const ArticleHistoryPage = lazy(() => import('./routes/infos/ArticleHistoryPage')); const GlobalHistoryPage = lazy(() => import('./routes/infos/GlobalHistoryPage')); +const LedgerHistoryPage = lazy(() => import('./routes/infos/LedgerHistoryPage')); function LoginRedirect() { const params = new URLSearchParams(window.location.search); @@ -72,6 +73,7 @@ const routes = [ { index: true, element: }, { path: 'articlehistory', element: }, { path: 'globalhistory', element: }, + { path: 'ledgerhistory', element: }, { path: 'statistics', element: }, ], }, diff --git a/src/automats/createChunkListAutomat.js b/src/automats/createChunkListAutomat.js index bef5923..12ba0c3 100644 --- a/src/automats/createChunkListAutomat.js +++ b/src/automats/createChunkListAutomat.js @@ -391,13 +391,15 @@ export function createChunkListAutomat({ ? res.items : Array.isArray(res?.history) ? res.history - : Array.isArray(res?.rows) - ? res.rows - : Array.isArray(res?.entries) - ? res.entries - : Array.isArray(res) - ? res - : []; + : Array.isArray(res?.ledger) + ? res.ledger + : Array.isArray(res?.rows) + ? res.rows + : Array.isArray(res?.entries) + ? res.entries + : Array.isArray(res) + ? res + : []; const total = Number(res?.total ?? res?.totalRows ?? res?.totalCount ?? records.length); const unfilteredTotal = res?.unfilteredTotal != null ? Number(res.unfilteredTotal) : total; return { records, total, unfilteredTotal }; diff --git a/src/automats/index.js b/src/automats/index.js index 948f8bf..d4a5077 100644 --- a/src/automats/index.js +++ b/src/automats/index.js @@ -191,6 +191,7 @@ export { export { default as articleHistoryGridAutomat } from './articleHistoryAutomat'; export { default as globalHistoryAutomat, globalHistoryGridAutomat } from './globalHistoryAutomat'; +export { default as ledgerHistoryAutomat, ledgerHistoryGridAutomat } from './ledgerHistoryAutomat'; export { default as chatAutomat } from './chatAutomat'; export { diff --git a/src/automats/ledgerHistoryAutomat.js b/src/automats/ledgerHistoryAutomat.js new file mode 100644 index 0000000..56ea98a --- /dev/null +++ b/src/automats/ledgerHistoryAutomat.js @@ -0,0 +1,26 @@ +/** + * @file ledgerHistoryAutomat.js + * @description Double-buffered chunk list automat (`ledgerHistoryAutomat`) for + * append-only ledger transactions and stock journal with multi-filter caching and LRU eviction. + * + * Upstream Triggers: + * - `authAutomat`: Subscribes to auth state changes — resets on `'anon'` or when + * `selectedCompanyId` changes. + * - Consumer UI: Calls `getItems()`, `requireChunk(start, end)`, `filter(search)`, + * `addSeekMonth()`, `addSeekYear()`, `resetNegativeSeek()`. + */ + +import { createChunkListAutomat } from './createChunkListAutomat'; +import { ledger } from '../api'; + +export const ledgerHistoryAutomat = createChunkListAutomat({ + name: 'ledgerHistory', + chunkSize: 50, + maxLruChunks: 50, + fetchPage: (params) => ledger.getHistory(params), +}); + +// Alias for consistency with other grid/chunk automats +export const ledgerHistoryGridAutomat = ledgerHistoryAutomat; + +export default ledgerHistoryAutomat; diff --git a/src/components/manifest/BookDeliveryDialog.jsx b/src/components/manifest/BookDeliveryDialog.jsx index 6efc545..5143eb3 100644 --- a/src/components/manifest/BookDeliveryDialog.jsx +++ b/src/components/manifest/BookDeliveryDialog.jsx @@ -102,7 +102,7 @@ export default class BookDeliveryDialog extends PureComponent { const articles = res?.articles || res?.supplier?.articles || []; const map = buildSupplierArticlesMap(articles); this.setState({ supplierArticlesMap: map }); - }).catch(() => {}); + }).catch(() => { }); } } @@ -122,7 +122,7 @@ export default class BookDeliveryDialog extends PureComponent { this.unsubArticle = artikelAutomat.subscribe(() => { this.forceUpdate(); }); - void warehouseAutomat.actions.load().catch(() => {}); + void warehouseAutomat.actions.load().catch(() => { }); this.loadSupplierArticlesIfNeeded(); if (this.props.open) { @@ -225,7 +225,7 @@ export default class BookDeliveryDialog extends PureComponent { supplierArticlesMap: currentSupplierMap, }, () => { - void warehouseAutomat.actions.load().catch(() => {}); + void warehouseAutomat.actions.load().catch(() => { }); this.loadSupplierArticlesIfNeeded(); if (this.props.summary) { this.initAmountsFromSummary(this.props.summary, this.props.prefillLineId); @@ -535,15 +535,15 @@ export default class BookDeliveryDialog extends PureComponent { (line.sku ? String(line.sku).trim() : null); if (targetSku && !cachedArticle) { - void artikelAutomat.actions.loadArticle(targetSku).catch(() => {}); + void artikelAutomat.actions.loadArticle(targetSku).catch(() => { }); } const isTracked = Boolean(line.trackStock) || (cachedArticle?.track_stock !== undefined ? cachedArticle.track_stock !== false && - cachedArticle.track_stock !== 0 && - cachedArticle.track_stock !== '0' + cachedArticle.track_stock !== 0 && + cachedArticle.track_stock !== '0' : false); const isEnablingThis = targetSku ? Boolean(enablingTrackStockBySku?.[targetSku]) : false; @@ -565,8 +565,8 @@ export default class BookDeliveryDialog extends PureComponent { const warehouseBreakdown = Array.isArray(cachedArticle?.warehouses) && cachedArticle.warehouses.length > 0 ? cachedArticle.warehouses - .map((w) => `${w.name || 'Lager'}: ${formatStock(w.stock ?? 0)}`) - .join(' • ') + .map((w) => `${w.name || 'Lager'}: ${formatStock(w.stock ?? 0)}`) + .join(' • ') : null; const eans = line.ean @@ -791,11 +791,7 @@ export default class BookDeliveryDialog extends PureComponent { )} - {isFullyBooked ? ( - }> - Alle Positionen dieser Lieferung sind bereits vollständig im Ledger verbucht. - - ) : isWarehouseMissing ? ( + {isFullyBooked ? null : isWarehouseMissing ? ( Bitte wählen Sie ein Ziellager aus, um die lagergeführten Artikel zu buchen. diff --git a/src/nav.js b/src/nav.js index 4b77d9c..e385265 100644 --- a/src/nav.js +++ b/src/nav.js @@ -22,6 +22,7 @@ import LabelIcon from '@mui/icons-material/Label'; import HistoryIcon from '@mui/icons-material/History'; import PublicIcon from '@mui/icons-material/Public'; +import ReceiptLongIcon from '@mui/icons-material/ReceiptLong'; import BarChartIcon from '@mui/icons-material/BarChart'; import { isKategorienPath } from './components/category/categoryRoute'; import { deliveryEditorAutomat } from './automats/deliveryEditorAutomat'; @@ -273,6 +274,13 @@ export const SUB_NAV = { icon: PublicIcon, help: 'Globale System- und Änderungshistorie', }, + { + segment: 'ledgerhistory', + label: 'LedgerHistory', + title: 'LedgerHistory', + icon: ReceiptLongIcon, + help: 'Unveränderliches Buchungs- und Bestandsjournal (Audit-Ledger)', + }, { segment: 'statistics', label: 'Statistics', diff --git a/src/routes/infos/LedgerHistoryPage.jsx b/src/routes/infos/LedgerHistoryPage.jsx new file mode 100644 index 0000000..7765aae --- /dev/null +++ b/src/routes/infos/LedgerHistoryPage.jsx @@ -0,0 +1,629 @@ +import { PureComponent, createRef } from 'react'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import CircularProgress from '@mui/material/CircularProgress'; +import ListItemButton from '@mui/material/ListItemButton'; +import Typography from '@mui/material/Typography'; +import Skeleton from '@mui/material/Skeleton'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import ViewModuleIcon from '@mui/icons-material/ViewModule'; +import Chip from '@mui/material/Chip'; +import SingleLineTextEdit from 'dashgui/SingleLineTextEdit'; +import FillSpace from 'dashgui/FillSpace'; +import { PanelTile, PanelTileGrid } from 'dashgui/panels'; +import BackendError from '../../components/BackendError'; +import ledgerHistoryAutomat from '../../automats/ledgerHistoryAutomat'; +import { router } from '../../appRouter'; +import { backendErrorFrom } from '../../utils/backendError'; +import { LINK } from '../../colors'; + +const CHUNK_SIZE = 50; + +function formatDateTime(val) { + if (!val) return '–'; + try { + const d = new Date(val); + if (isNaN(d.getTime())) return String(val); + return d.toLocaleString('de-DE', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }); + } catch { + return String(val); + } +} + +export function createDummyTestLedgerHistory() { + return { + id: 999999, + date: '2030-10-30T12:00:00Z', + created_at: '2030-10-30T12:00:00Z', + transaction_type: 'ACCEPTANCE', + sku: 'SKU-99999999', + amount: 9999, + transaction_text: 'Wareneingang Lieferschein LS-2030-999999 Musterbuchung', + user: 'Max Mustermann', + }; +} + +const rowGridSx = { + display: 'grid', + gridTemplateColumns: + 'minmax(60px, max-content) minmax(150px, max-content) minmax(110px, max-content) minmax(130px, max-content) minmax(90px, max-content) minmax(220px, 2fr) minmax(120px, max-content)', + gridTemplateRows: '0px 1fr', + rowGap: 0, + columnGap: 2, + alignItems: 'center', + width: '100%', + boxSizing: 'border-box', +}; + +const ellipsisSx = { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }; + +const measuringCellSx = { + visibility: 'hidden', + height: 0, + boxSizing: 'border-box', + pointerEvents: 'none', + textTransform: 'none', + letterSpacing: 'normal', +}; + +const headerCellSx = { + color: 'text.secondary', + typography: 'caption', + fontWeight: 600, + textTransform: 'uppercase', + letterSpacing: '0.04em', + userSelect: 'none', +}; + +function renderInvisibleDummyRow(dummy) { + const d = dummy || createDummyTestLedgerHistory(); + return ( + <> + + {d.id} + + + {formatDateTime(d.created_at || d.date)} + + + {d.transaction_type} + + + {d.sku} + + + +{d.amount} + + + {d.transaction_text} + + + {d.user} + + + ); +} + +function getTransactionTypeLabel(typeStr) { + const norm = String(typeStr || '').toUpperCase(); + if (norm === 'ACCEPTANCE') return 'Annahme'; + if (norm === 'WAREHOUSE') return 'Lager'; + if (norm === 'ADJUSTMENT' || norm === 'STOCK_ADJUST') return 'Korrektur'; + if (norm === 'SALE' || norm === 'OUTBOUND') return 'Verkauf'; + if (norm === 'RETURN') return 'Retoure'; + if (norm === 'DELIVERY') return 'Lieferung'; + if (norm === 'INVENTORY') return 'Inventur'; + return typeStr || '–'; +} + +class LedgerHistoryRow extends PureComponent { + render() { + const { item, disabled, headerItem } = this.props; + const dummy = headerItem || createDummyTestLedgerHistory(); + + if (!item) { + return ( + + {renderInvisibleDummyRow(dummy)} + + + + + + + + + + + + + + + + + + + + + + + ); + } + + const userName = + item.user || + item.employee || + item.created_by || + (item.companyuserId ? `User #${item.companyuserId}` : '—'); + + const typeRaw = item.transaction_type || item.type || '–'; + const typeLabel = getTransactionTypeLabel(typeRaw); + const sku = item.sku || null; + const hasSku = Boolean(sku && sku !== '–'); + + const amt = Number(item.amount); + const hasAmount = item.amount != null && !isNaN(amt); + const isPos = hasAmount && amt > 0; + const isNeg = hasAmount && amt < 0; + const amountStr = hasAmount ? (isPos ? `+${amt}` : `${amt}`) : (item.amount ?? '–'); + + const bookingText = item.transaction_text || item.text || item.reason || '–'; + + return ( + { + void router.navigate(`/artikel/${encodeURIComponent(sku)}`); + } + : undefined + } + > + {renderInvisibleDummyRow(dummy)} + + {/* 1. ID */} + + + {item.id} + + + + {/* 2. Datum */} + + + {formatDateTime(item.created_at || item.date || item.createdAt)} + + + + {/* 3. Typ */} + + + + + {/* 4. SKU */} + + {hasSku ? ( + { + e.preventDefault(); + e.stopPropagation(); + void router.navigate(`/artikel/${encodeURIComponent(sku)}`); + }} + sx={{ + color: LINK, + fontWeight: 600, + textDecoration: 'none', + ...ellipsisSx, + '&:hover': { textDecoration: 'underline' }, + }} + > + {sku} + + ) : ( + + – + + )} + + + {/* 5. Menge / Betrag */} + + + {amountStr} + + + + {/* 6. Buchungstext */} + + + {bookingText} + + + + {/* 7. Benutzer */} + + + {userName} + + + + ); + } +} + +class LedgerHistorySearchField extends PureComponent { + constructor(props) { + super(props); + const s = ledgerHistoryAutomat.state; + const stats = ledgerHistoryAutomat.getChunkStats?.(); + this.state = { + input: s.filter ?? '', + seekMonths: s.seekMonths ?? 0, + seekYears: s.seekYears ?? 0, + lruChunksCount: stats?.lruChunksCount || 0, + maxLruChunks: stats?.maxLruChunks || 50, + }; + } + + componentDidMount() { + this.unsub = ledgerHistoryAutomat.subscribe(this, (state) => ({ + input: state.filter ?? '', + seekMonths: state.seekMonths ?? 0, + seekYears: state.seekYears ?? 0, + })); + if (import.meta.env.DEV) { + this.unsubStats = ledgerHistoryAutomat.subscribeChunkStats?.((stats) => { + this.setState({ + lruChunksCount: stats.lruChunksCount, + maxLruChunks: stats.maxLruChunks, + }); + }); + } + } + + componentWillUnmount() { + this.unsub?.(); + this.unsubStats?.(); + } + + render() { + const { inputRef, onReload, loading } = this.props; + const { input, seekMonths, seekYears } = this.state; + const monthActive = seekMonths > 0; + const monthLabel = !monthActive ? '-1 Month' : `-${seekMonths} Month${seekMonths > 1 ? 's' : ''}`; + const yearActive = seekYears > 0; + const yearLabel = !yearActive ? '-1 Year' : `-${seekYears} Year${seekYears > 1 ? 's' : ''}`; + + return ( + + + {/* Top row: search field */} + + ledgerHistoryAutomat.actions.filter(next)} + /> + + + + {/* Filter Bar: Negativeseek (-1 Month, -1 Year, Reset) */} + + + + + {(monthActive || yearActive) && ( + + )} + + + {/* DEV: Chunk-Cache Debug Monitor trigger */} + {import.meta.env.DEV && ( + } + label={ + this.state.lruChunksCount > 0 + ? `Chunk-Cache (${this.state.lruChunksCount}/${this.state.maxLruChunks || 50})` + : 'Chunk-Cache' + } + onClick={() => window.dispatchEvent(new CustomEvent('dash:toggle-chunk-monitor'))} + sx={{ + cursor: 'pointer', + height: 28, + fontWeight: 600, + fontSize: '0.75rem', + bgcolor: 'background.paper', + boxShadow: 1, + '&:hover': { bgcolor: 'action.hover' }, + }} + /> + )} + + + + ); + } +} + +// MARK: calls getHistory from ./api/ledger.js via ledgerHistoryAutomat +export default class LedgerHistoryPage extends PureComponent { + constructor(props) { + super(props); + this.headerItem = createDummyTestLedgerHistory(); + this.state = { + items: ledgerHistoryAutomat.getItems(), + unfilteredTotal: ledgerHistoryAutomat.state.unfilteredTotal ?? 0, + filterKey: ledgerHistoryAutomat.getFilterKey?.() ?? '', + status: ledgerHistoryAutomat.state.status, + error: ledgerHistoryAutomat.state.error, + }; + this.searchRef = createRef(); + this.handleReload = this.handleReload.bind(this); + this.handleRetry = this.handleRetry.bind(this); + this.handlePageChange = this.handlePageChange.bind(this); + } + + componentDidMount() { + this.unsub = ledgerHistoryAutomat.subscribe(this, (state) => ({ + items: state.items, + unfilteredTotal: state.unfilteredTotal ?? 0, + filterKey: ledgerHistoryAutomat.getFilterKey(state), + status: state.status, + error: state.error, + })); + } + + componentWillUnmount() { + this.unsub?.(); + } + + handlePageChange(page, meta) { + const pageSize = meta?.pageSize ?? 10; + const startIndex = (page - 1) * pageSize; + const items = this.state.items; + const maxIndex = items ? items.length - 1 : startIndex; + const endIndex = Math.min(maxIndex, startIndex + pageSize - 1); + + const startChunk = Math.floor(startIndex / CHUNK_SIZE); + const endChunk = Math.floor(endIndex / CHUNK_SIZE); + + ledgerHistoryAutomat.requireChunk(startChunk, endChunk); + } + + handleReload() { + void ledgerHistoryAutomat.actions.reload().catch(() => {}); + } + + handleRetry() { + void ledgerHistoryAutomat.actions.requireChunk(0, 0, { immediate: true }).catch(() => {}); + } + + render() { + const { items, unfilteredTotal, filterKey, status, error } = this.state; + const loading = status === 'loading'; + + if (items === null && status === 'failed') { + return ( + <> + + + + + + + + ); + } + + return ( + <> + + + + + {renderInvisibleDummyRow(this.headerItem)} + ID + Datum + Typ + Artikel-Nr. + Menge + Buchungstext + Benutzer + + } + onPageChange={this.handlePageChange} + /> + {items === null && ( + + + + )} + + + + ); + } +} diff --git a/src/routes/infos/index.js b/src/routes/infos/index.js index 04a5fba..c7d69f4 100644 --- a/src/routes/infos/index.js +++ b/src/routes/infos/index.js @@ -1,3 +1,4 @@ export { default as ArticleHistoryPage } from './ArticleHistoryPage'; export { default as GlobalHistoryPage } from './GlobalHistoryPage'; +export { default as LedgerHistoryPage } from './LedgerHistoryPage'; export { default as StatisticsPage } from './StatisticsPage'; diff --git a/test/dummyBackend.mjs b/test/dummyBackend.mjs index 61c3b19..e5e24df 100644 --- a/test/dummyBackend.mjs +++ b/test/dummyBackend.mjs @@ -290,6 +290,7 @@ const server = http.createServer(async (req, res) => { if (ledgerSku) return json(res, 200, { ledger: [] }); const ledgerMa = p.match(/^\/api\/ledger\/manifestarticle\/(\d+)$/); if (ledgerMa) return json(res, 200, { ledger: [] }); + if (p === '/api/ledger/history') return json(res, 200, { ledger: misc.ledgerHistory ?? [], total: 0 }); if (p === '/api/ledger') return json(res, 200, { ledger: [] }); if (p === '/api/article/search') { const q = (url.searchParams.get('target') ?? '').toLowerCase();