Compare commits

..

11 Commits

Author SHA1 Message Date
seb
1b4b31b016 u 2026-10-18 03:25:57 +02:00
seb
dc73905a19 u 2026-10-18 03:16:24 +02:00
seb
ab9d5fe413 u 2026-10-18 03:14:15 +02:00
seb
0a59a0987a u 2026-10-18 00:38:45 +02:00
seb
691854fdd4 u 2026-10-18 00:27:27 +02:00
seb
666d84b9f3 u 2026-10-17 23:59:20 +02:00
seb
b8f226bd39 u 2026-10-17 23:32:21 +02:00
seb
0d11dd5014 u 2026-10-17 21:40:53 +02:00
seb
d1ccbf335c u 2026-10-17 20:54:04 +02:00
seb
c332be8916 u 2026-10-17 19:51:27 +02:00
seb
7080382f56 u 2026-10-17 18:38:15 +02:00
32 changed files with 1628 additions and 440 deletions

View File

@@ -131,7 +131,7 @@ Strip supplierName?: str from T11 (resolve via entityId in cached supplier list)
# Dash API Routes Overview (All) # Dash API Routes Overview (All)
Overview of all 261 API endpoints across all modules. Overview of all 263 API endpoints across all modules.
### Notation ### Notation
@@ -214,6 +214,9 @@ _none_
#### Export articles as ZIP — `GET /article/export.zip` #### Export articles as ZIP — `GET /article/export.zip`
- Params: `query: categoryId?: int` - Params: `query: categoryId?: int`
- Returns: `200 zip` - Returns: `200 zip`
#### Query company-wide article history — `GET /article/history`
- Params: `query: offset?: int, limit?: int, search?: str, seekMonths?: int`
- Returns: `200 { history: { id: int, date: str, sku: str, historytext: str, name?: str|null }[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Delete article by SKU — `DELETE /article/{sku}` #### Delete article by SKU — `DELETE /article/{sku}`
- Params: `sku: str, query: cascade?: bool` - Params: `sku: str, query: cascade?: bool`
- Returns: `204 void` - Returns: `204 void`
@@ -447,7 +450,7 @@ _none_
- Returns: `200 { category: { id: int, name: str, slug: str, parentId: int, sort: int } }` - Returns: `200 { category: { id: int, name: str, slug: str, parentId: int, sort: int } }`
#### Articles in category — `GET /category/{id}/articles` #### Articles in category — `GET /category/{id}/articles`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, track_stock: bool, optionsCount: int, articleListCount: int, childCount: int, net?: num|null, taxRate?: num|null, parentArticleId?: int|null, parentSku?: str|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null, brand?: T26|null }[] }` - Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, track_stock: bool, optionsCount: int, articleListCount: int, childCount: int, net?: num|null, taxRate?: num|null, parentArticleId?: int|null, parentSku?: str|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null, brand?: T26|null, articlewarehousesummary?: { id: int, articleId: int, warehouseId: int, stock: num, article_id?: int, warehouse_id?: int, warehouseName?: str|null }[], warehouses?: { id: int, name: str, stock: num }[] }[] }`
#### List category attribute values — `GET /category/{id}/attributevalue` #### List category attribute values — `GET /category/{id}/attributevalue`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { attributevalues: { id: int, attributeId: int, attributeName: str, attributeValueId: int, value: str, sort: int, categoryId?: int, exclusive?: bool }[] }` - Returns: `200 { attributevalues: { id: int, attributeId: int, attributeName: str, attributeValueId: int, value: str, sort: int, categoryId?: int, exclusive?: bool }[] }`
@@ -659,6 +662,9 @@ _none_
#### Query ledger by manifest article ID or SKU — `GET /ledger` #### Query ledger by manifest article ID or SKU — `GET /ledger`
- Params: `query: manifestarticle_id?: int, sku?: str, transaction_type?: str` - Params: `query: manifestarticle_id?: int, sku?: str, transaction_type?: str`
- Returns: `200 { ledger: T15[] }` - Returns: `200 { ledger: T15[] }`
#### Query company-wide ledger history — `GET /ledger/history`
- Params: `query: offset?: int, limit?: int, search?: str, seekMonths?: int`
- Returns: `200 { ledger: { id: int, date: str, transaction_type: str, transaction_text: str, sku?: str|null, name?: str|null, amount?: num }[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Query ledger by manifest article ID — `GET /ledger/manifestarticle/{manifestarticleId}` #### Query ledger by manifest article ID — `GET /ledger/manifestarticle/{manifestarticleId}`
- Params: `manifestarticleId: int, query: transaction_type?: str` - Params: `manifestarticleId: int, query: transaction_type?: str`
- Returns: `200 { ledger: T15[] }` - Returns: `200 { ledger: T15[] }`

View File

@@ -87,6 +87,7 @@ digraph automats {
fontcolor="#e2e8f0"; margin=14; fontcolor="#e2e8f0"; margin=14;
articleHistoryGridAutomat [label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4" COLOR="#3f4c5f"><TR><TD COLSPAN="3" BGCOLOR="#33415c" ALIGN="CENTER" CELLPADDING="1" BORDER="1"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD PORT="in0" BGCOLOR="#33415c" BORDER="0" WIDTH="10" HEIGHT="4" CELLPADDING="0"></TD></TR><TR><TD COLSPAN="1" ALIGN="CENTER" CELLPADDING="3"><FONT COLOR="#e2e8f0" POINT-SIZE="12"><B>articleHistoryGridAutomat</B></FONT><BR/><FONT COLOR="#8b95a7" POINT-SIZE="9">createGridAutomat (Article)</FONT></TD></TR></TABLE></TD></TR><TR><TD COLSPAN="3" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD BGCOLOR="#2b1a42" ALIGN="CENTER" CELLPADDING="3" WIDTH="48"><FONT COLOR="#c084fc" POINT-SIZE="8"><B>STATE</B></FONT></TD><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9" POINT-SIZE="10"><B>rows: [] / totalRows</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="8">Block size 50 virtual rows</FONT></TD></TR></TABLE></TD></TR><TR><TD COLSPAN="3" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD BGCOLOR="#143826" ALIGN="CENTER" CELLPADDING="3" WIDTH="48"><FONT COLOR="#4ade80" POINT-SIZE="8"><B>ACTION</B></FONT></TD><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9" POINT-SIZE="10"><B>fetchRows(viewport)</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="8">Infinite scroll query</FONT></TD></TR></TABLE></TD></TR><TR><TD COLSPAN="3" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD BGCOLOR="#143826" ALIGN="CENTER" CELLPADDING="3" WIDTH="48"><FONT COLOR="#4ade80" POINT-SIZE="8"><B>ACTION</B></FONT></TD><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9" POINT-SIZE="10"><B>reload()</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="8">Reload on company switch</FONT></TD></TR></TABLE></TD></TR><TR><TD COLSPAN="3" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD BGCOLOR="#3b2c12" ALIGN="CENTER" CELLPADDING="3" WIDTH="48"><FONT COLOR="#fbbf24" POINT-SIZE="8"><B>API</B></FONT></TD><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9" POINT-SIZE="10"><B>GET /api/article/history</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="8">Infinite audit window</FONT></TD></TR></TABLE></TD></TR></TABLE>>]; articleHistoryGridAutomat [label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4" COLOR="#3f4c5f"><TR><TD COLSPAN="3" BGCOLOR="#33415c" ALIGN="CENTER" CELLPADDING="1" BORDER="1"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD PORT="in0" BGCOLOR="#33415c" BORDER="0" WIDTH="10" HEIGHT="4" CELLPADDING="0"></TD></TR><TR><TD COLSPAN="1" ALIGN="CENTER" CELLPADDING="3"><FONT COLOR="#e2e8f0" POINT-SIZE="12"><B>articleHistoryGridAutomat</B></FONT><BR/><FONT COLOR="#8b95a7" POINT-SIZE="9">createGridAutomat (Article)</FONT></TD></TR></TABLE></TD></TR><TR><TD COLSPAN="3" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD BGCOLOR="#2b1a42" ALIGN="CENTER" CELLPADDING="3" WIDTH="48"><FONT COLOR="#c084fc" POINT-SIZE="8"><B>STATE</B></FONT></TD><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9" POINT-SIZE="10"><B>rows: [] / totalRows</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="8">Block size 50 virtual rows</FONT></TD></TR></TABLE></TD></TR><TR><TD COLSPAN="3" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD BGCOLOR="#143826" ALIGN="CENTER" CELLPADDING="3" WIDTH="48"><FONT COLOR="#4ade80" POINT-SIZE="8"><B>ACTION</B></FONT></TD><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9" POINT-SIZE="10"><B>fetchRows(viewport)</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="8">Infinite scroll query</FONT></TD></TR></TABLE></TD></TR><TR><TD COLSPAN="3" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD BGCOLOR="#143826" ALIGN="CENTER" CELLPADDING="3" WIDTH="48"><FONT COLOR="#4ade80" POINT-SIZE="8"><B>ACTION</B></FONT></TD><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9" POINT-SIZE="10"><B>reload()</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="8">Reload on company switch</FONT></TD></TR></TABLE></TD></TR><TR><TD COLSPAN="3" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD BGCOLOR="#3b2c12" ALIGN="CENTER" CELLPADDING="3" WIDTH="48"><FONT COLOR="#fbbf24" POINT-SIZE="8"><B>API</B></FONT></TD><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9" POINT-SIZE="10"><B>GET /api/article/history</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="8">Infinite audit window</FONT></TD></TR></TABLE></TD></TR></TABLE>>];
globalHistoryGridAutomat [label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4" COLOR="#3f4c5f"><TR><TD COLSPAN="3" BGCOLOR="#33415c" ALIGN="CENTER" CELLPADDING="1" BORDER="1"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD PORT="in0" BGCOLOR="#33415c" BORDER="0" WIDTH="10" HEIGHT="4" CELLPADDING="0"></TD></TR><TR><TD COLSPAN="1" ALIGN="CENTER" CELLPADDING="3"><FONT COLOR="#e2e8f0" POINT-SIZE="12"><B>globalHistoryGridAutomat</B></FONT><BR/><FONT COLOR="#8b95a7" POINT-SIZE="9">createGridAutomat (Global)</FONT></TD></TR></TABLE></TD></TR><TR><TD COLSPAN="3" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD BGCOLOR="#2b1a42" ALIGN="CENTER" CELLPADDING="3" WIDTH="48"><FONT COLOR="#c084fc" POINT-SIZE="8"><B>STATE</B></FONT></TD><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9" POINT-SIZE="10"><B>rows: [] / totalRows</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="8">Block size 50 virtual rows</FONT></TD></TR></TABLE></TD></TR><TR><TD COLSPAN="3" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD BGCOLOR="#143826" ALIGN="CENTER" CELLPADDING="3" WIDTH="48"><FONT COLOR="#4ade80" POINT-SIZE="8"><B>ACTION</B></FONT></TD><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9" POINT-SIZE="10"><B>fetchRows(viewport)</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="8">Infinite scroll query</FONT></TD></TR></TABLE></TD></TR><TR><TD COLSPAN="3" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD BGCOLOR="#143826" ALIGN="CENTER" CELLPADDING="3" WIDTH="48"><FONT COLOR="#4ade80" POINT-SIZE="8"><B>ACTION</B></FONT></TD><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9" POINT-SIZE="10"><B>reload()</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="8">Reload on company switch</FONT></TD></TR></TABLE></TD></TR><TR><TD COLSPAN="3" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD BGCOLOR="#3b2c12" ALIGN="CENTER" CELLPADDING="3" WIDTH="48"><FONT COLOR="#fbbf24" POINT-SIZE="8"><B>API</B></FONT></TD><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9" POINT-SIZE="10"><B>GET /api/globalhistory</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="8">Company global audit</FONT></TD></TR></TABLE></TD></TR></TABLE>>]; globalHistoryGridAutomat [label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4" COLOR="#3f4c5f"><TR><TD COLSPAN="3" BGCOLOR="#33415c" ALIGN="CENTER" CELLPADDING="1" BORDER="1"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD PORT="in0" BGCOLOR="#33415c" BORDER="0" WIDTH="10" HEIGHT="4" CELLPADDING="0"></TD></TR><TR><TD COLSPAN="1" ALIGN="CENTER" CELLPADDING="3"><FONT COLOR="#e2e8f0" POINT-SIZE="12"><B>globalHistoryGridAutomat</B></FONT><BR/><FONT COLOR="#8b95a7" POINT-SIZE="9">createGridAutomat (Global)</FONT></TD></TR></TABLE></TD></TR><TR><TD COLSPAN="3" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD BGCOLOR="#2b1a42" ALIGN="CENTER" CELLPADDING="3" WIDTH="48"><FONT COLOR="#c084fc" POINT-SIZE="8"><B>STATE</B></FONT></TD><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9" POINT-SIZE="10"><B>rows: [] / totalRows</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="8">Block size 50 virtual rows</FONT></TD></TR></TABLE></TD></TR><TR><TD COLSPAN="3" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD BGCOLOR="#143826" ALIGN="CENTER" CELLPADDING="3" WIDTH="48"><FONT COLOR="#4ade80" POINT-SIZE="8"><B>ACTION</B></FONT></TD><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9" POINT-SIZE="10"><B>fetchRows(viewport)</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="8">Infinite scroll query</FONT></TD></TR></TABLE></TD></TR><TR><TD COLSPAN="3" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD BGCOLOR="#143826" ALIGN="CENTER" CELLPADDING="3" WIDTH="48"><FONT COLOR="#4ade80" POINT-SIZE="8"><B>ACTION</B></FONT></TD><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9" POINT-SIZE="10"><B>reload()</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="8">Reload on company switch</FONT></TD></TR></TABLE></TD></TR><TR><TD COLSPAN="3" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD BGCOLOR="#3b2c12" ALIGN="CENTER" CELLPADDING="3" WIDTH="48"><FONT COLOR="#fbbf24" POINT-SIZE="8"><B>API</B></FONT></TD><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9" POINT-SIZE="10"><B>GET /api/globalhistory</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="8">Company global audit</FONT></TD></TR></TABLE></TD></TR></TABLE>>];
ledgerHistoryGridAutomat [label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4" COLOR="#3f4c5f"><TR><TD COLSPAN="3" BGCOLOR="#33415c" ALIGN="CENTER" CELLPADDING="1" BORDER="1"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD PORT="in0" BGCOLOR="#33415c" BORDER="0" WIDTH="10" HEIGHT="4" CELLPADDING="0"></TD></TR><TR><TD COLSPAN="1" ALIGN="CENTER" CELLPADDING="3"><FONT COLOR="#e2e8f0" POINT-SIZE="12"><B>ledgerHistoryGridAutomat</B></FONT><BR/><FONT COLOR="#8b95a7" POINT-SIZE="9">createGridAutomat (Ledger)</FONT></TD></TR></TABLE></TD></TR><TR><TD COLSPAN="3" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD BGCOLOR="#2b1a42" ALIGN="CENTER" CELLPADDING="3" WIDTH="48"><FONT COLOR="#c084fc" POINT-SIZE="8"><B>STATE</B></FONT></TD><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9" POINT-SIZE="10"><B>rows: [] / totalRows</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="8">Block size 50 virtual rows</FONT></TD></TR></TABLE></TD></TR><TR><TD COLSPAN="3" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD BGCOLOR="#143826" ALIGN="CENTER" CELLPADDING="3" WIDTH="48"><FONT COLOR="#4ade80" POINT-SIZE="8"><B>ACTION</B></FONT></TD><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9" POINT-SIZE="10"><B>fetchRows(viewport)</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="8">Infinite scroll query</FONT></TD></TR></TABLE></TD></TR><TR><TD COLSPAN="3" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD BGCOLOR="#143826" ALIGN="CENTER" CELLPADDING="3" WIDTH="48"><FONT COLOR="#4ade80" POINT-SIZE="8"><B>ACTION</B></FONT></TD><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9" POINT-SIZE="10"><B>reload()</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="8">Reload on company switch</FONT></TD></TR></TABLE></TD></TR><TR><TD COLSPAN="3" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD BGCOLOR="#3b2c12" ALIGN="CENTER" CELLPADDING="3" WIDTH="48"><FONT COLOR="#fbbf24" POINT-SIZE="8"><B>API</B></FONT></TD><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9" POINT-SIZE="10"><B>GET /api/ledger/history</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="8">Append-only ledger audit</FONT></TD></TR></TABLE></TD></TR></TABLE>>];
} }
subgraph cluster_system_ui { subgraph cluster_system_ui {
label="UI, Settings &amp; System"; labeljust="l"; fontsize=15; label="UI, Settings &amp; 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 -> 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 -> 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]; 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]; 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 -> 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 -> 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 -> 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 -> 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 -> artikelAutomat:in4:n [color="#c79bff", fontcolor="#c79bff", tooltip="sseAutomat → artikelAutomat ('article-&lt;sku&gt;' sse reload)", label=" 'article-&lt;sku&gt;' sse reload ", weight=3]; 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 -> articleDetailAutomat:in0:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="sseAutomat → articleDetailAutomat (invalidateArticle(sku))", label=" invalidateArticle(sku) ", constraint=false, weight=1]; sseAutomat:p_sse_events:w -> artikelAutomat:in4:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="sseAutomat → artikelAutomat ('article-&lt;sku&gt;' sse reload)", label=" 'article-&lt;sku&gt;' sse reload ", weight=3];
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 -> articleDetailAutomat:in0:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="sseAutomat → articleDetailAutomat (invalidateArticle(sku))", label=" invalidateArticle(sku) ", 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: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 -> 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: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:e -> attributeAutomat:in1:n [color="#58d18a", fontcolor="#58d18a", tooltip="sseAutomat → attributeAutomat ('attributename' sse)", label=" 'attributename' sse ", 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 -> propertyAutomat:in1:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="sseAutomat → propertyAutomat ('propertyname' sse)", label=" 'propertyname' sse ", 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 -> 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 -> propertyAutomat:in1:n [color="#a98cff", fontcolor="#a98cff", tooltip="sseAutomat → propertyAutomat ('propertyname' sse)", label=" 'propertyname' 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]; 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];
companyAutomat:p_comp_root:e -> categoryAutomat:in1:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="companyAutomat → categoryAutomat (rootCategoryId)", label=" rootCategoryId ", 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];
categoryAutomat:p_cat_loadTree:e -> categoryViewAutomat:in0:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="categoryAutomat → categoryViewAutomat (attachTree &amp; warm slugs)", label=" attachTree &amp; warm slugs ", weight=3]; companyAutomat:p_comp_root:e -> categoryAutomat:in1:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="companyAutomat → categoryAutomat (rootCategoryId)", label=" rootCategoryId ", constraint=false, weight=1];
categoryViewAutomat:p_catview_collect:e -> articleIndexAutomat:in1:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="categoryViewAutomat → articleIndexAutomat (derived union index)", label=" derived union index ", weight=3]; categoryAutomat:p_cat_loadTree:e -> categoryViewAutomat:in0:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="categoryAutomat → categoryViewAutomat (attachTree &amp; warm slugs)", label=" attachTree &amp; warm slugs ", 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]; categoryViewAutomat:p_catview_collect:e -> articleIndexAutomat:in1:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="categoryViewAutomat → articleIndexAutomat (derived union index)", label=" derived union index ", weight=3];
categoryAutomat:p_cat_categories:e -> articleSearchAutomat:in0:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="categoryAutomat → articleSearchAutomat (categoryFacets)", label=" categoryFacets ", constraint=false, weight=1]; articleIndexAutomat:p_idx_items:e -> articleSearchAutomat:in1:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="articleIndexAutomat → articleSearchAutomat (query index items)", label=" query index items ", weight=3];
artikelAutomat:p_art_setName:e -> articleIndexAutomat:in0:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="artikelAutomat → articleIndexAutomat (setArticleIndexName (optimistic))", label=" setArticleIndexName (optimistic) ", constraint=false, weight=1]; 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_set:e -> categoryAutomat:in3:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="artikelAutomat → categoryAutomat (articleCount updates)", label=" articleCount updates ", weight=3]; artikelAutomat:p_art_setName:e -> articleIndexAutomat:in0:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="artikelAutomat → articleIndexAutomat (setArticleIndexName (optimistic))", label=" setArticleIndexName (optimistic) ", constraint=false, weight=1];
articleDetailAutomat:p_artdet_hist:e -> artikelAutomat:in2:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="articleDetailAutomat → artikelAutomat (watch SKU details)", label=" watch SKU details ", weight=3]; artikelAutomat:p_art_set:e -> categoryAutomat:in3:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="artikelAutomat → categoryAutomat (articleCount updates)", label=" articleCount updates ", 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]; 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_lines:e -> artikelAutomat:in1:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="deliveryEditorAutomat → artikelAutomat (lookup SKU &amp; pack units)", label=" lookup SKU &amp; pack units ", constraint=false, weight=1]; 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_commit:e -> deliveryAutomat:in0:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="deliveryEditorAutomat → deliveryAutomat (commit delivery receipt)", label=" commit delivery receipt ", weight=3]; deliveryEditorAutomat:p_deled_lines:e -> artikelAutomat:in1:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="deliveryEditorAutomat → artikelAutomat (lookup SKU &amp; pack units)", label=" lookup SKU &amp; pack units ", constraint=false, weight=1];
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]; deliveryEditorAutomat:p_deled_commit:e -> deliveryAutomat:in0:n [color="#c79bff", fontcolor="#c79bff", tooltip="deliveryEditorAutomat → deliveryAutomat (commit delivery receipt)", label=" commit delivery receipt ", weight=3];
orderAutomat:p_order_items:e -> entityAutomat:in1:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="orderAutomat → entityAutomat (customer lookup)", label=" customer lookup ", 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];
companyAutomat:p_comp_curr:e -> customergroupAutomat:in0:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="companyAutomat → customergroupAutomat (poscustomergroup)", label=" poscustomergroup ", constraint=false, weight=1]; orderAutomat:p_order_items:e -> entityAutomat:in1:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="orderAutomat → entityAutomat (customer lookup)", label=" customer lookup ", weight=3];
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 -> customergroupAutomat:in0:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="companyAutomat → customergroupAutomat (poscustomergroup)", label=" poscustomergroup ", constraint=false, weight=1];
companyAutomat:p_comp_curr:e -> posterminalAutomat:in0:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="companyAutomat → posterminalAutomat (company context)", label=" company context ", weight=3]; posterminalAutomat:p_pos_term:e -> categoryAutomat:in2:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="posterminalAutomat → categoryAutomat (filter enabled roots)", label=" filter enabled roots ", 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]; 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_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_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_sse:e -> sseAutomat:in1:n [color="#a98cff", fontcolor="#a98cff", tooltip="chatAutomat → sseAutomat (getSseClientId())", label=" getSseClientId() ", weight=3]; 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];
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]; chatAutomat:p_chat_sse:e -> sseAutomat:in1:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="chatAutomat → sseAutomat (getSseClientId())", label=" getSseClientId() ", weight=3];
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_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_delpath:e -> deliveryEditorAutomat:in0:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="navigationAutomat → deliveryEditorAutomat (restore draft path)", label=" restore draft path ", weight=3]; 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];
userSettingsAutomat:p_usr_drawer:e -> chatAutomat:in0:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="userSettingsAutomat → chatAutomat (chatDrawerWidth)", label=" chatDrawerWidth ", weight=3]; navigationAutomat:p_nav_delpath:e -> deliveryEditorAutomat:in0:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="navigationAutomat → deliveryEditorAutomat (restore draft path)", label=" restore draft path ", 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]; userSettingsAutomat:p_usr_drawer:e -> chatAutomat:in0:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="userSettingsAutomat → chatAutomat (chatDrawerWidth)", label=" chatDrawerWidth ", 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]; helpAutomat:p_help_active:e -> navigationAutomat:in0:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="helpAutomat → navigationAutomat (route-contextual help)", label=" route-contextual help ", weight=3];
artikelAutomat:p_art_set:ne -> artikelAutomat:in3:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="artikelAutomat → artikelAutomat (variants &amp; parent SKU)", label=" variants &amp; parent SKU ", 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];
chatAutomat:p_chat_msg:nw -> chatAutomat:in2:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="chatAutomat → chatAutomat (stream delta appends)", label=" stream delta appends ", weight=3]; artikelAutomat:p_art_set:ne -> artikelAutomat:in3:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="artikelAutomat → artikelAutomat (variants &amp; parent SKU)", label=" variants &amp; 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];
} }

View File

@@ -4,11 +4,11 @@
<!-- Generated by graphviz version 16.1.0 (0) <!-- Generated by graphviz version 16.1.0 (0)
--> -->
<!-- Title: automats Pages: 1 --> <!-- Title: automats Pages: 1 -->
<svg width="8568pt" height="2129pt" <svg width="8916pt" height="2129pt"
viewBox="0.00 0.00 8568.00 2129.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="background-color:#1e2430;"> viewBox="0.00 0.00 8916.00 2129.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="background-color:#1e2430;">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(36 2092.9)"> <g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(36 2092.9)">
<title>automats</title> <title>automats</title>
<polygon fill="#1e2430" stroke="none" points="-36,36 -36,-2092.9 8531.76,-2092.9 8531.76,36 -36,36"/> <polygon fill="#1e2430" stroke="none" points="-36,36 -36,-2092.9 8879.76,-2092.9 8879.76,36 -36,36"/>
<g id="clust1" class="cluster"> <g id="clust1" class="cluster">
<title>cluster_session_realtime</title> <title>cluster_session_realtime</title>
<path fill="#202a3a" stroke="#3f4c5f" d="M4581.76,-1554C4581.76,-1554 5270.76,-1554 5270.76,-1554 5276.76,-1554 5282.76,-1560 5282.76,-1566 5282.76,-1566 5282.76,-1863 5282.76,-1863 5282.76,-1869 5276.76,-1875 5270.76,-1875 5270.76,-1875 4581.76,-1875 4581.76,-1875 4575.76,-1875 4569.76,-1869 4569.76,-1863 4569.76,-1863 4569.76,-1566 4569.76,-1566 4569.76,-1560 4575.76,-1554 4581.76,-1554"/> <path fill="#202a3a" stroke="#3f4c5f" d="M4581.76,-1554C4581.76,-1554 5270.76,-1554 5270.76,-1554 5276.76,-1554 5282.76,-1560 5282.76,-1566 5282.76,-1566 5282.76,-1863 5282.76,-1863 5282.76,-1869 5276.76,-1875 5270.76,-1875 5270.76,-1875 4581.76,-1875 4581.76,-1875 4575.76,-1875 4569.76,-1869 4569.76,-1863 4569.76,-1863 4569.76,-1566 4569.76,-1566 4569.76,-1560 4575.76,-1554 4581.76,-1554"/>
@@ -61,7 +61,7 @@
</g> </g>
<g id="clust11" class="cluster"> <g id="clust11" class="cluster">
<title>cluster_audit_grids</title> <title>cluster_audit_grids</title>
<path fill="#202a3a" stroke="#3f4c5f" d="M7873.76,-1609C7873.76,-1609 8475.76,-1609 8475.76,-1609 8481.76,-1609 8487.76,-1615 8487.76,-1621 8487.76,-1621 8487.76,-1808 8487.76,-1808 8487.76,-1814 8481.76,-1820 8475.76,-1820 8475.76,-1820 7873.76,-1820 7873.76,-1820 7867.76,-1820 7861.76,-1814 7861.76,-1808 7861.76,-1808 7861.76,-1621 7861.76,-1621 7861.76,-1615 7867.76,-1609 7873.76,-1609"/> <path fill="#202a3a" stroke="#3f4c5f" d="M7873.76,-1609C7873.76,-1609 8823.76,-1609 8823.76,-1609 8829.76,-1609 8835.76,-1615 8835.76,-1621 8835.76,-1621 8835.76,-1808 8835.76,-1808 8835.76,-1814 8829.76,-1820 8823.76,-1820 8823.76,-1820 7873.76,-1820 7873.76,-1820 7867.76,-1820 7861.76,-1814 7861.76,-1808 7861.76,-1808 7861.76,-1621 7861.76,-1621 7861.76,-1615 7867.76,-1609 7873.76,-1609"/>
<text xml:space="preserve" text-anchor="middle" x="7937.28" y="-1802.5" font-family="Helvetica,sans-Serif" font-size="15.00" fill="#e2e8f0">Audit &amp; Virtual Grids</text> <text xml:space="preserve" text-anchor="middle" x="7937.28" y="-1802.5" font-family="Helvetica,sans-Serif" font-size="15.00" fill="#e2e8f0">Audit &amp; Virtual Grids</text>
</g> </g>
<g id="clust12" class="cluster"> <g id="clust12" class="cluster">
@@ -344,7 +344,7 @@
<polygon fill="#ff7fc2" stroke="#ff7fc2" stroke-width="1.4" points="3466.8,-1791.03 3464.11,-1784.12 3461.9,-1791.2 3466.8,-1791.03"/> <polygon fill="#ff7fc2" stroke="#ff7fc2" stroke-width="1.4" points="3466.8,-1791.03 3464.11,-1784.12 3461.9,-1791.2 3466.8,-1791.03"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="4040.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff7fc2"> &#160;sync brands on authed &#160;</text> <text xml:space="preserve" text-anchor="middle" x="4129.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff7fc2"> &#160;sync brands on authed &#160;</text>
</g> </g>
<!-- unitAutomat --> <!-- unitAutomat -->
<g id="node8" class="node"> <g id="node8" class="node">
@@ -923,7 +923,7 @@
<polygon fill="#6bdf9a" stroke="#6bdf9a" stroke-width="1.4" points="2041.55,-1780.02 2038.84,-1773.12 2036.65,-1780.2 2041.55,-1780.02"/> <polygon fill="#6bdf9a" stroke="#6bdf9a" stroke-width="1.4" points="2041.55,-1780.02 2038.84,-1773.12 2036.65,-1780.2 2041.55,-1780.02"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="3116.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#6bdf9a"> &#160;load company profile &#160;</text> <text xml:space="preserve" text-anchor="middle" x="3449.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#6bdf9a"> &#160;load company profile &#160;</text>
</g> </g>
<!-- accountAutomat --> <!-- accountAutomat -->
<g id="node22" class="node"> <g id="node22" class="node">
@@ -960,7 +960,7 @@
<polygon fill="#56c8c8" stroke="#56c8c8" stroke-width="1.4" points="2390.49,-1747.04 2387.82,-1740.12 2385.59,-1747.19 2390.49,-1747.04"/> <polygon fill="#56c8c8" stroke="#56c8c8" stroke-width="1.4" points="2390.49,-1747.04 2387.82,-1740.12 2385.59,-1747.19 2390.49,-1747.04"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="2885.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#56c8c8"> &#160;sync payment accounts &#160;</text> <text xml:space="preserve" text-anchor="middle" x="3620.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#56c8c8"> &#160;sync payment accounts &#160;</text>
</g> </g>
<!-- numberAutomat --> <!-- numberAutomat -->
<g id="node23" class="node"> <g id="node23" class="node">
@@ -997,7 +997,7 @@
<polygon fill="#e0b25a" stroke="#e0b25a" stroke-width="1.4" points="2713.45,-1747.05 2710.81,-1740.12 2708.55,-1747.18 2713.45,-1747.05"/> <polygon fill="#e0b25a" stroke="#e0b25a" stroke-width="1.4" points="2713.45,-1747.05 2710.81,-1740.12 2708.55,-1747.18 2713.45,-1747.05"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="4409.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#e0b25a"> &#160;sync number series &#160;</text> <text xml:space="preserve" text-anchor="middle" x="3220.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#e0b25a"> &#160;sync number series &#160;</text>
</g> </g>
<!-- posterminalAutomat --> <!-- posterminalAutomat -->
<g id="node24" class="node"> <g id="node24" class="node">
@@ -1234,7 +1234,7 @@
<polygon fill="#66b3ff" stroke="#66b3ff" stroke-width="1.4" points="8009.89,-1758.2 8007.68,-1751.12 8004.99,-1758.03 8009.89,-1758.2"/> <polygon fill="#66b3ff" stroke="#66b3ff" stroke-width="1.4" points="8009.89,-1758.2 8007.68,-1751.12 8004.99,-1758.03 8009.89,-1758.2"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="5131.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#66b3ff"> &#160;reload history on authed &#160;</text> <text xml:space="preserve" text-anchor="middle" x="7317.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#66b3ff"> &#160;reload history on authed &#160;</text>
</g> </g>
<!-- globalHistoryGridAutomat --> <!-- globalHistoryGridAutomat -->
<g id="node28" class="node"> <g id="node28" class="node">
@@ -1277,17 +1277,60 @@
<polygon fill="#5aa7ff" stroke="#5aa7ff" stroke-width="1.4" points="8342.89,-1758.2 8340.68,-1751.12 8337.99,-1758.03 8342.89,-1758.2"/> <polygon fill="#5aa7ff" stroke="#5aa7ff" stroke-width="1.4" points="8342.89,-1758.2 8340.68,-1751.12 8337.99,-1758.03 8342.89,-1758.2"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="7483.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#5aa7ff"> &#160;reload audit on authed &#160;</text> <text xml:space="preserve" text-anchor="middle" x="5131.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#5aa7ff"> &#160;reload audit on authed &#160;</text>
</g> </g>
<!-- sseAutomat&#45;&gt;categoryAutomat --> <!-- ledgerHistoryGridAutomat -->
<g id="node29" class="node">
<title>ledgerHistoryGridAutomat</title>
<polygon fill="#33415c" stroke="none" points="8549.55,-1715 8549.55,-1750 8813.97,-1750 8813.97,-1715 8549.55,-1715"/>
<polygon fill="none" stroke="#3f4c5f" points="8549.55,-1715 8549.55,-1750 8813.97,-1750 8813.97,-1715 8549.55,-1715"/>
<polygon fill="#33415c" stroke="none" points="8551.55,-1744 8551.55,-1748 8811.97,-1748 8811.97,-1744 8551.55,-1744"/>
<text xml:space="preserve" text-anchor="start" x="8606.41" y="-1731.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00" fill="#e2e8f0">ledgerHistoryGridAutomat</text>
<text xml:space="preserve" text-anchor="start" x="8625.73" y="-1721.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">createGridAutomat (Ledger)</text>
<polygon fill="#2a3242" stroke="none" points="8549.55,-1693 8549.55,-1715 8813.97,-1715 8813.97,-1693 8549.55,-1693"/>
<polygon fill="none" stroke="#3f4c5f" points="8549.55,-1693 8549.55,-1715 8813.97,-1715 8813.97,-1693 8549.55,-1693"/>
<polygon fill="#2b1a42" stroke="none" points="8550.55,-1694 8550.55,-1714 8604.01,-1714 8604.01,-1694 8550.55,-1694"/>
<text xml:space="preserve" text-anchor="start" x="8564.17" y="-1702.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="8.00" fill="#c084fc">STATE</text>
<text xml:space="preserve" text-anchor="start" x="8608.01" y="-1702" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="#d8dee9">rows: [] / totalRows</text>
<text xml:space="preserve" text-anchor="start" x="8719.16" y="-1700.8" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Block size 50 virtual rows</text>
<polygon fill="#272e3d" stroke="none" points="8549.55,-1671 8549.55,-1693 8813.97,-1693 8813.97,-1671 8549.55,-1671"/>
<polygon fill="none" stroke="#3f4c5f" points="8549.55,-1671 8549.55,-1693 8813.97,-1693 8813.97,-1671 8549.55,-1671"/>
<polygon fill="#143826" stroke="none" points="8550.55,-1672 8550.55,-1692 8609.67,-1692 8609.67,-1672 8550.55,-1672"/>
<text xml:space="preserve" text-anchor="start" x="8564.78" y="-1680.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="8.00" fill="#4ade80">ACTION</text>
<text xml:space="preserve" text-anchor="start" x="8613.67" y="-1680" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="#d8dee9">fetchRows(viewport)</text>
<text xml:space="preserve" text-anchor="start" x="8742.27" y="-1678.8" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Infinite scroll query</text>
<polygon fill="#2a3242" stroke="none" points="8549.55,-1649 8549.55,-1671 8813.97,-1671 8813.97,-1649 8549.55,-1649"/>
<polygon fill="none" stroke="#3f4c5f" points="8549.55,-1649 8549.55,-1671 8813.97,-1671 8813.97,-1649 8549.55,-1649"/>
<polygon fill="#143826" stroke="none" points="8550.55,-1650 8550.55,-1670 8620.59,-1670 8620.59,-1650 8550.55,-1650"/>
<text xml:space="preserve" text-anchor="start" x="8570.24" y="-1658.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="8.00" fill="#4ade80">ACTION</text>
<text xml:space="preserve" text-anchor="start" x="8624.59" y="-1658" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="#d8dee9">reload()</text>
<text xml:space="preserve" text-anchor="start" x="8713.36" y="-1656.8" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Reload on company switch</text>
<polygon fill="#272e3d" stroke="none" points="8549.55,-1627 8549.55,-1649 8813.97,-1649 8813.97,-1627 8549.55,-1627"/>
<polygon fill="none" stroke="#3f4c5f" points="8549.55,-1627 8549.55,-1649 8813.97,-1649 8813.97,-1627 8549.55,-1627"/>
<polygon fill="#3b2c12" stroke="none" points="8550.55,-1628 8550.55,-1648 8598.55,-1648 8598.55,-1628 8550.55,-1628"/>
<text xml:space="preserve" text-anchor="start" x="8567.88" y="-1636.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="8.00" fill="#fbbf24">API</text>
<text xml:space="preserve" text-anchor="start" x="8602.55" y="-1636" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="#d8dee9">GET /api/ledger/history</text>
<text xml:space="preserve" text-anchor="start" x="8720.01" y="-1634.8" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Append&#45;only ledger audit</text>
</g>
<!-- authAutomat&#45;&gt;ledgerHistoryGridAutomat -->
<g id="edge22" class="edge"> <g id="edge22" class="edge">
<title>sseAutomat:w&#45;&gt;categoryAutomat:n</title> <title>authAutomat:e&#45;&gt;ledgerHistoryGridAutomat:n</title>
<g id="a_edge22"><a xlink:title="sseAutomat → categoryAutomat (&#39;category&#39; sse trigger)"> <g id="a_edge22"><a xlink:title="authAutomat → ledgerHistoryGridAutomat (reload ledger on authed)">
<path fill="none" stroke="#6bdf9a" stroke-width="1.4" d="M5021.83,-1627C4957.85,-1627 4959.41,-1571.11 4897.76,-1554 4836.57,-1537.02 2675.22,-1534.09 2611.76,-1532 2526.33,-1529.19 2505.17,-1524.18 2419.76,-1521.2 2411.62,-1520.92 1839,-1519 1833.28,-1513.2 1733.09,-1411.63 1740.3,-1300.41 1833.28,-1192.2 1864.85,-1155.46 2000.51,-1163.72 2048.76,-1159.4 2116.59,-1153.32 3232.54,-1161.48 3315.27,-1098.95"/> <path fill="none" stroke="#6bdf9a" stroke-width="1.4" d="M4857.07,-1629C4970.33,-1629 4883.15,-1794.36 4980.76,-1851.82 5065.19,-1901.52 8413.25,-1877.65 8507.76,-1851.82 8591.36,-1828.96 8675.62,-1838.12 8681.44,-1758.11"/>
<polygon fill="#6bdf9a" stroke="#6bdf9a" stroke-width="1.4" points="3317.26,-1100.4 3318.97,-1093.18 3313.13,-1097.75 3317.26,-1100.4"/> <polygon fill="#6bdf9a" stroke="#6bdf9a" stroke-width="1.4" points="8683.88,-1758.2 8681.68,-1751.12 8678.99,-1758.03 8683.88,-1758.2"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="1879.52" y="-1324" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#6bdf9a"> &#160;&#39;category&#39; sse trigger &#160;</text> <text xml:space="preserve" text-anchor="middle" x="7924.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#6bdf9a"> &#160;reload ledger on authed &#160;</text>
</g>
<!-- sseAutomat&#45;&gt;categoryAutomat -->
<g id="edge23" class="edge">
<title>sseAutomat:w&#45;&gt;categoryAutomat:n</title>
<g id="a_edge23"><a xlink:title="sseAutomat → categoryAutomat (&#39;category&#39; sse trigger)">
<path fill="none" stroke="#f5c15c" stroke-width="1.4" d="M5021.83,-1627C4957.85,-1627 4959.41,-1571.11 4897.76,-1554 4836.57,-1537.02 2675.22,-1534.09 2611.76,-1532 2526.33,-1529.19 2505.17,-1524.18 2419.76,-1521.2 2411.62,-1520.92 1839,-1519 1833.28,-1513.2 1733.09,-1411.63 1740.3,-1300.41 1833.28,-1192.2 1864.85,-1155.46 2000.51,-1163.72 2048.76,-1159.4 2116.59,-1153.32 3232.54,-1161.48 3315.27,-1098.95"/>
<polygon fill="#f5c15c" stroke="#f5c15c" stroke-width="1.4" points="3317.26,-1100.4 3318.97,-1093.18 3313.13,-1097.75 3317.26,-1100.4"/>
</a>
</g>
<text xml:space="preserve" text-anchor="middle" x="1879.52" y="-1324" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#f5c15c"> &#160;&#39;category&#39; sse trigger &#160;</text>
</g> </g>
<!-- categoryViewAutomat --> <!-- categoryViewAutomat -->
<g id="node4" class="node"> <g id="node4" class="node">
@@ -1342,14 +1385,14 @@
<text xml:space="preserve" text-anchor="start" x="3477.13" y="-587.4" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Category article batch</text> <text xml:space="preserve" text-anchor="start" x="3477.13" y="-587.4" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Category article batch</text>
</g> </g>
<!-- sseAutomat&#45;&gt;categoryViewAutomat --> <!-- sseAutomat&#45;&gt;categoryViewAutomat -->
<g id="edge23" class="edge"> <g id="edge24" class="edge">
<title>sseAutomat:w&#45;&gt;categoryViewAutomat:n</title> <title>sseAutomat:w&#45;&gt;categoryViewAutomat:n</title>
<g id="a_edge23"><a xlink:title="sseAutomat → categoryViewAutomat (invalidate views on sse)"> <g id="a_edge24"><a xlink:title="sseAutomat → categoryViewAutomat (invalidate views on sse)">
<path fill="none" stroke="#f5c15c" stroke-width="1.4" d="M5021.83,-1627C4957.85,-1627 4959.11,-1572.13 4897.76,-1554 4826.13,-1532.83 4285.83,-1558.91 4226.76,-1513.2 4105.67,-1419.5 4227.48,-1279.58 4101.76,-1192.2 4036.1,-1146.57 2718.81,-1227.17 2662.71,-1170.2 2547.14,-1052.85 2617.73,-914.64 2743.76,-808.6 2804.56,-757.44 3435.85,-843.3 3482.62,-776.07"/> <path fill="none" stroke="#ff7fc2" stroke-width="1.4" d="M5021.83,-1627C4957.85,-1627 4959.11,-1572.13 4897.76,-1554 4826.13,-1532.83 4285.83,-1558.91 4226.76,-1513.2 4105.67,-1419.5 4227.48,-1279.58 4101.76,-1192.2 4036.1,-1146.57 2718.81,-1227.17 2662.71,-1170.2 2547.14,-1052.85 2617.73,-914.64 2743.76,-808.6 2804.56,-757.44 3435.85,-843.3 3482.62,-776.07"/>
<polygon fill="#f5c15c" stroke="#f5c15c" stroke-width="1.4" points="3484.88,-777.03 3484.7,-769.62 3480.22,-775.52 3484.88,-777.03"/> <polygon fill="#ff7fc2" stroke="#ff7fc2" stroke-width="1.4" points="3484.88,-777.03 3484.7,-769.62 3480.22,-775.52 3484.88,-777.03"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="2713.73" y="-1162.1" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#f5c15c"> &#160;invalidate views on sse &#160;</text> <text xml:space="preserve" text-anchor="middle" x="2713.73" y="-1162.1" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff7fc2"> &#160;invalidate views on sse &#160;</text>
</g> </g>
<!-- artikelAutomat --> <!-- artikelAutomat -->
<g id="node5" class="node"> <g id="node5" class="node">
@@ -1413,14 +1456,14 @@
<text xml:space="preserve" text-anchor="start" x="3371.78" y="-1229" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Full article document</text> <text xml:space="preserve" text-anchor="start" x="3371.78" y="-1229" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Full article document</text>
</g> </g>
<!-- sseAutomat&#45;&gt;artikelAutomat --> <!-- sseAutomat&#45;&gt;artikelAutomat -->
<g id="edge25" class="edge"> <g id="edge26" class="edge">
<title>sseAutomat:w&#45;&gt;artikelAutomat:n</title> <title>sseAutomat:w&#45;&gt;artikelAutomat:n</title>
<g id="a_edge25"><a xlink:title="sseAutomat → artikelAutomat (&#39;article&#45;&lt;sku&gt;&#39; sse reload)"> <g id="a_edge26"><a xlink:title="sseAutomat → artikelAutomat (&#39;article&#45;&lt;sku&gt;&#39; sse reload)">
<path fill="none" stroke="#c79bff" stroke-width="1.4" d="M5021.83,-1627C4957.85,-1627 4958.91,-1572.82 4897.76,-1554 4743.29,-1506.46 3469.39,-1592.57 3422.17,-1440.11"/> <path fill="none" stroke="#5cd6d6" stroke-width="1.4" d="M5021.83,-1627C4957.85,-1627 4958.91,-1572.82 4897.76,-1554 4743.29,-1506.46 3469.39,-1592.57 3422.17,-1440.11"/>
<polygon fill="#c79bff" stroke="#c79bff" stroke-width="1.4" points="3424.61,-1439.85 3421.14,-1433.3 3419.76,-1440.58 3424.61,-1439.85"/> <polygon fill="#5cd6d6" stroke="#5cd6d6" stroke-width="1.4" points="3424.61,-1439.85 3421.14,-1433.3 3419.76,-1440.58 3424.61,-1439.85"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="3949.87" y="-1523.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#c79bff"> &#160;&#39;article&#45;&lt;sku&gt;&#39; sse reload &#160;</text> <text xml:space="preserve" text-anchor="middle" x="3949.87" y="-1523.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#5cd6d6"> &#160;&#39;article&#45;&lt;sku&gt;&#39; sse reload &#160;</text>
</g> </g>
<!-- articleDetailAutomat --> <!-- articleDetailAutomat -->
<g id="node6" class="node"> <g id="node6" class="node">
@@ -1468,24 +1511,24 @@
<text xml:space="preserve" text-anchor="start" x="3229.75" y="-1612.8" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Vendor links &amp; prices</text> <text xml:space="preserve" text-anchor="start" x="3229.75" y="-1612.8" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Vendor links &amp; prices</text>
</g> </g>
<!-- sseAutomat&#45;&gt;articleDetailAutomat --> <!-- sseAutomat&#45;&gt;articleDetailAutomat -->
<g id="edge26" class="edge"> <g id="edge27" class="edge">
<title>sseAutomat:w&#45;&gt;articleDetailAutomat:n</title> <title>sseAutomat:w&#45;&gt;articleDetailAutomat:n</title>
<g id="a_edge26"><a xlink:title="sseAutomat → articleDetailAutomat (invalidateArticle(sku))"> <g id="a_edge27"><a xlink:title="sseAutomat → articleDetailAutomat (invalidateArticle(sku))">
<path fill="none" stroke="#5cd6d6" stroke-width="1.4" d="M5021.83,-1627C4907.71,-1627 4996.04,-1793.8 4897.76,-1851.82 4823.84,-1895.45 3436.78,-1869.37 3352.76,-1851.82 3268.01,-1834.11 3174.58,-1858.87 3168.11,-1780.04"/> <path fill="none" stroke="#ff9f5a" stroke-width="1.4" d="M5021.83,-1627C4907.71,-1627 4996.04,-1793.8 4897.76,-1851.82 4823.84,-1895.45 3436.78,-1869.37 3352.76,-1851.82 3268.01,-1834.11 3174.58,-1858.87 3168.11,-1780.04"/>
<polygon fill="#5cd6d6" stroke="#5cd6d6" stroke-width="1.4" points="3170.56,-1780.02 3167.84,-1773.12 3165.67,-1780.21 3170.56,-1780.02"/> <polygon fill="#ff9f5a" stroke="#ff9f5a" stroke-width="1.4" points="3170.56,-1780.02 3167.84,-1773.12 3165.67,-1780.21 3170.56,-1780.02"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="3562.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#5cd6d6"> &#160;invalidateArticle(sku) &#160;</text> <text xml:space="preserve" text-anchor="middle" x="4467.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff9f5a"> &#160;invalidateArticle(sku) &#160;</text>
</g> </g>
<!-- sseAutomat&#45;&gt;brandAutomat --> <!-- sseAutomat&#45;&gt;brandAutomat -->
<g id="edge29" class="edge"> <g id="edge30" class="edge">
<title>sseAutomat:w&#45;&gt;brandAutomat:n</title> <title>sseAutomat:w&#45;&gt;brandAutomat:n</title>
<g id="a_edge29"><a xlink:title="sseAutomat → brandAutomat (&#39;brand&#39; sse trigger)"> <g id="a_edge30"><a xlink:title="sseAutomat → brandAutomat (&#39;brand&#39; sse trigger)">
<path fill="none" stroke="#ff6b6b" stroke-width="1.4" d="M5021.83,-1627C4907.71,-1627 4996.04,-1793.8 4897.76,-1851.82 4784.18,-1918.86 3837.5,-1888.28 3710.76,-1851.82 3657.71,-1836.56 3605.51,-1839.94 3599.95,-1791.03"/> <path fill="none" stroke="#58d18a" stroke-width="1.4" d="M5021.83,-1627C4907.71,-1627 4996.04,-1793.8 4897.76,-1851.82 4784.18,-1918.86 3837.5,-1888.28 3710.76,-1851.82 3657.71,-1836.56 3605.51,-1839.94 3599.95,-1791.03"/>
<polygon fill="#ff6b6b" stroke="#ff6b6b" stroke-width="1.4" points="3602.41,-1790.97 3599.58,-1784.12 3597.51,-1791.24 3602.41,-1790.97"/> <polygon fill="#58d18a" stroke="#58d18a" stroke-width="1.4" points="3602.41,-1790.97 3599.58,-1784.12 3597.51,-1791.24 3602.41,-1790.97"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="4258.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff6b6b"> &#160;&#39;brand&#39; sse trigger &#160;</text> <text xml:space="preserve" text-anchor="middle" x="4677.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#58d18a"> &#160;&#39;brand&#39; sse trigger &#160;</text>
</g> </g>
<!-- articleIndexAutomat --> <!-- articleIndexAutomat -->
<g id="node9" class="node"> <g id="node9" class="node">
@@ -1541,94 +1584,94 @@
<text xml:space="preserve" text-anchor="start" x="3636.84" y="-269.6" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Await warm views</text> <text xml:space="preserve" text-anchor="start" x="3636.84" y="-269.6" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Await warm views</text>
</g> </g>
<!-- sseAutomat&#45;&gt;articleIndexAutomat --> <!-- sseAutomat&#45;&gt;articleIndexAutomat -->
<g id="edge24" class="edge"> <g id="edge25" class="edge">
<title>sseAutomat:w&#45;&gt;articleIndexAutomat:n</title> <title>sseAutomat:w&#45;&gt;articleIndexAutomat:n</title>
<g id="a_edge24"><a xlink:title="sseAutomat → articleIndexAutomat (&#39;articles&#39; sse trigger)"> <g id="a_edge25"><a xlink:title="sseAutomat → articleIndexAutomat (&#39;articles&#39; sse trigger)">
<path fill="none" stroke="#ff7fc2" stroke-width="1.4" d="M5021.83,-1627C4957.85,-1627 4959.2,-1571.83 4897.76,-1554 4811.32,-1528.92 4155.67,-1573.39 4088.76,-1513.2 3979.6,-1415.02 4125.45,-1283.86 4010.76,-1192.2 3980.92,-1168.35 2673.84,-1173.14 2635.76,-1170.2 2220.06,-1138.15 1208.52,-859.87 794.76,-808.6 753.58,-803.5 77.38,-802.64 48.76,-772.6 -15.94,-704.71 -16.52,-628.93 48.76,-561.6 186.25,-419.77 3554.66,-643.54 3653.32,-458.33"/> <path fill="none" stroke="#c79bff" stroke-width="1.4" d="M5021.83,-1627C4957.85,-1627 4959.2,-1571.83 4897.76,-1554 4811.32,-1528.92 4155.67,-1573.39 4088.76,-1513.2 3979.6,-1415.02 4125.45,-1283.86 4010.76,-1192.2 3980.92,-1168.35 2673.84,-1173.14 2635.76,-1170.2 2220.06,-1138.15 1208.52,-859.87 794.76,-808.6 753.58,-803.5 77.38,-802.64 48.76,-772.6 -15.94,-704.71 -16.52,-628.93 48.76,-561.6 186.25,-419.77 3554.66,-643.54 3653.32,-458.33"/>
<polygon fill="#ff7fc2" stroke="#ff7fc2" stroke-width="1.4" points="3655.61,-459.24 3654.99,-451.85 3650.87,-458.01 3655.61,-459.24"/> <polygon fill="#c79bff" stroke="#c79bff" stroke-width="1.4" points="3655.61,-459.24 3654.99,-451.85 3650.87,-458.01 3655.61,-459.24"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="2519.99" y="-973.2" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff7fc2"> &#160;&#39;articles&#39; sse trigger &#160;</text> <text xml:space="preserve" text-anchor="middle" x="2519.99" y="-973.2" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#c79bff"> &#160;&#39;articles&#39; sse trigger &#160;</text>
</g> </g>
<!-- sseAutomat&#45;&gt;propertyAutomat --> <!-- sseAutomat&#45;&gt;propertyAutomat -->
<g id="edge31" class="edge"> <g id="edge32" class="edge">
<title>sseAutomat:e&#45;&gt;propertyAutomat:n</title> <title>sseAutomat:e&#45;&gt;propertyAutomat:n</title>
<g id="a_edge31"><a xlink:title="sseAutomat → propertyAutomat (&#39;propertyname&#39; sse)"> <g id="a_edge32"><a xlink:title="sseAutomat → propertyAutomat (&#39;propertyname&#39; sse)">
<path fill="none" stroke="#4fc4e8" stroke-width="1.4" d="M5261.68,-1627C5646.9,-1627 5716.85,-1801.42 6098.76,-1851.82 6378.99,-1888.79 6455.2,-1893.64 6734.76,-1851.82 6853.04,-1834.12 6991.55,-1890.72 6998.5,-1780"/> <path fill="none" stroke="#a98cff" stroke-width="1.4" d="M5261.68,-1627C5646.9,-1627 5716.85,-1801.42 6098.76,-1851.82 6378.99,-1888.79 6455.2,-1893.64 6734.76,-1851.82 6853.04,-1834.12 6991.55,-1890.72 6998.5,-1780"/>
<polygon fill="#4fc4e8" stroke="#4fc4e8" stroke-width="1.4" points="7000.95,-1780.19 6998.71,-1773.12 6996.05,-1780.04 7000.95,-1780.19"/> <polygon fill="#a98cff" stroke="#a98cff" stroke-width="1.4" points="7000.95,-1780.19 6998.71,-1773.12 6996.05,-1780.04 7000.95,-1780.19"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="5681.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#4fc4e8"> &#160;&#39;propertyname&#39; sse &#160;</text> <text xml:space="preserve" text-anchor="middle" x="5681.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#a98cff"> &#160;&#39;propertyname&#39; sse &#160;</text>
</g> </g>
<!-- sseAutomat&#45;&gt;attributeAutomat --> <!-- sseAutomat&#45;&gt;attributeAutomat -->
<g id="edge30" class="edge"> <g id="edge31" class="edge">
<title>sseAutomat:e&#45;&gt;attributeAutomat:n</title> <title>sseAutomat:e&#45;&gt;attributeAutomat:n</title>
<g id="a_edge30"><a xlink:title="sseAutomat → attributeAutomat (&#39;attributename&#39; sse)"> <g id="a_edge31"><a xlink:title="sseAutomat → attributeAutomat (&#39;attributename&#39; sse)">
<path fill="none" stroke="#58d18a" stroke-width="1.4" d="M5261.68,-1627C5646.9,-1627 5716.85,-1801.42 6098.76,-1851.82 6322.59,-1881.35 6891.51,-1885.55 7114.76,-1851.82 7231.91,-1834.11 7368.92,-1889.63 7375.8,-1779.92"/> <path fill="none" stroke="#4fc4e8" stroke-width="1.4" d="M5261.68,-1627C5646.9,-1627 5716.85,-1801.42 6098.76,-1851.82 6322.59,-1881.35 6891.51,-1885.55 7114.76,-1851.82 7231.91,-1834.11 7368.92,-1889.63 7375.8,-1779.92"/>
<polygon fill="#58d18a" stroke="#58d18a" stroke-width="1.4" points="7378.25,-1780.19 7376.01,-1773.12 7373.35,-1780.04 7378.25,-1780.19"/> <polygon fill="#4fc4e8" stroke="#4fc4e8" stroke-width="1.4" points="7378.25,-1780.19 7376.01,-1773.12 7373.35,-1780.04 7378.25,-1780.19"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="5591.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#58d18a"> &#160;&#39;attributename&#39; sse &#160;</text> <text xml:space="preserve" text-anchor="middle" x="5591.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#4fc4e8"> &#160;&#39;attributename&#39; sse &#160;</text>
</g> </g>
<!-- sseAutomat&#45;&gt;supplierAutomat --> <!-- sseAutomat&#45;&gt;supplierAutomat -->
<g id="edge28" class="edge"> <g id="edge29" class="edge">
<title>sseAutomat:w&#45;&gt;supplierAutomat:n</title> <title>sseAutomat:w&#45;&gt;supplierAutomat:n</title>
<g id="a_edge28"><a xlink:title="sseAutomat → supplierAutomat (&#39;supplier&#39; sse trigger)"> <g id="a_edge29"><a xlink:title="sseAutomat → supplierAutomat (&#39;supplier&#39; sse trigger)">
<path fill="none" stroke="#8f95ff" stroke-width="1.4" d="M5021.83,-1627C4957.85,-1627 4959.1,-1572.19 4897.76,-1554 4845.19,-1538.42 4442.65,-1571.63 4404.76,-1532 4401.44,-1528.53 4402.29,-1525.32 4404.76,-1521.2 4408.64,-1514.72 4415.87,-1519.68 4419.76,-1513.2 4493.16,-1390.86 4519.86,-1293.86 4419.76,-1192.2 4391.96,-1163.97 3033.28,-1172.84 2993.76,-1170.2 2953.12,-1167.48 2943.41,-1161.87 2902.76,-1159.4 2868.64,-1157.33 474.92,-1159.69 441.76,-1151.4 359.03,-1130.72 272.31,-1145.64 266.32,-1067.34"/> <path fill="none" stroke="#ff6b6b" stroke-width="1.4" d="M5021.83,-1627C4957.85,-1627 4959.1,-1572.19 4897.76,-1554 4845.19,-1538.42 4442.65,-1571.63 4404.76,-1532 4401.44,-1528.53 4402.29,-1525.32 4404.76,-1521.2 4408.64,-1514.72 4415.87,-1519.68 4419.76,-1513.2 4493.16,-1390.86 4519.86,-1293.86 4419.76,-1192.2 4391.96,-1163.97 3033.28,-1172.84 2993.76,-1170.2 2953.12,-1167.48 2943.41,-1161.87 2902.76,-1159.4 2868.64,-1157.33 474.92,-1159.69 441.76,-1151.4 359.03,-1130.72 272.31,-1145.64 266.32,-1067.34"/>
<polygon fill="#8f95ff" stroke="#8f95ff" stroke-width="1.4" points="268.77,-1067.42 266.07,-1060.52 263.88,-1067.6 268.77,-1067.42"/> <polygon fill="#ff6b6b" stroke="#ff6b6b" stroke-width="1.4" points="268.77,-1067.42 266.07,-1060.52 263.88,-1067.6 268.77,-1067.42"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="4529.93" y="-1324" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8f95ff"> &#160;&#39;supplier&#39; sse trigger &#160;</text> <text xml:space="preserve" text-anchor="middle" x="4529.93" y="-1324" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff6b6b"> &#160;&#39;supplier&#39; sse trigger &#160;</text>
</g> </g>
<!-- sseAutomat&#45;&gt;orderAutomat --> <!-- sseAutomat&#45;&gt;orderAutomat -->
<g id="edge32" class="edge"> <g id="edge33" class="edge">
<title>sseAutomat:e&#45;&gt;orderAutomat:n</title> <title>sseAutomat:e&#45;&gt;orderAutomat:n</title>
<g id="a_edge32"><a xlink:title="sseAutomat → orderAutomat (&#39;order&#39;/&#39;shipment&#39; sse)"> <g id="a_edge33"><a xlink:title="sseAutomat → orderAutomat (&#39;order&#39;/&#39;shipment&#39; sse)">
<path fill="none" stroke="#a98cff" stroke-width="1.4" d="M5261.68,-1627C5635.13,-1627 5725.99,-1576.57 6098.76,-1554 6134.71,-1551.82 6721.72,-1557.9 6746.76,-1532 6750.09,-1528.55 6749.71,-1524.99 6746.76,-1521.2 6741.02,-1513.84 6449.16,-1453.82 6396.77,-1435.36"/> <path fill="none" stroke="#ff8f66" stroke-width="1.4" d="M5261.68,-1627C5635.13,-1627 5725.99,-1576.57 6098.76,-1554 6134.71,-1551.82 6721.72,-1557.9 6746.76,-1532 6750.09,-1528.55 6749.71,-1524.99 6746.76,-1521.2 6741.02,-1513.84 6449.16,-1453.82 6396.77,-1435.36"/>
<polygon fill="#a98cff" stroke="#a98cff" stroke-width="1.4" points="6397.98,-1433.23 6390.64,-1432.18 6395.72,-1437.58 6397.98,-1433.23"/> <polygon fill="#ff8f66" stroke="#ff8f66" stroke-width="1.4" points="6397.98,-1433.23 6390.64,-1432.18 6395.72,-1437.58 6397.98,-1433.23"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="6795.82" y="-1523.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#a98cff"> &#160;&#39;order&#39;/&#39;shipment&#39; sse &#160;</text> <text xml:space="preserve" text-anchor="middle" x="6795.82" y="-1523.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff8f66"> &#160;&#39;order&#39;/&#39;shipment&#39; sse &#160;</text>
</g> </g>
<!-- sseAutomat&#45;&gt;warehouseAutomat --> <!-- sseAutomat&#45;&gt;warehouseAutomat -->
<g id="edge27" class="edge"> <g id="edge28" class="edge">
<title>sseAutomat:e&#45;&gt;warehouseAutomat:n</title> <title>sseAutomat:e&#45;&gt;warehouseAutomat:n</title>
<g id="a_edge27"><a xlink:title="sseAutomat → warehouseAutomat (&#39;warehouse&#39; sse trigger)"> <g id="a_edge28"><a xlink:title="sseAutomat → warehouseAutomat (&#39;warehouse&#39; sse trigger)">
<path fill="none" stroke="#ff9f5a" stroke-width="1.4" d="M5261.68,-1627C5646.9,-1627 5716.85,-1801.42 6098.76,-1851.82 6405.21,-1892.25 7184.64,-1901.36 7489.76,-1851.82 7598.74,-1834.12 7724.94,-1881.79 7731.76,-1779.95"/> <path fill="none" stroke="#8f95ff" stroke-width="1.4" d="M5261.68,-1627C5646.9,-1627 5716.85,-1801.42 6098.76,-1851.82 6405.21,-1892.25 7184.64,-1901.36 7489.76,-1851.82 7598.74,-1834.12 7724.94,-1881.79 7731.76,-1779.95"/>
<polygon fill="#ff9f5a" stroke="#ff9f5a" stroke-width="1.4" points="7734.21,-1780.19 7731.98,-1773.12 7729.31,-1780.04 7734.21,-1780.19"/> <polygon fill="#8f95ff" stroke="#8f95ff" stroke-width="1.4" points="7734.21,-1780.19 7731.98,-1773.12 7729.31,-1780.04 7734.21,-1780.19"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="5387.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff9f5a"> &#160;&#39;warehouse&#39; sse trigger &#160;</text> <text xml:space="preserve" text-anchor="middle" x="5387.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8f95ff"> &#160;&#39;warehouse&#39; sse trigger &#160;</text>
</g> </g>
<!-- sseAutomat&#45;&gt;vfsAutomat --> <!-- sseAutomat&#45;&gt;vfsAutomat -->
<g id="edge33" class="edge"> <g id="edge34" class="edge">
<title>sseAutomat:e&#45;&gt;vfsAutomat:n</title> <title>sseAutomat:e&#45;&gt;vfsAutomat:n</title>
<g id="a_edge33"><a xlink:title="sseAutomat → vfsAutomat (&#39;vfs&#39; sse mutation)"> <g id="a_edge34"><a xlink:title="sseAutomat → vfsAutomat (&#39;vfs&#39; sse mutation)">
<path fill="none" stroke="#ff8f66" stroke-width="1.4" d="M5261.68,-1627C5287.91,-1627 5278.47,-1587.27 5260.68,-1568 5204.25,-1506.88 4831.14,-1580.68 4889.29,-1521.2 4896.14,-1514.2 5232.06,-1514.6 5241.76,-1513.2 5395.56,-1491.01 5578.72,-1575.29 5585.82,-1429.21"/> <path fill="none" stroke="#4ecb8f" stroke-width="1.4" d="M5261.68,-1627C5287.91,-1627 5278.47,-1587.27 5260.68,-1568 5204.25,-1506.88 4831.14,-1580.68 4889.29,-1521.2 4896.14,-1514.2 5232.06,-1514.6 5241.76,-1513.2 5395.56,-1491.01 5578.72,-1575.29 5585.82,-1429.21"/>
<polygon fill="#ff8f66" stroke="#ff8f66" stroke-width="1.4" points="5588.26,-1429.37 5585.98,-1422.32 5583.37,-1429.26 5588.26,-1429.37"/> <polygon fill="#4ecb8f" stroke="#4ecb8f" stroke-width="1.4" points="5588.26,-1429.37 5585.98,-1422.32 5583.37,-1429.26 5588.26,-1429.37"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="4928.52" y="-1523.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff8f66"> &#160;&#39;vfs&#39; sse mutation &#160;</text> <text xml:space="preserve" text-anchor="middle" x="4928.52" y="-1523.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#4ecb8f"> &#160;&#39;vfs&#39; sse mutation &#160;</text>
</g> </g>
<!-- categoryAutomat&#45;&gt;categoryAutomat --> <!-- categoryAutomat&#45;&gt;categoryAutomat -->
<g id="edge58" class="edge"> <g id="edge59" class="edge">
<title>categoryAutomat:ne&#45;&gt;categoryAutomat:n</title> <title>categoryAutomat:ne&#45;&gt;categoryAutomat:n</title>
<g id="a_edge58"><a xlink:title="categoryAutomat → categoryAutomat (nested tree hierarchy)"> <g id="a_edge59"><a xlink:title="categoryAutomat → categoryAutomat (nested tree hierarchy)">
<path fill="none" stroke="#ff5f9e" stroke-width="1.4" d="M3341.51,-1057.4C3372.18,-1016.93 3417.51,-1016.93 3417.51,-1073.9 3417.51,-1128.87 3320.96,-1130.8 3250.17,-1094.52"/> <path fill="none" stroke="#ffb25a" stroke-width="1.4" d="M3341.51,-1057.4C3372.18,-1016.93 3417.51,-1016.93 3417.51,-1073.9 3417.51,-1128.87 3320.96,-1130.8 3250.17,-1094.52"/>
<polygon fill="#ff5f9e" stroke="#ff5f9e" stroke-width="1.4" points="3251.73,-1092.58 3244.41,-1091.41 3249.4,-1096.89 3251.73,-1092.58"/> <polygon fill="#ffb25a" stroke="#ffb25a" stroke-width="1.4" points="3251.73,-1092.58 3244.41,-1091.41 3249.4,-1096.89 3251.73,-1092.58"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="3464.78" y="-973.2" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff5f9e"> &#160;nested tree hierarchy &#160;</text> <text xml:space="preserve" text-anchor="middle" x="3464.78" y="-973.2" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ffb25a"> &#160;nested tree hierarchy &#160;</text>
</g> </g>
<!-- categoryAutomat&#45;&gt;categoryViewAutomat --> <!-- categoryAutomat&#45;&gt;categoryViewAutomat -->
<g id="edge35" class="edge"> <g id="edge36" class="edge">
<title>categoryAutomat:e&#45;&gt;categoryViewAutomat:n</title> <title>categoryAutomat:e&#45;&gt;categoryViewAutomat:n</title>
<g id="a_edge35"><a xlink:title="categoryAutomat → categoryViewAutomat (attachTree &amp; warm slugs)"> <g id="a_edge36"><a xlink:title="categoryAutomat → categoryViewAutomat (attachTree &amp; warm slugs)">
<path fill="none" stroke="#ff7aa8" stroke-width="1.4" d="M3342.51,-980.4C3434.14,-980.4 3346.03,-869.46 3340.44,-776.53"/> <path fill="none" stroke="#e0b25a" stroke-width="1.4" d="M3342.51,-980.4C3434.14,-980.4 3346.03,-869.46 3340.44,-776.53"/>
<polygon fill="#ff7aa8" stroke="#ff7aa8" stroke-width="1.4" points="3342.89,-776.64 3340.23,-769.72 3337.99,-776.79 3342.89,-776.64"/> <polygon fill="#e0b25a" stroke="#e0b25a" stroke-width="1.4" points="3342.89,-776.64 3340.23,-769.72 3337.99,-776.79 3342.89,-776.64"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="3401.19" y="-811.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff7aa8"> &#160;attachTree &amp; warm slugs &#160;</text> <text xml:space="preserve" text-anchor="middle" x="3401.19" y="-811.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#e0b25a"> &#160;attachTree &amp; warm slugs &#160;</text>
</g> </g>
<!-- articleSearchAutomat --> <!-- articleSearchAutomat -->
<g id="node10" class="node"> <g id="node10" class="node">
@@ -1671,74 +1714,74 @@
<text xml:space="preserve" text-anchor="start" x="3608.59" y="-33.8" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Reactive search outcome</text> <text xml:space="preserve" text-anchor="start" x="3608.59" y="-33.8" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Reactive search outcome</text>
</g> </g>
<!-- categoryAutomat&#45;&gt;articleSearchAutomat --> <!-- categoryAutomat&#45;&gt;articleSearchAutomat -->
<g id="edge38" class="edge"> <g id="edge39" class="edge">
<title>categoryAutomat:e&#45;&gt;articleSearchAutomat:n</title> <title>categoryAutomat:e&#45;&gt;articleSearchAutomat:n</title>
<g id="a_edge38"><a xlink:title="categoryAutomat → articleSearchAutomat (categoryFacets)"> <g id="a_edge39"><a xlink:title="categoryAutomat → articleSearchAutomat (categoryFacets)">
<path fill="none" stroke="#ff5f9e" stroke-width="1.4" d="M3342.51,-1046.4C3427.4,-1046.4 3380.83,-930.63 3341.51,-855.4 3311.88,-798.7 3253.24,-829.38 3223.76,-772.6 3180.54,-689.38 3158.95,-629.38 3223.76,-561.6 3261.5,-522.13 3662.29,-554.76 3714.76,-539.6 3730.97,-534.91 3739.27,-535.39 3747.76,-520.8 3816.95,-401.75 3837.15,-313.99 3745.76,-211 3710.44,-171.2 3519.13,-220.1 3496.83,-178.8"/> <path fill="none" stroke="#ffb25a" stroke-width="1.4" d="M3342.51,-1046.4C3427.4,-1046.4 3380.83,-930.63 3341.51,-855.4 3311.88,-798.7 3253.24,-829.38 3223.76,-772.6 3180.54,-689.38 3158.95,-629.38 3223.76,-561.6 3261.5,-522.13 3662.29,-554.76 3714.76,-539.6 3730.97,-534.91 3739.27,-535.39 3747.76,-520.8 3816.95,-401.75 3837.15,-313.99 3745.76,-211 3710.44,-171.2 3519.13,-220.1 3496.83,-178.8"/>
<polygon fill="#ff5f9e" stroke="#ff5f9e" stroke-width="1.4" points="3499.23,-178.29 3495.2,-172.06 3494.46,-179.44 3499.23,-178.29"/> <polygon fill="#ffb25a" stroke="#ffb25a" stroke-width="1.4" points="3499.23,-178.29 3495.2,-172.06 3494.46,-179.44 3499.23,-178.29"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="3777.37" y="-531.5" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff5f9e"> &#160;categoryFacets &#160;</text> <text xml:space="preserve" text-anchor="middle" x="3777.37" y="-531.5" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ffb25a"> &#160;categoryFacets &#160;</text>
</g> </g>
<!-- categoryViewAutomat&#45;&gt;articleIndexAutomat --> <!-- categoryViewAutomat&#45;&gt;articleIndexAutomat -->
<g id="edge36" class="edge"> <g id="edge37" class="edge">
<title>categoryViewAutomat:e&#45;&gt;articleIndexAutomat:n</title> <title>categoryViewAutomat:e&#45;&gt;articleIndexAutomat:n</title>
<g id="a_edge36"><a xlink:title="categoryViewAutomat → articleIndexAutomat (derived union index)"> <g id="a_edge37"><a xlink:title="categoryViewAutomat → articleIndexAutomat (derived union index)">
<path fill="none" stroke="#e0b25a" stroke-width="1.4" d="M3560.95,-634.6C3640.2,-634.6 3565.41,-539.03 3560.05,-458.53"/> <path fill="none" stroke="#56c8c8" stroke-width="1.4" d="M3560.95,-634.6C3640.2,-634.6 3565.41,-539.03 3560.05,-458.53"/>
<polygon fill="#e0b25a" stroke="#e0b25a" stroke-width="1.4" points="3562.52,-458.83 3559.83,-451.92 3557.62,-459 3562.52,-458.83"/> <polygon fill="#56c8c8" stroke="#56c8c8" stroke-width="1.4" points="3562.52,-458.83 3559.83,-451.92 3557.62,-459 3562.52,-458.83"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="3623.74" y="-531.5" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#e0b25a"> &#160;derived union index &#160;</text> <text xml:space="preserve" text-anchor="middle" x="3623.74" y="-531.5" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#56c8c8"> &#160;derived union index &#160;</text>
</g> </g>
<!-- artikelAutomat&#45;&gt;categoryAutomat --> <!-- artikelAutomat&#45;&gt;categoryAutomat -->
<g id="edge40" class="edge"> <g id="edge41" class="edge">
<title>artikelAutomat:e&#45;&gt;categoryAutomat:n</title> <title>artikelAutomat:e&#45;&gt;categoryAutomat:n</title>
<g id="a_edge40"><a xlink:title="artikelAutomat → categoryAutomat (articleCount updates)"> <g id="a_edge41"><a xlink:title="artikelAutomat → categoryAutomat (articleCount updates)">
<path fill="none" stroke="#66b3ff" stroke-width="1.4" d="M3451.59,-1386.2C3526.7,-1386.2 3490.02,-1281.13 3450.59,-1217.2 3387.59,-1115.06 3212.93,-1209.48 3204.1,-1100.35"/> <path fill="none" stroke="#5aa7ff" stroke-width="1.4" d="M3451.59,-1386.2C3526.7,-1386.2 3490.02,-1281.13 3450.59,-1217.2 3387.59,-1115.06 3212.93,-1209.48 3204.1,-1100.35"/>
<polygon fill="#66b3ff" stroke="#66b3ff" stroke-width="1.4" points="3206.56,-1100.42 3203.84,-1093.52 3201.66,-1100.61 3206.56,-1100.42"/> <polygon fill="#5aa7ff" stroke="#5aa7ff" stroke-width="1.4" points="3206.56,-1100.42 3203.84,-1093.52 3201.66,-1100.61 3206.56,-1100.42"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="3425.76" y="-1162.1" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#66b3ff"> &#160;articleCount updates &#160;</text> <text xml:space="preserve" text-anchor="middle" x="3425.76" y="-1162.1" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#5aa7ff"> &#160;articleCount updates &#160;</text>
</g> </g>
<!-- artikelAutomat&#45;&gt;artikelAutomat --> <!-- artikelAutomat&#45;&gt;artikelAutomat -->
<g id="edge59" class="edge"> <g id="edge60" class="edge">
<title>artikelAutomat:ne&#45;&gt;artikelAutomat:n</title> <title>artikelAutomat:ne&#45;&gt;artikelAutomat:n</title>
<g id="a_edge59"><a xlink:title="artikelAutomat → artikelAutomat (variants &amp; parent SKU)"> <g id="a_edge60"><a xlink:title="artikelAutomat → artikelAutomat (variants &amp; parent SKU)">
<path fill="none" stroke="#ffb25a" stroke-width="1.4" d="M3450.59,-1397.2C3481.25,-1356.72 3526.59,-1356.72 3526.59,-1413.7 3526.59,-1468.67 3443.71,-1470.6 3372.97,-1434.32"/> <path fill="none" stroke="#66b3ff" stroke-width="1.4" d="M3450.59,-1397.2C3481.25,-1356.72 3526.59,-1356.72 3526.59,-1413.7 3526.59,-1468.67 3443.71,-1470.6 3372.97,-1434.32"/>
<polygon fill="#ffb25a" stroke="#ffb25a" stroke-width="1.4" points="3374.48,-1432.35 3367.16,-1431.2 3372.17,-1436.67 3374.48,-1432.35"/> <polygon fill="#66b3ff" stroke="#66b3ff" stroke-width="1.4" points="3374.48,-1432.35 3367.16,-1431.2 3372.17,-1436.67 3374.48,-1432.35"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="3576.11" y="-1324" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ffb25a"> &#160;variants &amp; parent SKU &#160;</text> <text xml:space="preserve" text-anchor="middle" x="3576.11" y="-1324" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#66b3ff"> &#160;variants &amp; parent SKU &#160;</text>
</g> </g>
<!-- artikelAutomat&#45;&gt;articleIndexAutomat --> <!-- artikelAutomat&#45;&gt;articleIndexAutomat -->
<g id="edge39" class="edge"> <g id="edge40" class="edge">
<title>artikelAutomat:e&#45;&gt;articleIndexAutomat:n</title> <title>artikelAutomat:e&#45;&gt;articleIndexAutomat:n</title>
<g id="a_edge39"><a xlink:title="artikelAutomat → articleIndexAutomat (setArticleIndexName (optimistic))"> <g id="a_edge40"><a xlink:title="artikelAutomat → articleIndexAutomat (setArticleIndexName (optimistic))">
<path fill="none" stroke="#ffb25a" stroke-width="1.4" d="M3451.59,-1320.2C3497.37,-1320.2 3481.85,-1250.65 3450.59,-1217.2 3358.23,-1118.38 2937.24,-1267.95 2843.76,-1170.2 2656.76,-974.67 2642.01,-755.43 2830.76,-561.6 2894.96,-495.66 3594.55,-605.54 3658.76,-539.6 3723.05,-473.58 3480.98,-540.38 3464.83,-458.75"/> <path fill="none" stroke="#66b3ff" stroke-width="1.4" d="M3451.59,-1320.2C3497.37,-1320.2 3481.85,-1250.65 3450.59,-1217.2 3358.23,-1118.38 2937.24,-1267.95 2843.76,-1170.2 2656.76,-974.67 2642.01,-755.43 2830.76,-561.6 2894.96,-495.66 3594.55,-605.54 3658.76,-539.6 3723.05,-473.58 3480.98,-540.38 3464.83,-458.75"/>
<polygon fill="#ffb25a" stroke="#ffb25a" stroke-width="1.4" points="3467.28,-458.65 3464.19,-451.91 3462.4,-459.11 3467.28,-458.65"/> <polygon fill="#66b3ff" stroke="#66b3ff" stroke-width="1.4" points="3467.28,-458.65 3464.19,-451.91 3462.4,-459.11 3467.28,-458.65"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="2768.25" y="-811.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ffb25a"> &#160;setArticleIndexName (optimistic) &#160;</text> <text xml:space="preserve" text-anchor="middle" x="2768.25" y="-811.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#66b3ff"> &#160;setArticleIndexName (optimistic) &#160;</text>
</g> </g>
<!-- articleDetailAutomat&#45;&gt;artikelAutomat --> <!-- articleDetailAutomat&#45;&gt;artikelAutomat -->
<g id="edge41" class="edge"> <g id="edge42" class="edge">
<title>articleDetailAutomat:e&#45;&gt;artikelAutomat:n</title> <title>articleDetailAutomat:e&#45;&gt;artikelAutomat:n</title>
<g id="a_edge41"><a xlink:title="articleDetailAutomat → artikelAutomat (watch SKU details)"> <g id="a_edge42"><a xlink:title="articleDetailAutomat → artikelAutomat (watch SKU details)">
<path fill="none" stroke="#5aa7ff" stroke-width="1.4" d="M3310.9,-1726C3373.72,-1726 3315,-1515.13 3310.08,-1440.06"/> <path fill="none" stroke="#6bdf9a" stroke-width="1.4" d="M3310.9,-1726C3373.72,-1726 3315,-1515.13 3310.08,-1440.06"/>
<polygon fill="#5aa7ff" stroke="#5aa7ff" stroke-width="1.4" points="3312.54,-1440.22 3309.83,-1433.32 3307.64,-1440.4 3312.54,-1440.22"/> <polygon fill="#6bdf9a" stroke="#6bdf9a" stroke-width="1.4" points="3312.54,-1440.22 3309.83,-1433.32 3307.64,-1440.4 3312.54,-1440.22"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="3365.01" y="-1523.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#5aa7ff"> &#160;watch SKU details &#160;</text> <text xml:space="preserve" text-anchor="middle" x="3365.01" y="-1523.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#6bdf9a"> &#160;watch SKU details &#160;</text>
</g> </g>
<!-- articleIndexAutomat&#45;&gt;articleSearchAutomat --> <!-- articleIndexAutomat&#45;&gt;articleSearchAutomat -->
<g id="edge37" class="edge"> <g id="edge38" class="edge">
<title>articleIndexAutomat:e&#45;&gt;articleSearchAutomat:n</title> <title>articleIndexAutomat:e&#45;&gt;articleSearchAutomat:n</title>
<g id="a_edge37"><a xlink:title="articleIndexAutomat → articleSearchAutomat (query index items)"> <g id="a_edge38"><a xlink:title="articleIndexAutomat → articleSearchAutomat (query index items)">
<path fill="none" stroke="#56c8c8" stroke-width="1.4" d="M3706.41,-404.8C3771.75,-404.8 3736.23,-315.41 3705.41,-257.8 3691.94,-232.63 3669.89,-244.22 3652.22,-221.8 3639.03,-205.05 3634.12,-196.89 3633.04,-178.98"/> <path fill="none" stroke="#ff5f9e" stroke-width="1.4" d="M3706.41,-404.8C3771.75,-404.8 3736.23,-315.41 3705.41,-257.8 3691.94,-232.63 3669.89,-244.22 3652.22,-221.8 3639.03,-205.05 3634.12,-196.89 3633.04,-178.98"/>
<polygon fill="#56c8c8" stroke="#56c8c8" stroke-width="1.4" points="3635.5,-179.05 3632.86,-172.12 3630.6,-179.18 3635.5,-179.05"/> <polygon fill="#ff5f9e" stroke="#ff5f9e" stroke-width="1.4" points="3635.5,-179.05 3632.86,-172.12 3630.6,-179.18 3635.5,-179.05"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="3692.49" y="-213.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#56c8c8"> &#160;query index items &#160;</text> <text xml:space="preserve" text-anchor="middle" x="3692.49" y="-213.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff5f9e"> &#160;query index items &#160;</text>
</g> </g>
<!-- deliveryEditorAutomat --> <!-- deliveryEditorAutomat -->
<g id="node15" class="node"> <g id="node15" class="node">
@@ -1798,127 +1841,127 @@
<text xml:space="preserve" text-anchor="start" x="676.23" y="-878.2" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Create goods receipt</text> <text xml:space="preserve" text-anchor="start" x="676.23" y="-878.2" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Create goods receipt</text>
</g> </g>
<!-- deliveryEditorAutomat&#45;&gt;artikelAutomat --> <!-- deliveryEditorAutomat&#45;&gt;artikelAutomat -->
<g id="edge43" class="edge"> <g id="edge44" class="edge">
<title>deliveryEditorAutomat:e&#45;&gt;artikelAutomat:n</title> <title>deliveryEditorAutomat:e&#45;&gt;artikelAutomat:n</title>
<g id="a_edge43"><a xlink:title="deliveryEditorAutomat → artikelAutomat (lookup SKU &amp; pack units)"> <g id="a_edge44"><a xlink:title="deliveryEditorAutomat → artikelAutomat (lookup SKU &amp; pack units)">
<path fill="none" stroke="#f5c15c" stroke-width="1.4" d="M756.5,-1035.4C1377.87,-1035.4 1529.1,-1121.67 2149.76,-1151.4 2184.83,-1153.08 2748.39,-1148.47 2781.76,-1159.4 2978.56,-1223.84 2964.06,-1365.95 3160.93,-1430.2 3197.11,-1442.01 3245.48,-1467.92 3253.17,-1440.02"/> <path fill="none" stroke="#ff7fc2" stroke-width="1.4" d="M756.5,-1035.4C1377.87,-1035.4 1529.1,-1121.67 2149.76,-1151.4 2184.83,-1153.08 2748.39,-1148.47 2781.76,-1159.4 2978.56,-1223.84 2964.06,-1365.95 3160.93,-1430.2 3197.11,-1442.01 3245.48,-1467.92 3253.17,-1440.02"/>
<polygon fill="#f5c15c" stroke="#f5c15c" stroke-width="1.4" points="3255.58,-1440.54 3253.97,-1433.3 3250.71,-1439.97 3255.58,-1440.54"/> <polygon fill="#ff7fc2" stroke="#ff7fc2" stroke-width="1.4" points="3255.58,-1440.54 3253.97,-1433.3 3250.71,-1439.97 3255.58,-1440.54"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="2864.27" y="-1162.1" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#f5c15c"> &#160;lookup SKU &amp; pack units &#160;</text> <text xml:space="preserve" text-anchor="middle" x="2864.27" y="-1162.1" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff7fc2"> &#160;lookup SKU &amp; pack units &#160;</text>
</g> </g>
<!-- deliveryEditorAutomat&#45;&gt;supplierAutomat --> <!-- deliveryEditorAutomat&#45;&gt;supplierAutomat -->
<g id="edge42" class="edge"> <g id="edge43" class="edge">
<title>deliveryEditorAutomat:w&#45;&gt;supplierAutomat:n</title> <title>deliveryEditorAutomat:w&#45;&gt;supplierAutomat:n</title>
<g id="a_edge42"><a xlink:title="deliveryEditorAutomat → supplierAutomat (match vendor catalog)"> <g id="a_edge43"><a xlink:title="deliveryEditorAutomat → supplierAutomat (match vendor catalog)">
<path fill="none" stroke="#6bdf9a" stroke-width="1.4" d="M483.02,-1013.4C324.2,-1013.4 129.3,-1211.16 119.88,-1069.67"/> <path fill="none" stroke="#f5c15c" stroke-width="1.4" d="M483.02,-1013.4C324.2,-1013.4 129.3,-1211.16 119.88,-1069.67"/>
<polygon fill="#6bdf9a" stroke="#6bdf9a" stroke-width="1.4" points="122.26,-1067.44 119.59,-1060.52 117.36,-1067.59 122.26,-1067.44"/> <polygon fill="#f5c15c" stroke="#f5c15c" stroke-width="1.4" points="122.26,-1067.44 119.59,-1060.52 117.36,-1067.59 122.26,-1067.44"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="394.31" y="-1048.07" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#6bdf9a"> &#160;match vendor catalog &#160;</text> <text xml:space="preserve" text-anchor="middle" x="394.31" y="-1048.07" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#f5c15c"> &#160;match vendor catalog &#160;</text>
</g> </g>
<!-- deliveryEditorAutomat&#45;&gt;deliveryAutomat --> <!-- deliveryEditorAutomat&#45;&gt;deliveryAutomat -->
<g id="edge44" class="edge"> <g id="edge45" class="edge">
<title>deliveryEditorAutomat:e&#45;&gt;deliveryAutomat:n</title> <title>deliveryEditorAutomat:e&#45;&gt;deliveryAutomat:n</title>
<g id="a_edge44"><a xlink:title="deliveryEditorAutomat → deliveryAutomat (commit delivery receipt)"> <g id="a_edge45"><a xlink:title="deliveryEditorAutomat → deliveryAutomat (commit delivery receipt)">
<path fill="none" stroke="#ff7fc2" stroke-width="1.4" d="M756.5,-925.4C782.72,-925.4 767.95,-889.48 755.5,-866.4 711.73,-785.28 586.91,-837.21 578.67,-754.6"/> <path fill="none" stroke="#c79bff" stroke-width="1.4" d="M756.5,-925.4C782.72,-925.4 767.95,-889.48 755.5,-866.4 711.73,-785.28 586.91,-837.21 578.67,-754.6"/>
<polygon fill="#ff7fc2" stroke="#ff7fc2" stroke-width="1.4" points="581.12,-754.59 578.34,-747.72 576.23,-754.83 581.12,-754.59"/> <polygon fill="#c79bff" stroke="#c79bff" stroke-width="1.4" points="581.12,-754.59 578.34,-747.72 576.23,-754.83 581.12,-754.59"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="740.44" y="-811.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff7fc2"> &#160;commit delivery receipt &#160;</text> <text xml:space="preserve" text-anchor="middle" x="740.44" y="-811.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#c79bff"> &#160;commit delivery receipt &#160;</text>
</g> </g>
<!-- quoteAutomat&#45;&gt;orderAutomat --> <!-- quoteAutomat&#45;&gt;orderAutomat -->
<g id="edge45" class="edge"> <g id="edge46" class="edge">
<title>quoteAutomat:w&#45;&gt;orderAutomat:n</title> <title>quoteAutomat:w&#45;&gt;orderAutomat:n</title>
<g id="a_edge45"><a xlink:title="quoteAutomat → orderAutomat (convert quote to order)"> <g id="a_edge46"><a xlink:title="quoteAutomat → orderAutomat (convert quote to order)">
<path fill="none" stroke="#c79bff" stroke-width="1.4" d="M6474.28,-1704C6416.38,-1704 6470.58,-1510.46 6475.57,-1440.25"/> <path fill="none" stroke="#5cd6d6" stroke-width="1.4" d="M6474.28,-1704C6416.38,-1704 6470.58,-1510.46 6475.57,-1440.25"/>
<polygon fill="#c79bff" stroke="#c79bff" stroke-width="1.4" points="6478.01,-1440.41 6475.84,-1433.32 6473.12,-1440.21 6478.01,-1440.41"/> <polygon fill="#5cd6d6" stroke="#5cd6d6" stroke-width="1.4" points="6478.01,-1440.41 6475.84,-1433.32 6473.12,-1440.21 6478.01,-1440.41"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="6512.1" y="-1523.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#c79bff"> &#160;convert quote to order &#160;</text> <text xml:space="preserve" text-anchor="middle" x="6512.1" y="-1523.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#5cd6d6"> &#160;convert quote to order &#160;</text>
</g> </g>
<!-- orderAutomat&#45;&gt;entityAutomat --> <!-- orderAutomat&#45;&gt;entityAutomat -->
<g id="edge46" class="edge"> <g id="edge47" class="edge">
<title>orderAutomat:e&#45;&gt;entityAutomat:n</title> <title>orderAutomat:e&#45;&gt;entityAutomat:n</title>
<g id="a_edge46"><a xlink:title="orderAutomat → entityAutomat (customer lookup)"> <g id="a_edge47"><a xlink:title="orderAutomat → entityAutomat (customer lookup)">
<path fill="none" stroke="#5cd6d6" stroke-width="1.4" d="M6522.51,-1386.2C6675.1,-1386.2 6527.24,-1199.01 6521.7,-1045.09"/> <path fill="none" stroke="#ff9f5a" stroke-width="1.4" d="M6522.51,-1386.2C6675.1,-1386.2 6527.24,-1199.01 6521.7,-1045.09"/>
<polygon fill="#5cd6d6" stroke="#5cd6d6" stroke-width="1.4" points="6524.16,-1045.47 6521.58,-1038.52 6519.26,-1045.56 6524.16,-1045.47"/> <polygon fill="#ff9f5a" stroke="#ff9f5a" stroke-width="1.4" points="6524.16,-1045.47 6521.58,-1038.52 6519.26,-1045.56 6524.16,-1045.47"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="6584.05" y="-1162.1" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#5cd6d6"> &#160;customer lookup &#160;</text> <text xml:space="preserve" text-anchor="middle" x="6584.05" y="-1162.1" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff9f5a"> &#160;customer lookup &#160;</text>
</g> </g>
<!-- companyAutomat&#45;&gt;categoryAutomat --> <!-- companyAutomat&#45;&gt;categoryAutomat -->
<g id="edge34" class="edge"> <g id="edge35" class="edge">
<title>companyAutomat:e&#45;&gt;categoryAutomat:n</title> <title>companyAutomat:e&#45;&gt;categoryAutomat:n</title>
<g id="a_edge34"><a xlink:title="companyAutomat → categoryAutomat (rootCategoryId)"> <g id="a_edge35"><a xlink:title="companyAutomat → categoryAutomat (rootCategoryId)">
<path fill="none" stroke="#4ecb8f" stroke-width="1.4" d="M2188.79,-1660C2239.3,-1660 2192.91,-1588.55 2229.76,-1554 2254.82,-1530.5 2323.71,-1545.83 2299.76,-1521.2 2277.71,-1498.52 2037.27,-1535.88 2015.22,-1513.2 1915.77,-1410.91 1922.28,-1300.44 2015.22,-1192.2 2094,-1100.46 3076.45,-1209.75 3124.37,-1100.05"/> <path fill="none" stroke="#ff7aa8" stroke-width="1.4" d="M2188.79,-1660C2239.3,-1660 2192.91,-1588.55 2229.76,-1554 2254.82,-1530.5 2323.71,-1545.83 2299.76,-1521.2 2277.71,-1498.52 2037.27,-1535.88 2015.22,-1513.2 1915.77,-1410.91 1922.28,-1300.44 2015.22,-1192.2 2094,-1100.46 3076.45,-1209.75 3124.37,-1100.05"/>
<polygon fill="#4ecb8f" stroke="#4ecb8f" stroke-width="1.4" points="3126.71,-1100.83 3125.75,-1093.47 3121.91,-1099.82 3126.71,-1100.83"/> <polygon fill="#ff7aa8" stroke="#ff7aa8" stroke-width="1.4" points="3126.71,-1100.83 3125.75,-1093.47 3121.91,-1099.82 3126.71,-1100.83"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="2049.99" y="-1324" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#4ecb8f"> &#160;rootCategoryId &#160;</text> <text xml:space="preserve" text-anchor="middle" x="2049.99" y="-1324" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff7aa8"> &#160;rootCategoryId &#160;</text>
</g> </g>
<!-- companyAutomat&#45;&gt;customergroupAutomat --> <!-- companyAutomat&#45;&gt;customergroupAutomat -->
<g id="edge47" class="edge"> <g id="edge48" class="edge">
<title>companyAutomat:e&#45;&gt;customergroupAutomat:n</title> <title>companyAutomat:e&#45;&gt;customergroupAutomat:n</title>
<g id="a_edge47"><a xlink:title="companyAutomat → customergroupAutomat (poscustomergroup)"> <g id="a_edge48"><a xlink:title="companyAutomat → customergroupAutomat (poscustomergroup)">
<path fill="none" stroke="#ff9f5a" stroke-width="1.4" d="M2188.79,-1726C2247.6,-1726 2182.17,-1817.27 2229.76,-1851.82 2298.76,-1901.92 5214.65,-1857.16 5299.76,-1851.82 5498.47,-1839.34 6181.23,-1937.13 6201.85,-1746.97"/> <path fill="none" stroke="#8f95ff" stroke-width="1.4" d="M2188.79,-1726C2247.6,-1726 2182.17,-1817.27 2229.76,-1851.82 2298.76,-1901.92 5214.65,-1857.16 5299.76,-1851.82 5498.47,-1839.34 6181.23,-1937.13 6201.85,-1746.97"/>
<polygon fill="#ff9f5a" stroke="#ff9f5a" stroke-width="1.4" points="6204.29,-1747.24 6202.22,-1740.12 6199.4,-1746.98 6204.29,-1747.24"/> <polygon fill="#8f95ff" stroke="#8f95ff" stroke-width="1.4" points="6204.29,-1747.24 6202.22,-1740.12 6199.4,-1746.98 6204.29,-1747.24"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="3274.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff9f5a"> &#160;poscustomergroup &#160;</text> <text xml:space="preserve" text-anchor="middle" x="4824.76" y="-1885.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8f95ff"> &#160;poscustomergroup &#160;</text>
</g> </g>
<!-- companyAutomat&#45;&gt;posterminalAutomat --> <!-- companyAutomat&#45;&gt;posterminalAutomat -->
<g id="edge49" class="edge"> <g id="edge50" class="edge">
<title>companyAutomat:e&#45;&gt;posterminalAutomat:n</title> <title>companyAutomat:e&#45;&gt;posterminalAutomat:n</title>
<g id="a_edge49"><a xlink:title="companyAutomat → posterminalAutomat (company context)"> <g id="a_edge50"><a xlink:title="companyAutomat → posterminalAutomat (company context)">
<path fill="none" stroke="#ff6b6b" stroke-width="1.4" d="M2188.79,-1726C2256.44,-1726 2192.65,-1498.47 2187.56,-1418.23"/> <path fill="none" stroke="#58d18a" stroke-width="1.4" d="M2188.79,-1726C2256.44,-1726 2192.65,-1498.47 2187.56,-1418.23"/>
<polygon fill="#ff6b6b" stroke="#ff6b6b" stroke-width="1.4" points="2190.01,-1418.23 2187.32,-1411.32 2185.11,-1418.4 2190.01,-1418.23"/> <polygon fill="#58d18a" stroke="#58d18a" stroke-width="1.4" points="2190.01,-1418.23 2187.32,-1411.32 2185.11,-1418.4 2190.01,-1418.23"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="2243.51" y="-1523.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff6b6b"> &#160;company context &#160;</text> <text xml:space="preserve" text-anchor="middle" x="2243.51" y="-1523.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#58d18a"> &#160;company context &#160;</text>
</g> </g>
<!-- posterminalAutomat&#45;&gt;categoryAutomat --> <!-- posterminalAutomat&#45;&gt;categoryAutomat -->
<g id="edge48" class="edge"> <g id="edge49" class="edge">
<title>posterminalAutomat:e&#45;&gt;categoryAutomat:n</title> <title>posterminalAutomat:e&#45;&gt;categoryAutomat:n</title>
<g id="a_edge48"><a xlink:title="posterminalAutomat → categoryAutomat (filter enabled roots)"> <g id="a_edge49"><a xlink:title="posterminalAutomat → categoryAutomat (filter enabled roots)">
<path fill="none" stroke="#8f95ff" stroke-width="1.4" d="M2374.78,-1364.2C2430.34,-1364.2 2401.8,-1287.17 2373.78,-1239.2 2341.47,-1183.87 2210.12,-1206.36 2253.71,-1159.4 2257.78,-1155.02 3063.46,-1104.14 3156.28,-1092.98"/> <path fill="none" stroke="#ff6b6b" stroke-width="1.4" d="M2374.78,-1364.2C2430.34,-1364.2 2401.8,-1287.17 2373.78,-1239.2 2341.47,-1183.87 2210.12,-1206.36 2253.71,-1159.4 2257.78,-1155.02 3063.46,-1104.14 3156.28,-1092.98"/>
<polygon fill="#8f95ff" stroke="#8f95ff" stroke-width="1.4" points="3156.44,-1095.44 3162.88,-1091.78 3155.56,-1090.62 3156.44,-1095.44"/> <polygon fill="#ff6b6b" stroke="#ff6b6b" stroke-width="1.4" points="3156.44,-1095.44 3162.88,-1091.78 3155.56,-1090.62 3156.44,-1095.44"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="2295.73" y="-1162.1" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8f95ff"> &#160;filter enabled roots &#160;</text> <text xml:space="preserve" text-anchor="middle" x="2295.73" y="-1162.1" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff6b6b"> &#160;filter enabled roots &#160;</text>
</g> </g>
<!-- chatAutomat&#45;&gt;sseAutomat --> <!-- chatAutomat&#45;&gt;sseAutomat -->
<g id="edge52" class="edge"> <g id="edge53" class="edge">
<title>chatAutomat:e&#45;&gt;sseAutomat:n</title> <title>chatAutomat:e&#45;&gt;sseAutomat:n</title>
<g id="a_edge52"><a xlink:title="chatAutomat → sseAutomat (getSseClientId())"> <g id="a_edge53"><a xlink:title="chatAutomat → sseAutomat (getSseClientId())">
<path fill="none" stroke="#a98cff" stroke-width="1.4" d="M5200.85,-1331.2C5306.77,-1331.2 5343.03,-1727.56 5268.68,-1803 5249.74,-1822.22 5210.56,-1832.07 5201.93,-1812.84"/> <path fill="none" stroke="#ff8f66" stroke-width="1.4" d="M5200.85,-1331.2C5306.77,-1331.2 5343.03,-1727.56 5268.68,-1803 5249.74,-1822.22 5210.56,-1832.07 5201.93,-1812.84"/>
<polygon fill="#a98cff" stroke="#a98cff" stroke-width="1.4" points="5204.35,-1812.49 5200.62,-1806.08 5199.54,-1813.42 5204.35,-1812.49"/> <polygon fill="#ff8f66" stroke="#ff8f66" stroke-width="1.4" points="5204.35,-1812.49 5200.62,-1806.08 5199.54,-1813.42 5204.35,-1812.49"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="5338.61" y="-1523.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#a98cff"> &#160;getSseClientId() &#160;</text> <text xml:space="preserve" text-anchor="middle" x="5338.61" y="-1523.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff8f66"> &#160;getSseClientId() &#160;</text>
</g> </g>
<!-- chatAutomat&#45;&gt;chatAutomat --> <!-- chatAutomat&#45;&gt;chatAutomat -->
<g id="edge60" class="edge"> <g id="edge61" class="edge">
<title>chatAutomat:nw&#45;&gt;chatAutomat:n</title> <title>chatAutomat:nw&#45;&gt;chatAutomat:n</title>
<g id="a_edge60"><a xlink:title="chatAutomat → chatAutomat (stream delta appends)"> <g id="a_edge61"><a xlink:title="chatAutomat → chatAutomat (stream delta appends)">
<path fill="none" stroke="#66b3ff" stroke-width="1.4" d="M4907.66,-1364.2C4877,-1323.72 4831.66,-1323.72 4831.66,-1402.7 4831.66,-1479.05 5059.85,-1481.59 5141.9,-1445.1"/> <path fill="none" stroke="#5aa7ff" stroke-width="1.4" d="M4907.66,-1364.2C4877,-1323.72 4831.66,-1323.72 4831.66,-1402.7 4831.66,-1479.05 5059.85,-1481.59 5141.9,-1445.1"/>
<polygon fill="#66b3ff" stroke="#66b3ff" stroke-width="1.4" points="5142.72,-1447.42 5147.92,-1442.14 5140.55,-1443.03 5142.72,-1447.42"/> <polygon fill="#5aa7ff" stroke="#5aa7ff" stroke-width="1.4" points="5142.72,-1447.42 5147.92,-1442.14 5140.55,-1443.03 5142.72,-1447.42"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="4783.38" y="-1324" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#66b3ff"> &#160;stream delta appends &#160;</text> <text xml:space="preserve" text-anchor="middle" x="4783.38" y="-1324" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#5aa7ff"> &#160;stream delta appends &#160;</text>
</g> </g>
<!-- chatAutomat&#45;&gt;vfsAutomat --> <!-- chatAutomat&#45;&gt;vfsAutomat -->
<g id="edge50" class="edge"> <g id="edge51" class="edge">
<title>chatAutomat:e&#45;&gt;vfsAutomat:n</title> <title>chatAutomat:e&#45;&gt;vfsAutomat:n</title>
<g id="a_edge50"><a xlink:title="chatAutomat → vfsAutomat (inspect / edit applets)"> <g id="a_edge51"><a xlink:title="chatAutomat → vfsAutomat (inspect / edit applets)">
<path fill="none" stroke="#58d18a" stroke-width="1.4" d="M5200.85,-1353.2C5240.75,-1353.2 5328.67,-1411.59 5367.85,-1419.2 5422.45,-1429.81 5491.85,-1473.2 5501.6,-1431.54"/> <path fill="none" stroke="#4fc4e8" stroke-width="1.4" d="M5200.85,-1353.2C5240.75,-1353.2 5328.67,-1411.59 5367.85,-1419.2 5422.45,-1429.81 5491.85,-1473.2 5501.6,-1431.54"/>
<polygon fill="#58d18a" stroke="#58d18a" stroke-width="1.4" points="5504.27,-1429.52 5502.54,-1422.31 5499.39,-1429.02 5504.27,-1429.52"/> <polygon fill="#4fc4e8" stroke="#4fc4e8" stroke-width="1.4" points="5504.27,-1429.52 5502.54,-1422.31 5499.39,-1429.02 5504.27,-1429.52"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="5287.85" y="-1409.16" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#58d18a"> &#160;inspect / edit applets &#160;</text> <text xml:space="preserve" text-anchor="middle" x="5287.85" y="-1409.16" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#4fc4e8"> &#160;inspect / edit applets &#160;</text>
</g> </g>
<!-- navigationAutomat --> <!-- navigationAutomat -->
<g id="node29" class="node"> <g id="node30" class="node">
<title>navigationAutomat</title> <title>navigationAutomat</title>
<polygon fill="#33415c" stroke="none" points="351.27,-1386.2 351.27,-1421.2 620.24,-1421.2 620.24,-1386.2 351.27,-1386.2"/> <polygon fill="#33415c" stroke="none" points="351.27,-1386.2 351.27,-1421.2 620.24,-1421.2 620.24,-1386.2 351.27,-1386.2"/>
<polygon fill="none" stroke="#3f4c5f" points="351.27,-1386.2 351.27,-1421.2 620.24,-1421.2 620.24,-1386.2 351.27,-1386.2"/> <polygon fill="none" stroke="#3f4c5f" points="351.27,-1386.2 351.27,-1421.2 620.24,-1421.2 620.24,-1386.2 351.27,-1386.2"/>
@@ -1970,47 +2013,47 @@
<text xml:space="preserve" text-anchor="start" x="524.09" y="-1240" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Reset on company switch</text> <text xml:space="preserve" text-anchor="start" x="524.09" y="-1240" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Reset on company switch</text>
</g> </g>
<!-- chatAutomat&#45;&gt;navigationAutomat --> <!-- chatAutomat&#45;&gt;navigationAutomat -->
<g id="edge51" class="edge"> <g id="edge52" class="edge">
<title>chatAutomat:w&#45;&gt;navigationAutomat:n</title> <title>chatAutomat:w&#45;&gt;navigationAutomat:n</title>
<g id="a_edge51"><a xlink:title="chatAutomat → navigationAutomat (set_user_route tool)"> <g id="a_edge52"><a xlink:title="chatAutomat → navigationAutomat (set_user_route tool)">
<path fill="none" stroke="#4fc4e8" stroke-width="1.4" d="M4906.66,-1265.2C4774.13,-1265.2 4831.39,-1435.13 4710.76,-1490.02 4545.35,-1565.28 1621.34,-1497.15 1439.76,-1490.02 1245.11,-1482.37 573.44,-1613.88 552.5,-1429.23"/> <path fill="none" stroke="#a98cff" stroke-width="1.4" d="M4906.66,-1265.2C4774.13,-1265.2 4831.39,-1435.13 4710.76,-1490.02 4545.35,-1565.28 1621.34,-1497.15 1439.76,-1490.02 1245.11,-1482.37 573.44,-1613.88 552.5,-1429.23"/>
<polygon fill="#4fc4e8" stroke="#4fc4e8" stroke-width="1.4" points="554.95,-1429.17 552.11,-1422.32 550.06,-1429.44 554.95,-1429.17"/> <polygon fill="#a98cff" stroke="#a98cff" stroke-width="1.4" points="554.95,-1429.17 552.11,-1422.32 550.06,-1429.44 554.95,-1429.17"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="2942.76" y="-1523.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#4fc4e8"> &#160;set_user_route tool &#160;</text> <text xml:space="preserve" text-anchor="middle" x="2942.76" y="-1523.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#a98cff"> &#160;set_user_route tool &#160;</text>
</g> </g>
<!-- navigationAutomat&#45;&gt;categoryAutomat --> <!-- navigationAutomat&#45;&gt;categoryAutomat -->
<g id="edge54" class="edge"> <g id="edge55" class="edge">
<title>navigationAutomat:e&#45;&gt;categoryAutomat:n</title> <title>navigationAutomat:e&#45;&gt;categoryAutomat:n</title>
<g id="a_edge54"><a xlink:title="navigationAutomat → categoryAutomat (restore last category)"> <g id="a_edge55"><a xlink:title="navigationAutomat → categoryAutomat (restore last category)">
<path fill="none" stroke="#4ecb8f" stroke-width="1.4" d="M621.24,-1331.2C990.23,-1331.2 1072.47,-1227.63 1439.76,-1192.2 1842.89,-1153.31 1946.61,-1196.65 2350.76,-1170.2 2370.01,-1168.94 3000.6,-1118.87 3079.35,-1095.66"/> <path fill="none" stroke="#ff7aa8" stroke-width="1.4" d="M621.24,-1331.2C990.23,-1331.2 1072.47,-1227.63 1439.76,-1192.2 1842.89,-1153.31 1946.61,-1196.65 2350.76,-1170.2 2370.01,-1168.94 3000.6,-1118.87 3079.35,-1095.66"/>
<polygon fill="#4ecb8f" stroke="#4ecb8f" stroke-width="1.4" points="3080.48,-1097.83 3085.52,-1092.39 3078.19,-1093.5 3080.48,-1097.83"/> <polygon fill="#ff7aa8" stroke="#ff7aa8" stroke-width="1.4" points="3080.48,-1097.83 3085.52,-1092.39 3078.19,-1093.5 3080.48,-1097.83"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="2520.29" y="-1162.1" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#4ecb8f"> &#160;restore last category &#160;</text> <text xml:space="preserve" text-anchor="middle" x="2520.29" y="-1162.1" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff7aa8"> &#160;restore last category &#160;</text>
</g> </g>
<!-- navigationAutomat&#45;&gt;artikelAutomat --> <!-- navigationAutomat&#45;&gt;artikelAutomat -->
<g id="edge53" class="edge"> <g id="edge54" class="edge">
<title>navigationAutomat:e&#45;&gt;artikelAutomat:n</title> <title>navigationAutomat:e&#45;&gt;artikelAutomat:n</title>
<g id="a_edge53"><a xlink:title="navigationAutomat → artikelAutomat (restore last SKU)"> <g id="a_edge54"><a xlink:title="navigationAutomat → artikelAutomat (restore last SKU)">
<path fill="none" stroke="#ff8f66" stroke-width="1.4" d="M621.24,-1353.2C990.07,-1353.2 1072.19,-1459.51 1439.76,-1490.02 1870.72,-1525.79 1980.61,-1506.16 2412.76,-1490.02 2584.7,-1483.59 3177.35,-1602.06 3198.13,-1440.16"/> <path fill="none" stroke="#4ecb8f" stroke-width="1.4" d="M621.24,-1353.2C990.07,-1353.2 1072.19,-1459.51 1439.76,-1490.02 1870.72,-1525.79 1980.61,-1506.16 2412.76,-1490.02 2584.7,-1483.59 3177.35,-1602.06 3198.13,-1440.16"/>
<polygon fill="#ff8f66" stroke="#ff8f66" stroke-width="1.4" points="3200.57,-1440.45 3198.56,-1433.31 3195.68,-1440.15 3200.57,-1440.45"/> <polygon fill="#4ecb8f" stroke="#4ecb8f" stroke-width="1.4" points="3200.57,-1440.45 3198.56,-1433.31 3195.68,-1440.15 3200.57,-1440.45"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="2666.76" y="-1523.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff8f66"> &#160;restore last SKU &#160;</text> <text xml:space="preserve" text-anchor="middle" x="2666.76" y="-1523.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#4ecb8f"> &#160;restore last SKU &#160;</text>
</g> </g>
<!-- navigationAutomat&#45;&gt;deliveryEditorAutomat --> <!-- navigationAutomat&#45;&gt;deliveryEditorAutomat -->
<g id="edge55" class="edge"> <g id="edge56" class="edge">
<title>navigationAutomat:e&#45;&gt;deliveryEditorAutomat:n</title> <title>navigationAutomat:e&#45;&gt;deliveryEditorAutomat:n</title>
<g id="a_edge55"><a xlink:title="navigationAutomat → deliveryEditorAutomat (restore draft path)"> <g id="a_edge56"><a xlink:title="navigationAutomat → deliveryEditorAutomat (restore draft path)">
<path fill="none" stroke="#ff7aa8" stroke-width="1.4" d="M621.24,-1375.2C749.32,-1375.2 625.58,-1218.7 619.95,-1089.28"/> <path fill="none" stroke="#e0b25a" stroke-width="1.4" d="M621.24,-1375.2C749.32,-1375.2 625.58,-1218.7 619.95,-1089.28"/>
<polygon fill="#ff7aa8" stroke="#ff7aa8" stroke-width="1.4" points="622.41,-1089.46 619.8,-1082.52 617.51,-1089.57 622.41,-1089.46"/> <polygon fill="#e0b25a" stroke="#e0b25a" stroke-width="1.4" points="622.41,-1089.46 619.8,-1082.52 617.51,-1089.57 622.41,-1089.46"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="673.66" y="-1162.1" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff7aa8"> &#160;restore draft path &#160;</text> <text xml:space="preserve" text-anchor="middle" x="673.66" y="-1162.1" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#e0b25a"> &#160;restore draft path &#160;</text>
</g> </g>
<!-- userSettingsAutomat --> <!-- userSettingsAutomat -->
<g id="node30" class="node"> <g id="node31" class="node">
<title>userSettingsAutomat</title> <title>userSettingsAutomat</title>
<polygon fill="#33415c" stroke="none" points="504.13,-1728 504.13,-1759 777.38,-1759 777.38,-1728 504.13,-1728"/> <polygon fill="#33415c" stroke="none" points="504.13,-1728 504.13,-1759 777.38,-1759 777.38,-1728 504.13,-1728"/>
<polygon fill="none" stroke="#3f4c5f" points="504.13,-1728 504.13,-1759 777.38,-1759 777.38,-1728 504.13,-1728"/> <polygon fill="none" stroke="#3f4c5f" points="504.13,-1728 504.13,-1759 777.38,-1759 777.38,-1728 504.13,-1728"/>
@@ -2048,17 +2091,17 @@
<text xml:space="preserve" text-anchor="start" x="693.23" y="-1625.8" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Survives page reloads</text> <text xml:space="preserve" text-anchor="start" x="693.23" y="-1625.8" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Survives page reloads</text>
</g> </g>
<!-- userSettingsAutomat&#45;&gt;chatAutomat --> <!-- userSettingsAutomat&#45;&gt;chatAutomat -->
<g id="edge56" class="edge"> <g id="edge57" class="edge">
<title>userSettingsAutomat:e&#45;&gt;chatAutomat:n</title> <title>userSettingsAutomat:e&#45;&gt;chatAutomat:n</title>
<g id="a_edge56"><a xlink:title="userSettingsAutomat → chatAutomat (chatDrawerWidth)"> <g id="a_edge57"><a xlink:title="userSettingsAutomat → chatAutomat (chatDrawerWidth)">
<path fill="none" stroke="#e0b25a" stroke-width="1.4" d="M778.38,-1695C843.57,-1695 767.21,-1593.9 818.76,-1554 875.4,-1510.16 2036.11,-1522.22 2107.73,-1521.2 2180.03,-1520.17 4639.17,-1523.4 4710.76,-1513.2 4820.72,-1497.53 4950.39,-1553.21 4957.4,-1451.18"/> <path fill="none" stroke="#56c8c8" stroke-width="1.4" d="M778.38,-1695C843.57,-1695 767.21,-1593.9 818.76,-1554 875.4,-1510.16 2036.11,-1522.22 2107.73,-1521.2 2180.03,-1520.17 4639.17,-1523.4 4710.76,-1513.2 4820.72,-1497.53 4950.39,-1553.21 4957.4,-1451.18"/>
<polygon fill="#e0b25a" stroke="#e0b25a" stroke-width="1.4" points="4959.84,-1451.39 4957.62,-1444.32 4954.94,-1451.23 4959.84,-1451.39"/> <polygon fill="#56c8c8" stroke="#56c8c8" stroke-width="1.4" points="4959.84,-1451.39 4957.62,-1444.32 4954.94,-1451.23 4959.84,-1451.39"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="2147.25" y="-1523.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#e0b25a"> &#160;chatDrawerWidth &#160;</text> <text xml:space="preserve" text-anchor="middle" x="2147.25" y="-1523.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#56c8c8"> &#160;chatDrawerWidth &#160;</text>
</g> </g>
<!-- helpAutomat --> <!-- helpAutomat -->
<g id="node31" class="node"> <g id="node32" class="node">
<title>helpAutomat</title> <title>helpAutomat</title>
<polygon fill="#33415c" stroke="none" points="141.51,-1728 141.51,-1759 420.01,-1759 420.01,-1728 141.51,-1728"/> <polygon fill="#33415c" stroke="none" points="141.51,-1728 141.51,-1759 420.01,-1759 420.01,-1728 141.51,-1728"/>
<polygon fill="none" stroke="#3f4c5f" points="141.51,-1728 141.51,-1759 420.01,-1759 420.01,-1728 141.51,-1728"/> <polygon fill="none" stroke="#3f4c5f" points="141.51,-1728 141.51,-1759 420.01,-1759 420.01,-1728 141.51,-1728"/>
@@ -2096,17 +2139,17 @@
<text xml:space="preserve" text-anchor="start" x="335.85" y="-1625.8" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Survives page reloads</text> <text xml:space="preserve" text-anchor="start" x="335.85" y="-1625.8" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Survives page reloads</text>
</g> </g>
<!-- helpAutomat&#45;&gt;navigationAutomat --> <!-- helpAutomat&#45;&gt;navigationAutomat -->
<g id="edge57" class="edge"> <g id="edge58" class="edge">
<title>helpAutomat:e&#45;&gt;navigationAutomat:n</title> <title>helpAutomat:e&#45;&gt;navigationAutomat:n</title>
<g id="a_edge57"><a xlink:title="helpAutomat → navigationAutomat (route&#45;contextual help)"> <g id="a_edge58"><a xlink:title="helpAutomat → navigationAutomat (route&#45;contextual help)">
<path fill="none" stroke="#56c8c8" stroke-width="1.4" d="M421.01,-1695C479.45,-1695 424.66,-1499.24 419.85,-1428.89"/> <path fill="none" stroke="#ff5f9e" stroke-width="1.4" d="M421.01,-1695C479.45,-1695 424.66,-1499.24 419.85,-1428.89"/>
<polygon fill="#56c8c8" stroke="#56c8c8" stroke-width="1.4" points="422.31,-1429.22 419.6,-1422.32 417.42,-1429.41 422.31,-1429.22"/> <polygon fill="#ff5f9e" stroke="#ff5f9e" stroke-width="1.4" points="422.31,-1429.22 419.6,-1422.32 417.42,-1429.41 422.31,-1429.22"/>
</a> </a>
</g> </g>
<text xml:space="preserve" text-anchor="middle" x="482.6" y="-1523.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#56c8c8"> &#160;route&#45;contextual help &#160;</text> <text xml:space="preserve" text-anchor="middle" x="482.6" y="-1523.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#ff5f9e"> &#160;route&#45;contextual help &#160;</text>
</g> </g>
<!-- pwaAutomat --> <!-- pwaAutomat -->
<g id="node32" class="node"> <g id="node33" class="node">
<title>pwaAutomat</title> <title>pwaAutomat</title>
<polygon fill="#33415c" stroke="none" points="861.11,-1728 861.11,-1759 1122.4,-1759 1122.4,-1728 861.11,-1728"/> <polygon fill="#33415c" stroke="none" points="861.11,-1728 861.11,-1759 1122.4,-1759 1122.4,-1728 861.11,-1728"/>
<polygon fill="none" stroke="#3f4c5f" points="861.11,-1728 861.11,-1759 1122.4,-1759 1122.4,-1728 861.11,-1728"/> <polygon fill="none" stroke="#3f4c5f" points="861.11,-1728 861.11,-1759 1122.4,-1759 1122.4,-1728 861.11,-1728"/>
@@ -2144,7 +2187,7 @@
<text xml:space="preserve" text-anchor="start" x="1065.38" y="-1625.8" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Modal controls</text> <text xml:space="preserve" text-anchor="start" x="1065.38" y="-1625.8" font-family="Helvetica,sans-Serif" font-size="8.00" fill="#8b95a7">Modal controls</text>
</g> </g>
<!-- Legend --> <!-- Legend -->
<g id="node33" class="node"> <g id="node34" class="node">
<title>Legend</title> <title>Legend</title>
<polygon fill="#2a3242" stroke="none" points="1206.46,-1591.7 1206.46,-1785.3 1421.06,-1785.3 1421.06,-1591.7 1206.46,-1591.7"/> <polygon fill="#2a3242" stroke="none" points="1206.46,-1591.7 1206.46,-1785.3 1421.06,-1785.3 1421.06,-1591.7 1206.46,-1591.7"/>
<polygon fill="#33415c" stroke="none" points="1206.46,-1762.1 1206.46,-1785.3 1421.06,-1785.3 1421.06,-1762.1 1206.46,-1762.1"/> <polygon fill="#33415c" stroke="none" points="1206.46,-1762.1 1206.46,-1785.3 1421.06,-1785.3 1421.06,-1762.1 1206.46,-1762.1"/>

Before

Width:  |  Height:  |  Size: 255 KiB

After

Width:  |  Height:  |  Size: 260 KiB

View File

@@ -106,7 +106,7 @@ const CLUSTERS = [
{ {
id: 'audit_grids', id: 'audit_grids',
label: 'Audit & Virtual Grids', label: 'Audit & Virtual Grids',
automats: ['articleHistoryGridAutomat', 'globalHistoryGridAutomat'], automats: ['articleHistoryGridAutomat', 'globalHistoryGridAutomat', 'ledgerHistoryGridAutomat'],
}, },
{ {
id: 'system_ui', id: 'system_ui',
@@ -520,6 +520,17 @@ const AUTOMAT_SPECS = [
{ type: 'API', name: 'GET /api/globalhistory', desc: 'Company global audit' }, { 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 // 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: '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: '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: '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 // Real-time SSE dispatch
{ from: 'sseAutomat', to: 'categoryAutomat', label: '\'category\' sse trigger', port: 'p_sse_events' }, { from: 'sseAutomat', to: 'categoryAutomat', label: '\'category\' sse trigger', port: 'p_sse_events' },

View File

@@ -118,4 +118,15 @@ export const delivery = {
return http(`/api/delivery/${id}/file/${fileId}`, { method: 'DELETE' }); return http(`/api/delivery/${id}/file/${fileId}`, { method: 'DELETE' });
}, },
// #endregion // #endregion
/**
* POST /api/delivery/:id/pdf
* Generates the "Wareneingang" PDF and stores it as a manifest attachment.
* → 201 { attachment, fileUrl }
*/
// #region POST /api/delivery/:id/pdf — createPdf
createPdf(id) {
return http(`/api/delivery/${id}/pdf`, { method: 'POST' });
},
// #endregion
}; };

View File

@@ -7,6 +7,30 @@ import { http } from './http.js';
*/ */
export const ledger = { 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 * GET /api/ledger/manifestarticle/:manifestarticleId
* Query: ?transaction_type=... * Query: ?transaction_type=...

View File

@@ -12,6 +12,13 @@ export const supplier = {
}, },
// #endregion // #endregion
/** GET /api/supplier/:id → { supplier: Supplier } */
// #region GET /api/supplier/:id — get
get(id) {
return http(`/api/supplier/${encodeURIComponent(id)}`);
},
// #endregion
/** /**
* POST /api/supplier * POST /api/supplier
* Body: { name } — empty name ⇒ 400 Name required. * Body: { name } — empty name ⇒ 400 Name required.

View File

@@ -34,6 +34,7 @@ import OAuthAuthorizePage from './routes/oauth/OAuthAuthorizePage';
const ArticleHistoryPage = lazy(() => import('./routes/infos/ArticleHistoryPage')); const ArticleHistoryPage = lazy(() => import('./routes/infos/ArticleHistoryPage'));
const GlobalHistoryPage = lazy(() => import('./routes/infos/GlobalHistoryPage')); const GlobalHistoryPage = lazy(() => import('./routes/infos/GlobalHistoryPage'));
const LedgerHistoryPage = lazy(() => import('./routes/infos/LedgerHistoryPage'));
function LoginRedirect() { function LoginRedirect() {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
@@ -72,6 +73,7 @@ const routes = [
{ index: true, element: <ArticleHistoryPage /> }, { index: true, element: <ArticleHistoryPage /> },
{ path: 'articlehistory', element: <ArticleHistoryPage /> }, { path: 'articlehistory', element: <ArticleHistoryPage /> },
{ path: 'globalhistory', element: <GlobalHistoryPage /> }, { path: 'globalhistory', element: <GlobalHistoryPage /> },
{ path: 'ledgerhistory', element: <LedgerHistoryPage /> },
{ path: 'statistics', element: <StatisticsPage /> }, { path: 'statistics', element: <StatisticsPage /> },
], ],
}, },

View File

@@ -99,6 +99,19 @@ function toIndexRecord(article, categoryId) {
const eans = article.eans ?? article.eanCodes ?? article.ean ?? null; const eans = article.eans ?? article.eanCodes ?? article.ean ?? null;
const childCount = Number(article.childCount) || 0; const childCount = Number(article.childCount) || 0;
const price = article.price ?? article.net ?? article.vknetto ?? null; const price = article.price ?? article.net ?? article.vknetto ?? null;
const warehouses = Array.isArray(article.warehouses) ? article.warehouses : [];
const trackStock = article.track_stock ?? article.trackStock;
const stock = trackStock === false
? null
: (article.stock != null && !Number.isNaN(Number(article.stock))
? Number(article.stock)
: (trackStock || warehouses.length > 0
? warehouses.reduce((sum, wh) => sum + (Number(wh.stock) || 0), 0)
: null));
const available = article.available != null && !Number.isNaN(Number(article.available))
? Number(article.available)
: stock;
return { return {
...article, ...article,
id: article.id, id: article.id,
@@ -106,6 +119,12 @@ function toIndexRecord(article, categoryId) {
name: article.name, name: article.name,
price, price,
net: article.net ?? price, net: article.net ?? price,
stock,
available,
warehouses,
articlewarehousesummary: Array.isArray(article.articlewarehousesummary)
? article.articlewarehousesummary
: [],
eans: Array.isArray(eans) ? eans.filter(Boolean) : eans ? [eans] : [], eans: Array.isArray(eans) ? eans.filter(Boolean) : eans ? [eans] : [],
categories: categoryId != null ? String(categoryId) : null, categories: categoryId != null ? String(categoryId) : null,
isParent: Boolean(article.isParent ?? article.is_parent ?? (childCount > 0)), isParent: Boolean(article.isParent ?? article.is_parent ?? (childCount > 0)),
@@ -154,6 +173,15 @@ export function buildArticleIndex(views) {
existing.price = p; existing.price = p;
existing.net = article.net ?? p; existing.net = article.net ?? p;
} }
if (existing.stock == null && (article.warehouses?.length > 0 || article.stock != null)) {
const rec = toIndexRecord(article, categoryId);
if (rec.stock != null) {
existing.stock = rec.stock;
existing.available = rec.available;
existing.warehouses = rec.warehouses;
existing.articlewarehousesummary = rec.articlewarehousesummary;
}
}
} }
} }
@@ -280,6 +308,41 @@ export function setArticleIndexName(sku, name) {
); );
} }
/**
* Patches a single record's stock in the derived index (e.g. after detail load or stock adjustment).
*
* @param {string|number} sku Article SKU.
* @param {object} stockData Stock updates.
* @param {number|null} [stockData.stock] Total stock.
* @param {number|null} [stockData.available] Available stock.
* @param {Array<object>} [stockData.warehouses] Warehouse array.
* @param {Array<object>} [stockData.articlewarehousesummary] Summary array.
* @returns {void}
*/
export function setArticleIndexStock(sku, { stock, available, warehouses, articlewarehousesummary } = {}) {
const items = articleIndexAutomat.state.items;
if (!items.length) return;
articleIndexAutomat.actions.replace(
items.map((row) => {
if (String(row.sku) !== String(sku)) return row;
const whs = warehouses ?? row.warehouses ?? [];
const computedStock = stock !== undefined
? stock
: (whs.length > 0 ? whs.reduce((sum, w) => sum + (Number(w.stock) || 0), 0) : row.stock);
const computedAvailable = available !== undefined
? available
: (computedStock !== undefined ? computedStock : row.available);
return {
...row,
stock: computedStock,
available: computedAvailable,
...(warehouses ? { warehouses } : {}),
...(articlewarehousesummary ? { articlewarehousesummary } : {}),
};
}),
);
}
/** /**
* Selects the derived index records. * Selects the derived index records.
* *

View File

@@ -20,7 +20,7 @@ import { Automat } from 'react-automat';
import { article as articleApi } from '../api'; import { article as articleApi } from '../api';
import { requireDefined, requireObject } from '../utils/apiShape'; import { requireDefined, requireObject } from '../utils/apiShape';
import authAutomat from './authAutomat'; import authAutomat from './authAutomat';
import articleIndexAutomat, { setArticleIndexName } from './articleIndexAutomat'; import articleIndexAutomat, { setArticleIndexName, setArticleIndexStock } from './articleIndexAutomat';
/** /**
* @typedef {Object} ArtikelState * @typedef {Object} ArtikelState
@@ -113,6 +113,12 @@ const actions = {
artikelAutomat.setState({ artikelAutomat.setState({
bySku: { ...artikelAutomat.state.bySku, [key]: article }, bySku: { ...artikelAutomat.state.bySku, [key]: article },
}); });
if (article.warehouses || article.stock != null) {
setArticleIndexStock(article.sku, {
stock: article.stock,
warehouses: article.warehouses,
});
}
return article; return article;
}, },

View File

@@ -391,6 +391,8 @@ export function createChunkListAutomat({
? res.items ? res.items
: Array.isArray(res?.history) : Array.isArray(res?.history)
? res.history ? res.history
: Array.isArray(res?.ledger)
? res.ledger
: Array.isArray(res?.rows) : Array.isArray(res?.rows)
? res.rows ? res.rows
: Array.isArray(res?.entries) : Array.isArray(res?.entries)

View File

@@ -83,7 +83,7 @@ const initialState = {
total: 0, total: 0,
unfilteredTotal: 0, unfilteredTotal: 0,
filter: '', filter: '',
onlyUnbooked: false, onlyUnbooked: true,
supplierId: null, supplierId: null,
seekMonths: 0, seekMonths: 0,
seekYears: 0, seekYears: 0,
@@ -365,7 +365,7 @@ export function getChunkStats() {
status: automat?.state?.status ?? 'idle', status: automat?.state?.status ?? 'idle',
queryEpoch, queryEpoch,
filter: automat?.state?.filter ?? '', filter: automat?.state?.filter ?? '',
onlyUnbooked: automat?.state?.onlyUnbooked ?? false, onlyUnbooked: automat?.state?.onlyUnbooked ?? true,
supplierId: automat?.state?.supplierId ?? null, supplierId: automat?.state?.supplierId ?? null,
negativeSeekMonths: automat?.state?.negativeSeekMonths ?? 0, negativeSeekMonths: automat?.state?.negativeSeekMonths ?? 0,
items, items,
@@ -885,7 +885,7 @@ const actions = {
resetFilters() { resetFilters() {
automat.setState({ automat.setState({
filter: '', filter: '',
onlyUnbooked: false, onlyUnbooked: true,
supplierId: null, supplierId: null,
seekMonths: 0, seekMonths: 0,
seekYears: 0, seekYears: 0,

View File

@@ -62,6 +62,14 @@ import { article as articleApi, supplier as supplierApi } from '../api';
* @property {string|null} unmatchedSupplierName Parsed supplier name without a matching entity. * @property {string|null} unmatchedSupplierName Parsed supplier name without a matching entity.
*/ */
function parseNum(value) {
if (value == null || value === '') return null;
const cleaned = String(value).trim().replace('%', '').trim().replace(',', '.');
if (cleaned === '') return null;
const n = Number(cleaned);
return Number.isFinite(n) ? n : null;
}
/** /**
* Normalizes a partial line into an editor line with a stable key. * Normalizes a partial line into an editor line with a stable key.
* *
@@ -69,14 +77,29 @@ import { article as articleApi, supplier as supplierApi } from '../api';
* @returns {DeliveryLine} Normalized line. * @returns {DeliveryLine} Normalized line.
*/ */
export function createEmptyLineitem(line = {}) { export function createEmptyLineitem(line = {}) {
const netVal = line.netprice ?? line.net ?? null; let netVal = null;
const taxVal = line.taxrate ?? line.tax ?? null; if (line.netprice != null || line.net != null || line.netPrice != null) {
const netRaw = line.netprice ?? line.net ?? line.netPrice;
netVal = netRaw != null ? (parseNum(netRaw) ?? netRaw) : null;
} else if (line.eknetto != null && line.eknetto !== '') {
const parsedCent = parseNum(line.eknetto);
netVal = parsedCent != null ? Math.round((parsedCent / 100) * 10000) / 10000 : null;
}
const taxRaw = line.taxrate ?? line.tax ?? line.mwst ?? line.taxRate ?? null;
const amountRaw = line.amount ?? line.count ?? line.qty ?? line.quantity ?? null;
const puRaw = line.pu ?? line.vpe ?? null;
const taxVal = taxRaw != null ? (parseNum(taxRaw) ?? taxRaw) : null;
const amountVal = amountRaw != null ? (parseNum(amountRaw) ?? amountRaw) : null;
const puVal = puRaw != null ? (parseNum(puRaw) ?? puRaw) : null;
return { return {
key: line.key ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, key: line.key ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
name: line.name ?? null, name: line.name ?? line.articleName ?? line.title ?? null,
sku: line.sku ?? null, sku: line.sku ?? line.liefArtNr ?? line.articleNumber ?? null,
amount: line.amount ?? null, amount: amountVal,
pu: line.pu ?? null, pu: puVal,
unit: line.unit ?? null, unit: line.unit ?? null,
netprice: netVal, netprice: netVal,
net: netVal, net: netVal,
@@ -657,6 +680,82 @@ export function createDeliveryEditorAutomat(options = {}) {
}); });
}, },
/**
* Loads a delivery manifest (e.g. from draft_delivery_manifest tool) into the editor.
* @param {Object} manifestData
* @returns {void}
*/
loadDraftManifest(manifestData) {
if (!manifestData || typeof manifestData !== 'object') return;
const data = manifestData.manifest ?? manifestData;
const basedata = data.basedata ?? data.baseData ?? data;
const rawItems =
data.items?.items ??
data.items ??
data.lines ??
data.lineitems ??
[];
const itemsList = Array.isArray(rawItems) ? rawItems : [];
const partyId =
basedata.lieferant_id ??
basedata.lieferantId ??
basedata.partyId ??
basedata.deliveryEntity ??
basedata.supplier_id ??
basedata.supplierId ??
null;
const supplierName =
basedata.lieferantName ??
basedata.supplierName ??
basedata.unmatchedSupplierName ??
null;
const extNum =
basedata.manifestNr ??
basedata.manifestNumber ??
basedata.externalNumber ??
basedata.external_number ??
basedata.invoiceNumber ??
basedata.invoice_number ??
null;
const date = basedata.date ?? null;
const type = basedata.manifestType ?? basedata.type ?? 'rechnung';
let matchedPartyId = partyId != null && partyId !== '' ? partyId : null;
if (!matchedPartyId && supplierName && automat.state.deliveryEntities?.length > 0) {
const normName = String(supplierName).trim().toLowerCase();
const found = automat.state.deliveryEntities.find(
(e) =>
(e.name && String(e.name).trim().toLowerCase() === normName) ||
(e.company && String(e.company).trim().toLowerCase() === normName)
);
if (found?.id != null) {
matchedPartyId = found.id;
}
}
const lines = itemsList.map((item, idx) =>
createEmptyLineitem({
...item,
key: item.key ?? `manifest-${Date.now()}-${idx}-${Math.random().toString(36).slice(2, 6)}`,
})
);
automat.actions.setDelivery({
id: null,
deliveryId: null,
date,
partyId: matchedPartyId,
deliveryEntity: matchedPartyId,
unmatchedSupplierName: matchedPartyId != null ? null : supplierName,
externalNumber: extNum,
external_number: extNum,
type,
lines,
lineitems: lines,
});
},
/** Serializes the delivery into a VFS draft manifest format. @returns {object} */ /** Serializes the delivery into a VFS draft manifest format. @returns {object} */
getManifest() { getManifest() {
const state = automat.state; const state = automat.state;
@@ -724,6 +823,7 @@ export const {
setId, setId,
setDeliveryEntities, setDeliveryEntities,
setDelivery, setDelivery,
loadDraftManifest,
setDate, setDate,
setDeliveryEntity, setDeliveryEntity,
setPartyId, setPartyId,

View File

@@ -62,6 +62,7 @@ export {
export { export {
default as articleIndexAutomat, default as articleIndexAutomat,
setArticleIndexName, setArticleIndexName,
setArticleIndexStock,
buildArticleIndex, buildArticleIndex,
selectArticleIndexItems, selectArticleIndexItems,
} from './articleIndexAutomat'; } from './articleIndexAutomat';
@@ -190,6 +191,7 @@ export {
export { default as articleHistoryGridAutomat } from './articleHistoryAutomat'; export { default as articleHistoryGridAutomat } from './articleHistoryAutomat';
export { default as globalHistoryAutomat, globalHistoryGridAutomat } from './globalHistoryAutomat'; export { default as globalHistoryAutomat, globalHistoryGridAutomat } from './globalHistoryAutomat';
export { default as ledgerHistoryAutomat, ledgerHistoryGridAutomat } from './ledgerHistoryAutomat';
export { default as chatAutomat } from './chatAutomat'; export { default as chatAutomat } from './chatAutomat';
export { export {

View File

@@ -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;

View File

@@ -222,6 +222,10 @@ function connect() {
void warehouseAutomat.actions.reload().catch((err) => { void warehouseAutomat.actions.reload().catch((err) => {
console.error('[sse] warehouse refresh failed', err); console.error('[sse] warehouse refresh failed', err);
}); });
invalidateAllCategoryViews();
void articleIndexAutomat.actions.reload().catch((err) => {
console.error('[sse] article index refresh on warehouse event failed', err);
});
}); });
addTrackedListener(es, 'carrier', () => { addTrackedListener(es, 'carrier', () => {

View File

@@ -229,7 +229,21 @@ class SearchResultRow extends PureComponent {
</> </>
)} )}
{article.sku} {article.sku}
{article.stock != null && ` · Bestand ${formatStock(article.stock)}`} {article.stock != null && (
<>
{' · '}
<Box
component="span"
title={
article.warehouses?.length > 0
? article.warehouses.map((w) => `${w.name || 'Lager'}: ${formatStock(w.stock)}`).join(' · ')
: undefined
}
>
Bestand {formatStock(article.stock)}
</Box>
</>
)}
{article.available != null && ` · Verfügbar ${formatStock(article.available)}`} {article.available != null && ` · Verfügbar ${formatStock(article.available)}`}
{primaryEan ? ` · EAN ${primaryEan}` : ''} {primaryEan ? ` · EAN ${primaryEan}` : ''}
</Typography> </Typography>

View File

@@ -19,13 +19,17 @@ import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import GoogleIcon from '@mui/icons-material/Google'; import GoogleIcon from '@mui/icons-material/Google';
import WebIcon from '@mui/icons-material/Web'; import WebIcon from '@mui/icons-material/Web';
import EscalatorWarningIcon from '@mui/icons-material/EscalatorWarning'; import EngineeringIcon from '@mui/icons-material/Engineering';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import ManageSearchIcon from '@mui/icons-material/ManageSearch'; import ManageSearchIcon from '@mui/icons-material/ManageSearch';
import LensBlurIcon from '@mui/icons-material/LensBlur'; import LensBlurIcon from '@mui/icons-material/LensBlur';
import CalculateIcon from '@mui/icons-material/Calculate'; import CalculateIcon from '@mui/icons-material/Calculate';
import QrCodeScannerIcon from '@mui/icons-material/QrCodeScanner'; import QrCodeScannerIcon from '@mui/icons-material/QrCodeScanner';
import DirectionsIcon from '@mui/icons-material/Directions'; import DirectionsIcon from '@mui/icons-material/Directions';
import EditNoteIcon from '@mui/icons-material/EditNote';
import Button from '@mui/material/Button';
import { enqueueSnackbar } from 'notistack';
import { deliveryEditorAutomat, setLastDeliveryPath } from '../../automats';
import HealingMarkdown from './HealingMarkdown'; import HealingMarkdown from './HealingMarkdown';
import { import {
VfsAttachmentLink, VfsAttachmentLink,
@@ -87,7 +91,9 @@ const TOOL_ICON_MAP = {
rm: DeleteIcon, rm: DeleteIcon,
web_search: GoogleFaviconIcon, web_search: GoogleFaviconIcon,
url_get: WebIcon, url_get: WebIcon,
sub_agent: EscalatorWarningIcon, subagent: EngineeringIcon,
sub_agent: EngineeringIcon,
'sub-agent': EngineeringIcon,
pdf_read: PictureAsPdfIcon, pdf_read: PictureAsPdfIcon,
search_articles: ManageSearchIcon, search_articles: ManageSearchIcon,
edit_image: LensBlurIcon, edit_image: LensBlurIcon,
@@ -95,6 +101,8 @@ const TOOL_ICON_MAP = {
ean_checksum: QrCodeScannerIcon, ean_checksum: QrCodeScannerIcon,
'set-user-route': DirectionsIcon, 'set-user-route': DirectionsIcon,
set_user_route: DirectionsIcon, set_user_route: DirectionsIcon,
draft_delivery_manifest: EditNoteIcon,
'draft-delivery-manifest': EditNoteIcon,
}; };
function formatMessageTime(isoString) { function formatMessageTime(isoString) {
@@ -486,16 +494,145 @@ class UrlFaviconIcon extends PureComponent {
} }
} }
export function isDraftDeliveryManifestTool(name) {
if (!name || typeof name !== 'string') return false;
const n = name.trim().toLowerCase();
return n === 'draft_delivery_manifest' || n === 'draft-delivery-manifest';
}
function tryExtractObject(val) {
if (!val) return null;
if (typeof val === 'object') return val;
if (typeof val === 'string') {
const trimmed = val.trim();
try {
return JSON.parse(trimmed);
} catch {}
const match = trimmed.match(/\{[\s\S]*\}/);
if (match) {
try {
return JSON.parse(match[0]);
} catch {}
}
}
return null;
}
export function parseDraftDeliveryManifest(tool) {
if (!tool) return null;
const candidates = [tool.parameters, tool.arguments, tool.content, tool.result, tool];
for (const c of candidates) {
const obj = tryExtractObject(c);
if (obj && typeof obj === 'object') {
if (obj.manifest && typeof obj.manifest === 'object') {
return obj.manifest;
}
if (obj.basedata || obj.baseData || obj.items || obj.lines || obj.lineitems) {
return obj;
}
}
}
return null;
}
class DraftDeliveryManifestUseButton extends PureComponent {
state = {
applied: false,
};
handleClick = async (e) => {
e?.stopPropagation();
const { tool } = this.props;
const manifest = parseDraftDeliveryManifest(tool);
if (!manifest) {
enqueueSnackbar('Keine gültigen Lieferscheindaten gefunden', { variant: 'warning' });
return;
}
deliveryEditorAutomat.actions.loadDraftManifest(manifest);
setLastDeliveryPath('/lieferung/neu');
this.setState({ applied: true });
setTimeout(() => {
this.setState({ applied: false });
}, 2500);
const count =
manifest.items?.numberOfItemsToAdd ??
(Array.isArray(manifest.items?.items)
? manifest.items.items.length
: Array.isArray(manifest.items)
? manifest.items.length
: Array.isArray(manifest.lines)
? manifest.lines.length
: 0);
enqueueSnackbar(
count > 0
? `${count} Positionen in den Lieferschein-Editor geladen`
: 'Lieferschein in den Editor geladen',
{ variant: 'success' }
);
try {
const { router } = await import('../../appRouter');
if (router?.navigate) {
const curPath =
router.state?.location?.pathname ||
(typeof window !== 'undefined' ? window.location.pathname : '');
if (!curPath.startsWith('/lieferung/neu')) {
void router.navigate('/lieferung/neu');
}
}
} catch (err) {
console.warn('[DraftDeliveryManifestUseButton] Navigation failed:', err);
}
};
render() {
const { applied } = this.state;
return (
<Button
size="small"
variant={applied ? 'contained' : 'outlined'}
color={applied ? 'success' : 'primary'}
onClick={this.handleClick}
sx={{
height: 20,
minHeight: 20,
py: 0,
px: 0.9,
fontSize: '0.72rem',
fontWeight: 700,
textTransform: 'none',
lineHeight: 1,
borderRadius: '4px',
borderColor: applied ? undefined : 'rgba(0, 0, 0, 0.23)',
bgcolor: applied ? 'success.main' : 'transparent',
color: applied ? '#ffffff' : 'inherit',
'&:hover': {
bgcolor: applied ? 'success.dark' : 'action.hover',
borderColor: applied ? undefined : 'primary.main',
},
}}
>
{applied ? '✓ Use' : 'Use'}
</Button>
);
}
}
function WorkIconView({ tool }) { function WorkIconView({ tool }) {
if (!tool || !tool.name) return null; if (!tool || !tool.name) return null;
const isGreen = tool?.state === 'green'; const isGreen = tool?.state === 'green';
const isUrlGet = tool?.name === 'url_get'; const isUrlGet = tool?.name === 'url_get';
const isDraftDelivery = isDraftDeliveryManifestTool(tool?.name);
const ToolNameIcon = tool?.name ? TOOL_ICON_MAP[tool.name] : null; const ToolNameIcon = tool?.name ? TOOL_ICON_MAP[tool.name] : null;
const prefix = ToolNameIcon ? tool?.name : null; const prefix = ToolNameIcon ? tool?.name : null;
const tooltipText = formatToolTooltip(tool?.content, tool?.result, tool?.parameters || tool?.arguments, prefix); const tooltipText = formatToolTooltip(tool?.content, tool?.result, tool?.parameters || tool?.arguments, prefix);
const targetUrl = isUrlGet ? extractUrlFromTool(tool) : null; const targetUrl = isUrlGet ? extractUrlFromTool(tool) : null;
return ( const iconElement = (
<ClickTooltip <ClickTooltip
title={ title={
<Box <Box
@@ -554,6 +691,17 @@ function WorkIconView({ tool }) {
</Box> </Box>
</ClickTooltip> </ClickTooltip>
); );
if (isDraftDelivery) {
return (
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.5 }}>
{iconElement}
<DraftDeliveryManifestUseButton tool={tool} />
</Box>
);
}
return iconElement;
} }
function UsageChipView({ usage }) { function UsageChipView({ usage }) {

View File

@@ -162,6 +162,10 @@ export class Attachment extends PureComponent {
const name = attachment?.name || 'Dokument'; const name = attachment?.name || 'Dokument';
const size = attachment?.size; const size = attachment?.size;
const isPdf = attachment?.type === 'application/pdf' || name.toLowerCase().endsWith('.pdf'); const isPdf = attachment?.type === 'application/pdf' || name.toLowerCase().endsWith('.pdf');
const isGeneratedDeliveryPdf =
name.toLowerCase().startsWith('wareneingang-') ||
attachment?.meta?.type === 'delivery_pdf' ||
attachment?.meta?.type === 'wareneingang_pdf';
const isCsv = attachment?.type === 'text/csv' || name.toLowerCase().endsWith('.csv'); const isCsv = attachment?.type === 'text/csv' || name.toLowerCase().endsWith('.csv');
const isText = attachment?.type?.startsWith('text/') || name.toLowerCase().endsWith('.txt') || isCsv; const isText = attachment?.type?.startsWith('text/') || name.toLowerCase().endsWith('.txt') || isCsv;
@@ -282,7 +286,7 @@ export class Attachment extends PureComponent {
}} }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{kind === 'delivery' && Boolean(onRescan) && isPdf ? ( {kind === 'delivery' && Boolean(onRescan) && isPdf && !isGeneratedDeliveryPdf ? (
<IconButton <IconButton
size="small" size="small"
disabled={disabled} disabled={disabled}

View File

@@ -98,11 +98,11 @@ export default class BookDeliveryDialog extends PureComponent {
loadSupplierArticlesIfNeeded() { loadSupplierArticlesIfNeeded() {
const supplierId = this.props.delivery?.supplierId ?? this.props.delivery?.partyId; const supplierId = this.props.delivery?.supplierId ?? this.props.delivery?.partyId;
if (supplierId && Object.keys(this.state.supplierArticlesMap).length === 0) { if (supplierId && Object.keys(this.state.supplierArticlesMap).length === 0) {
void supplierApi.get(supplierId).then((res) => { void supplierApi.getArticles(supplierId).then((res) => {
const articles = res?.supplier?.articles || res?.articles || []; const articles = res?.articles || res?.supplier?.articles || [];
const map = buildSupplierArticlesMap(articles); const map = buildSupplierArticlesMap(articles);
this.setState({ supplierArticlesMap: map }); this.setState({ supplierArticlesMap: map });
}).catch(() => {}); }).catch(() => { });
} }
} }
@@ -122,7 +122,7 @@ export default class BookDeliveryDialog extends PureComponent {
this.unsubArticle = artikelAutomat.subscribe(() => { this.unsubArticle = artikelAutomat.subscribe(() => {
this.forceUpdate(); this.forceUpdate();
}); });
void warehouseAutomat.actions.load().catch(() => {}); void warehouseAutomat.actions.load().catch(() => { });
this.loadSupplierArticlesIfNeeded(); this.loadSupplierArticlesIfNeeded();
if (this.props.open) { if (this.props.open) {
@@ -225,7 +225,7 @@ export default class BookDeliveryDialog extends PureComponent {
supplierArticlesMap: currentSupplierMap, supplierArticlesMap: currentSupplierMap,
}, },
() => { () => {
void warehouseAutomat.actions.load().catch(() => {}); void warehouseAutomat.actions.load().catch(() => { });
this.loadSupplierArticlesIfNeeded(); this.loadSupplierArticlesIfNeeded();
if (this.props.summary) { if (this.props.summary) {
this.initAmountsFromSummary(this.props.summary, this.props.prefillLineId); this.initAmountsFromSummary(this.props.summary, this.props.prefillLineId);
@@ -535,7 +535,7 @@ export default class BookDeliveryDialog extends PureComponent {
(line.sku ? String(line.sku).trim() : null); (line.sku ? String(line.sku).trim() : null);
if (targetSku && !cachedArticle) { if (targetSku && !cachedArticle) {
void artikelAutomat.actions.loadArticle(targetSku).catch(() => {}); void artikelAutomat.actions.loadArticle(targetSku).catch(() => { });
} }
const isTracked = const isTracked =
@@ -791,11 +791,7 @@ export default class BookDeliveryDialog extends PureComponent {
</Alert> </Alert>
)} )}
{isFullyBooked ? ( {isFullyBooked ? null : isWarehouseMissing ? (
<Alert severity="success" icon={<CheckCircleOutlineOutlinedIcon />}>
Alle Positionen dieser Lieferung sind bereits vollständig im Ledger verbucht.
</Alert>
) : isWarehouseMissing ? (
<Alert severity="warning"> <Alert severity="warning">
Bitte wählen Sie ein <strong>Ziellager</strong> aus, um die lagergeführten Artikel zu buchen. Bitte wählen Sie ein <strong>Ziellager</strong> aus, um die lagergeführten Artikel zu buchen.
</Alert> </Alert>

View File

@@ -267,7 +267,6 @@ class DeliveryRow extends Component {
{totalPositions === 0 ? ( {totalPositions === 0 ? (
<Typography variant="body2" color="text.secondary">0</Typography> <Typography variant="body2" color="text.secondary">0</Typography>
) : unbooked === 0 ? ( ) : unbooked === 0 ? (
<Tooltip title={`Alle ${totalPositions} Positionen vollständig ins Lager gebucht`}>
<Chip <Chip
size="small" size="small"
icon={<CheckCircleOutlineOutlinedIcon sx={{ '&&': { fontSize: 16 } }} />} icon={<CheckCircleOutlineOutlinedIcon sx={{ '&&': { fontSize: 16 } }} />}
@@ -277,10 +276,8 @@ class DeliveryRow extends Component {
onClick={() => onOpenBookDialog?.(delivery)} onClick={() => onOpenBookDialog?.(delivery)}
sx={{ fontWeight: 600, cursor: 'pointer' }} sx={{ fontWeight: 600, cursor: 'pointer' }}
/> />
</Tooltip>
) : ( ) : (
<> <>
<Tooltip title={`${unbooked} von ${totalPositions} Positionen noch ungebucht`}>
<Chip <Chip
size="small" size="small"
icon={<Inventory2OutlinedIcon sx={{ '&&': { fontSize: 16 } }} />} icon={<Inventory2OutlinedIcon sx={{ '&&': { fontSize: 16 } }} />}
@@ -290,7 +287,6 @@ class DeliveryRow extends Component {
onClick={() => onOpenBookDialog?.(delivery)} onClick={() => onOpenBookDialog?.(delivery)}
sx={{ fontWeight: 600, cursor: 'pointer' }} sx={{ fontWeight: 600, cursor: 'pointer' }}
/> />
</Tooltip>
<Button <Button
size="small" size="small"
variant="contained" variant="contained"
@@ -411,7 +407,7 @@ class DeliverySearchField extends PureComponent {
const stats = deliveryAutomat.getChunkStats?.(); const stats = deliveryAutomat.getChunkStats?.();
this.state = { this.state = {
input: s.filter ?? '', input: s.filter ?? '',
onlyUnbooked: s.onlyUnbooked ?? false, onlyUnbooked: s.onlyUnbooked ?? true,
seekMonths: s.seekMonths ?? 0, seekMonths: s.seekMonths ?? 0,
seekYears: s.seekYears ?? 0, seekYears: s.seekYears ?? 0,
negativeSeekMonths: s.negativeSeekMonths ?? 0, negativeSeekMonths: s.negativeSeekMonths ?? 0,
@@ -424,7 +420,7 @@ class DeliverySearchField extends PureComponent {
componentDidMount() { componentDidMount() {
this.unsub = deliveryAutomat.subscribe(this, (state) => ({ this.unsub = deliveryAutomat.subscribe(this, (state) => ({
input: state.filter ?? '', input: state.filter ?? '',
onlyUnbooked: state.onlyUnbooked ?? false, onlyUnbooked: state.onlyUnbooked ?? true,
seekMonths: state.seekMonths ?? 0, seekMonths: state.seekMonths ?? 0,
seekYears: state.seekYears ?? 0, seekYears: state.seekYears ?? 0,
negativeSeekMonths: state.negativeSeekMonths ?? 0, negativeSeekMonths: state.negativeSeekMonths ?? 0,
@@ -606,7 +602,6 @@ class DeliverySearchField extends PureComponent {
</Box> </Box>
{import.meta.env.DEV && ( {import.meta.env.DEV && (
<Tooltip title={`Chunk-Cache-Monitor öffnen (Alt+Shift+C) — ${this.state.lruChunksCount || 0}/${this.state.maxLruChunks || 50} Chunks im RAM über ${this.state.filterCachesCount || 1} Filter`}>
<Chip <Chip
size="small" size="small"
icon={<ViewModuleIcon sx={{ fontSize: 16 }} />} icon={<ViewModuleIcon sx={{ fontSize: 16 }} />}
@@ -622,7 +617,6 @@ class DeliverySearchField extends PureComponent {
'&:hover': { bgcolor: 'action.hover' }, '&:hover': { bgcolor: 'action.hover' },
}} }}
/> />
</Tooltip>
)} )}
</Box> </Box>
</Box> </Box>
@@ -676,7 +670,7 @@ export class DeliveryList extends PureComponent {
this.headerItem = createDummyTestDelivery(); this.headerItem = createDummyTestDelivery();
this.forceUpdate(); this.forceUpdate();
}); });
void numberAutomat.actions.ensureLoaded().catch(() => {}); void numberAutomat.actions.ensureLoaded().catch(() => { });
void this.loadSuppliers(); void this.loadSuppliers();
} }
@@ -711,7 +705,7 @@ export class DeliveryList extends PureComponent {
} }
handleRetry() { handleRetry() {
void deliveryAutomat.actions.requireChunk(0, 0, { immediate: true }).catch(() => {}); void deliveryAutomat.actions.requireChunk(0, 0, { immediate: true }).catch(() => { });
} }
handleCreate() { handleCreate() {

View File

@@ -530,7 +530,7 @@ class ManifestHeader extends PureComponent {
{orderCreating ? 'Wird erstellt…' : 'Bestellung erstellen'} {orderCreating ? 'Wird erstellt…' : 'Bestellung erstellen'}
</Button> </Button>
)} )}
{(kind === 'quote' || kind === 'order') && onPdfCreate && docId != null && Boolean(number) && ( {(kind === 'quote' || kind === 'order' || kind === 'delivery') && onPdfCreate && docId != null && Boolean(number) && (
<Button <Button
type="button" type="button"
variant="outlined" variant="outlined"

View File

@@ -394,6 +394,7 @@ export async function restoreAttachmentsFromApi(apiFiles = [], existingAttachmen
text, text,
parentPdfId, parentPdfId,
uploadedAt: apiFile.created, uploadedAt: apiFile.created,
meta: apiFile.meta || matchingExisting?.meta || null,
}); });
} catch (err) { } catch (err) {
console.error('Fehler beim Wiederherstellen von Datei:', err); console.error('Fehler beim Wiederherstellen von Datei:', err);
@@ -454,7 +455,7 @@ export function collectFilesForUpload(attachments = [], { onlyNew = false } = {}
*/ */
export async function buildManifestBody( export async function buildManifestBody(
form, form,
{ partyField, requireAtLeastOne, externalNumber: useExternalNumber, kind, isUpdate, mode }, { partyField, requireAtLeastOne, externalNumber: useExternalNumber, kind, isUpdate, mode, omitLines },
) { ) {
const date = form.date; const date = form.date;
if (!date) throw new Error('Datum fehlt.'); if (!date) throw new Error('Datum fehlt.');
@@ -462,6 +463,7 @@ export async function buildManifestBody(
const externalNumber = String(form.externalNumber ?? '').trim(); const externalNumber = String(form.externalNumber ?? '').trim();
const isEdit = isUpdate != null ? Boolean(isUpdate) : (mode === 'edit' || !requireAtLeastOne); const isEdit = isUpdate != null ? Boolean(isUpdate) : (mode === 'edit' || !requireAtLeastOne);
const shouldRequireLine = requireAtLeastOne !== false; const shouldRequireLine = requireAtLeastOne !== false;
const shouldIncludeLines = !omitLines && form.lines !== undefined && form.lines !== null;
let body; let body;
if (kind === 'delivery') { if (kind === 'delivery') {
@@ -470,8 +472,10 @@ export async function buildManifestBody(
body = { body = {
externalNumber: externalNumber || null, externalNumber: externalNumber || null,
date, date,
lines: buildLines(form.lines, { requireAtLeastOne: shouldRequireLine }),
}; };
if (shouldIncludeLines) {
body.lines = buildLines(form.lines, { requireAtLeastOne: shouldRequireLine });
}
} else { } else {
let number = String(form.number ?? '').trim(); let number = String(form.number ?? '').trim();
if (!number) { if (!number) {
@@ -481,8 +485,10 @@ export async function buildManifestBody(
body = { body = {
number, number,
date, date,
lines: buildLines(form.lines, { requireAtLeastOne: shouldRequireLine }),
}; };
if (shouldIncludeLines) {
body.lines = buildLines(form.lines, { requireAtLeastOne: shouldRequireLine });
}
if (useExternalNumber || form.externalNumber != null) { if (useExternalNumber || form.externalNumber != null) {
body.externalNumber = externalNumber || null; body.externalNumber = externalNumber || null;
} }

View File

@@ -5,6 +5,7 @@ import ButtonBase from '@mui/material/ButtonBase';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { TILE_SIZE, Tile, tileBaseSx } from 'dashgui/tiles'; import { TILE_SIZE, Tile, tileBaseSx } from 'dashgui/tiles';
import { formatPrice, getArticleGross } from '../../utils/price'; import { formatPrice, getArticleGross } from '../../utils/price';
import { formatStock } from '../article/searchFormat';
import { import {
getCachedImage, getCachedImage,
getThumbHashDataUrl, getThumbHashDataUrl,
@@ -357,6 +358,19 @@ export class ArticleTile extends Tile {
const sku = articleSku(article); const sku = articleSku(article);
const grossPrice = getArticleGross(article, companyAutomat.state.current?.taxrate); const grossPrice = getArticleGross(article, companyAutomat.state.current?.taxrate);
const warehouses = Array.isArray(article?.warehouses) ? article.warehouses : [];
const isTracked = article?.track_stock ?? article?.trackStock ?? (warehouses.length > 0);
const stockVal = isTracked === false
? null
: (article?.stock != null && !Number.isNaN(Number(article.stock))
? Number(article.stock)
: (warehouses.length > 0 || isTracked
? warehouses.reduce((sum, wh) => sum + (Number(wh.stock) || 0), 0)
: null));
const warehouseTooltip = warehouses.length > 0
? warehouses.map((w) => `${w.name || 'Lager'}: ${formatStock(w.stock)}`).join(' · ')
: undefined;
return ( return (
<ButtonBase <ButtonBase
component="button" component="button"
@@ -488,7 +502,17 @@ export class ArticleTile extends Tile {
{sku || '\u00A0'} {sku || '\u00A0'}
</Typography> </Typography>
{/* Row 5: Price */} {/* Row 5: Price & Stock */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 0.5,
mt: 0.25,
height: '1.2em',
}}
>
<Typography <Typography
variant="caption" variant="caption"
component="div" component="div"
@@ -497,8 +521,6 @@ export class ArticleTile extends Tile {
fontWeight: 700, fontWeight: 700,
fontSize: '0.8rem', fontSize: '0.8rem',
lineHeight: 1.2, lineHeight: 1.2,
height: '1.2em',
mt: 0.25,
textShadow: TEXT_GLOW_DARK, textShadow: TEXT_GLOW_DARK,
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
@@ -507,6 +529,25 @@ export class ArticleTile extends Tile {
> >
{grossPrice != null ? formatPrice(grossPrice) : '\u00A0'} {grossPrice != null ? formatPrice(grossPrice) : '\u00A0'}
</Typography> </Typography>
{stockVal != null && (
<Typography
variant="caption"
component="div"
title={warehouseTooltip}
sx={{
color: ICE,
fontWeight: 600,
fontSize: '0.75rem',
lineHeight: 1.2,
textShadow: TEXT_GLOW_DARK,
whiteSpace: 'nowrap',
flexShrink: 0,
}}
>
{formatStock(stockVal)}
</Typography>
)}
</Box>
</Box> </Box>
</ButtonBase> </ButtonBase>
); );

View File

@@ -22,6 +22,7 @@ import LabelIcon from '@mui/icons-material/Label';
import HistoryIcon from '@mui/icons-material/History'; import HistoryIcon from '@mui/icons-material/History';
import PublicIcon from '@mui/icons-material/Public'; import PublicIcon from '@mui/icons-material/Public';
import ReceiptLongIcon from '@mui/icons-material/ReceiptLong';
import BarChartIcon from '@mui/icons-material/BarChart'; import BarChartIcon from '@mui/icons-material/BarChart';
import { isKategorienPath } from './components/category/categoryRoute'; import { isKategorienPath } from './components/category/categoryRoute';
import { deliveryEditorAutomat } from './automats/deliveryEditorAutomat'; import { deliveryEditorAutomat } from './automats/deliveryEditorAutomat';
@@ -273,6 +274,13 @@ export const SUB_NAV = {
icon: PublicIcon, icon: PublicIcon,
help: 'Globale System- und Änderungshistorie', help: 'Globale System- und Änderungshistorie',
}, },
{
segment: 'ledgerhistory',
label: 'LedgerHistory',
title: 'LedgerHistory',
icon: ReceiptLongIcon,
help: 'Unveränderliches Buchungs- und Bestandsjournal (Audit-Ledger)',
},
{ {
segment: 'statistics', segment: 'statistics',
label: 'Statistics', label: 'Statistics',

View File

@@ -218,7 +218,16 @@ class ResultRow extends Component {
)} )}
{row.sku} {row.sku}
{' · '} {' · '}
<Box
component="span"
title={
row.warehouses?.length > 0
? row.warehouses.map((w) => `${w.name || 'Lager'}: ${formatStock(w.stock)}`).join(' · ')
: undefined
}
>
Bestand {formatStock(row.stock)} Bestand {formatStock(row.stock)}
</Box>
{' · '} {' · '}
Verfügbar {formatStock(row.available)} Verfügbar {formatStock(row.available)}
{row.categories {row.categories

View File

@@ -52,7 +52,7 @@ export function createDummyTestArticleHistory() {
const rowGridSx = { const rowGridSx = {
display: 'grid', display: 'grid',
gridTemplateColumns: gridTemplateColumns:
'minmax(60px, max-content) minmax(150px, max-content) minmax(120px, max-content) minmax(160px, 1.2fr) minmax(240px, 2fr) minmax(120px, max-content)', 'minmax(150px, max-content) minmax(130px, max-content) minmax(180px, 1.2fr) minmax(260px, 2fr)',
gridTemplateRows: '0px 1fr', gridTemplateRows: '0px 1fr',
rowGap: 0, rowGap: 0,
columnGap: 2, columnGap: 2,
@@ -86,23 +86,17 @@ function renderInvisibleDummyRow(dummy) {
return ( return (
<> <>
<Box aria-hidden sx={{ gridColumn: 1, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}> <Box aria-hidden sx={{ gridColumn: 1, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
<Typography variant="body2">{d.id}</Typography>
</Box>
<Box aria-hidden sx={{ gridColumn: 2, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
<Typography variant="body2">{formatDateTime(d.date)}</Typography> <Typography variant="body2">{formatDateTime(d.date)}</Typography>
</Box> </Box>
<Box aria-hidden sx={{ gridColumn: 3, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}> <Box aria-hidden sx={{ gridColumn: 2, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
<Typography variant="body2">{d.sku}</Typography> <Typography variant="body2">{d.sku}</Typography>
</Box> </Box>
<Box aria-hidden sx={{ gridColumn: 4, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}> <Box aria-hidden sx={{ gridColumn: 3, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
<Typography variant="body2">{d.name}</Typography> <Typography variant="body2">{d.name}</Typography>
</Box> </Box>
<Box aria-hidden sx={{ gridColumn: 5, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}> <Box aria-hidden sx={{ gridColumn: 4, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
<Typography variant="body2">{d.text}</Typography> <Typography variant="body2">{d.text}</Typography>
</Box> </Box>
<Box aria-hidden sx={{ gridColumn: 6, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
<Typography variant="body2">{d.user}</Typography>
</Box>
</> </>
); );
} }
@@ -121,23 +115,17 @@ class ArticleHistoryRow extends PureComponent {
> >
{renderInvisibleDummyRow(dummy)} {renderInvisibleDummyRow(dummy)}
<Box sx={{ gridColumn: 1, gridRow: 2 }}> <Box sx={{ gridColumn: 1, gridRow: 2 }}>
<Skeleton variant="text" width="80%" height={20} /> <Skeleton variant="text" width="90%" height={20} />
</Box> </Box>
<Box sx={{ gridColumn: 2, gridRow: 2 }}> <Box sx={{ gridColumn: 2, gridRow: 2 }}>
<Skeleton variant="text" width="90%" height={20} />
</Box>
<Box sx={{ gridColumn: 3, gridRow: 2 }}>
<Skeleton variant="text" width="85%" height={20} /> <Skeleton variant="text" width="85%" height={20} />
</Box> </Box>
<Box sx={{ gridColumn: 4, gridRow: 2 }}> <Box sx={{ gridColumn: 3, gridRow: 2 }}>
<Skeleton variant="text" width="70%" height={20} /> <Skeleton variant="text" width="70%" height={20} />
</Box> </Box>
<Box sx={{ gridColumn: 5, gridRow: 2 }}> <Box sx={{ gridColumn: 4, gridRow: 2 }}>
<Skeleton variant="text" width="90%" height={20} /> <Skeleton variant="text" width="90%" height={20} />
</Box> </Box>
<Box sx={{ gridColumn: 6, gridRow: 2 }}>
<Skeleton variant="text" width="60%" height={20} />
</Box>
</ListItemButton> </ListItemButton>
); );
} }
@@ -163,19 +151,15 @@ class ArticleHistoryRow extends PureComponent {
> >
{renderInvisibleDummyRow(dummy)} {renderInvisibleDummyRow(dummy)}
{/* 1. Datum */}
<Box sx={{ gridColumn: 1, gridRow: 2, minWidth: 0 }}> <Box sx={{ gridColumn: 1, gridRow: 2, minWidth: 0 }}>
<Typography variant="body2" color="text.secondary" sx={{ fontVariantNumeric: 'tabular-nums', ...ellipsisSx }}>
{item.id}
</Typography>
</Box>
<Box sx={{ gridColumn: 2, gridRow: 2, minWidth: 0 }}>
<Typography variant="body2" color="text.secondary" sx={{ fontVariantNumeric: 'tabular-nums', ...ellipsisSx }}> <Typography variant="body2" color="text.secondary" sx={{ fontVariantNumeric: 'tabular-nums', ...ellipsisSx }}>
{formatDateTime(item.date || item.createdAt)} {formatDateTime(item.date || item.createdAt)}
</Typography> </Typography>
</Box> </Box>
<Box sx={{ gridColumn: 3, gridRow: 2, minWidth: 0 }}> {/* 2. SKU */}
<Box sx={{ gridColumn: 2, gridRow: 2, minWidth: 0 }}>
{item.sku ? ( {item.sku ? (
<Typography <Typography
variant="body2" variant="body2"
@@ -203,25 +187,21 @@ class ArticleHistoryRow extends PureComponent {
)} )}
</Box> </Box>
<Box sx={{ gridColumn: 4, gridRow: 2, minWidth: 0 }}> {/* 3. Artikelname */}
<Box sx={{ gridColumn: 3, gridRow: 2, minWidth: 0 }}>
<Typography variant="body2" sx={{ fontWeight: 500, ...ellipsisSx }}> <Typography variant="body2" sx={{ fontWeight: 500, ...ellipsisSx }}>
{item.name || item.articlename || '–'} {item.name || item.articlename || '–'}
</Typography> </Typography>
</Box> </Box>
<Box sx={{ gridColumn: 5, gridRow: 2, minWidth: 0 }}> {/* 4. Änderung / Ereignis */}
<Tooltip title={item.text || item.message || ''} placement="top-start" enterDelay={500}> <Box sx={{ gridColumn: 4, gridRow: 2, minWidth: 0 }}>
<Tooltip title={item.historytext || item.text || item.message || ''} placement="top-start" enterDelay={500}>
<Typography variant="body2" sx={ellipsisSx}> <Typography variant="body2" sx={ellipsisSx}>
{item.text || item.message || '–'} {item.historytext || item.text || item.message || '–'}
</Typography> </Typography>
</Tooltip> </Tooltip>
</Box> </Box>
<Box sx={{ gridColumn: 6, gridRow: 2, minWidth: 0 }}>
<Typography variant="body2" color="text.secondary" sx={ellipsisSx}>
{userName}
</Typography>
</Box>
</ListItemButton> </ListItemButton>
); );
} }
@@ -525,12 +505,10 @@ export default class ArticleHistoryPage extends PureComponent {
}} }}
> >
{renderInvisibleDummyRow(this.headerItem)} {renderInvisibleDummyRow(this.headerItem)}
<Box sx={{ gridColumn: 1, gridRow: 2, ...headerCellSx }}>ID</Box> <Box sx={{ gridColumn: 1, gridRow: 2, ...headerCellSx }}>Datum</Box>
<Box sx={{ gridColumn: 2, gridRow: 2, ...headerCellSx }}>Datum</Box> <Box sx={{ gridColumn: 2, gridRow: 2, ...headerCellSx }}>Artikel-Nr.</Box>
<Box sx={{ gridColumn: 3, gridRow: 2, ...headerCellSx }}>Artikel-Nr.</Box> <Box sx={{ gridColumn: 3, gridRow: 2, ...headerCellSx }}>Artikelname</Box>
<Box sx={{ gridColumn: 4, gridRow: 2, ...headerCellSx }}>Artikelname</Box> <Box sx={{ gridColumn: 4, gridRow: 2, ...headerCellSx }}>Änderung / Ereignis</Box>
<Box sx={{ gridColumn: 5, gridRow: 2, ...headerCellSx }}>Änderung / Ereignis</Box>
<Box sx={{ gridColumn: 6, gridRow: 2, ...headerCellSx }}>Benutzer</Box>
</Box> </Box>
} }
onPageChange={this.handlePageChange} onPageChange={this.handlePageChange}

View File

@@ -52,7 +52,7 @@ export function createDummyTestGlobalHistory() {
const rowGridSx = { const rowGridSx = {
display: 'grid', display: 'grid',
gridTemplateColumns: gridTemplateColumns:
'minmax(60px, max-content) minmax(150px, max-content) minmax(110px, max-content) minmax(140px, max-content) minmax(240px, 2fr) minmax(120px, max-content)', 'minmax(150px, max-content) minmax(110px, max-content) minmax(140px, max-content) minmax(240px, 1fr)',
gridTemplateRows: '0px 1fr', gridTemplateRows: '0px 1fr',
rowGap: 0, rowGap: 0,
columnGap: 2, columnGap: 2,
@@ -86,23 +86,17 @@ function renderInvisibleDummyRow(dummy) {
return ( return (
<> <>
<Box aria-hidden sx={{ gridColumn: 1, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}> <Box aria-hidden sx={{ gridColumn: 1, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
<Typography variant="body2">{d.id}</Typography>
</Box>
<Box aria-hidden sx={{ gridColumn: 2, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
<Typography variant="body2">{formatDateTime(d.date)}</Typography> <Typography variant="body2">{formatDateTime(d.date)}</Typography>
</Box> </Box>
<Box aria-hidden sx={{ gridColumn: 3, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}> <Box aria-hidden sx={{ gridColumn: 2, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
<Typography variant="body2">{d.type}</Typography> <Typography variant="body2">{d.type}</Typography>
</Box> </Box>
<Box aria-hidden sx={{ gridColumn: 4, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}> <Box aria-hidden sx={{ gridColumn: 3, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
<Typography variant="body2">{d.reference}</Typography> <Typography variant="body2">{d.reference}</Typography>
</Box> </Box>
<Box aria-hidden sx={{ gridColumn: 5, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}> <Box aria-hidden sx={{ gridColumn: 4, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
<Typography variant="body2">{d.text}</Typography> <Typography variant="body2">{d.text}</Typography>
</Box> </Box>
<Box aria-hidden sx={{ gridColumn: 6, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
<Typography variant="body2">{d.user}</Typography>
</Box>
</> </>
); );
} }
@@ -121,23 +115,17 @@ class GlobalHistoryRow extends PureComponent {
> >
{renderInvisibleDummyRow(dummy)} {renderInvisibleDummyRow(dummy)}
<Box sx={{ gridColumn: 1, gridRow: 2 }}> <Box sx={{ gridColumn: 1, gridRow: 2 }}>
<Skeleton variant="text" width="80%" height={20} /> <Skeleton variant="text" width="90%" height={20} />
</Box> </Box>
<Box sx={{ gridColumn: 2, gridRow: 2 }}> <Box sx={{ gridColumn: 2, gridRow: 2 }}>
<Skeleton variant="text" width="90%" height={20} />
</Box>
<Box sx={{ gridColumn: 3, gridRow: 2 }}>
<Skeleton variant="rounded" width={70} height={22} /> <Skeleton variant="rounded" width={70} height={22} />
</Box> </Box>
<Box sx={{ gridColumn: 4, gridRow: 2 }}> <Box sx={{ gridColumn: 3, gridRow: 2 }}>
<Skeleton variant="text" width="80%" height={20} /> <Skeleton variant="text" width="80%" height={20} />
</Box> </Box>
<Box sx={{ gridColumn: 5, gridRow: 2 }}> <Box sx={{ gridColumn: 4, gridRow: 2 }}>
<Skeleton variant="text" width="90%" height={20} /> <Skeleton variant="text" width="90%" height={20} />
</Box> </Box>
<Box sx={{ gridColumn: 6, gridRow: 2 }}>
<Skeleton variant="text" width="60%" height={20} />
</Box>
</ListItemButton> </ListItemButton>
); );
} }
@@ -167,19 +155,15 @@ class GlobalHistoryRow extends PureComponent {
> >
{renderInvisibleDummyRow(dummy)} {renderInvisibleDummyRow(dummy)}
{/* 1. Datum */}
<Box sx={{ gridColumn: 1, gridRow: 2, minWidth: 0 }}> <Box sx={{ gridColumn: 1, gridRow: 2, minWidth: 0 }}>
<Typography variant="body2" color="text.secondary" sx={{ fontVariantNumeric: 'tabular-nums', ...ellipsisSx }}>
{item.id}
</Typography>
</Box>
<Box sx={{ gridColumn: 2, gridRow: 2, minWidth: 0 }}>
<Typography variant="body2" color="text.secondary" sx={{ fontVariantNumeric: 'tabular-nums', ...ellipsisSx }}> <Typography variant="body2" color="text.secondary" sx={{ fontVariantNumeric: 'tabular-nums', ...ellipsisSx }}>
{formatDateTime(item.date || item.createdAt)} {formatDateTime(item.date || item.createdAt)}
</Typography> </Typography>
</Box> </Box>
<Box sx={{ gridColumn: 3, gridRow: 2, minWidth: 0 }}> {/* 2. Typ */}
<Box sx={{ gridColumn: 2, gridRow: 2, minWidth: 0 }}>
<Chip <Chip
size="small" size="small"
label={typeStr} label={typeStr}
@@ -193,7 +177,8 @@ class GlobalHistoryRow extends PureComponent {
/> />
</Box> </Box>
<Box sx={{ gridColumn: 4, gridRow: 2, minWidth: 0 }}> {/* 3. Referenz / Key */}
<Box sx={{ gridColumn: 3, gridRow: 2, minWidth: 0 }}>
{isArticle ? ( {isArticle ? (
<Typography <Typography
variant="body2" variant="body2"
@@ -221,19 +206,14 @@ class GlobalHistoryRow extends PureComponent {
)} )}
</Box> </Box>
<Box sx={{ gridColumn: 5, gridRow: 2, minWidth: 0 }}> {/* 4. Meldung / Beschreibung */}
<Box sx={{ gridColumn: 4, gridRow: 2, minWidth: 0 }}>
<Tooltip title={item.text || item.value || ''} placement="top-start" enterDelay={500}> <Tooltip title={item.text || item.value || ''} placement="top-start" enterDelay={500}>
<Typography variant="body2" sx={ellipsisSx}> <Typography variant="body2" sx={ellipsisSx}>
{item.text || item.value || '–'} {item.text || item.value || '–'}
</Typography> </Typography>
</Tooltip> </Tooltip>
</Box> </Box>
<Box sx={{ gridColumn: 6, gridRow: 2, minWidth: 0 }}>
<Typography variant="body2" color="text.secondary" sx={ellipsisSx}>
{userName}
</Typography>
</Box>
</ListItemButton> </ListItemButton>
); );
} }
@@ -537,12 +517,10 @@ export default class GlobalHistoryPage extends PureComponent {
}} }}
> >
{renderInvisibleDummyRow(this.headerItem)} {renderInvisibleDummyRow(this.headerItem)}
<Box sx={{ gridColumn: 1, gridRow: 2, ...headerCellSx }}>ID</Box> <Box sx={{ gridColumn: 1, gridRow: 2, ...headerCellSx }}>Datum</Box>
<Box sx={{ gridColumn: 2, gridRow: 2, ...headerCellSx }}>Datum</Box> <Box sx={{ gridColumn: 2, gridRow: 2, ...headerCellSx }}>Typ</Box>
<Box sx={{ gridColumn: 3, gridRow: 2, ...headerCellSx }}>Typ</Box> <Box sx={{ gridColumn: 3, gridRow: 2, ...headerCellSx }}>Referenz / Key</Box>
<Box sx={{ gridColumn: 4, gridRow: 2, ...headerCellSx }}>Referenz / Key</Box> <Box sx={{ gridColumn: 4, gridRow: 2, ...headerCellSx }}>Meldung / Beschreibung</Box>
<Box sx={{ gridColumn: 5, gridRow: 2, ...headerCellSx }}>Meldung / Beschreibung</Box>
<Box sx={{ gridColumn: 6, gridRow: 2, ...headerCellSx }}>Benutzer</Box>
</Box> </Box>
} }
onPageChange={this.handlePageChange} onPageChange={this.handlePageChange}

View File

@@ -0,0 +1,597 @@
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(150px, max-content) minmax(110px, max-content) minmax(130px, max-content) minmax(90px, max-content) minmax(220px, 1fr)',
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 (
<>
<Box aria-hidden sx={{ gridColumn: 1, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
<Typography variant="body2">{formatDateTime(d.created_at || d.date)}</Typography>
</Box>
<Box aria-hidden sx={{ gridColumn: 2, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
<Typography variant="body2">{d.transaction_type}</Typography>
</Box>
<Box aria-hidden sx={{ gridColumn: 3, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
<Typography variant="body2">{d.sku}</Typography>
</Box>
<Box aria-hidden sx={{ gridColumn: 4, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
<Typography variant="body2">+{d.amount}</Typography>
</Box>
<Box aria-hidden sx={{ gridColumn: 5, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
<Typography variant="body2">{d.transaction_text}</Typography>
</Box>
</>
);
}
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 (
<ListItemButton
disabled
divider
sx={{ ...rowGridSx, height: 56, minHeight: 56, maxHeight: 56, px: 2, py: 0 }}
>
{renderInvisibleDummyRow(dummy)}
<Box sx={{ gridColumn: 1, gridRow: 2 }}>
<Skeleton variant="text" width="90%" height={20} />
</Box>
<Box sx={{ gridColumn: 2, gridRow: 2 }}>
<Skeleton variant="rounded" width={70} height={22} />
</Box>
<Box sx={{ gridColumn: 3, gridRow: 2 }}>
<Skeleton variant="text" width="80%" height={20} />
</Box>
<Box sx={{ gridColumn: 4, gridRow: 2 }}>
<Skeleton variant="text" width="70%" height={20} />
</Box>
<Box sx={{ gridColumn: 5, gridRow: 2 }}>
<Skeleton variant="text" width="90%" height={20} />
</Box>
</ListItemButton>
);
}
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 (
<ListItemButton
divider
disabled={disabled}
sx={{ ...rowGridSx, height: 56, minHeight: 56, maxHeight: 56, px: 2, py: 0 }}
onClick={
hasSku
? () => {
void router.navigate(`/artikel/${encodeURIComponent(sku)}`);
}
: undefined
}
>
{renderInvisibleDummyRow(dummy)}
{/* 1. Datum */}
<Box sx={{ gridColumn: 1, gridRow: 2, minWidth: 0 }}>
<Typography variant="body2" color="text.secondary" sx={{ fontVariantNumeric: 'tabular-nums', ...ellipsisSx }}>
{formatDateTime(item.created_at || item.date || item.createdAt)}
</Typography>
</Box>
{/* 2. Typ */}
<Box sx={{ gridColumn: 2, gridRow: 2, minWidth: 0 }}>
<Chip
size="small"
label={typeLabel}
sx={{
height: 22,
fontSize: '0.75rem',
fontWeight: 500,
bgcolor: 'action.hover',
textTransform: 'capitalize',
}}
/>
</Box>
{/* 3. SKU */}
<Box sx={{ gridColumn: 3, gridRow: 2, minWidth: 0 }}>
{hasSku ? (
<Typography
variant="body2"
component="a"
href={`/artikel/${encodeURIComponent(sku)}`}
title={item.name ? `${item.name} (${sku})` : sku}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
void router.navigate(`/artikel/${encodeURIComponent(sku)}`);
}}
sx={{
color: LINK,
fontWeight: 600,
textDecoration: 'none',
...ellipsisSx,
'&:hover': { textDecoration: 'underline' },
}}
>
{sku}
</Typography>
) : (
<Typography variant="body2" color="text.secondary" sx={ellipsisSx}>
–
</Typography>
)}
</Box>
{/* 4. Menge / Betrag */}
<Box sx={{ gridColumn: 4, gridRow: 2, minWidth: 0, textAlign: 'right', pr: 1 }}>
<Typography
variant="body2"
sx={{
fontWeight: 600,
fontVariantNumeric: 'tabular-nums',
color: isPos ? 'success.main' : isNeg ? 'error.main' : 'text.primary',
...ellipsisSx,
}}
>
{amountStr}
</Typography>
</Box>
{/* 5. Buchungstext */}
<Box sx={{ gridColumn: 5, gridRow: 2, minWidth: 0 }}>
<Typography variant="body2" title={bookingText} sx={ellipsisSx}>
{bookingText}
</Typography>
</Box>
</ListItemButton>
);
}
}
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 (
<PanelTileGrid>
<Box
ref={inputRef}
sx={{
gridColumn: { xs: '1 / -1', sm: 'span 2', md: 'span 2' },
display: 'flex',
flexDirection: 'column',
gap: 1.25,
pb: 2,
}}
>
{/* Top row: search field */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<SingleLineTextEdit
sx={{ flex: 1 }}
size="small"
search
clear
value={input}
placeholder="Artikel-Nr., Buchungstext, Typ oder Benutzer suchen…"
onChange={(next) => ledgerHistoryAutomat.actions.filter(next)}
/>
<Button
size="small"
variant="outlined"
startIcon={<RefreshIcon />}
onClick={onReload}
disabled={loading}
sx={{ height: 40, px: 2, whiteSpace: 'nowrap' }}
>
Aktualisieren
</Button>
</Box>
{/* Filter Bar: Negativeseek (-1 Month, -1 Year, Reset) */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
flexWrap: 'wrap',
gap: 1.5,
}}
>
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.75,
px: 1.25,
py: 0.25,
borderRadius: 2,
bgcolor: 'background.paper',
boxShadow: 1,
}}
>
<Button
size="small"
variant={monthActive ? 'contained' : 'outlined'}
color={monthActive ? 'primary' : 'inherit'}
onClick={() => ledgerHistoryAutomat.actions.addSeekMonth()}
sx={{
minWidth: 88,
px: 1.25,
py: 0.25,
height: 28,
fontSize: '0.75rem',
fontWeight: 600,
textTransform: 'none',
boxSizing: 'border-box',
border: '1px solid',
borderColor: monthActive ? 'primary.main' : 'divider',
whiteSpace: 'nowrap',
}}
>
{monthLabel}
</Button>
<Button
size="small"
variant={yearActive ? 'contained' : 'outlined'}
color={yearActive ? 'primary' : 'inherit'}
onClick={() => ledgerHistoryAutomat.actions.addSeekYear()}
sx={{
minWidth: 80,
px: 1.25,
py: 0.25,
height: 28,
fontSize: '0.75rem',
fontWeight: 600,
textTransform: 'none',
boxSizing: 'border-box',
border: '1px solid',
borderColor: yearActive ? 'primary.main' : 'divider',
whiteSpace: 'nowrap',
}}
>
{yearLabel}
</Button>
{(monthActive || yearActive) && (
<Button
size="small"
variant="text"
color="inherit"
onClick={() => ledgerHistoryAutomat.actions.resetNegativeSeek()}
sx={{
px: 1,
py: 0.25,
height: 28,
fontSize: '0.75rem',
fontWeight: 600,
textTransform: 'none',
color: 'text.secondary',
whiteSpace: 'nowrap',
}}
>
Zurücksetzen
</Button>
)}
</Box>
{/* DEV: Chunk-Cache Debug Monitor trigger */}
{import.meta.env.DEV && (
<Chip
size="small"
variant="outlined"
icon={<ViewModuleIcon sx={{ fontSize: '1rem !important' }} />}
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' },
}}
/>
)}
</Box>
</Box>
</PanelTileGrid>
);
}
}
// 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 (
<>
<LedgerHistorySearchField
inputRef={this.searchRef}
onReload={this.handleReload}
loading={loading}
/>
<PanelTileGrid>
<PanelTile wide={3} minHeight={0} sx={{ p: 2 }}>
<BackendError error={backendErrorFrom(error)} onRetry={this.handleRetry} />
</PanelTile>
</PanelTileGrid>
</>
);
}
return (
<>
<LedgerHistorySearchField
inputRef={this.searchRef}
onReload={this.handleReload}
loading={loading}
/>
<PanelTileGrid>
<PanelTile wide={3} minHeight={0} sx={{ pb: 0, position: 'relative' }}>
<FillSpace
aboveRef={[this.searchRef]}
reserveBottom={32}
minPageSize={8}
minHeight={0}
resetKey={filterKey}
items={items || []}
totalCount={items ? items.length : 0}
unfilteredTotal={unfilteredTotal}
headerItem={this.headerItem}
renderItem={LedgerHistoryRow}
virtualScrollbar
header={
<Box
sx={{
...rowGridSx,
height: 36,
minHeight: 36,
px: 2,
borderBottom: '1px solid',
borderColor: 'divider',
}}
>
{renderInvisibleDummyRow(this.headerItem)}
<Box sx={{ gridColumn: 1, gridRow: 2, ...headerCellSx }}>Datum</Box>
<Box sx={{ gridColumn: 2, gridRow: 2, ...headerCellSx }}>Typ</Box>
<Box sx={{ gridColumn: 3, gridRow: 2, ...headerCellSx }}>Artikel-Nr.</Box>
<Box sx={{ gridColumn: 4, gridRow: 2, ...headerCellSx, textAlign: 'right', pr: 1 }}>Menge</Box>
<Box sx={{ gridColumn: 5, gridRow: 2, ...headerCellSx }}>Buchungstext</Box>
</Box>
}
onPageChange={this.handlePageChange}
/>
{items === null && (
<Box
sx={{
position: 'absolute',
top: 52,
left: 0,
right: 0,
bottom: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'none',
zIndex: 2,
}}
>
<CircularProgress size={32} />
</Box>
)}
</PanelTile>
</PanelTileGrid>
</>
);
}
}

View File

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

View File

@@ -57,6 +57,51 @@ import {
YELLOW_BG, YELLOW_BG,
} from '../../colors'; } from '../../colors';
function areLinesEqual(lines1, lines2) {
if (!Array.isArray(lines1) || !Array.isArray(lines2)) return false;
if (lines1.length !== lines2.length) return false;
for (let i = 0; i < lines1.length; i++) {
const a = lines1[i] || {};
const b = lines2[i] || {};
const aSku = String(a.sku ?? '').trim();
const bSku = String(b.sku ?? '').trim();
if (aSku !== bSku) return false;
const aName = String(a.name ?? '').trim();
const bName = String(b.name ?? '').trim();
if (aName !== bName) return false;
const aAmount = a.amount != null && a.amount !== '' ? Number(a.amount) : null;
const bAmount = b.amount != null && b.amount !== '' ? Number(b.amount) : null;
if (aAmount !== bAmount) return false;
const aNet =
(a.netprice ?? a.net) != null && (a.netprice ?? a.net) !== ''
? Number(a.netprice ?? a.net)
: null;
const bNet =
(b.netprice ?? b.net) != null && (b.netprice ?? b.net) !== ''
? Number(b.netprice ?? b.net)
: null;
if (aNet !== bNet) return false;
const aTax =
(a.taxrate ?? a.tax) != null && (a.taxrate ?? a.tax) !== ''
? Number(a.taxrate ?? a.tax)
: null;
const bTax =
(b.taxrate ?? b.tax) != null && (b.taxrate ?? b.tax) !== ''
? Number(b.taxrate ?? b.tax)
: null;
if (aTax !== bTax) return false;
const aPu = a.pu != null && a.pu !== '' ? Number(a.pu) : 1;
const bPu = b.pu != null && b.pu !== '' ? Number(b.pu) : 1;
if (aPu !== bPu) return false;
}
return true;
}
class LieferungNeuPage extends PureComponent { class LieferungNeuPage extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -80,6 +125,7 @@ class LieferungNeuPage extends PureComponent {
ledgerSummary: null, ledgerSummary: null,
bookDeliveryDialogOpen: false, bookDeliveryDialogOpen: false,
bookDeliveryPrefillLineId: null, bookDeliveryPrefillLineId: null,
pdfCreating: false,
warehouses: warehouseAutomat.state.warehouses || [], warehouses: warehouseAutomat.state.warehouses || [],
}; };
this._cancelled = false; this._cancelled = false;
@@ -89,6 +135,7 @@ class LieferungNeuPage extends PureComponent {
this.handleDateChange = this.handleDateChange.bind(this); this.handleDateChange = this.handleDateChange.bind(this);
this.handleOpenBookDialog = this.handleOpenBookDialog.bind(this); this.handleOpenBookDialog = this.handleOpenBookDialog.bind(this);
this.handleBookLine = this.handleBookLine.bind(this); this.handleBookLine = this.handleBookLine.bind(this);
this.handleCreatePdf = this.handleCreatePdf.bind(this);
this.handleCloseBookDialog = this.handleCloseBookDialog.bind(this); this.handleCloseBookDialog = this.handleCloseBookDialog.bind(this);
this.handleBookSuccess = this.handleBookSuccess.bind(this); this.handleBookSuccess = this.handleBookSuccess.bind(this);
@@ -128,7 +175,7 @@ class LieferungNeuPage extends PureComponent {
this._warehouseUnsub = warehouseAutomat.subscribe(this, (state) => ({ this._warehouseUnsub = warehouseAutomat.subscribe(this, (state) => ({
warehouses: state.warehouses, warehouses: state.warehouses,
})); }));
void warehouseAutomat.actions.load().catch(() => {}); void warehouseAutomat.actions.load().catch(() => { });
this._cancelled = false; this._cancelled = false;
if (this.props.docId != null) { if (this.props.docId != null) {
setLastDeliveryPath(`/lieferung/${this.props.docId}`); setLastDeliveryPath(`/lieferung/${this.props.docId}`);
@@ -148,6 +195,7 @@ class LieferungNeuPage extends PureComponent {
this.automat = getDeliveryEditorAutomat(this.props.docId); this.automat = getDeliveryEditorAutomat(this.props.docId);
this.unsubscribe = this.automat.subscribe(this); this.unsubscribe = this.automat.subscribe(this);
this.setState({ ...this.automat.state }); this.setState({ ...this.automat.state });
this._initialLines = null;
if (this.props.docId != null) { if (this.props.docId != null) {
void this.loadEdit(); void this.loadEdit();
} else { } else {
@@ -225,6 +273,7 @@ class LieferungNeuPage extends PureComponent {
if (doc.number != null) this.automat.actions.setNumber(doc.number); if (doc.number != null) this.automat.actions.setNumber(doc.number);
if (doc.externalNumber != null) this.automat.actions.setExternalNumber(String(doc.externalNumber)); if (doc.externalNumber != null) this.automat.actions.setExternalNumber(String(doc.externalNumber));
this.automat.actions.setLineitems(lines); this.automat.actions.setLineitems(lines);
this._initialLines = lines;
// Restore attachments with thumbnails // Restore attachments with thumbnails
const apiFiles = doc.files ?? []; const apiFiles = doc.files ?? [];
@@ -692,6 +741,10 @@ class LieferungNeuPage extends PureComponent {
async handlePdfRescan(attachment) { async handlePdfRescan(attachment) {
if (!attachment || this.state.pdfUploading) return; if (!attachment || this.state.pdfUploading) return;
const name = attachment.name || '';
if (name.toLowerCase().startsWith('wareneingang-') || attachment.meta?.type === 'delivery_pdf') {
return;
}
let targetAttachment = attachment; let targetAttachment = attachment;
if (!targetAttachment.file || targetAttachment.file.size === 0) { if (!targetAttachment.file || targetAttachment.file.size === 0) {
@@ -784,9 +837,24 @@ class LieferungNeuPage extends PureComponent {
}); });
} }
handlePdfDelete(attachment) { async handlePdfDelete(attachment) {
if (!attachment?.id) return; if (!attachment) return;
this.automat.actions.removeAttachment(attachment.id); const fileId = attachment.fileId ?? attachment.id;
const docId = this.props.docId ?? attachment.manifestId ?? this.state.id;
this.automat.actions.removeAttachment(attachment.id ?? fileId);
if (fileId != null && fileId !== attachment.id) {
this.automat.actions.removeAttachment(fileId);
}
if (docId != null && fileId != null && attachment.isPersisted) {
try {
await deliveryApi.deleteFile(docId, fileId);
enqueueSnackbar('Anhang gelöscht.', { variant: 'success' });
} catch (err) {
notifyError(err);
}
}
} }
async ensureAttachmentFile(attachment) { async ensureAttachmentFile(attachment) {
@@ -1029,6 +1097,9 @@ class LieferungNeuPage extends PureComponent {
if (this.state.saving) return; if (this.state.saving) return;
const isCreate = docId == null; const isCreate = docId == null;
const linesModified =
isCreate || !this._initialLines || !areLinesEqual(this.state.lines, this._initialLines);
let body; let body;
try { try {
body = await buildManifestBody( body = await buildManifestBody(
@@ -1037,13 +1108,17 @@ class LieferungNeuPage extends PureComponent {
externalNumber: this.state.externalNumber ?? '', externalNumber: this.state.externalNumber ?? '',
date: this.state.date ?? '', date: this.state.date ?? '',
partyId: this.state.partyId, partyId: this.state.partyId,
lines: this.state.lines, lines: linesModified ? this.state.lines : undefined,
attachments: this.state.attachments, attachments: this.state.attachments,
}, },
{ {
kind: cfg.kind,
partyField: cfg.partyField, partyField: cfg.partyField,
requireAtLeastOne: isCreate, requireAtLeastOne: isCreate,
externalNumber: cfg.externalNumber, externalNumber: cfg.externalNumber,
isUpdate: !isCreate,
mode: isCreate ? 'create' : 'edit',
omitLines: !linesModified,
}, },
); );
} catch (err) { } catch (err) {
@@ -1081,6 +1156,7 @@ class LieferungNeuPage extends PureComponent {
if (doc.number != null) this.automat.actions.setNumber(doc.number); if (doc.number != null) this.automat.actions.setNumber(doc.number);
if (doc.externalNumber != null) this.automat.actions.setExternalNumber(String(doc.externalNumber)); if (doc.externalNumber != null) this.automat.actions.setExternalNumber(String(doc.externalNumber));
this.automat.actions.setLineitems(lines); this.automat.actions.setLineitems(lines);
this._initialLines = lines;
void this.reloadLedgerSummary(); void this.reloadLedgerSummary();
// Restore attachments with thumbnails // Restore attachments with thumbnails
@@ -1098,6 +1174,32 @@ class LieferungNeuPage extends PureComponent {
} }
} }
/** Generates the "Wareneingang" PDF server-side, stores it and shows it among the attachments. */
async handleCreatePdf(e) {
e?.preventDefault?.();
e?.stopPropagation?.();
const { docId } = this.props;
if (docId == null || this.state.pdfCreating || this.state.saving) return;
this.setState({ pdfCreating: true });
try {
const res = await deliveryApi.createPdf(docId);
requireObject(res, 'POST /api/delivery/:id/pdf');
const getRes = await deliveryApi.get(docId);
const doc = getRes?.delivery;
if (doc?.files && !this._cancelled) {
const attachments = await restoreAttachmentsFromApi(doc.files, this.state.attachments);
this.automat.actions.setAttachments(attachments);
}
enqueueSnackbar('Wareneingang-PDF erfolgreich erstellt.', { variant: 'success' });
} catch (err) {
if (!this._cancelled) notifyError(err);
} finally {
if (!this._cancelled) this.setState({ pdfCreating: false });
}
}
render() { render() {
const cfg = this.cfg; const cfg = this.cfg;
const { const {
@@ -1151,6 +1253,8 @@ class LieferungNeuPage extends PureComponent {
externalNumber={externalNumber ?? ''} externalNumber={externalNumber ?? ''}
saving={saving} saving={saving}
pdfUploading={pdfUploading} pdfUploading={pdfUploading}
pdfCreating={this.state.pdfCreating}
onPdfCreate={this.handleCreatePdf}
attachments={attachments} attachments={attachments}
lines={lines} lines={lines}
onPdfUpload={this.handlePdfUpload} onPdfUpload={this.handlePdfUpload}
@@ -1320,7 +1424,6 @@ class LieferungNeuPage extends PureComponent {
return ( return (
<PanelTile <PanelTile
wide={4} wide={4}
title="Buchungen"
color={SURFACE_BASE} color={SURFACE_BASE}
sx={{ sx={{
gridColumn: { xs: '1 / -1', sm: '1 / -1', md: 'span 4' }, gridColumn: { xs: '1 / -1', sm: '1 / -1', md: 'span 4' },

View File

@@ -290,6 +290,7 @@ const server = http.createServer(async (req, res) => {
if (ledgerSku) return json(res, 200, { ledger: [] }); if (ledgerSku) return json(res, 200, { ledger: [] });
const ledgerMa = p.match(/^\/api\/ledger\/manifestarticle\/(\d+)$/); const ledgerMa = p.match(/^\/api\/ledger\/manifestarticle\/(\d+)$/);
if (ledgerMa) return json(res, 200, { ledger: [] }); 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/ledger') return json(res, 200, { ledger: [] });
if (p === '/api/article/search') { if (p === '/api/article/search') {
const q = (url.searchParams.get('target') ?? '').toLowerCase(); const q = (url.searchParams.get('target') ?? '').toLowerCase();