u
This commit is contained in:
@@ -609,6 +609,7 @@ _none_
|
||||
- Returns: `200 SSE`
|
||||
|
||||
- Events:
|
||||
- `company`: `{ companyId }`
|
||||
- `category`: `{}`
|
||||
- `articles`: `{}`
|
||||
- `article-<sku>`: `{}`
|
||||
|
||||
File diff suppressed because one or more lines are too long
3432
automat-graph.svg
3432
automat-graph.svg
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 260 KiB After Width: | Height: | Size: 197 KiB |
@@ -58,45 +58,30 @@ const CLUSTERS = [
|
||||
label: 'Session & Real-Time Hub',
|
||||
automats: ['authAutomat', 'sseAutomat'],
|
||||
},
|
||||
{
|
||||
id: 'company_basedata',
|
||||
label: 'Company & Consolidated Base Data',
|
||||
automats: ['companyAutomat'],
|
||||
},
|
||||
{
|
||||
id: 'catalog_products',
|
||||
label: 'Catalog & Products',
|
||||
automats: ['categoryAutomat', 'categoryViewAutomat', 'artikelAutomat', 'articleDetailAutomat', 'brandAutomat', 'unitAutomat'],
|
||||
automats: ['categoryAutomat', 'categoryViewAutomat', 'artikelAutomat', 'articleDetailAutomat'],
|
||||
},
|
||||
{
|
||||
id: 'search_indexing',
|
||||
label: 'Search & Indexing',
|
||||
automats: ['articleIndexAutomat', 'articleSearchAutomat'],
|
||||
},
|
||||
{
|
||||
id: 'properties_attributes',
|
||||
label: 'Properties & Attributes',
|
||||
automats: ['propertyAutomat', 'attributeAutomat'],
|
||||
},
|
||||
{
|
||||
id: 'purchasing_logistics',
|
||||
label: 'Suppliers & Deliveries',
|
||||
automats: ['supplierAutomat', 'deliveryAutomat', 'deliveryEditorAutomat'],
|
||||
label: 'Deliveries & Inbound',
|
||||
automats: ['deliveryAutomat', 'deliveryEditorAutomat'],
|
||||
},
|
||||
{
|
||||
id: 'sales_crm',
|
||||
label: 'Sales & CRM',
|
||||
automats: ['quoteAutomat', 'orderAutomat', 'entityAutomat', 'customergroupAutomat'],
|
||||
},
|
||||
{
|
||||
id: 'inventory_storage',
|
||||
label: 'Inventory & Warehousing',
|
||||
automats: ['warehouseAutomat'],
|
||||
},
|
||||
{
|
||||
id: 'finance_config',
|
||||
label: 'Company & Finance',
|
||||
automats: ['companyAutomat', 'accountAutomat', 'numberAutomat'],
|
||||
},
|
||||
{
|
||||
id: 'hardware_pos',
|
||||
label: 'POS & Hardware',
|
||||
automats: ['posterminalAutomat'],
|
||||
automats: ['quoteAutomat', 'orderAutomat', 'entityAutomat'],
|
||||
},
|
||||
{
|
||||
id: 'ai_workspace',
|
||||
@@ -110,8 +95,8 @@ const CLUSTERS = [
|
||||
},
|
||||
{
|
||||
id: 'system_ui',
|
||||
label: 'UI, Settings & System',
|
||||
automats: ['navigationAutomat', 'userSettingsAutomat', 'helpAutomat', 'pwaAutomat', 'Legend'],
|
||||
label: 'UI, Navigation & System',
|
||||
automats: ['navigationAutomat', 'helpAutomat', 'pwaAutomat', 'Legend'],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -136,16 +121,16 @@ const AUTOMAT_SPECS = [
|
||||
{
|
||||
id: 'authAutomat',
|
||||
title: 'authAutomat',
|
||||
subtitle: 'Session & Multi-Tenant Gateway',
|
||||
subtitle: 'Always-On Session & Multi-Tenant Gateway',
|
||||
entries: [
|
||||
{ type: 'STATE', name: 'user: AuthUser', desc: 'Current user & company' },
|
||||
{ type: 'STATE', name: 'status', desc: '\'anon\' | \'authed\' | \'loading\'' },
|
||||
{ type: 'STATE', name: 'hasAuth: boolean', desc: 'Authenticated session bit', port: 'p_auth_hasAuth' },
|
||||
{ type: 'STATE', name: 'hasCompany: boolean', desc: 'hasAuth && selectedCompanyId', port: 'p_auth_hasCompany' },
|
||||
{ type: 'STATE', name: 'user: AuthUser', desc: 'Current user & company', port: 'p_auth_user' },
|
||||
{ type: 'STATE', name: 'status', desc: '\'anon\' | \'authed\' | \'loading\'', port: 'p_auth_status' },
|
||||
{ type: 'ACTION', name: 'bootstrapSession()', desc: 'Restore or fetch session' },
|
||||
{ type: 'ACTION', name: 'switchCompany(id)', desc: 'Multi-tenant switch', port: 'p_auth_switchCompany' },
|
||||
{ type: 'ACTION', name: 'logout()', desc: 'Clear session cookie' },
|
||||
{ type: 'SELECT', name: 'selectSelectedCompanyId()', desc: 'Tenant selector' },
|
||||
{ type: 'HOOK', name: 'auth/sessionEstablished', desc: 'Session ready lifecycle', port: 'p_auth_status' },
|
||||
{ type: 'HOOK', name: 'auth/companyChanged', desc: 'Company change signal', port: 'p_auth_user' },
|
||||
{ type: 'SELECT', name: 'selectHasAuth() / selectHasCompany()', desc: 'Auth & tenant gates' },
|
||||
{ type: 'API', name: 'GET /api/auth/session', desc: 'Cookie session check' },
|
||||
],
|
||||
},
|
||||
@@ -166,7 +151,32 @@ const AUTOMAT_SPECS = [
|
||||
],
|
||||
},
|
||||
|
||||
// 2. Catalog & Products
|
||||
// 2. Company & Consolidated Base Data
|
||||
{
|
||||
id: 'companyAutomat',
|
||||
title: 'companyAutomat',
|
||||
subtitle: 'Consolidated Company & Base Data Hub',
|
||||
entries: [
|
||||
{ type: 'STATE', name: 'current: Company', desc: 'Tax rates, legal & root cat', port: 'p_comp_curr' },
|
||||
{ type: 'STATE', name: 'numbers: NumberSeq[]', desc: 'Document number sequences', port: 'p_comp_num' },
|
||||
{ type: 'STATE', name: 'terminals: Posterminal[]', desc: 'POS hardware & categories', port: 'p_comp_pos' },
|
||||
{ type: 'STATE', name: 'suppliers: Supplier[]', desc: 'Vendors & supplier articles', port: 'p_comp_sup' },
|
||||
{ type: 'STATE', name: 'units: Unit[]', desc: 'Allowed SI & sales units', port: 'p_comp_unit' },
|
||||
{ type: 'STATE', name: 'viewerPosition / chatDrawerWidth', desc: 'Persisted user UI settings', port: 'p_comp_usr' },
|
||||
{ type: 'STATE', name: 'warehouses: Warehouse[]', desc: 'Storage locations & stock', port: 'p_comp_wh' },
|
||||
{ type: 'STATE', name: 'properties: PropertyName[]', desc: 'Custom text properties', port: 'p_comp_prop' },
|
||||
{ type: 'STATE', name: 'customergroups: Group[]', desc: 'Pricing discount tiers', port: 'p_comp_cgrp' },
|
||||
{ type: 'STATE', name: 'carriers: Carrier[]', desc: 'Shipping methods & carriers', port: 'p_comp_car' },
|
||||
{ type: 'STATE', name: 'brands: Brand[]', desc: 'Catalog brands & logos', port: 'p_comp_brand' },
|
||||
{ type: 'STATE', name: 'attributes: AttributeName[]', desc: 'Attributes & discrete values', port: 'p_comp_attr' },
|
||||
{ type: 'STATE', name: 'accounts: Account[]', desc: 'Bank & payment ledgers', port: 'p_comp_acc' },
|
||||
{ type: 'ACTION', name: 'loadAll() / reset()', desc: 'Fetch or reset all base data' },
|
||||
{ type: 'SELECT', name: 'selectCompanyRootCategoryId()', desc: 'Root category pointer', port: 'p_comp_root' },
|
||||
{ type: 'API', name: '/api/company, /api/brand, …', desc: '13 consolidated endpoints' },
|
||||
],
|
||||
},
|
||||
|
||||
// 3. Catalog & Products
|
||||
{
|
||||
id: 'categoryAutomat',
|
||||
title: 'categoryAutomat',
|
||||
@@ -225,35 +235,8 @@ const AUTOMAT_SPECS = [
|
||||
{ type: 'API', name: 'GET /api/article/:sku/supplier', desc: 'Vendor links & prices' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'brandAutomat',
|
||||
title: 'brandAutomat',
|
||||
subtitle: 'Catalog Brands & Makers',
|
||||
entries: [
|
||||
{ type: 'STATE', name: 'brands: Brand[]', desc: 'Decoupled brand catalog' },
|
||||
{ type: 'STATE', name: 'filteredBrands: Brand[]', desc: 'Filtered search slice' },
|
||||
{ type: 'ACTION', name: 'load(silent)', desc: 'Deduplicated list fetch' },
|
||||
{ type: 'ACTION', name: 'create() / rename()', desc: 'Brand mutations' },
|
||||
{ type: 'ACTION', name: 'addImage() / deleteImage()', desc: 'Brand logo AVIF image' },
|
||||
{ type: 'API', name: 'GET /api/brand', desc: 'Paginated brand records' },
|
||||
{ type: 'SSE', name: 'brand', desc: 'SSE signal with sender tab' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'unitAutomat',
|
||||
title: 'unitAutomat',
|
||||
subtitle: 'Units of Measure (UoM)',
|
||||
entries: [
|
||||
{ type: 'STATE', name: 'units: Unit[]', desc: 'Allowed SI & sales units' },
|
||||
{ type: 'STATE', name: 'status: \'idle\'|\'ready\'', desc: 'Load lifecycle state' },
|
||||
{ type: 'ACTION', name: 'load()', desc: 'Fetch localized units' },
|
||||
{ type: 'ACTION', name: 'upsert(body)', desc: 'Create or update unit' },
|
||||
{ type: 'ACTION', name: 'delete(id)', desc: 'Remove unused unit' },
|
||||
{ type: 'API', name: 'GET /api/unit', desc: 'Company unit directory' },
|
||||
],
|
||||
},
|
||||
|
||||
// 3. Search & Discovery
|
||||
// 4. Search & Discovery
|
||||
{
|
||||
id: 'articleIndexAutomat',
|
||||
title: 'articleIndexAutomat',
|
||||
@@ -281,52 +264,11 @@ const AUTOMAT_SPECS = [
|
||||
],
|
||||
},
|
||||
|
||||
// 4. Properties & Attributes
|
||||
{
|
||||
id: 'propertyAutomat',
|
||||
title: 'propertyAutomat',
|
||||
subtitle: 'Custom Metadata Properties',
|
||||
entries: [
|
||||
{ type: 'STATE', name: 'properties: PropertyName[]', desc: 'Sorted company properties' },
|
||||
{ type: 'ACTION', name: 'load()', desc: 'Fetch property definitions' },
|
||||
{ type: 'ACTION', name: 'createProperty()', desc: 'Add new property definition' },
|
||||
{ type: 'ACTION', name: 'updateProperty()', desc: 'Edit title & sort' },
|
||||
{ type: 'API', name: 'GET /api/propertyname', desc: 'Company properties' },
|
||||
{ type: 'SSE', name: 'propertyname', desc: 'Real-time property signal' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'attributeAutomat',
|
||||
title: 'attributeAutomat',
|
||||
subtitle: 'Product Attributes & Discrete Values',
|
||||
entries: [
|
||||
{ type: 'STATE', name: 'attributes: AttributeName[]', desc: 'Attributes with sorted values' },
|
||||
{ type: 'ACTION', name: 'load()', desc: 'Fetch attribute definitions' },
|
||||
{ type: 'ACTION', name: 'createAttribute()', desc: 'Create attribute group' },
|
||||
{ type: 'ACTION', name: 'addValue() / deleteValue()', desc: 'Discrete value options' },
|
||||
{ type: 'API', name: 'GET /api/attributename', desc: 'Attributes & values' },
|
||||
{ type: 'SSE', name: 'attributename', desc: 'Real-time attribute signal' },
|
||||
],
|
||||
},
|
||||
|
||||
// 5. Suppliers & Deliveries
|
||||
{
|
||||
id: 'supplierAutomat',
|
||||
title: 'supplierAutomat',
|
||||
subtitle: 'Supplier Directory & Catalogs',
|
||||
entries: [
|
||||
{ type: 'STATE', name: 'suppliers: Supplier[]', desc: 'Active vendor list' },
|
||||
{ type: 'STATE', name: 'status: \'ready\'', desc: 'Load lifecycle state' },
|
||||
{ type: 'ACTION', name: 'load()', desc: 'Deduplicated fetch' },
|
||||
{ type: 'ACTION', name: 'reload()', desc: 'Silent background sync' },
|
||||
{ type: 'API', name: 'GET /api/supplier', desc: 'Vendor records' },
|
||||
{ type: 'SSE', name: 'supplier', desc: 'Real-time supplier signal' },
|
||||
],
|
||||
},
|
||||
// 5. Deliveries & Inbound
|
||||
{
|
||||
id: 'deliveryAutomat',
|
||||
title: 'deliveryAutomat',
|
||||
subtitle: 'createListAutomat (Deliveries)',
|
||||
subtitle: 'Chunk-Paginated Deliveries',
|
||||
entries: [
|
||||
{ type: 'STATE', name: 'items: Delivery[]', desc: 'Company delivery receipts' },
|
||||
{ type: 'STATE', name: 'status: \'ready\'', desc: 'List lifecycle state' },
|
||||
@@ -355,7 +297,7 @@ const AUTOMAT_SPECS = [
|
||||
{
|
||||
id: 'quoteAutomat',
|
||||
title: 'quoteAutomat',
|
||||
subtitle: 'createListAutomat (Quotes)',
|
||||
subtitle: 'Chunk-Paginated Quotes',
|
||||
entries: [
|
||||
{ type: 'STATE', name: 'items: Quote[]', desc: 'Sales quotes / Angebote', port: 'p_quote_items' },
|
||||
{ type: 'ACTION', name: 'load() / reload()', desc: 'Deduplicated fetch' },
|
||||
@@ -381,7 +323,7 @@ const AUTOMAT_SPECS = [
|
||||
{
|
||||
id: 'entityAutomat',
|
||||
title: 'entityAutomat',
|
||||
subtitle: 'createListAutomat (Entities)',
|
||||
subtitle: 'Customer & Partner Directory',
|
||||
entries: [
|
||||
{ type: 'STATE', name: 'items: Entity[]', desc: 'Customers & partners' },
|
||||
{ type: 'ACTION', name: 'load() / reload()', desc: 'Customer directory load' },
|
||||
@@ -389,83 +331,8 @@ const AUTOMAT_SPECS = [
|
||||
{ type: 'API', name: 'GET /api/entity', desc: 'Directory records' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'customergroupAutomat',
|
||||
title: 'customergroupAutomat',
|
||||
subtitle: 'createListAutomat (Groups)',
|
||||
entries: [
|
||||
{ type: 'STATE', name: 'items: Customergroup[]', desc: 'Pricing discount tiers' },
|
||||
{ type: 'ACTION', name: 'load() / reload()', desc: 'Discount group load' },
|
||||
{ type: 'API', name: 'GET /api/customergroup', desc: 'Pricing tiers' },
|
||||
],
|
||||
},
|
||||
|
||||
// 7. Inventory & Storage
|
||||
{
|
||||
id: 'warehouseAutomat',
|
||||
title: 'warehouseAutomat',
|
||||
subtitle: 'Warehouse Locations & Stock',
|
||||
entries: [
|
||||
{ type: 'STATE', name: 'warehouses: Warehouse[]', desc: 'Storage areas & bins' },
|
||||
{ type: 'STATE', name: 'filteredWarehouses', desc: 'Filter search slice' },
|
||||
{ type: 'ACTION', name: 'load() / reload()', desc: 'Deduplicated sync' },
|
||||
{ type: 'ACTION', name: 'setFilter(query)', desc: 'Local filter query' },
|
||||
{ type: 'API', name: 'GET /api/warehouse', desc: 'Warehouse listings' },
|
||||
{ type: 'SSE', name: 'warehouse', desc: 'Real-time stock movement' },
|
||||
],
|
||||
},
|
||||
|
||||
// 8. Company & Finance
|
||||
{
|
||||
id: 'companyAutomat',
|
||||
title: 'companyAutomat',
|
||||
subtitle: 'Active Company Configuration',
|
||||
entries: [
|
||||
{ type: 'STATE', name: 'current: Company', desc: 'Tax rates & ISO language', port: 'p_comp_curr' },
|
||||
{ type: 'ACTION', name: 'load() / reload()', desc: 'Fetch tenant settings' },
|
||||
{ type: 'SELECT', name: 'selectCompanyTaxrate()', desc: 'Standard tax rate' },
|
||||
{ type: 'SELECT', name: 'selectCompanyRootCategoryId()', desc: 'Root category pointer', port: 'p_comp_root' },
|
||||
{ type: 'SELECT', name: 'selectCompanyName()', desc: 'Company legal name' },
|
||||
{ type: 'API', name: 'GET /api/company/:id', desc: 'Legal & tax profile' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'accountAutomat',
|
||||
title: 'accountAutomat',
|
||||
subtitle: 'createListAutomat (Accounts)',
|
||||
entries: [
|
||||
{ type: 'STATE', name: 'items: Account[]', desc: 'Bank & payment ledgers' },
|
||||
{ type: 'ACTION', name: 'load() / reload()', desc: 'Payment accounts load' },
|
||||
{ type: 'API', name: 'GET /api/account', desc: 'Chart of accounts' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'numberAutomat',
|
||||
title: 'numberAutomat',
|
||||
subtitle: 'createListAutomat (Sequences)',
|
||||
entries: [
|
||||
{ type: 'STATE', name: 'items: NumberSeq[]', desc: 'Invoice/receipt counters' },
|
||||
{ type: 'ACTION', name: 'load() / reload()', desc: 'Number series sync' },
|
||||
{ type: 'API', name: 'GET /api/number', desc: 'Document counters' },
|
||||
],
|
||||
},
|
||||
|
||||
// 9. POS & Hardware
|
||||
{
|
||||
id: 'posterminalAutomat',
|
||||
title: 'posterminalAutomat',
|
||||
subtitle: 'POS Terminals & Gating',
|
||||
entries: [
|
||||
{ type: 'STATE', name: 'terminals: Posterminal[]', desc: 'Hardware terminals', port: 'p_pos_term' },
|
||||
{ type: 'ACTION', name: 'load()', desc: 'Fetch POS registers' },
|
||||
{ type: 'ACTION', name: 'create() / delete()', desc: 'Terminal registrations' },
|
||||
{ type: 'ACTION', name: 'toggleCategory()', desc: 'Gated category filters' },
|
||||
{ type: 'SELECT', name: 'selectPosterminals()', desc: 'Active registers' },
|
||||
{ type: 'API', name: 'GET /api/posterminal', desc: 'Terminal device configs' },
|
||||
],
|
||||
},
|
||||
|
||||
// 10. AI Assistant & Workspace
|
||||
// 7. AI Assistant & Workspace
|
||||
{
|
||||
id: 'chatAutomat',
|
||||
title: 'chatAutomat',
|
||||
@@ -497,7 +364,7 @@ const AUTOMAT_SPECS = [
|
||||
],
|
||||
},
|
||||
|
||||
// 11. Audit & Virtual Grids
|
||||
// 8. Audit & Virtual Grids
|
||||
{
|
||||
id: 'articleHistoryGridAutomat',
|
||||
title: 'articleHistoryGridAutomat',
|
||||
@@ -532,7 +399,7 @@ const AUTOMAT_SPECS = [
|
||||
],
|
||||
},
|
||||
|
||||
// 12. UI, Settings & System
|
||||
// 9. UI, Navigation & System
|
||||
{
|
||||
id: 'navigationAutomat',
|
||||
title: 'navigationAutomat',
|
||||
@@ -547,18 +414,6 @@ const AUTOMAT_SPECS = [
|
||||
{ type: 'ACTION', name: 'clearNavigation()', desc: 'Reset on company switch' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'userSettingsAutomat',
|
||||
title: 'userSettingsAutomat',
|
||||
subtitle: 'IndexedDB User Preferences',
|
||||
entries: [
|
||||
{ type: 'STATE', name: 'viewerPosition: \'top\'|\'right\'', desc: 'Editor docking location' },
|
||||
{ type: 'STATE', name: 'chatDrawerWidth: 460', desc: 'AI drawer pixel width', port: 'p_usr_drawer' },
|
||||
{ type: 'ACTION', name: 'setViewerPosition()', desc: 'Persist docking' },
|
||||
{ type: 'ACTION', name: 'setChatDrawerWidth()', desc: 'Persist drawer width' },
|
||||
{ type: 'HOOK', name: 'persist: true', desc: 'Survives page reloads' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'helpAutomat',
|
||||
title: 'helpAutomat',
|
||||
@@ -595,23 +450,13 @@ const AUTOMAT_SPECS = [
|
||||
*/
|
||||
const EDGE_DEFS = [
|
||||
// Session & Auth lifecycle
|
||||
{ from: 'authAutomat', to: 'sseAutomat', label: 'bind session & cid', port: 'p_auth_user' },
|
||||
{ from: 'authAutomat', to: 'companyAutomat', label: 'load company profile', port: 'p_auth_switchCompany' },
|
||||
{ from: 'authAutomat', to: 'categoryAutomat', label: 'sync tree on authed', port: 'p_auth_status' },
|
||||
{ from: 'authAutomat', to: 'brandAutomat', label: 'sync brands on authed', port: 'p_auth_status' },
|
||||
{ from: 'authAutomat', to: 'unitAutomat', label: 'sync units on authed', port: 'p_auth_status' },
|
||||
{ from: 'authAutomat', to: 'attributeAutomat', label: 'sync attributes on authed', port: 'p_auth_status' },
|
||||
{ from: 'authAutomat', to: 'propertyAutomat', label: 'sync properties on authed', port: 'p_auth_status' },
|
||||
{ from: 'authAutomat', to: 'supplierAutomat', label: 'sync suppliers on authed', port: 'p_auth_status' },
|
||||
{ from: 'authAutomat', to: 'warehouseAutomat', label: 'sync warehouses on authed', port: 'p_auth_status' },
|
||||
{ from: 'authAutomat', to: 'posterminalAutomat', label: 'sync terminals on authed', port: 'p_auth_status' },
|
||||
{ from: 'authAutomat', to: 'sseAutomat', label: 'hasAuth -> connect SSE', port: 'p_auth_hasAuth' },
|
||||
{ from: 'authAutomat', to: 'companyAutomat', label: 'hasCompany -> loadAll()', port: 'p_auth_hasCompany' },
|
||||
{ from: 'authAutomat', to: 'categoryAutomat', label: 'sync tree on hasCompany', port: 'p_auth_hasCompany' },
|
||||
{ from: 'authAutomat', to: 'deliveryAutomat', label: 'sync deliveries on authed', port: 'p_auth_status' },
|
||||
{ from: 'authAutomat', to: 'quoteAutomat', label: 'sync quotes on authed', port: 'p_auth_status' },
|
||||
{ from: 'authAutomat', to: 'orderAutomat', label: 'sync orders on authed', port: 'p_auth_status' },
|
||||
{ from: 'authAutomat', to: 'entityAutomat', label: 'sync entities on authed', port: 'p_auth_status' },
|
||||
{ from: 'authAutomat', to: 'customergroupAutomat', label: 'sync customer groups', port: 'p_auth_status' },
|
||||
{ from: 'authAutomat', to: 'numberAutomat', label: 'sync number series', port: 'p_auth_status' },
|
||||
{ from: 'authAutomat', to: 'accountAutomat', label: 'sync payment accounts', port: 'p_auth_status' },
|
||||
{ from: 'authAutomat', to: 'chatAutomat', label: 'auth chat & reload', 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' },
|
||||
@@ -624,16 +469,12 @@ const EDGE_DEFS = [
|
||||
{ from: 'sseAutomat', to: 'articleIndexAutomat', label: '\'articles\' sse trigger', port: 'p_sse_events' },
|
||||
{ from: 'sseAutomat', to: 'artikelAutomat', label: '\'article-<sku>\' sse reload', port: 'p_sse_events' },
|
||||
{ from: 'sseAutomat', to: 'articleDetailAutomat', label: 'invalidateArticle(sku)', port: 'p_sse_events' },
|
||||
{ from: 'sseAutomat', to: 'warehouseAutomat', label: '\'warehouse\' sse trigger', port: 'p_sse_events' },
|
||||
{ from: 'sseAutomat', to: 'supplierAutomat', label: '\'supplier\' sse trigger', port: 'p_sse_events' },
|
||||
{ from: 'sseAutomat', to: 'brandAutomat', label: '\'brand\' sse trigger', port: 'p_sse_events' },
|
||||
{ from: 'sseAutomat', to: 'attributeAutomat', label: '\'attributename\' sse', port: 'p_sse_events' },
|
||||
{ from: 'sseAutomat', to: 'propertyAutomat', label: '\'propertyname\' sse', port: 'p_sse_events' },
|
||||
{ from: 'sseAutomat', to: 'companyAutomat', label: 'warehouse/supplier/brand/attr/prop/carrier sse', port: 'p_sse_events' },
|
||||
{ from: 'sseAutomat', to: 'orderAutomat', label: '\'order\'/\'shipment\' sse', port: 'p_sse_events' },
|
||||
{ from: 'sseAutomat', to: 'vfsAutomat', label: '\'vfs\' sse mutation', port: 'p_sse_events' },
|
||||
|
||||
// Catalog, Indexing & Search pipeline
|
||||
{ from: 'companyAutomat', to: 'categoryAutomat', label: 'rootCategoryId', port: 'p_comp_root' },
|
||||
{ from: 'companyAutomat', to: 'categoryAutomat', label: 'rootCategoryId & terminals', port: 'p_comp_root' },
|
||||
{ from: 'categoryAutomat', to: 'categoryViewAutomat', label: 'attachTree & warm slugs', port: 'p_cat_loadTree' },
|
||||
{ from: 'categoryViewAutomat', to: 'articleIndexAutomat', label: 'derived union index', port: 'p_catview_collect' },
|
||||
{ from: 'articleIndexAutomat', to: 'articleSearchAutomat', label: 'query index items', port: 'p_idx_items' },
|
||||
@@ -643,18 +484,13 @@ const EDGE_DEFS = [
|
||||
{ from: 'articleDetailAutomat', to: 'artikelAutomat', label: 'watch SKU details', port: 'p_artdet_hist' },
|
||||
|
||||
// Purchasing & Deliveries
|
||||
{ from: 'deliveryEditorAutomat', to: 'supplierAutomat', label: 'match vendor catalog', port: 'p_deled_supplier' },
|
||||
{ from: 'deliveryEditorAutomat', to: 'companyAutomat', label: 'match vendor catalog', port: 'p_deled_supplier' },
|
||||
{ from: 'deliveryEditorAutomat', to: 'artikelAutomat', label: 'lookup SKU & pack units', port: 'p_deled_lines' },
|
||||
{ from: 'deliveryEditorAutomat', to: 'deliveryAutomat', label: 'commit delivery receipt', port: 'p_deled_commit' },
|
||||
|
||||
// Sales & CRM
|
||||
{ from: 'quoteAutomat', to: 'orderAutomat', label: 'convert quote to order', port: 'p_quote_items' },
|
||||
{ from: 'orderAutomat', to: 'entityAutomat', label: 'customer lookup', port: 'p_order_items' },
|
||||
{ from: 'companyAutomat', to: 'customergroupAutomat', label: 'poscustomergroup', port: 'p_comp_curr' },
|
||||
|
||||
// POS & Hardware
|
||||
{ from: 'posterminalAutomat', to: 'categoryAutomat', label: 'filter enabled roots', port: 'p_pos_term' },
|
||||
{ from: 'companyAutomat', to: 'posterminalAutomat', label: 'company context', port: 'p_comp_curr' },
|
||||
|
||||
// AI Assistant & Workspace
|
||||
{ from: 'chatAutomat', to: 'vfsAutomat', label: 'inspect / edit applets', port: 'p_chat_msg' },
|
||||
@@ -665,7 +501,7 @@ const EDGE_DEFS = [
|
||||
{ from: 'navigationAutomat', to: 'artikelAutomat', label: 'restore last SKU', port: 'p_nav_sku' },
|
||||
{ from: 'navigationAutomat', to: 'categoryAutomat', label: 'restore last category', port: 'p_nav_catslug' },
|
||||
{ from: 'navigationAutomat', to: 'deliveryEditorAutomat', label: 'restore draft path', port: 'p_nav_delpath' },
|
||||
{ from: 'userSettingsAutomat', to: 'chatAutomat', label: 'chatDrawerWidth', port: 'p_usr_drawer' },
|
||||
{ from: 'companyAutomat', to: 'chatAutomat', label: 'chatDrawerWidth', port: 'p_comp_usr' },
|
||||
{ from: 'helpAutomat', to: 'navigationAutomat', label: 'route-contextual help', port: 'p_help_active' },
|
||||
|
||||
// Self-loops
|
||||
|
||||
48
src/App.jsx
48
src/App.jsx
@@ -1,11 +1,15 @@
|
||||
import { PureComponent, Suspense } from 'react';
|
||||
import { PureComponent, lazy, Suspense } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import authAutomat from './automats/authAutomat';
|
||||
import LoginPage from './components/LoginPage';
|
||||
import CompanySetupDialog from './components/CompanySetupDialog';
|
||||
import { router } from './appRouter';
|
||||
|
||||
const CompanySetupDialog = lazy(() => import('./components/CompanySetupDialog'));
|
||||
const AppRouter = lazy(() => import('./appRouter'));
|
||||
const OAuthAuthorizePage = lazy(() => import('./routes/oauth/OAuthAuthorizePage'));
|
||||
const SseMonitor = import.meta.env.DEV
|
||||
? lazy(() => import('./devtools/SseMonitor'))
|
||||
: null;
|
||||
|
||||
function PageFallback() {
|
||||
return (
|
||||
@@ -30,7 +34,7 @@ class App extends PureComponent {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsubscribe = authAutomat.subscribe(this, ['status', 'user']);
|
||||
this.unsubscribe = authAutomat.subscribe(this, ['status', 'user', 'hasAuth', 'hasCompany']);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -38,7 +42,9 @@ class App extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { status, user } = this.state;
|
||||
const { status, user, hasAuth, hasCompany } = this.state;
|
||||
const isAuthed = Boolean(hasAuth ?? (status === 'authed' && user));
|
||||
const isCompanySelected = Boolean(hasCompany ?? (isAuthed && user?.selectedCompanyId != null));
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
@@ -56,7 +62,7 @@ class App extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
if (status !== 'authed') {
|
||||
if (!isAuthed) {
|
||||
if (typeof window !== 'undefined' && window.location.pathname === '/oauth/authorize') {
|
||||
const returnTo = encodeURIComponent(window.location.pathname + window.location.search);
|
||||
window.history.replaceState(null, '', `/login?returnTo=${returnTo}`);
|
||||
@@ -64,16 +70,28 @@ class App extends PureComponent {
|
||||
return <LoginPage />;
|
||||
}
|
||||
|
||||
const isOAuthAuthorize = typeof window !== 'undefined' && window.location.pathname === '/oauth/authorize';
|
||||
|
||||
if (user?.selectedCompanyId == null && !isOAuthAuthorize) {
|
||||
return <CompanySetupDialog />;
|
||||
}
|
||||
const isOAuthAuthorize =
|
||||
typeof window !== 'undefined' && window.location.pathname === '/oauth/authorize';
|
||||
|
||||
return (
|
||||
<Suspense fallback={<PageFallback />}>
|
||||
<RouterProvider router={router} />
|
||||
</Suspense>
|
||||
<>
|
||||
<Suspense fallback={<PageFallback />}>
|
||||
{!isCompanySelected ? (
|
||||
isOAuthAuthorize ? (
|
||||
<OAuthAuthorizePage />
|
||||
) : (
|
||||
<CompanySetupDialog />
|
||||
)
|
||||
) : (
|
||||
<AppRouter />
|
||||
)}
|
||||
</Suspense>
|
||||
{SseMonitor ? (
|
||||
<Suspense fallback={null}>
|
||||
<SseMonitor />
|
||||
</Suspense>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
import { lazy } from 'react';
|
||||
import { Navigate, createBrowserRouter } from 'react-router-dom';
|
||||
import { Navigate, RouterProvider, createBrowserRouter } from 'react-router-dom';
|
||||
import AppShell from './components/AppShell';
|
||||
import SectionLayout from './components/SectionLayout';
|
||||
|
||||
import ArtikelUebersichtPage from './routes/artikel';
|
||||
import ArtikelKategorienPage from './routes/artikel/kategorien';
|
||||
import RedirectArtikelKategorien from './routes/artikel/RedirectArtikelKategorien';
|
||||
import ArtikelLagerPage from './routes/artikel/lager';
|
||||
import ArtikelMarkenPage from './routes/artikel/marken';
|
||||
import ArtikelVorlagenPage from './routes/artikel/vorlagen';
|
||||
import ArtikelMerkmalePage from './routes/artikel/merkmale';
|
||||
import ArtikelSkuPage from './routes/artikel/_sku';
|
||||
import LieferungListePage from './routes/lieferung';
|
||||
import LieferungNeuPage from './routes/lieferung/neu';
|
||||
import LieferungLieferantenPage from './routes/lieferung/lieferanten';
|
||||
import LieferungEditPage from './routes/lieferung/_deliveryId';
|
||||
import AuftragAngebotPage from './routes/auftrag';
|
||||
import AuftragAngebotNeuPage from './routes/auftrag/neuesangebot';
|
||||
import AuftragAngebotEditPage from './routes/auftrag/_quoteId';
|
||||
import AuftragBestellungPage from './routes/auftrag/bestellung';
|
||||
import AuftragBestellungNeuPage from './routes/auftrag/bestellung/neu';
|
||||
import AuftragBestellungEditPage from './routes/auftrag/bestellung/_orderId';
|
||||
import AuftragAusliefernPage from './routes/auftrag/ausliefern';
|
||||
import AuftragRetourePage from './routes/auftrag/retoure';
|
||||
import AuftragRetoureNeuPage from './routes/auftrag/retoure/neu';
|
||||
import AuftragRetoureEditPage from './routes/auftrag/retoure/_retoureId';
|
||||
import AuftragZahlungenPage from './routes/auftrag/zahlungen';
|
||||
import AuftragVersandartenPage from './routes/auftrag/versandarten';
|
||||
const ArtikelUebersichtPage = lazy(() => import('./routes/artikel'));
|
||||
const ArtikelKategorienPage = lazy(() => import('./routes/artikel/kategorien'));
|
||||
const RedirectArtikelKategorien = lazy(() => import('./routes/artikel/RedirectArtikelKategorien'));
|
||||
const ArtikelLagerPage = lazy(() => import('./routes/artikel/lager'));
|
||||
const ArtikelMarkenPage = lazy(() => import('./routes/artikel/marken'));
|
||||
const ArtikelVorlagenPage = lazy(() => import('./routes/artikel/vorlagen'));
|
||||
const ArtikelMerkmalePage = lazy(() => import('./routes/artikel/merkmale'));
|
||||
const ArtikelSkuPage = lazy(() => import('./routes/artikel/_sku'));
|
||||
const LieferungListePage = lazy(() => import('./routes/lieferung'));
|
||||
const LieferungNeuPage = lazy(() => import('./routes/lieferung/neu'));
|
||||
const LieferungLieferantenPage = lazy(() => import('./routes/lieferung/lieferanten'));
|
||||
const LieferungEditPage = lazy(() => import('./routes/lieferung/_deliveryId'));
|
||||
const AuftragAngebotPage = lazy(() => import('./routes/auftrag'));
|
||||
const AuftragAngebotNeuPage = lazy(() => import('./routes/auftrag/neuesangebot'));
|
||||
const AuftragAngebotEditPage = lazy(() => import('./routes/auftrag/_quoteId'));
|
||||
const AuftragBestellungPage = lazy(() => import('./routes/auftrag/bestellung'));
|
||||
const AuftragBestellungNeuPage = lazy(() => import('./routes/auftrag/bestellung/neu'));
|
||||
const AuftragBestellungEditPage = lazy(() => import('./routes/auftrag/bestellung/_orderId'));
|
||||
const AuftragAusliefernPage = lazy(() => import('./routes/auftrag/ausliefern'));
|
||||
const AuftragRetourePage = lazy(() => import('./routes/auftrag/retoure'));
|
||||
const AuftragRetoureNeuPage = lazy(() => import('./routes/auftrag/retoure/neu'));
|
||||
const AuftragRetoureEditPage = lazy(() => import('./routes/auftrag/retoure/_retoureId'));
|
||||
const AuftragZahlungenPage = lazy(() => import('./routes/auftrag/zahlungen'));
|
||||
const AuftragVersandartenPage = lazy(() => import('./routes/auftrag/versandarten'));
|
||||
|
||||
import KundeListePage from './routes/kunde';
|
||||
import KundeNeuPage from './routes/kunde/neu';
|
||||
import KundeGruppenPage from './routes/kunde/gruppen';
|
||||
import KundeEditPage from './routes/kunde/_entityId';
|
||||
import StatisticsPage from './routes/infos/StatisticsPage';
|
||||
import OAuthAuthorizePage from './routes/oauth/OAuthAuthorizePage';
|
||||
const KundeListePage = lazy(() => import('./routes/kunde'));
|
||||
const KundeNeuPage = lazy(() => import('./routes/kunde/neu'));
|
||||
const KundeGruppenPage = lazy(() => import('./routes/kunde/gruppen'));
|
||||
const KundeEditPage = lazy(() => import('./routes/kunde/_entityId'));
|
||||
const StatisticsPage = lazy(() => import('./routes/infos/StatisticsPage'));
|
||||
const OAuthAuthorizePage = lazy(() => import('./routes/oauth/OAuthAuthorizePage'));
|
||||
|
||||
const ArticleHistoryPage = lazy(() => import('./routes/infos/ArticleHistoryPage'));
|
||||
const GlobalHistoryPage = lazy(() => import('./routes/infos/GlobalHistoryPage'));
|
||||
@@ -119,7 +119,6 @@ const routes = [
|
||||
{ path: 'versandarten', element: <AuftragVersandartenPage /> },
|
||||
{ path: 'lieferarten', element: <Navigate to="/auftrag/versandarten" replace /> },
|
||||
{ path: ':quoteId', element: <AuftragAngebotEditPage /> },
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -144,3 +143,7 @@ export const router =
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.data.router = router;
|
||||
}
|
||||
|
||||
export default function AppRouter() {
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* @file accountAutomat.js
|
||||
* @description Manages cached payment accounts (`GET /api/account`) for the selected company.
|
||||
*/
|
||||
|
||||
import { createListAutomat } from './createListAutomat';
|
||||
import { account as accountApi } from '../api';
|
||||
|
||||
export const accountAutomat = createListAutomat({
|
||||
name: 'account',
|
||||
fetch: async () => {
|
||||
const res = await accountApi.list();
|
||||
return Array.isArray(res?.accounts) ? res.accounts : [];
|
||||
},
|
||||
sort: (items) =>
|
||||
[...items].sort((a, b) =>
|
||||
String(a.name ?? '').localeCompare(String(b.name ?? ''), 'de')
|
||||
),
|
||||
});
|
||||
|
||||
export default accountAutomat;
|
||||
@@ -286,6 +286,9 @@ categoryAutomat.subscribe((state) => {
|
||||
scheduleRebuild();
|
||||
}
|
||||
});
|
||||
if (categoryAutomat.state.treeStatus === 'succeeded' && categoryAutomat.state.viewsLoading === false) {
|
||||
scheduleRebuild();
|
||||
}
|
||||
|
||||
authAutomat.subscribe((auth) => {
|
||||
if (auth.status === 'anon') actions.reset();
|
||||
|
||||
@@ -14,8 +14,7 @@ import { Automat } from 'react-automat';
|
||||
import FlexSearch from 'flexsearch';
|
||||
import articleIndexAutomat from './articleIndexAutomat';
|
||||
import categoryAutomat from './categoryAutomat';
|
||||
import brandAutomat from './brandAutomat';
|
||||
import supplierAutomat from './supplierAutomat';
|
||||
import companyAutomat from './companyAutomat';
|
||||
import { parseFilter } from '../components/article/searchFormat';
|
||||
|
||||
// #region Types & Initial State
|
||||
@@ -585,7 +584,7 @@ function buildCategoryFacets(matchedArticles) {
|
||||
*/
|
||||
function buildBrandFacets(matchedArticles) {
|
||||
const facets = {};
|
||||
const allBrands = brandAutomat.state?.brands ?? [];
|
||||
const allBrands = companyAutomat.state?.brands ?? [];
|
||||
const brandById = new Map();
|
||||
const brandByName = new Map();
|
||||
for (const b of allBrands) {
|
||||
@@ -633,8 +632,8 @@ function buildBrandFacets(matchedArticles) {
|
||||
*/
|
||||
function buildSupplierFacets(matchedArticles) {
|
||||
const facets = {};
|
||||
const suppliers = supplierAutomat.state?.suppliers ?? [];
|
||||
const articlesBySupplier = supplierAutomat.state?.articlesBySupplier ?? {};
|
||||
const suppliers = companyAutomat.state?.suppliers ?? [];
|
||||
const articlesBySupplier = companyAutomat.state?.articlesBySupplier ?? {};
|
||||
|
||||
const supplierArticleSets = new Map();
|
||||
for (const s of suppliers) {
|
||||
@@ -738,7 +737,7 @@ const actions = {
|
||||
// Apply supplier filter if set
|
||||
const selectedSuppliers = parseFilter(supplier);
|
||||
if (selectedSuppliers.length > 0) {
|
||||
const articlesBySupplier = supplierAutomat.state?.articlesBySupplier ?? {};
|
||||
const articlesBySupplier = companyAutomat.state?.articlesBySupplier ?? {};
|
||||
const allowedSkus = new Set();
|
||||
for (const sId of selectedSuppliers) {
|
||||
const list = articlesBySupplier[String(sId)] ?? [];
|
||||
@@ -898,15 +897,10 @@ categoryAutomat.subscribe((catState) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Update facets when brands are loaded or updated.
|
||||
brandAutomat.subscribe(() => {
|
||||
// Update facets when brands, suppliers, or supplier articles are loaded or updated.
|
||||
companyAutomat.subscribe(() => {
|
||||
actions.runSearch();
|
||||
});
|
||||
|
||||
// Update facets when suppliers or supplier articles are loaded or updated.
|
||||
supplierAutomat.subscribe(() => {
|
||||
actions.runSearch();
|
||||
});
|
||||
}, ['brands', 'suppliers', 'articlesBySupplier']);
|
||||
// #endregion
|
||||
|
||||
// #region Selectors & Exports
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
/**
|
||||
* @file attributeAutomat.js
|
||||
* @description Manages catalog product attributes and their discrete values
|
||||
* (e.g. Color, Size) for the active company.
|
||||
*
|
||||
* Upstream Triggers:
|
||||
* - `authAutomat`: Subscribes to auth state changes — calls `actions.load()` on
|
||||
* `'authed'` with a `selectedCompanyId`, and `actions.reset()` on `'anon'`.
|
||||
* - `sseAutomat`: Calls `actions.load()` upon receiving an `'attributename'` SSE event.
|
||||
* - UI / Dialogs: Mutating actions (`createAttribute`, `updateAttribute`,
|
||||
* `deleteAttribute`, `addValue`, `deleteValue`, `reorderValues`).
|
||||
*/
|
||||
|
||||
import { Automat } from 'react-automat';
|
||||
import { attributename as attributenameApi, attributevalue as attributevalueApi } from '../api';
|
||||
import authAutomat from './authAutomat';
|
||||
|
||||
/**
|
||||
* @typedef {Object} AttributeValue
|
||||
* @property {number} id
|
||||
* @property {number} attributeId
|
||||
* @property {string} value
|
||||
* @property {number} sort
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} AttributeName
|
||||
* @property {number} id
|
||||
* @property {string} name
|
||||
* @property {number} sort
|
||||
* @property {boolean} exclusive
|
||||
* @property {AttributeValue[]} values
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} AttributeState
|
||||
* @property {AttributeName[]} attributes
|
||||
* @property {'idle'|'loading'|'succeeded'|'failed'} status
|
||||
* @property {string|null} error
|
||||
*/
|
||||
|
||||
/** @type {AttributeState} */
|
||||
const initialState = {
|
||||
attributes: [],
|
||||
status: 'idle',
|
||||
error: null,
|
||||
};
|
||||
|
||||
function sortValues(values) {
|
||||
if (!Array.isArray(values)) return [];
|
||||
return [...values].sort((a, b) => {
|
||||
const sDiff = (a.sort ?? 0) - (b.sort ?? 0);
|
||||
if (sDiff !== 0) return sDiff;
|
||||
return (a.id ?? 0) - (b.id ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeAttributes(attributes) {
|
||||
if (!Array.isArray(attributes)) return [];
|
||||
return [...attributes]
|
||||
.map((attr) => ({
|
||||
...attr,
|
||||
exclusive: Boolean(attr.exclusive),
|
||||
values: sortValues(attr.values),
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
const sDiff = (a.sort ?? 0) - (b.sort ?? 0);
|
||||
if (sDiff !== 0) return sDiff;
|
||||
return (a.id ?? 0) - (b.id ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
let attributeAutomat;
|
||||
|
||||
const actions = {
|
||||
/** Load all attribute names and their values for the selected company. */
|
||||
async load() {
|
||||
attributeAutomat.setState({ status: 'loading', error: null });
|
||||
try {
|
||||
const res = await attributenameApi.list();
|
||||
const attributes = normalizeAttributes(res.attributenames);
|
||||
attributeAutomat.setState({ attributes, status: 'succeeded' });
|
||||
return attributes;
|
||||
} catch (err) {
|
||||
attributeAutomat.setState({ status: 'failed', error: err?.message ?? String(err) });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create an attribute name.
|
||||
* @param {{ name: string, sort?: number, exclusive?: boolean }} body
|
||||
*/
|
||||
async createAttribute(body) {
|
||||
const res = await attributenameApi.create(body);
|
||||
await actions.load();
|
||||
return res.attributename;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an attribute name.
|
||||
* @param {number} id
|
||||
* @param {{ name?: string, sort?: number, exclusive?: boolean }} body
|
||||
*/
|
||||
async updateAttribute(id, body) {
|
||||
const res = await attributenameApi.update(id, body);
|
||||
await actions.load();
|
||||
return res.attributename;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete an attribute name.
|
||||
* @param {number} id
|
||||
*/
|
||||
async deleteAttribute(id) {
|
||||
await attributenameApi.delete(id);
|
||||
await actions.load();
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete multiple attribute names.
|
||||
* @param {number[]} ids
|
||||
*/
|
||||
async deleteAttributes(ids) {
|
||||
for (const id of ids) {
|
||||
await attributenameApi.delete(id);
|
||||
}
|
||||
await actions.load();
|
||||
},
|
||||
|
||||
/**
|
||||
* Reorder attribute names.
|
||||
* @param {number[]} attributenameIds
|
||||
*/
|
||||
async reorderAttributes(attributenameIds) {
|
||||
await attributenameApi.order(attributenameIds);
|
||||
await actions.load();
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a value for an attribute.
|
||||
* @param {number} attributeId
|
||||
* @param {{ value: string, sort?: number }} body
|
||||
*/
|
||||
async createValue(attributeId, body) {
|
||||
const res = await attributevalueApi.create(attributeId, body);
|
||||
await actions.load();
|
||||
return res.attributevalue;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an attribute value.
|
||||
* @param {number} id
|
||||
* @param {{ value?: string, sort?: number }} body
|
||||
*/
|
||||
async updateValue(id, body) {
|
||||
const res = await attributevalueApi.update(id, body);
|
||||
await actions.load();
|
||||
return res.attributevalue;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete an attribute value.
|
||||
* @param {number} id
|
||||
*/
|
||||
async deleteValue(id) {
|
||||
await attributevalueApi.delete(id);
|
||||
await actions.load();
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete multiple attribute values.
|
||||
* @param {number[]} ids
|
||||
*/
|
||||
async deleteValues(ids) {
|
||||
for (const id of ids) {
|
||||
await attributevalueApi.delete(id);
|
||||
}
|
||||
await actions.load();
|
||||
},
|
||||
|
||||
/**
|
||||
* Reorder values of an attribute.
|
||||
* @param {number} attributeId
|
||||
* @param {number[]} attributevalueIds
|
||||
*/
|
||||
async reorderValues(attributeId, attributevalueIds) {
|
||||
await attributevalueApi.order(attributeId, attributevalueIds);
|
||||
await actions.load();
|
||||
},
|
||||
|
||||
/** Reset to idle state (called on logout / company switch). */
|
||||
reset() {
|
||||
attributeAutomat.setState(initialState);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Global attribute state container.
|
||||
* Loads automatically when authAutomat reaches 'authed' with a selectedCompanyId.
|
||||
*
|
||||
* @type {Automat<AttributeState>}
|
||||
*/
|
||||
attributeAutomat = new Automat(initialState, actions, { name: 'attribute' });
|
||||
|
||||
// Selectors
|
||||
export function selectAttributes() {
|
||||
return attributeAutomat.state.attributes;
|
||||
}
|
||||
|
||||
export function selectAttributeStatus() {
|
||||
return attributeAutomat.state.status;
|
||||
}
|
||||
|
||||
// Subscribe to auth changes
|
||||
authAutomat.subscribe((auth) => {
|
||||
const companyId = auth.user?.selectedCompanyId;
|
||||
if (auth.status === 'authed' && companyId) {
|
||||
void attributeAutomat.actions.load();
|
||||
} else if (auth.status === 'anon') {
|
||||
attributeAutomat.actions.reset();
|
||||
}
|
||||
});
|
||||
|
||||
export default attributeAutomat;
|
||||
@@ -1,30 +1,24 @@
|
||||
/**
|
||||
* @file authAutomat.js
|
||||
* @description Central authentication and multi-tenant session manager.
|
||||
* Tracks current user, Google OAuth client ID, and active company selection.
|
||||
* Tracks current user, Google OAuth client ID, active company selection, and
|
||||
* the two core session bits: `hasAuth` and `hasCompany` (`hasCompany` requires `hasAuth`).
|
||||
*
|
||||
* Upstream Triggers:
|
||||
* - `AuthBootstrap.jsx`: Calls `bootstrapSession(session)` on initial app load.
|
||||
* - `LoginPage.jsx`: Calls `loginWithGoogle(credential)` or `loginWithCredentials(body)`.
|
||||
* - `UserMenu.jsx`: Calls `switchCompany(companyId)` or `logout()`.
|
||||
* - `LoginPage.jsx`: Calls `loginWithGoogle(credential)`.
|
||||
* - `CompanySetupDialog.jsx` / `UserMenu.jsx`: Calls `selectCompany(companyId)`,
|
||||
* `createCompany(payload)`, or `logout()`.
|
||||
*
|
||||
* Downstream Triggers:
|
||||
* Subscribed to by almost all company-scoped automats, orchestrating their loading
|
||||
* when authenticated (`authed` + `selectedCompanyId`) and reset on logout (`anon`):
|
||||
* - `companyAutomat`, `categoryAutomat`, `brandAutomat`, `unitAutomat`, `posterminalAutomat`
|
||||
* - `attributeAutomat`, `propertyAutomat`, `supplierAutomat`, `warehouseAutomat`
|
||||
* - `sseAutomat`: Disconnects/reconnects SSE stream to `/api/events` per company
|
||||
* - `chatAutomat`: Reconnects AI chat SSE stream and reloads conversations
|
||||
* - `articleIndexAutomat`: Resets the derived index on logout; loads it lazily from
|
||||
* the category views once the session is established
|
||||
* - `createListAutomat` instances (`deliveryAutomat`, `quoteAutomat`,
|
||||
* `orderAutomat`, `entityAutomat`, `customergroupAutomat`, `numberAutomat`)
|
||||
* - Grid history automats (`articleHistoryGridAutomat`, `globalHistoryGridAutomat`)
|
||||
* - `artikelAutomat`: Clears cached articles on logout
|
||||
* - With `hasAuth`: SSE (`sseAutomat`) is connected to `/api/events`.
|
||||
* - With `hasAuth && !hasCompany`: `CompanySetupDialog` is shown (`companyAutomat` not instantiated yet).
|
||||
* - With `hasCompany` (requires `hasAuth`): 1st-level menu (`AppShell`) and `companyAutomat`
|
||||
* are mounted and company APIs are triggered.
|
||||
*/
|
||||
|
||||
import { Automat } from 'react-automat';
|
||||
import { auth, company } from '../api';
|
||||
import { auth, company, number as numberApi } from '../api';
|
||||
|
||||
/**
|
||||
* @typedef {Object} AuthUser
|
||||
@@ -37,6 +31,8 @@ import { auth, company } from '../api';
|
||||
* @property {AuthUser|null} user Authenticated user, or `null` for anonymous sessions.
|
||||
* @property {string|null} googleClientId Google Identity Services client ID.
|
||||
* @property {'loading'|'anon'|'authed'} status Current authentication lifecycle state.
|
||||
* @property {boolean} hasAuth True when the session has a valid authenticated user.
|
||||
* @property {boolean} hasCompany True when `hasAuth` is true AND a company is selected.
|
||||
*/
|
||||
|
||||
/** Emitted after an authenticated session has been established. */
|
||||
@@ -51,10 +47,13 @@ const initialState = {
|
||||
user: null,
|
||||
googleClientId: null,
|
||||
status: 'loading',
|
||||
hasAuth: false,
|
||||
hasCompany: false,
|
||||
};
|
||||
|
||||
let emit = () => { };
|
||||
|
||||
// #region helpers
|
||||
/**
|
||||
* Registers the application-level dispatcher used for authentication events.
|
||||
*
|
||||
@@ -93,9 +92,47 @@ function mergeUser(current, incoming) {
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes `hasAuth` and `hasCompany` (`hasCompany` strictly requires `hasAuth`).
|
||||
*
|
||||
* @param {AuthUser|null} user
|
||||
* @param {'loading'|'anon'|'authed'} status
|
||||
* @param {object} [extra]
|
||||
* @returns {Partial<AuthState>}
|
||||
*/
|
||||
function buildAuthState(user, status, extra = {}) {
|
||||
const hasAuth = status === 'authed' && Boolean(user);
|
||||
const hasCompany = hasAuth && user?.selectedCompanyId != null;
|
||||
return {
|
||||
user: hasAuth ? user : null,
|
||||
status,
|
||||
hasAuth,
|
||||
hasCompany,
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
function syncSse(state) {
|
||||
if (state.hasAuth) {
|
||||
void import('./sseAutomat')
|
||||
.then(({ default: sseAutomat }) => {
|
||||
sseAutomat.actions.syncWithAuth?.(state);
|
||||
})
|
||||
.catch(() => { });
|
||||
} else {
|
||||
const sseAutomat = Automat.get('sse');
|
||||
if (sseAutomat?.actions?.syncWithAuth) {
|
||||
sseAutomat.actions.syncWithAuth(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
// #endregion
|
||||
|
||||
let authAutomat;
|
||||
|
||||
const actions = {
|
||||
// #region bootstrapSession
|
||||
// MARK: calls auth.me from ../api
|
||||
/**
|
||||
* Restores an existing session or requests it from the authentication API.
|
||||
*
|
||||
@@ -111,7 +148,10 @@ const actions = {
|
||||
|
||||
if (!nextSession.user) {
|
||||
if (authAutomat.state.status !== 'authed') {
|
||||
authAutomat.setState({ user: null, googleClientId, status: 'anon' });
|
||||
const nextState = authAutomat.setState(
|
||||
buildAuthState(null, 'anon', { googleClientId })
|
||||
);
|
||||
syncSse(nextState);
|
||||
} else {
|
||||
authAutomat.setState({ googleClientId });
|
||||
}
|
||||
@@ -119,17 +159,24 @@ const actions = {
|
||||
}
|
||||
|
||||
const user = mergeUser(authAutomat.state.user, nextSession.user);
|
||||
authAutomat.setState({ user, googleClientId, status: 'authed' });
|
||||
const nextState = authAutomat.setState(
|
||||
buildAuthState(user, 'authed', { googleClientId })
|
||||
);
|
||||
syncSse(nextState);
|
||||
emit({ type: AUTH_SESSION_ESTABLISHED });
|
||||
return nextSession;
|
||||
} catch (error) {
|
||||
if (authAutomat.state.status === 'loading') {
|
||||
authAutomat.setState({ user: null, status: 'anon' });
|
||||
const nextState = authAutomat.setState(buildAuthState(null, 'anon'));
|
||||
syncSse(nextState);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
// #endregion
|
||||
|
||||
// #region loginWithGoogle
|
||||
// MARK: calls auth.loginWithGoogle from ../api
|
||||
/**
|
||||
* Exchanges a Google credential for an authenticated application session.
|
||||
*
|
||||
@@ -143,19 +190,24 @@ const actions = {
|
||||
throw new Error('Login ok but session cookie not accepted (/api/auth/me still unauthorized)');
|
||||
}
|
||||
const user = mergeUser(authAutomat.state.user, session.user);
|
||||
authAutomat.setState({
|
||||
user,
|
||||
googleClientId: session.googleClientId ?? authAutomat.state.googleClientId,
|
||||
status: 'authed',
|
||||
});
|
||||
const nextState = authAutomat.setState(
|
||||
buildAuthState(user, 'authed', {
|
||||
googleClientId: session.googleClientId ?? authAutomat.state.googleClientId,
|
||||
})
|
||||
);
|
||||
syncSse(nextState);
|
||||
emit({ type: AUTH_SESSION_ESTABLISHED });
|
||||
return session;
|
||||
} catch (error) {
|
||||
authAutomat.setState({ user: null, status: 'anon' });
|
||||
const nextState = authAutomat.setState(buildAuthState(null, 'anon'));
|
||||
syncSse(nextState);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
// #endregion
|
||||
|
||||
// #region selectCompany
|
||||
// MARK: calls company.select from ../api
|
||||
/**
|
||||
* Selects the company used by the current session.
|
||||
*
|
||||
@@ -168,11 +220,15 @@ const actions = {
|
||||
authAutomat.state.user,
|
||||
data.user ?? { selectedCompanyId: companyId },
|
||||
);
|
||||
authAutomat.setState({ user, status: 'authed' });
|
||||
const nextState = authAutomat.setState(buildAuthState(user, 'authed'));
|
||||
syncSse(nextState);
|
||||
emit({ type: AUTH_COMPANY_CHANGED });
|
||||
return user;
|
||||
},
|
||||
// #endregion
|
||||
|
||||
// #region createCompany
|
||||
// MARK: calls company.create and company.select from ../api
|
||||
/**
|
||||
* Creates a company and selects it for the current user.
|
||||
*
|
||||
@@ -180,7 +236,8 @@ const actions = {
|
||||
* @returns {Promise<AuthUser>} Updated user.
|
||||
*/
|
||||
async createCompany(payload) {
|
||||
const data = await company.create(payload);
|
||||
const { numbers, ...companyPayload } = payload || {};
|
||||
const data = await company.create(companyPayload);
|
||||
let incoming = data.user;
|
||||
if (!incoming && data.company?.id) {
|
||||
const selected = await company.select(data.company.id);
|
||||
@@ -190,12 +247,34 @@ const actions = {
|
||||
throw new Error('Unternehmen angelegt, aber keine Benutzerdaten erhalten.');
|
||||
}
|
||||
|
||||
if (numbers) {
|
||||
try {
|
||||
const listRes = await numberApi.list();
|
||||
const serverNumbers = listRes?.numbers ?? [];
|
||||
for (const serverRow of serverNumbers) {
|
||||
const userConfig = numbers[serverRow.type];
|
||||
if (userConfig) {
|
||||
const prefix = String(userConfig.prefix ?? '');
|
||||
const padding = Math.max(0, Number(userConfig.padding) || 0);
|
||||
const next = Math.max(1, Number(userConfig.next) || 1);
|
||||
await numberApi.update(serverRow.id, { prefix, padding, next });
|
||||
}
|
||||
}
|
||||
} catch (numErr) {
|
||||
console.warn('Initial number sequences could not be saved:', numErr);
|
||||
}
|
||||
}
|
||||
|
||||
const user = mergeUser(authAutomat.state.user, incoming);
|
||||
authAutomat.setState({ user, status: 'authed' });
|
||||
const nextState = authAutomat.setState(buildAuthState(user, 'authed'));
|
||||
syncSse(nextState);
|
||||
emit({ type: AUTH_COMPANY_CHANGED });
|
||||
return user;
|
||||
},
|
||||
// #endregion
|
||||
|
||||
// #region logout
|
||||
// MARK: calls auth.logout from ../api
|
||||
/**
|
||||
* Ends the server session, clears the current user, and returns to `/`.
|
||||
*
|
||||
@@ -204,9 +283,11 @@ const actions = {
|
||||
async logout() {
|
||||
await auth.logout();
|
||||
window.history.replaceState(null, '', '/');
|
||||
authAutomat.setState({ user: null, status: 'anon' });
|
||||
const nextState = authAutomat.setState(buildAuthState(null, 'anon'));
|
||||
syncSse(nextState);
|
||||
emit({ type: AUTH_LOGGED_OUT });
|
||||
},
|
||||
// #endregion
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -216,6 +297,12 @@ const actions = {
|
||||
*/
|
||||
authAutomat = new Automat(initialState, actions, { name: 'auth' });
|
||||
|
||||
// #region selectors
|
||||
export const selectHasAuth = () => Boolean(authAutomat.state.hasAuth);
|
||||
export const selectHasCompany = () => Boolean(authAutomat.state.hasCompany);
|
||||
export const selectSelectedCompanyId = () =>
|
||||
authAutomat.state.hasCompany ? authAutomat.state.user?.selectedCompanyId ?? null : null;
|
||||
|
||||
/**
|
||||
* Re-reads `GET /api/auth/me` from the server.
|
||||
*
|
||||
@@ -227,5 +314,6 @@ authAutomat = new Automat(initialState, actions, { name: 'auth' });
|
||||
export function bootstrapAuthMe() {
|
||||
return auth.me();
|
||||
}
|
||||
// #endregion
|
||||
|
||||
export default authAutomat;
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
/**
|
||||
* @file brandAutomat.js
|
||||
* @description Manages catalog brands and manufacturers for the active company.
|
||||
*
|
||||
* Upstream Triggers:
|
||||
* - `authAutomat`: Subscribes to auth state changes — calls `actions.load()` on
|
||||
* `'authed'` with a `selectedCompanyId`, and `actions.reset()` on `'anon'`.
|
||||
* - `sseAutomat`: Subscribes to the single `'brand'` event carrying
|
||||
* `{ sender, brandId }` — any create/rename/delete/picture change triggers
|
||||
* `actions.load(true)`, which silently re-fetches the list. Subscribed views
|
||||
* (e.g. `ArtikelMarkenPage` via `brandAutomat.subscribe(...)`) then
|
||||
* re-render from the updated state. Events sent by this tab (matching `sender`)
|
||||
* are ignored.
|
||||
* - UI / Settings: Mutating actions (`create`, `rename`, `remove`, `addImage`, `deleteImage`).
|
||||
*/
|
||||
|
||||
import { Automat } from 'react-automat';
|
||||
import { brand as brandApi } from '../api';
|
||||
import authAutomat from './authAutomat';
|
||||
import sseAutomat, { getSseClientId } from './sseAutomat';
|
||||
|
||||
/**
|
||||
* @typedef {Object} BrandState
|
||||
* @property {Array<object>} brands All brands (full list, decoupled from page UI).
|
||||
* @property {Array<object>} filteredBrands Filtered brands according to active filter query.
|
||||
* @property {string} filter Active search/filter string.
|
||||
* @property {'idle'|'loading'|'succeeded'|'failed'} status
|
||||
* @property {string|null} error
|
||||
*/
|
||||
|
||||
/** @type {BrandState} */
|
||||
const initialState = {
|
||||
brands: [],
|
||||
filteredBrands: [],
|
||||
filter: '',
|
||||
status: 'idle',
|
||||
error: null,
|
||||
};
|
||||
|
||||
const sortBrands = (brands) =>
|
||||
[...brands].sort((a, b) => String(a.name ?? '').localeCompare(String(b.name ?? ''), 'de'));
|
||||
|
||||
const applyFilter = (brands, filter) => {
|
||||
const query = String(filter ?? '').trim().toLowerCase();
|
||||
return query ? brands.filter((b) => String(b.name ?? '').toLowerCase().includes(query)) : brands;
|
||||
};
|
||||
|
||||
let brandAutomat;
|
||||
let inflightLoad = null;
|
||||
const FETCH_PAGE_SIZE = 100;
|
||||
// #region fetch
|
||||
async function fetchAllBrands() {
|
||||
let offset = 0, total = Infinity;
|
||||
const collected = [];
|
||||
while (offset === 0 || collected.length < total) {
|
||||
const res = await brandApi.list({ offset, limit: FETCH_PAGE_SIZE });
|
||||
if (offset === 0) total = Number(res.total ?? res.brands?.length ?? 0);
|
||||
const page = Array.isArray(res.brands) ? res.brands : [];
|
||||
collected.push(...page);
|
||||
offset += page.length;
|
||||
if (page.length === 0) break;
|
||||
}
|
||||
return { brands: sortBrands(collected) };
|
||||
}
|
||||
|
||||
const updateStateWithBrands = (brands, extra = {}) => {
|
||||
const filteredBrands = applyFilter(brands, brandAutomat.state.filter);
|
||||
brandAutomat.setState({ brands, filteredBrands, ...extra });
|
||||
return brands;
|
||||
};
|
||||
|
||||
const actions = {
|
||||
/** Sets the active brand search filter and updates filteredBrands. */
|
||||
filter(query) {
|
||||
const filter = String(query ?? '');
|
||||
brandAutomat.setState({ filter, filteredBrands: applyFilter(brandAutomat.state.brands, filter) });
|
||||
},
|
||||
|
||||
/**
|
||||
* Load all brands for the selected company (pages through the API).
|
||||
* @param {boolean} [silent=false] If true, re-fetches without setting status to 'loading'.
|
||||
*/
|
||||
async load(silent = false) {
|
||||
if (inflightLoad) return inflightLoad;
|
||||
if (!silent) {
|
||||
brandAutomat.setState({ status: 'loading', error: null });
|
||||
}
|
||||
return (inflightLoad = fetchAllBrands()
|
||||
.then(({ brands }) => updateStateWithBrands(brands, { status: 'succeeded', error: null }))
|
||||
.catch((err) => {
|
||||
const error = err?.message ?? String(err);
|
||||
brandAutomat.setState(silent ? { error } : { status: 'failed', error });
|
||||
throw err;
|
||||
})
|
||||
.finally(() => { inflightLoad = null; }));
|
||||
},
|
||||
// #region add
|
||||
async create(body) {
|
||||
const res = await brandApi.create(body);
|
||||
const row = res.brand;
|
||||
updateStateWithBrands(sortBrands([...brandAutomat.state.brands.filter((b) => b.id !== row.id), row]));
|
||||
return row;
|
||||
},
|
||||
// #region rename
|
||||
async rename(id, body) {
|
||||
const res = await brandApi.rename(id, body);
|
||||
const updated = res.brand ?? { id: res.id, name: res.name, gsprpinfo: res.gsprpinfo };
|
||||
updateStateWithBrands(sortBrands(brandAutomat.state.brands.map((b) => (b.id === id ? { ...b, ...updated } : b))));
|
||||
return res;
|
||||
},
|
||||
// #region del
|
||||
async remove(id) {
|
||||
await brandApi.delete(id);
|
||||
updateStateWithBrands(brandAutomat.state.brands.filter((b) => b.id !== id));
|
||||
},
|
||||
async removeBrands(ids) {
|
||||
const idSet = new Set(ids);
|
||||
for (const id of ids) {
|
||||
await brandApi.delete(id);
|
||||
}
|
||||
updateStateWithBrands(brandAutomat.state.brands.filter((b) => !idSet.has(b.id)));
|
||||
await actions.load(true);
|
||||
},
|
||||
// #region addPic
|
||||
async addImage(id, file) {
|
||||
const res = await brandApi.addImage(id, file);
|
||||
await brandAutomat.actions.load(true);
|
||||
return res.brand;
|
||||
},
|
||||
// #region delPic
|
||||
async deleteImage(id) {
|
||||
const res = await brandApi.deleteImage(id);
|
||||
const updated = res.brand ?? { pics: [] };
|
||||
updateStateWithBrands(brandAutomat.state.brands.map((b) => (b.id === id ? { ...b, ...updated, pics: [] } : b)));
|
||||
return res.brand;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Global brand state container.
|
||||
* @type {Automat<BrandState>}
|
||||
*/
|
||||
brandAutomat = new Automat(initialState, actions, { name: 'brand' });
|
||||
|
||||
authAutomat.subscribe((auth) => {
|
||||
const companyId = auth.user?.selectedCompanyId;
|
||||
if (auth.status === 'authed' && companyId) void brandAutomat.actions.load();
|
||||
else if (auth.status === 'anon') brandAutomat.setState(initialState);
|
||||
});
|
||||
|
||||
// #region onSse
|
||||
sseAutomat.actions.on('brand', (event) => {
|
||||
try {
|
||||
const payload = JSON.parse(event?.data ?? '{}');
|
||||
if (payload?.sender && payload.sender === getSseClientId()) return;
|
||||
} catch { }
|
||||
void brandAutomat.actions.load(true).catch(() => { });
|
||||
});
|
||||
|
||||
export default brandAutomat;
|
||||
@@ -1,251 +0,0 @@
|
||||
/**
|
||||
* @file carrierAutomat.js
|
||||
* @description Manages cached shipping carriers / Versandarten (`GET /api/carrier`) for the active company.
|
||||
*
|
||||
* Upstream Triggers:
|
||||
* - `authAutomat`: Subscribes to auth state changes (`syncWithAuth`) — triggers
|
||||
* `actions.load()` on `'authed'` with a `selectedCompanyId`, and `actions.clear()` on `'anon'`.
|
||||
* - `sseAutomat`: Calls `actions.reload()` upon receiving a `'carrier'` SSE event.
|
||||
* - UI / Routes: Calls to `actions.load()`, `actions.reload()`, or mutation actions.
|
||||
*/
|
||||
|
||||
import { Automat } from 'react-automat';
|
||||
import { carrier as carrierApi } from '../api';
|
||||
import authAutomat from './authAutomat';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Carrier
|
||||
* @property {number} id Carrier identifier.
|
||||
* @property {string} name Carrier name.
|
||||
* @property {number} sort Sort order.
|
||||
* @property {boolean} hasPackages Whether carrier uses package tracking.
|
||||
* @property {boolean} isReverse Whether carrier is used for returns.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CarrierState
|
||||
* @property {Array<Carrier>} carriers Company-scoped carriers.
|
||||
* @property {Array<Carrier>} filteredCarriers Filtered carriers according to active filter query.
|
||||
* @property {string} filter Active search/filter string.
|
||||
* @property {'idle'|'loading'|'ready'|'failed'} status Load lifecycle state.
|
||||
* @property {Error|null} error Last load error.
|
||||
*/
|
||||
|
||||
/** @type {CarrierState} */
|
||||
const initialState = {
|
||||
carriers: [],
|
||||
filteredCarriers: [],
|
||||
filter: '',
|
||||
status: 'idle',
|
||||
error: null,
|
||||
};
|
||||
|
||||
let carrierAutomat;
|
||||
|
||||
const applyFilter = (carriers, filter) => {
|
||||
const query = String(filter ?? '').trim().toLowerCase();
|
||||
return query ? carriers.filter((c) => String(c.name ?? '').toLowerCase().includes(query)) : carriers;
|
||||
};
|
||||
|
||||
const updateStateWithCarriers = (carriers, extra = {}) => {
|
||||
const list = Array.isArray(carriers) ? carriers : [];
|
||||
const filteredCarriers = applyFilter(list, carrierAutomat.state.filter);
|
||||
carrierAutomat.setState({ carriers: list, filteredCarriers, ...extra });
|
||||
return list;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the company-scoped carrier list.
|
||||
*
|
||||
* @returns {Promise<Array<Carrier>>} Carrier list.
|
||||
*/
|
||||
async function fetchCarriers() {
|
||||
const data = await carrierApi.list();
|
||||
if (Array.isArray(data)) return data;
|
||||
if (data && Array.isArray(data.carriers)) return data.carriers;
|
||||
return [];
|
||||
}
|
||||
|
||||
/** In-flight `load()` promise so concurrent callers share one request. */
|
||||
let inflightLoad = null;
|
||||
|
||||
/**
|
||||
* Loads the carrier list once, deduplicating concurrent callers.
|
||||
*
|
||||
* @returns {Promise<Array<Carrier>>} Loaded carriers.
|
||||
*/
|
||||
function loadCarriers() {
|
||||
if (inflightLoad) return inflightLoad;
|
||||
inflightLoad = (async () => {
|
||||
carrierAutomat.setState({ status: 'loading', error: null });
|
||||
try {
|
||||
const carriers = await fetchCarriers();
|
||||
updateStateWithCarriers(carriers, { status: 'ready', error: null });
|
||||
return carriers;
|
||||
} catch (error) {
|
||||
carrierAutomat.setState({ status: 'failed', error });
|
||||
throw error;
|
||||
} finally {
|
||||
inflightLoad = null;
|
||||
}
|
||||
})();
|
||||
return inflightLoad;
|
||||
}
|
||||
|
||||
const actions = {
|
||||
/** Sets the active carrier search filter and updates filteredCarriers. */
|
||||
filter(query) {
|
||||
const filter = String(query ?? '');
|
||||
carrierAutomat.setState({
|
||||
filter,
|
||||
filteredCarriers: applyFilter(carrierAutomat.state.carriers, filter),
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads the carrier list, exposing a loading/failed lifecycle.
|
||||
*
|
||||
* @returns {Promise<Array<Carrier>>} Loaded carriers.
|
||||
*/
|
||||
load() {
|
||||
return loadCarriers();
|
||||
},
|
||||
|
||||
/**
|
||||
* Silently re-fetches the carrier list while keeping the current state.
|
||||
*
|
||||
* @returns {Promise<Array<Carrier>>} Fresh carriers.
|
||||
*/
|
||||
async reload() {
|
||||
try {
|
||||
const carriers = await fetchCarriers();
|
||||
updateStateWithCarriers(carriers, { status: 'ready', error: null });
|
||||
return carriers;
|
||||
} catch (error) {
|
||||
carrierAutomat.setState({ error });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a carrier and refreshes the cached list.
|
||||
*
|
||||
* @param {{name: string, sort?: number, hasPackages?: boolean, isReverse?: boolean}} body Creation payload.
|
||||
* @returns {Promise<Carrier>} Created carrier.
|
||||
*/
|
||||
async create(body) {
|
||||
const data = await carrierApi.create(body);
|
||||
const created = data?.carrier || data;
|
||||
await carrierAutomat.actions.reload();
|
||||
return created;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates a carrier and refreshes the cached list.
|
||||
*
|
||||
* @param {string|number} id Carrier identifier.
|
||||
* @param {{name?: string, sort?: number, hasPackages?: boolean, isReverse?: boolean}} body Update payload.
|
||||
* @returns {Promise<Carrier>} Updated carrier.
|
||||
*/
|
||||
async update(id, body) {
|
||||
const data = await carrierApi.update(id, body);
|
||||
const updated = data?.carrier || data;
|
||||
await carrierAutomat.actions.reload();
|
||||
return updated;
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes a carrier and refreshes the cached list.
|
||||
*
|
||||
* @param {string|number} id Carrier identifier.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async remove(id) {
|
||||
await carrierApi.delete(id);
|
||||
await carrierAutomat.actions.reload();
|
||||
},
|
||||
|
||||
/**
|
||||
* Persists a new sort order for all carriers.
|
||||
*
|
||||
* @param {Array<string|number>} carrierIds IDs in the desired order.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async reorder(carrierIds) {
|
||||
const byId = new Map(carrierAutomat.state.carriers.map((c) => [c.id, c]));
|
||||
const reordered = carrierIds.map((id) => byId.get(id)).filter(Boolean);
|
||||
if (reordered.length === carrierAutomat.state.carriers.length) {
|
||||
updateStateWithCarriers(reordered);
|
||||
}
|
||||
await carrierApi.order({ carrierIds });
|
||||
await carrierAutomat.actions.reload();
|
||||
},
|
||||
|
||||
/**
|
||||
* Replaces the cached list without a round-trip.
|
||||
*
|
||||
* @param {Array<Carrier>} carriers Carriers to cache.
|
||||
* @returns {void}
|
||||
*/
|
||||
setCarriers(carriers) {
|
||||
updateStateWithCarriers(carriers, {
|
||||
status: 'ready',
|
||||
error: null,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Forgets the cached list and returns to the initial state.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
clear() {
|
||||
carrierAutomat.setState({ ...initialState });
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Company-scoped carrier cache. Loads automatically once an authenticated
|
||||
* session with a selected company exists and is cleared when auth is gone.
|
||||
*
|
||||
* @type {Automat<CarrierState>}
|
||||
*/
|
||||
carrierAutomat = new Automat(initialState, actions, { name: 'carrier' });
|
||||
|
||||
/**
|
||||
* Derives the cache key from the auth state: the selected company while
|
||||
* authenticated, otherwise `null`.
|
||||
*
|
||||
* @param {import('./authAutomat').AuthState} authState Auth automat state.
|
||||
* @returns {string|null} Cache key.
|
||||
*/
|
||||
function authKey(authState) {
|
||||
if (authState?.status !== 'authed' || !authState.user) return null;
|
||||
const companyId = authState.user.selectedCompanyId;
|
||||
return companyId == null ? null : String(companyId);
|
||||
}
|
||||
|
||||
let lastAuthKey;
|
||||
|
||||
/**
|
||||
* Loads the carrier list when auth/company becomes available and forgets it
|
||||
* when the session ends.
|
||||
*
|
||||
* @param {import('./authAutomat').AuthState} authState Auth automat state.
|
||||
* @returns {void}
|
||||
*/
|
||||
function syncWithAuth(authState) {
|
||||
const key = authKey(authState);
|
||||
if (key === lastAuthKey) return;
|
||||
lastAuthKey = key;
|
||||
if (key == null) {
|
||||
carrierAutomat.actions.clear();
|
||||
} else {
|
||||
void carrierAutomat.actions.load().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
authAutomat.subscribe(syncWithAuth);
|
||||
syncWithAuth(authAutomat.state);
|
||||
|
||||
export default carrierAutomat;
|
||||
@@ -452,6 +452,11 @@ const actions = {
|
||||
refreshCategoryViewChildren(nextCategories);
|
||||
},
|
||||
|
||||
/** Invalidate all loaded category views. */
|
||||
invalidateAllCategoryViews() {
|
||||
invalidateAllCategoryViews();
|
||||
},
|
||||
|
||||
/** Reset state to idle (logout / company switch). */
|
||||
reset() {
|
||||
disposeAllCategoryViewAutomats();
|
||||
@@ -479,9 +484,9 @@ categoryAutomat.views = categoryViews;
|
||||
|
||||
// Load tree whenever the company data changes (or first becomes available)
|
||||
let _lastCompanyId = null;
|
||||
companyAutomat.subscribe((company) => {
|
||||
const id = company.current?.id ?? null;
|
||||
const rootCategoryId = company.current?.rootCategoryId ?? null;
|
||||
function syncCategoryWithCompany(company) {
|
||||
const id = company?.current?.id ?? null;
|
||||
const rootCategoryId = company?.current?.rootCategoryId ?? null;
|
||||
if (id && rootCategoryId && id !== _lastCompanyId) {
|
||||
const isCompanySwitch = _lastCompanyId !== null;
|
||||
_lastCompanyId = id;
|
||||
@@ -494,7 +499,10 @@ companyAutomat.subscribe((company) => {
|
||||
_lastCompanyId = null;
|
||||
categoryAutomat.actions.reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
companyAutomat.subscribe(syncCategoryWithCompany);
|
||||
syncCategoryWithCompany(companyAutomat.state);
|
||||
|
||||
// Reset tree on logout
|
||||
authAutomat.subscribe((auth) => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,165 +0,0 @@
|
||||
/**
|
||||
* @file createListAutomat.js
|
||||
* @description Factory function for company-scoped list automats with
|
||||
* deduplicated `load()`, silent `reload()`, and automatic session synchronization.
|
||||
*
|
||||
* Upstream Triggers:
|
||||
* - `authAutomat`: Subscribes each created list automat to auth state — calls
|
||||
* `actions.load()` on `'authed'` with a `selectedCompanyId`, and `actions.reset()` on `'anon'`.
|
||||
*
|
||||
* Used by:
|
||||
* - `deliveryAutomat`, `quoteAutomat`, `orderAutomat`,
|
||||
* `entityAutomat`, `customergroupAutomat`, `numberAutomat`.
|
||||
*
|
||||
* NOT used by `articleIndexAutomat`: that store is derived from the loaded
|
||||
* category views rather than fetched from a whole-company endpoint.
|
||||
*/
|
||||
|
||||
import { Automat } from 'react-automat';
|
||||
import authAutomat from './authAutomat';
|
||||
|
||||
/**
|
||||
* @typedef {Object} ListAutomatState
|
||||
* @property {Array<object>} items Loaded records (empty until the first success).
|
||||
* @property {'idle'|'loading'|'ready'|'failed'} status Load lifecycle state.
|
||||
* @property {Error|string|null} error Last load error.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a company-scoped list store following the brand/unit/warehouse
|
||||
* pattern: `load()` deduplicates concurrent callers, `reload()` refreshes
|
||||
* silently, and `reset()` clears on logout.
|
||||
*
|
||||
* This replaces the stale-while-revalidate list caches that previously lived
|
||||
* in `StorageManager` (articleIndex, delivery, quote, order, entity,
|
||||
* customergroup, number).
|
||||
*
|
||||
* @template T
|
||||
* @param {object} options Factory options.
|
||||
* @param {string} options.name Automat registry name (also used for logging).
|
||||
* @param {() => Promise<Array<T>>} options.fetch Loads the full record list.
|
||||
* @param {(items: Array<T>) => Array<T>} [options.sort] Optional stable ordering.
|
||||
* @returns {Automat<ListAutomatState>} Configured list automat instance.
|
||||
*/
|
||||
export function createListAutomat({ name, fetch: fetchItems, sort }) {
|
||||
const initialState = { items: [], status: 'idle', error: null };
|
||||
|
||||
let automat;
|
||||
/** In-flight `load()` promise so concurrent consumers share one request. */
|
||||
let inflightLoad = null;
|
||||
|
||||
/**
|
||||
* Fetches and normalizes the record list.
|
||||
*
|
||||
* @returns {Promise<Array<T>>} Records from the backend.
|
||||
*/
|
||||
const fetchRecords = async () => {
|
||||
const items = await fetchItems();
|
||||
return sort ? sort(items) : items;
|
||||
};
|
||||
|
||||
const actions = {
|
||||
/**
|
||||
* Loads the list, deduplicating concurrent callers.
|
||||
*
|
||||
* @returns {Promise<Array<T>>} Loaded records.
|
||||
*/
|
||||
load() {
|
||||
if (inflightLoad) return inflightLoad;
|
||||
inflightLoad = (async () => {
|
||||
automat.setState({ status: 'loading', error: null });
|
||||
try {
|
||||
const items = await fetchRecords();
|
||||
automat.setState({ items, status: 'ready', error: null });
|
||||
return items;
|
||||
} catch (error) {
|
||||
automat.setState({ status: 'failed', error });
|
||||
throw error;
|
||||
} finally {
|
||||
inflightLoad = null;
|
||||
}
|
||||
})();
|
||||
return inflightLoad;
|
||||
},
|
||||
|
||||
/**
|
||||
* Silently re-fetches the list while keeping the current state, so
|
||||
* subscribers do not flash a loading state.
|
||||
*
|
||||
* @returns {Promise<Array<T>>} Fresh records.
|
||||
*/
|
||||
async reload() {
|
||||
try {
|
||||
const items = await fetchRecords();
|
||||
automat.setState({ items, status: 'ready', error: null });
|
||||
return items;
|
||||
} catch (error) {
|
||||
automat.setState({ error });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Replaces the cached list without a round-trip.
|
||||
*
|
||||
* @param {Array<T>} items Records to cache.
|
||||
* @returns {Array<T>} Cached records.
|
||||
*/
|
||||
replace(items) {
|
||||
automat.setState({ items, status: 'ready', error: null });
|
||||
return items;
|
||||
},
|
||||
|
||||
/**
|
||||
* Re-fetches the list once the selected company is known.
|
||||
*
|
||||
* @returns {Promise<Array<T>>} Loaded records.
|
||||
*/
|
||||
ensureLoaded() {
|
||||
if (automat.state.status === 'ready') {
|
||||
return Promise.resolve(automat.state.items);
|
||||
}
|
||||
return actions.load();
|
||||
},
|
||||
|
||||
/** Reset to idle state (called on logout). */
|
||||
reset() {
|
||||
inflightLoad = null;
|
||||
automat.setState(initialState);
|
||||
},
|
||||
};
|
||||
|
||||
automat = new Automat(initialState, actions, { name });
|
||||
|
||||
// Load on session/company establishment, reset on logout.
|
||||
authAutomat.subscribe((auth) => {
|
||||
const companyId = auth.user?.selectedCompanyId;
|
||||
if (auth.status === 'authed' && companyId) {
|
||||
void actions.load().catch(() => {});
|
||||
} else if (auth.status === 'anon') {
|
||||
actions.reset();
|
||||
}
|
||||
});
|
||||
|
||||
return automat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the record list of a list automat.
|
||||
*
|
||||
* @param {Automat<ListAutomatState>} automat List automat.
|
||||
* @returns {Array<object>} Current records.
|
||||
*/
|
||||
export function selectListItems(automat) {
|
||||
return automat.state.items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the load status of a list automat.
|
||||
*
|
||||
* @param {Automat<ListAutomatState>} automat List automat.
|
||||
* @returns {'idle'|'loading'|'ready'|'failed'} Current status.
|
||||
*/
|
||||
export function selectListStatus(automat) {
|
||||
return automat.state.status;
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
/**
|
||||
* @file customergroupAutomat.js
|
||||
* @description Manages cached customer groups (`GET /api/customergroup`) for
|
||||
* the active company. Follows the brand/warehouse automat pattern with
|
||||
* filtering, deduplicated loads, silent reloads, and CRUD actions.
|
||||
*
|
||||
* Upstream Triggers:
|
||||
* - `authAutomat`: Triggers `actions.load()` on `'authed'` with a `selectedCompanyId`,
|
||||
* and `actions.reset()` on `'anon'`.
|
||||
* - UI / Routes: Mutating actions (`create`, `update`, `remove`, `reorder`, `filter`).
|
||||
*/
|
||||
|
||||
import { Automat } from 'react-automat';
|
||||
import { customergroup as customergroupApi } from '../api';
|
||||
import { requireArray, requireDefined, requireObject } from '../utils/apiShape';
|
||||
import authAutomat from './authAutomat';
|
||||
|
||||
/**
|
||||
* @typedef {Object} CustomerGroup
|
||||
* @property {string|number} id Customer group id.
|
||||
* @property {string} name Group name.
|
||||
* @property {number} sort Sort index.
|
||||
* @property {boolean} editable False for reserved POS / Lieferanten groups.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CustomerGroupState
|
||||
* @property {Array<CustomerGroup>} items Loaded records.
|
||||
* @property {Array<CustomerGroup>} filteredItems Records matching active search filter.
|
||||
* @property {string} filter Active search/filter string.
|
||||
* @property {'idle'|'loading'|'ready'|'failed'} status Load lifecycle state.
|
||||
* @property {Error|string|null} error Last load error.
|
||||
*/
|
||||
|
||||
/** @type {CustomerGroupState} */
|
||||
const initialState = {
|
||||
items: [],
|
||||
filteredItems: [],
|
||||
filter: '',
|
||||
status: 'idle',
|
||||
error: null,
|
||||
};
|
||||
|
||||
const applyFilter = (groups, filter) => {
|
||||
const query = String(filter ?? '').trim().toLowerCase();
|
||||
return query
|
||||
? groups.filter((g) => String(g.name ?? '').toLowerCase().includes(query))
|
||||
: groups;
|
||||
};
|
||||
|
||||
const sortGroups = (items) =>
|
||||
[...items].sort((a, b) => (Number(a.sort) || 0) - (Number(b.sort) || 0));
|
||||
|
||||
let customergroupAutomat;
|
||||
|
||||
const updateStateWithGroups = (groups, extra = {}) => {
|
||||
const list = sortGroups(Array.isArray(groups) ? groups : []);
|
||||
const filteredItems = applyFilter(list, customergroupAutomat.state.filter);
|
||||
customergroupAutomat.setState({ items: list, filteredItems, ...extra });
|
||||
return list;
|
||||
};
|
||||
|
||||
async function fetchGroups() {
|
||||
const data = await customergroupApi.list();
|
||||
requireObject(data, 'GET /api/customergroup');
|
||||
return requireArray(data.customergroups, 'customergroups');
|
||||
}
|
||||
|
||||
let inflightLoad = null;
|
||||
|
||||
function loadGroups() {
|
||||
if (inflightLoad) return inflightLoad;
|
||||
inflightLoad = (async () => {
|
||||
customergroupAutomat.setState({ status: 'loading', error: null });
|
||||
try {
|
||||
const groups = await fetchGroups();
|
||||
updateStateWithGroups(groups, { status: 'ready', error: null });
|
||||
return groups;
|
||||
} catch (error) {
|
||||
customergroupAutomat.setState({ status: 'failed', error });
|
||||
throw error;
|
||||
} finally {
|
||||
inflightLoad = null;
|
||||
}
|
||||
})();
|
||||
return inflightLoad;
|
||||
}
|
||||
|
||||
const actions = {
|
||||
/** Sets the active search filter query and updates filteredItems. */
|
||||
filter(query) {
|
||||
const filter = String(query ?? '');
|
||||
customergroupAutomat.setState({
|
||||
filter,
|
||||
filteredItems: applyFilter(customergroupAutomat.state.items, filter),
|
||||
});
|
||||
},
|
||||
|
||||
/** Loads the list, deduplicating concurrent callers. */
|
||||
load() {
|
||||
return loadGroups();
|
||||
},
|
||||
|
||||
/** Silently refreshes the customer groups without flashing loading state. */
|
||||
async reload() {
|
||||
try {
|
||||
const groups = await fetchGroups();
|
||||
updateStateWithGroups(groups, { status: 'ready', error: null });
|
||||
return groups;
|
||||
} catch (error) {
|
||||
customergroupAutomat.setState({ error });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/** Replaces the cached list without an API call. */
|
||||
replace(items) {
|
||||
return updateStateWithGroups(items, { status: 'ready', error: null });
|
||||
},
|
||||
|
||||
/** Returns cached items if ready, otherwise loads. */
|
||||
ensureLoaded() {
|
||||
if (customergroupAutomat.state.status === 'ready') {
|
||||
return Promise.resolve(customergroupAutomat.state.items);
|
||||
}
|
||||
return actions.load();
|
||||
},
|
||||
|
||||
/** Resets state to initial idle values. */
|
||||
reset() {
|
||||
inflightLoad = null;
|
||||
customergroupAutomat.setState(initialState);
|
||||
},
|
||||
|
||||
/** Creates a customer group and refreshes the cache. */
|
||||
async create(body) {
|
||||
const data = await customergroupApi.create(body);
|
||||
requireObject(data, 'POST /api/customergroup');
|
||||
requireObject(data.customergroup, 'customergroup');
|
||||
requireDefined(data.customergroup.id, 'customergroup.id');
|
||||
await customergroupAutomat.actions.reload();
|
||||
return data.customergroup;
|
||||
},
|
||||
|
||||
/** Updates a customer group name and refreshes the cache. */
|
||||
async update(id, body) {
|
||||
const data = await customergroupApi.update(id, body);
|
||||
requireObject(data, 'PATCH /api/customergroup/:id');
|
||||
requireObject(data.customergroup, 'customergroup');
|
||||
requireDefined(data.customergroup.id, 'customergroup.id');
|
||||
await customergroupAutomat.actions.reload();
|
||||
return data.customergroup;
|
||||
},
|
||||
|
||||
rename(id, body) {
|
||||
return actions.update(id, body);
|
||||
},
|
||||
|
||||
/** Deletes a customer group and refreshes the cache. */
|
||||
async remove(id) {
|
||||
await customergroupApi.delete(id);
|
||||
await customergroupAutomat.actions.reload();
|
||||
},
|
||||
|
||||
/** Reorders customer groups and refreshes the cache. */
|
||||
async reorder(customergroupIds) {
|
||||
const data = await customergroupApi.order({ customergroupIds });
|
||||
requireObject(data, 'PUT /api/customergroup/order');
|
||||
await customergroupAutomat.actions.reload();
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Company customer groups (`GET /api/customergroup`).
|
||||
*
|
||||
* Reserved groups (`POS`, `Lieferanten`) are flagged `editable: false` and must
|
||||
* not be renamed or deleted by the UI.
|
||||
*
|
||||
* @type {Automat<CustomerGroupState>}
|
||||
*/
|
||||
customergroupAutomat = new Automat(initialState, actions, {
|
||||
name: 'customergroup',
|
||||
});
|
||||
|
||||
export { customergroupAutomat };
|
||||
|
||||
// Load on session/company establishment, reset on logout.
|
||||
authAutomat.subscribe((auth) => {
|
||||
const companyId = auth.user?.selectedCompanyId;
|
||||
if (auth.status === 'authed' && companyId) {
|
||||
void actions.load().catch(() => {});
|
||||
} else if (auth.status === 'anon') {
|
||||
actions.reset();
|
||||
}
|
||||
});
|
||||
|
||||
export default customergroupAutomat;
|
||||
@@ -14,7 +14,7 @@ import { Automat } from 'react-automat';
|
||||
import { entity as entityApi } from '../api';
|
||||
import { requireArray, requireDefined, requireObject } from '../utils/apiShape';
|
||||
import authAutomat from './authAutomat';
|
||||
import customergroupAutomat from './customergroupAutomat';
|
||||
import companyAutomat from './companyAutomat';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Entity
|
||||
@@ -48,7 +48,7 @@ const initialState = {
|
||||
const applyFilter = (entities, filter) => {
|
||||
const query = String(filter ?? '').trim().toLowerCase();
|
||||
if (!query) return entities;
|
||||
const groups = customergroupAutomat?.state?.items ?? [];
|
||||
const groups = companyAutomat?.state?.customergroups ?? [];
|
||||
const groupsById = Object.fromEntries(groups.map((g) => [g.id, g]));
|
||||
|
||||
return entities.filter((e) => {
|
||||
@@ -200,13 +200,15 @@ entityAutomat = new Automat(initialState, actions, {
|
||||
export { entityAutomat };
|
||||
|
||||
// Load on session/company establishment, reset on logout.
|
||||
authAutomat.subscribe((auth) => {
|
||||
const syncEntityWithAuth = (auth) => {
|
||||
const companyId = auth.user?.selectedCompanyId;
|
||||
if (auth.status === 'authed' && companyId) {
|
||||
void actions.load().catch(() => {});
|
||||
} else if (auth.status === 'anon') {
|
||||
actions.reset();
|
||||
}
|
||||
});
|
||||
};
|
||||
authAutomat.subscribe(syncEntityWithAuth);
|
||||
syncEntityWithAuth(authAutomat.state);
|
||||
|
||||
export default entityAutomat;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @file index.js
|
||||
* @description Central barrel exporting all domain, configuration, and list
|
||||
* @description Central barrel exporting domain, configuration, and list
|
||||
* automats, along with their action creators and reactive selectors.
|
||||
*/
|
||||
|
||||
@@ -22,9 +22,6 @@ export {
|
||||
clearNavigation,
|
||||
} from './navigationAutomat';
|
||||
|
||||
export { default as userSettingsAutomat } from './userSettingsAutomat';
|
||||
export { setViewerPosition } from './userSettingsAutomat';
|
||||
|
||||
export { default as helpAutomat } from './helpAutomat';
|
||||
export {
|
||||
toggleHelp,
|
||||
@@ -44,10 +41,6 @@ export {
|
||||
applyPwaUpdate,
|
||||
} from './pwaAutomat';
|
||||
|
||||
export { default as supplierAutomat } from './supplierAutomat';
|
||||
export { default as warehouseAutomat } from './warehouseAutomat';
|
||||
export { default as carrierAutomat } from './carrierAutomat';
|
||||
|
||||
/** Article search over the derived index (replaces StorageManager.doSearch). */
|
||||
export {
|
||||
default as articleSearchAutomat,
|
||||
@@ -112,9 +105,6 @@ export {
|
||||
getChunkStats as getRetoureChunkStats,
|
||||
subscribeChunkStats as subscribeRetoureChunkStats,
|
||||
} from './retoureAutomat';
|
||||
export { default as customergroupAutomat } from './customergroupAutomat';
|
||||
export { default as numberAutomat } from './numberAutomat';
|
||||
export { default as accountAutomat } from './accountAutomat';
|
||||
export { default as backupAutomat } from './backupAutomat';
|
||||
export {
|
||||
default as shipmentAutomat,
|
||||
@@ -127,8 +117,6 @@ export {
|
||||
getChunkStats as getShipmentChunkStats,
|
||||
subscribeChunkStats as subscribeShipmentChunkStats,
|
||||
} from './shipmentAutomat';
|
||||
export { createListAutomat, selectListItems, selectListStatus } from './createListAutomat';
|
||||
|
||||
|
||||
/** Per-SKU article detail views keyed by SKU. */
|
||||
export { articleHistoryAutomat, getArticleHistoryAutomat, articleSupplierAutomat } from './articleDetailAutomat';
|
||||
@@ -156,19 +144,33 @@ export {
|
||||
selectCompanyReducedTaxrate,
|
||||
selectCompanyTaxfreerate,
|
||||
selectCompanyRootCategoryId,
|
||||
} from './companyAutomat';
|
||||
|
||||
export { default as brandAutomat } from './brandAutomat';
|
||||
export { default as unitAutomat } from './unitAutomat';
|
||||
export { default as attributeAutomat, selectAttributes, selectAttributeStatus } from './attributeAutomat';
|
||||
export { default as propertyAutomat, selectProperties, selectPropertyStatus } from './propertyAutomat';
|
||||
|
||||
export { default as posterminalAutomat } from './posterminalAutomat';
|
||||
export {
|
||||
selectNumbers,
|
||||
selectNumbersStatus,
|
||||
selectPosterminals,
|
||||
selectPosterminalStatus,
|
||||
selectHasPosterminals,
|
||||
} from './posterminalAutomat';
|
||||
selectSuppliers,
|
||||
selectSuppliersStatus,
|
||||
selectUnits,
|
||||
selectUnitsStatus,
|
||||
setViewerPosition,
|
||||
setChatDrawerWidth,
|
||||
selectChatDrawerWidth,
|
||||
selectWarehouses,
|
||||
selectWarehousesStatus,
|
||||
selectProperties,
|
||||
selectPropertyStatus,
|
||||
selectCustomergroups,
|
||||
selectCustomergroupsStatus,
|
||||
selectCarriers,
|
||||
selectCarriersStatus,
|
||||
selectBrands,
|
||||
selectBrandsStatus,
|
||||
selectAttributes,
|
||||
selectAttributeStatus,
|
||||
selectAccounts,
|
||||
selectAccountsStatus,
|
||||
} from './companyAutomat';
|
||||
|
||||
export { default as categoryAutomat } from './categoryAutomat';
|
||||
export {
|
||||
@@ -232,84 +234,3 @@ export {
|
||||
closeExecution,
|
||||
normalizeRoute,
|
||||
} from './pinnedAppletAutomat';
|
||||
|
||||
|
||||
/**
|
||||
* ============================================================================
|
||||
* Overview of Automats in Dashfront
|
||||
* ============================================================================
|
||||
*
|
||||
* Automats are lightweight, reactive, out-of-tree state containers powered by
|
||||
* `react-automat`. They manage all client-side state without React context,
|
||||
* hooks, or HOCs, enabling pure, zero-flash rendering in `React.PureComponent`
|
||||
* via constructor reads and fine-grained subscriptions.
|
||||
*
|
||||
* 1. Multi-Tenant Session & Security
|
||||
* ----------------------------------------------------------------------------
|
||||
* - `authAutomat`: Tracks authenticated session (`user`, `googleClientId`,
|
||||
* `selectedCompanyId`, and auth status `'loading' | 'anon' | 'authed'`).
|
||||
* Coordinates session bootstrap, company switching, and acts as the root
|
||||
* lifecycle trigger that loads or resets all company-scoped automats.
|
||||
*
|
||||
* 2. Real-Time SSE Hub
|
||||
* ----------------------------------------------------------------------------
|
||||
* - `sseAutomat`: Manages the single persistent Server-Sent Events connection
|
||||
* (`GET /api/events`). Automatically reconnects when the active company changes
|
||||
* and terminates on logout. Dispatches incoming server mutation signals into
|
||||
* targeted automats (`artikelAutomat`, `articleDetailAutomat`, `categoryAutomat`,
|
||||
* `articleIndexAutomat`, `warehouseAutomat`, `supplierAutomat`, `attributeAutomat`,
|
||||
* `propertyAutomat`).
|
||||
*
|
||||
* 3. Domain & Entity Caches
|
||||
* ----------------------------------------------------------------------------
|
||||
* - `artikelAutomat`: In-memory cache for full article records (`GET /api/article/:sku`)
|
||||
* with inflight request deduplication, optimistic renames, and reactive selectors.
|
||||
* - `articleDetailAutomat`: Keyed automats (`articleHistoryAutomat`, `articleSupplierAutomat`)
|
||||
* providing per-SKU supplier catalog bindings and change history.
|
||||
* - `deliveryEditorAutomat`: Interactive draft state machine for incoming goods receipts
|
||||
* and invoice reconciliation (line items, supplier article auto-matching, PDF/receipt OCR).
|
||||
*
|
||||
* 4. Company Configuration & Catalogs
|
||||
* ----------------------------------------------------------------------------
|
||||
* - `companyAutomat`: Active company settings (tax rates, ISO language, root category ID).
|
||||
* - `categoryAutomat`: Hierarchical category tree (ID -> node map), mutations (create,
|
||||
* rename, move, reorder, delete), and breadcrumb resolution.
|
||||
* - `categoryViewAutomat`: Keyed view container (`categoryViews`) holding articles and child
|
||||
* categories per slug path, auto-preloaded when category tree finishes loading.
|
||||
* - `brandAutomat`: Catalog manufacturer and brand records.
|
||||
* - `unitAutomat`: Allowed units of measure (Stück, kg, Liter, etc.).
|
||||
* - `posterminalAutomat`: Point-of-sale hardware terminals and enabled category filters.
|
||||
* - `attributeAutomat`: Product attribute definitions and discrete values (e.g. Size, Color).
|
||||
* - `propertyAutomat`: Custom product property and metadata names.
|
||||
* - `supplierAutomat`: Directory of vendor entities and supplier article mappings.
|
||||
* - `warehouseAutomat`: Warehouse locations and stock storage areas.
|
||||
*
|
||||
* 5. Company-Scoped List Automats (`createListAutomat`)
|
||||
* ----------------------------------------------------------------------------
|
||||
* Built with the shared list factory with deduplicated `load()`, silent `reload()`,
|
||||
* and automatic synchronization with `authAutomat`:
|
||||
* - `deliveryAutomat`: Delivery receipt list (`GET /api/delivery`).
|
||||
* - `quoteAutomat`: Sales quotes / Angebote (`GET /api/quote`).
|
||||
* - `orderAutomat`: Sales orders / Bestellungen (`GET /api/order`).
|
||||
* - `entityAutomat`: Customers and business partners (`GET /api/entity`).
|
||||
* - `customergroupAutomat`: Customer discount groups (`GET /api/customergroup`).
|
||||
* - `numberAutomat`: Document numbering sequences (`GET /api/number`).
|
||||
*
|
||||
* 6. Search & Infinite-Scroll Grid Automats
|
||||
* ----------------------------------------------------------------------------
|
||||
* - `articleIndexAutomat`: Fast client-side search index, **derived** from the loaded
|
||||
* category views (`GET /api/category/:id/articles`) — no dedicated index endpoint.
|
||||
* - `articleSearchAutomat`: Client-side search engine querying `articleIndexAutomat`.
|
||||
* - `articleHistoryGridAutomat`: Virtualized infinite-scroll grid for article history.
|
||||
* - `globalHistoryGridAutomat`: Virtualized infinite-scroll grid for global audit logs.
|
||||
*
|
||||
* 7. UI State & Utilities
|
||||
* ----------------------------------------------------------------------------
|
||||
* - `chatAutomat`: AI assistant chat drawer state, conversations, and live SSE streaming tokens.
|
||||
* - `navigationAutomat`: Persists last visited delivery document path and article SKU.
|
||||
* - `userSettingsAutomat`: User layout preferences (viewer position, drawer widths in IndexedDB).
|
||||
* - `helpAutomat`: Interactive help badge states and popper positioning (in IndexedDB).
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
/**
|
||||
* @file numberAutomat.js
|
||||
* @description Manages cached document numbering sequences (`GET /api/number`)
|
||||
* (e.g. delivery, entity) for the active company. Built via `createListAutomat`.
|
||||
*
|
||||
* Upstream Triggers:
|
||||
* - `authAutomat`: Via `createListAutomat` — triggers `load()` on `'authed'` with
|
||||
* a `selectedCompanyId`, and `reset()` on `'anon'`.
|
||||
* - UI / Routes: Manual calls to `load()` or `reload()`.
|
||||
*/
|
||||
|
||||
import { number as numberApi } from '../api';
|
||||
import { createListAutomat } from './createListAutomat';
|
||||
|
||||
/**
|
||||
* @typedef {Object} NumberSequence
|
||||
* @property {string|number} id Sequence id.
|
||||
* @property {string} type Document type key (e.g. `delivery`, `entity`).
|
||||
* @property {string} prefix Prefix rendered before the counter.
|
||||
* @property {number} padding Zero-padding width.
|
||||
* @property {number} next Next counter value.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Company document number sequences (`GET /api/number`).
|
||||
*
|
||||
* @type {import('react-automat').Automat<import('./createListAutomat').ListAutomatState>}
|
||||
*/
|
||||
export const numberAutomat = createListAutomat({
|
||||
name: 'number',
|
||||
fetch: async () => {
|
||||
const data = await numberApi.list();
|
||||
return data.numbers ?? [];
|
||||
},
|
||||
sort: (items) =>
|
||||
[...items].sort((a, b) =>
|
||||
String(a.type ?? '').localeCompare(String(b.type ?? ''), 'de'),
|
||||
),
|
||||
});
|
||||
|
||||
export default numberAutomat;
|
||||
@@ -1,170 +0,0 @@
|
||||
/**
|
||||
* @file posterminalAutomat.js
|
||||
* @description Manages registered Point-of-Sale (POS) hardware terminals and
|
||||
* their enabled category mappings for the active company.
|
||||
*
|
||||
* Upstream Triggers:
|
||||
* - `authAutomat`: Subscribes to auth state changes — calls `actions.load()` on
|
||||
* `'authed'` with a `selectedCompanyId`, and `actions.reset()` on `'anon'`.
|
||||
* - `companyAutomat`: Reads `companyAutomat.state.current?.id` as fallback when loading.
|
||||
* - UI / Settings: POS terminal actions (`create`, `delete`, `toggleCategory`, `setCategoryEnabled`).
|
||||
*/
|
||||
|
||||
import { Automat } from 'react-automat';
|
||||
import { posterminal as posterminalApi } from '../api';
|
||||
import authAutomat from './authAutomat';
|
||||
import companyAutomat from './companyAutomat';
|
||||
|
||||
/**
|
||||
* @typedef {Object} PosterminalState
|
||||
* @property {Array<object>} terminals
|
||||
* @property {number|string|null} companyId
|
||||
* @property {'idle'|'loading'|'succeeded'|'failed'} status
|
||||
* @property {string|null} error
|
||||
*/
|
||||
|
||||
/** @type {PosterminalState} */
|
||||
const initialState = {
|
||||
terminals: [],
|
||||
companyId: null,
|
||||
status: 'idle',
|
||||
error: null,
|
||||
};
|
||||
|
||||
let posterminalAutomat;
|
||||
|
||||
const actions = {
|
||||
/** Load all posterminals for the selected company. */
|
||||
async load() {
|
||||
const companyId = authAutomat.state.user?.selectedCompanyId ?? companyAutomat.state.current?.id;
|
||||
if (!companyId) return [];
|
||||
posterminalAutomat.setState({ status: 'loading', error: null });
|
||||
try {
|
||||
const res = await posterminalApi.list();
|
||||
const terminals = Array.isArray(res.posterminals) ? res.posterminals : [];
|
||||
posterminalAutomat.setState({ terminals, companyId, status: 'succeeded' });
|
||||
return terminals;
|
||||
} catch (err) {
|
||||
posterminalAutomat.setState({ status: 'failed', error: err?.message ?? String(err) });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new posterminal.
|
||||
* @param {{ name: string }} param0
|
||||
*/
|
||||
async create({ name }) {
|
||||
const res = await posterminalApi.create({ name });
|
||||
const terminal = res.posterminal;
|
||||
posterminalAutomat.setState({
|
||||
terminals: [...posterminalAutomat.state.terminals, terminal],
|
||||
});
|
||||
return terminal;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a posterminal.
|
||||
* @param {number} terminalId
|
||||
* @param {object} patch
|
||||
*/
|
||||
async update(terminalId, patch) {
|
||||
const res = await posterminalApi.update(terminalId, patch);
|
||||
const updated = res.posterminal;
|
||||
posterminalAutomat.setState({
|
||||
terminals: posterminalAutomat.state.terminals.map((t) =>
|
||||
t.id === terminalId ? { ...t, ...updated } : t
|
||||
),
|
||||
});
|
||||
return updated;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a posterminal.
|
||||
* @param {number} terminalId
|
||||
*/
|
||||
async delete(terminalId) {
|
||||
await posterminalApi.delete(terminalId);
|
||||
posterminalAutomat.setState({
|
||||
terminals: posterminalAutomat.state.terminals.filter((t) => t.id !== terminalId),
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Enable all categories on a terminal.
|
||||
* @param {{ terminalId: number }} param0
|
||||
*/
|
||||
async enableAll({ terminalId }) {
|
||||
const res = await posterminalApi.enableAllCategories(terminalId);
|
||||
patchTerminalCategories(terminalId, res.categoryIds);
|
||||
return res;
|
||||
},
|
||||
|
||||
/**
|
||||
* Disable all categories on a terminal.
|
||||
* @param {{ terminalId: number }} param0
|
||||
*/
|
||||
async disableAll({ terminalId }) {
|
||||
const res = await posterminalApi.disableAllCategories(terminalId);
|
||||
patchTerminalCategories(terminalId, res.categoryIds);
|
||||
return res;
|
||||
},
|
||||
|
||||
/**
|
||||
* Enable a specific category on a terminal.
|
||||
* @param {{ terminalId: number, categoryId: number }} param0
|
||||
*/
|
||||
async enableCategory({ terminalId, categoryId }) {
|
||||
const res = await posterminalApi.enableCategory(terminalId, categoryId);
|
||||
patchTerminalCategories(terminalId, res.categoryIds);
|
||||
return res;
|
||||
},
|
||||
|
||||
/**
|
||||
* Disable a specific category on a terminal.
|
||||
* @param {{ terminalId: number, categoryId: number }} param0
|
||||
*/
|
||||
async disableCategory({ terminalId, categoryId }) {
|
||||
const res = await posterminalApi.disableCategory(terminalId, categoryId);
|
||||
patchTerminalCategories(terminalId, res.categoryIds);
|
||||
return res;
|
||||
},
|
||||
|
||||
/** Reset to idle state (called on logout / company switch). */
|
||||
reset() {
|
||||
posterminalAutomat.setState(initialState);
|
||||
},
|
||||
};
|
||||
|
||||
function patchTerminalCategories(terminalId, categoryIds) {
|
||||
posterminalAutomat.setState({
|
||||
terminals: posterminalAutomat.state.terminals.map((t) =>
|
||||
t.id === terminalId ? { ...t, categoryIds } : t,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Global posterminal state container.
|
||||
* Loads automatically when authAutomat reaches 'authed' with a selectedCompanyId.
|
||||
*
|
||||
* @type {Automat<PosterminalState>}
|
||||
*/
|
||||
posterminalAutomat = new Automat(initialState, actions, { name: 'posterminal' });
|
||||
|
||||
// Subscribe to auth changes
|
||||
authAutomat.subscribe((auth) => {
|
||||
const companyId = auth.user?.selectedCompanyId;
|
||||
if (auth.status === 'authed' && companyId) {
|
||||
void posterminalAutomat.actions.load();
|
||||
} else if (auth.status === 'anon') {
|
||||
posterminalAutomat.actions.reset();
|
||||
}
|
||||
});
|
||||
|
||||
// Selectors
|
||||
export const selectPosterminals = () => posterminalAutomat.state.terminals;
|
||||
export const selectPosterminalStatus = () => posterminalAutomat.state.status;
|
||||
export const selectHasPosterminals = () => posterminalAutomat.state.terminals.length > 0;
|
||||
|
||||
export default posterminalAutomat;
|
||||
@@ -1,153 +0,0 @@
|
||||
/**
|
||||
* @file propertyAutomat.js
|
||||
* @description Manages catalog product property names (custom metadata definitions)
|
||||
* for the active company.
|
||||
*
|
||||
* Upstream Triggers:
|
||||
* - `authAutomat`: Subscribes to auth state changes — calls `actions.load()` on
|
||||
* `'authed'` with a `selectedCompanyId`, and `actions.reset()` on `'anon'`.
|
||||
* - `sseAutomat`: Calls `actions.load()` upon receiving a `'propertyname'` SSE event.
|
||||
* - UI / Settings: Mutating actions (`createProperty`, `updateProperty`,
|
||||
* `deleteProperty`, `reorderProperties`).
|
||||
*/
|
||||
|
||||
import { Automat } from 'react-automat';
|
||||
import { propertyname as propertynameApi } from '../api';
|
||||
import authAutomat from './authAutomat';
|
||||
|
||||
/**
|
||||
* @typedef {Object} PropertyName
|
||||
* @property {number} id
|
||||
* @property {string} name
|
||||
* @property {number} sort
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PropertyState
|
||||
* @property {PropertyName[]} properties
|
||||
* @property {'idle'|'loading'|'succeeded'|'failed'} status
|
||||
* @property {string|null} error
|
||||
*/
|
||||
|
||||
/** @type {PropertyState} */
|
||||
const initialState = {
|
||||
properties: [],
|
||||
status: 'idle',
|
||||
error: null,
|
||||
};
|
||||
|
||||
function normalizeProperties(properties) {
|
||||
if (!Array.isArray(properties)) return [];
|
||||
return [...properties]
|
||||
.map((p) => ({
|
||||
...p,
|
||||
exclusive: Boolean(p.exclusive),
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
const sDiff = (a.sort ?? 0) - (b.sort ?? 0);
|
||||
if (sDiff !== 0) return sDiff;
|
||||
return (a.id ?? 0) - (b.id ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
let propertyAutomat;
|
||||
|
||||
const actions = {
|
||||
/** Load all property names for the selected company. */
|
||||
async load() {
|
||||
propertyAutomat.setState({ status: 'loading', error: null });
|
||||
try {
|
||||
const res = await propertynameApi.list();
|
||||
const properties = normalizeProperties(res.propertynames);
|
||||
propertyAutomat.setState({ properties, status: 'succeeded' });
|
||||
return properties;
|
||||
} catch (err) {
|
||||
propertyAutomat.setState({ status: 'failed', error: err?.message ?? String(err) });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a property name.
|
||||
* @param {{ name: string, sort?: number, exclusive?: boolean }} body
|
||||
*/
|
||||
async createProperty(body) {
|
||||
const res = await propertynameApi.create(body);
|
||||
await actions.load();
|
||||
return res.propertyname;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a property name.
|
||||
* @param {number} id
|
||||
* @param {{ name?: string, sort?: number, exclusive?: boolean }} body
|
||||
*/
|
||||
async updateProperty(id, body) {
|
||||
const res = await propertynameApi.update(id, body);
|
||||
await actions.load();
|
||||
return res.propertyname;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a property name.
|
||||
* @param {number} id
|
||||
*/
|
||||
async deleteProperty(id) {
|
||||
await propertynameApi.delete(id);
|
||||
await actions.load();
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete multiple property names.
|
||||
* @param {number[]} ids
|
||||
*/
|
||||
async deleteProperties(ids) {
|
||||
for (const id of ids) {
|
||||
await propertynameApi.delete(id);
|
||||
}
|
||||
await actions.load();
|
||||
},
|
||||
|
||||
/**
|
||||
* Reorder property names.
|
||||
* @param {number[]} propertynameIds
|
||||
*/
|
||||
async reorderProperties(propertynameIds) {
|
||||
await propertynameApi.order(propertynameIds);
|
||||
await actions.load();
|
||||
},
|
||||
|
||||
/** Reset to idle state (called on logout / company switch). */
|
||||
reset() {
|
||||
propertyAutomat.setState(initialState);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Global property state container.
|
||||
* Loads automatically when authAutomat reaches 'authed' with a selectedCompanyId.
|
||||
*
|
||||
* @type {Automat<PropertyState>}
|
||||
*/
|
||||
propertyAutomat = new Automat(initialState, actions, { name: 'property' });
|
||||
|
||||
// Selectors
|
||||
export function selectProperties() {
|
||||
return propertyAutomat.state.properties;
|
||||
}
|
||||
|
||||
export function selectPropertyStatus() {
|
||||
return propertyAutomat.state.status;
|
||||
}
|
||||
|
||||
// Subscribe to auth changes
|
||||
authAutomat.subscribe((auth) => {
|
||||
const companyId = auth.user?.selectedCompanyId;
|
||||
if (auth.status === 'authed' && companyId) {
|
||||
void propertyAutomat.actions.load();
|
||||
} else if (auth.status === 'anon') {
|
||||
propertyAutomat.actions.reset();
|
||||
}
|
||||
});
|
||||
|
||||
export default propertyAutomat;
|
||||
@@ -1,44 +1,26 @@
|
||||
/**
|
||||
* @file sseAutomat.js
|
||||
* @description Central Server-Sent Events hub managing the single persistent
|
||||
* `GET /api/events` connection and fanning mutation signals into domain automats.
|
||||
* `GET /api/events` connection and fanning mutation signals into active automats.
|
||||
*
|
||||
* Upstream Triggers:
|
||||
* - `authAutomat`: Subscribes to auth state changes — reconnects stream when
|
||||
* `selectedCompanyId` changes/becomes available; disconnects on `'anon'` (logout).
|
||||
* - `authAutomat`: Connects stream when `hasAuth` is true (and reconnects when
|
||||
* `selectedCompanyId` changes); disconnects when `hasAuth` is false (logout).
|
||||
*
|
||||
* Downstream Triggers:
|
||||
* Dispatches real-time SSE mutation events into specific automats:
|
||||
* - `artikelAutomat`: Re-fetches article via `reloadArticle(sku)` on `article-<sku>` signals.
|
||||
* - `articleDetailAutomat` (`articleHistoryAutomat`, `articleSupplierAutomat`):
|
||||
* Reloads SKU-keyed views on `article-<sku>` signals.
|
||||
* - `categoryAutomat`: Triggers `actions.loadTree()` and `invalidateAllCategoryViews()`
|
||||
* on `'category'` SSE events.
|
||||
* - `articleIndexAutomat`: Triggers `actions.reload()` on `'articles'` SSE events;
|
||||
* the index is derived from the reloaded category views, so no extra request.
|
||||
* - `warehouseAutomat`: Triggers `actions.reload()` on `'warehouse'` SSE events.
|
||||
* - `supplierAutomat`: Triggers `actions.reload()` on `'supplier'` SSE events.
|
||||
* - `attributeAutomat`: Triggers `actions.load()` on `'attributename'` SSE events.
|
||||
* - `propertyAutomat`: Triggers `actions.load()` on `'propertyname'` SSE events.
|
||||
* Dispatches real-time SSE mutation events into active automats without eagerly
|
||||
* importing or instantiating unmounted stores:
|
||||
* - `companyAutomat`: Refreshes warehouses, carriers, attributes, properties,
|
||||
* suppliers, and brands when `companyAutomat` is active.
|
||||
* - `categoryAutomat`: Refreshes category tree and open category views on `'category'`.
|
||||
* - `articleIndexAutomat`: Refreshes derived article index on `'articles'` / `'warehouse'`.
|
||||
* - `artikelAutomat` & `articleDetailAutomat`: Refreshes watched SKU on `article-<sku>`.
|
||||
* - `orderAutomat`, `quoteAutomat`, `shipmentAutomat`, `deliveryAutomat`, `retoureAutomat`:
|
||||
* Reloads when those automats are active.
|
||||
*/
|
||||
|
||||
import { Automat } from 'react-automat';
|
||||
import authAutomat from './authAutomat';
|
||||
import artikelAutomat from './artikelAutomat';
|
||||
import { articleHistoryAutomat, articleSupplierAutomat } from './articleDetailAutomat';
|
||||
import { articleIndexAutomat } from './articleIndexAutomat';
|
||||
import categoryAutomat, { invalidateAllCategoryViews } from './categoryAutomat';
|
||||
import warehouseAutomat from './warehouseAutomat';
|
||||
import attributeAutomat from './attributeAutomat';
|
||||
import propertyAutomat from './propertyAutomat';
|
||||
import supplierAutomat from './supplierAutomat';
|
||||
import deliveryAutomat from './deliveryAutomat';
|
||||
import orderAutomat from './orderAutomat';
|
||||
import quoteAutomat from './quoteAutomat';
|
||||
import retoureAutomat from './retoureAutomat';
|
||||
import shipmentAutomat from './shipmentAutomat';
|
||||
import carrierAutomat from './carrierAutomat';
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {Object} SseState
|
||||
@@ -49,7 +31,6 @@ import carrierAutomat from './carrierAutomat';
|
||||
*/
|
||||
|
||||
const EVENT_HISTORY_LIMIT = 200;
|
||||
|
||||
const SSE_CLIENT_ID_KEY = 'sse.cid';
|
||||
|
||||
function createClientId() {
|
||||
@@ -63,8 +44,6 @@ function createClientId() {
|
||||
|
||||
/**
|
||||
* Returns the tab-scoped SSE client id, creating and persisting it on first use.
|
||||
* Created when the automat module is evaluated so every `EventSource` in this
|
||||
* tab shares the same `cid`.
|
||||
*
|
||||
* @returns {string} Client id.
|
||||
*/
|
||||
@@ -81,7 +60,6 @@ export function getSseClientId() {
|
||||
}
|
||||
}
|
||||
|
||||
// Created once per tab at automat creation time and stored to sessionStorage.
|
||||
const sseClientId = typeof window !== 'undefined' ? getSseClientId() : null;
|
||||
|
||||
/** @type {SseState} */
|
||||
@@ -172,10 +150,6 @@ function attachHandlers(es) {
|
||||
/**
|
||||
* Binds the single `article-<sku>` listener for a watched SKU.
|
||||
*
|
||||
* The listener resolves the handler set at call time, so a SKU that lost all
|
||||
* watchers turns into a cheap no-op instead of needing removal. Listeners are
|
||||
* reclaimed whenever the stream is rebuilt on a company switch.
|
||||
*
|
||||
* @param {string} sku Article SKU.
|
||||
* @returns {void}
|
||||
*/
|
||||
@@ -189,119 +163,179 @@ function attachArticleListener(sku) {
|
||||
});
|
||||
}
|
||||
|
||||
// #region connect
|
||||
// MARK: opens GET /api/events EventSource when hasAuth is true
|
||||
function connect() {
|
||||
const companyId = authAutomat.state.user?.selectedCompanyId;
|
||||
if (companyId == null) {
|
||||
const auth = authAutomat.state;
|
||||
const hasAuth = auth.hasAuth ?? (auth.status === 'authed' && Boolean(auth.user));
|
||||
if (!hasAuth) {
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
const companyId =
|
||||
auth.user?.selectedCompanyId != null ? String(auth.user.selectedCompanyId) : null;
|
||||
|
||||
const url = new URL('/api/events', window.location.origin);
|
||||
if (sseClientId) url.searchParams.set('cid', sseClientId);
|
||||
console.log(`[sse] connecting`);
|
||||
sseAutomat.setState({ status: 'connecting', companyId: String(companyId) });
|
||||
console.log('[sse] connecting');
|
||||
sseAutomat.setState({ status: 'connecting', companyId });
|
||||
|
||||
const es = new EventSource(url.href, { withCredentials: true });
|
||||
|
||||
// Category tree changed: refresh the tree and every open category view.
|
||||
addTrackedListener(es, 'category', () => {
|
||||
void categoryAutomat.actions.loadTree().catch((err) => {
|
||||
console.error('[sse] category tree refresh failed', err);
|
||||
});
|
||||
invalidateAllCategoryViews();
|
||||
const cat = Automat.get('category');
|
||||
if (cat?.actions?.loadTree) {
|
||||
void cat.actions.loadTree().catch((err) => {
|
||||
console.error('[sse] category tree refresh failed', err);
|
||||
});
|
||||
}
|
||||
cat?.actions?.invalidateAllCategoryViews?.();
|
||||
});
|
||||
|
||||
// Article index changed: refresh the index and open category views.
|
||||
addTrackedListener(es, 'articles', () => {
|
||||
invalidateAllCategoryViews();
|
||||
void articleIndexAutomat.actions.reload().catch((err) => {
|
||||
console.error('[sse] article index refresh failed', err);
|
||||
});
|
||||
Automat.get('category')?.actions?.invalidateAllCategoryViews?.();
|
||||
const idx = Automat.get('articleIndex');
|
||||
if (idx?.actions?.reload) {
|
||||
void idx.actions.reload().catch((err) => {
|
||||
console.error('[sse] article index refresh failed', err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
addTrackedListener(es, 'warehouse', () => {
|
||||
void warehouseAutomat.actions.reload().catch((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);
|
||||
});
|
||||
const comp = Automat.get('company');
|
||||
if (comp?.actions?.reloadWarehouses) {
|
||||
void comp.actions.reloadWarehouses().catch((err) => {
|
||||
console.error('[sse] warehouse refresh failed', err);
|
||||
});
|
||||
}
|
||||
Automat.get('category')?.actions?.invalidateAllCategoryViews?.();
|
||||
const idx = Automat.get('articleIndex');
|
||||
if (idx?.actions?.reload) {
|
||||
void idx.actions.reload().catch((err) => {
|
||||
console.error('[sse] article index refresh on warehouse event failed', err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
addTrackedListener(es, 'carrier', () => {
|
||||
void carrierAutomat.actions.reload().catch((err) => {
|
||||
console.error('[sse] carrier refresh failed', err);
|
||||
});
|
||||
const comp = Automat.get('company');
|
||||
if (comp?.actions?.reloadCarriers) {
|
||||
void comp.actions.reloadCarriers().catch((err) => {
|
||||
console.error('[sse] carrier refresh failed', err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
addTrackedListener(es, 'attributename', () => {
|
||||
void attributeAutomat.actions.load().catch((err) => {
|
||||
console.error('[sse] attributename refresh failed', err);
|
||||
});
|
||||
const comp = Automat.get('company');
|
||||
if (comp?.actions?.loadAttributes) {
|
||||
void comp.actions.loadAttributes().catch((err) => {
|
||||
console.error('[sse] attributename refresh failed', err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
addTrackedListener(es, 'propertyname', () => {
|
||||
void propertyAutomat.actions.load().catch((err) => {
|
||||
console.error('[sse] propertyname refresh failed', err);
|
||||
});
|
||||
const comp = Automat.get('company');
|
||||
if (comp?.actions?.loadProperties) {
|
||||
void comp.actions.loadProperties().catch((err) => {
|
||||
console.error('[sse] propertyname refresh failed', err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
addTrackedListener(es, 'supplier', () => {
|
||||
void supplierAutomat.actions.reload().catch((err) => {
|
||||
console.error('[sse] supplier refresh failed', err);
|
||||
});
|
||||
const comp = Automat.get('company');
|
||||
if (comp?.actions?.reloadSuppliers) {
|
||||
void comp.actions.reloadSuppliers().catch((err) => {
|
||||
console.error('[sse] supplier refresh failed', err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
addTrackedListener(es, 'brand', (event) => {
|
||||
try {
|
||||
const payload = JSON.parse(event?.data ?? '{}');
|
||||
if (payload?.sender && payload.sender === getSseClientId()) return;
|
||||
} catch {}
|
||||
const comp = Automat.get('company');
|
||||
if (comp?.actions?.loadBrands) {
|
||||
void comp.actions.loadBrands(true).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
addTrackedListener(es, 'order', () => {
|
||||
void orderAutomat.actions.reload().catch((err) => {
|
||||
console.error('[sse] order refresh failed', err);
|
||||
});
|
||||
void shipmentAutomat.actions.reload().catch((err) => {
|
||||
console.error('[sse] shipment refresh failed', err);
|
||||
});
|
||||
const ord = Automat.get('order');
|
||||
if (ord?.actions?.reload) {
|
||||
void ord.actions.reload().catch((err) => {
|
||||
console.error('[sse] order refresh failed', err);
|
||||
});
|
||||
}
|
||||
const shp = Automat.get('shipment');
|
||||
if (shp?.actions?.reload) {
|
||||
void shp.actions.reload().catch((err) => {
|
||||
console.error('[sse] shipment refresh failed', err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
addTrackedListener(es, 'quote', () => {
|
||||
void quoteAutomat.actions.reload().catch((err) => {
|
||||
console.error('[sse] quote refresh failed', err);
|
||||
});
|
||||
const q = Automat.get('quote');
|
||||
if (q?.actions?.reload) {
|
||||
void q.actions.reload().catch((err) => {
|
||||
console.error('[sse] quote refresh failed', err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
addTrackedListener(es, 'shipment', () => {
|
||||
void shipmentAutomat.actions.reload().catch((err) => {
|
||||
console.error('[sse] shipment refresh failed', err);
|
||||
});
|
||||
void orderAutomat.actions.reload().catch((err) => {
|
||||
console.error('[sse] order refresh failed', err);
|
||||
});
|
||||
const shp = Automat.get('shipment');
|
||||
if (shp?.actions?.reload) {
|
||||
void shp.actions.reload().catch((err) => {
|
||||
console.error('[sse] shipment refresh failed', err);
|
||||
});
|
||||
}
|
||||
const ord = Automat.get('order');
|
||||
if (ord?.actions?.reload) {
|
||||
void ord.actions.reload().catch((err) => {
|
||||
console.error('[sse] order refresh failed', err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
addTrackedListener(es, 'delivery', () => {
|
||||
void deliveryAutomat.actions.reload().catch((err) => {
|
||||
console.error('[sse] delivery refresh failed', err);
|
||||
});
|
||||
const del = Automat.get('delivery');
|
||||
if (del?.actions?.reload) {
|
||||
void del.actions.reload().catch((err) => {
|
||||
console.error('[sse] delivery refresh failed', err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
addTrackedListener(es, 'retoure', () => {
|
||||
void retoureAutomat.actions.reload().catch((err) => {
|
||||
console.error('[sse] retoure refresh failed', err);
|
||||
});
|
||||
void shipmentAutomat.actions.reload().catch((err) => {
|
||||
console.error('[sse] shipment refresh failed', err);
|
||||
});
|
||||
const ret = Automat.get('retoure');
|
||||
if (ret?.actions?.reload) {
|
||||
void ret.actions.reload().catch((err) => {
|
||||
console.error('[sse] retoure refresh failed', err);
|
||||
});
|
||||
}
|
||||
const shp = Automat.get('shipment');
|
||||
if (shp?.actions?.reload) {
|
||||
void shp.actions.reload().catch((err) => {
|
||||
console.error('[sse] shipment refresh failed', err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
addTrackedListener(es, 'open', () => {
|
||||
sseAutomat.setState({ status: 'connected' });
|
||||
});
|
||||
|
||||
addTrackedListener(es, 'message', () => {});
|
||||
|
||||
// EventSource reconnects on its own; only surface the state.
|
||||
es.onerror = (e) => {
|
||||
console.error('[sse] event stream error', e);
|
||||
recordEvent(e || { type: 'error', data: '', lastEventId: '' });
|
||||
@@ -311,11 +345,37 @@ function connect() {
|
||||
eventSource = es;
|
||||
attachHandlers(es);
|
||||
}
|
||||
// #endregion
|
||||
|
||||
let wasAuthed = false;
|
||||
|
||||
function handleAuthSync(auth) {
|
||||
const isAuthed = Boolean(auth?.hasAuth ?? (auth?.status === 'authed' && auth?.user));
|
||||
const companyId =
|
||||
isAuthed && auth?.user?.selectedCompanyId != null
|
||||
? String(auth.user.selectedCompanyId)
|
||||
: null;
|
||||
const relogin = isAuthed && !wasAuthed;
|
||||
wasAuthed = isAuthed;
|
||||
|
||||
if (!isAuthed) {
|
||||
disconnect();
|
||||
sseAutomat.setState({ companyId: null });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!relogin && eventSource && companyId === sseAutomat.state.companyId) {
|
||||
return;
|
||||
}
|
||||
|
||||
disconnect();
|
||||
sseAutomat.setState({ companyId });
|
||||
connect();
|
||||
}
|
||||
|
||||
const actions = {
|
||||
/**
|
||||
* Closes and reopens the event stream. Required after a company switch,
|
||||
* because the stream is scoped to the selected company at connect time.
|
||||
* Closes and reopens the event stream.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
@@ -324,6 +384,15 @@ const actions = {
|
||||
connect();
|
||||
},
|
||||
|
||||
/**
|
||||
* Synchronizes the SSE connection with `authAutomat` state.
|
||||
*
|
||||
* @param {import('./authAutomat').AuthState} authState
|
||||
*/
|
||||
syncWithAuth(authState) {
|
||||
handleAuthSync(authState ?? authAutomat.state);
|
||||
},
|
||||
|
||||
clearEvents() {
|
||||
sseAutomat.setState({ events: [] });
|
||||
},
|
||||
@@ -331,9 +400,6 @@ const actions = {
|
||||
/**
|
||||
* Registers a handler for a statically named backend event.
|
||||
*
|
||||
* Registration order does not matter: handlers added while disconnected are
|
||||
* attached on the next connect.
|
||||
*
|
||||
* @param {string} event Event name (e.g. `category`, `articles`).
|
||||
* @param {(event: MessageEvent) => void} handler Signal handler.
|
||||
* @returns {() => void} Unsubscribe callback.
|
||||
@@ -360,9 +426,6 @@ const actions = {
|
||||
/**
|
||||
* Subscribes to `article-<sku>` signals for one SKU.
|
||||
*
|
||||
* The backend emits this event whenever that article changes. Exactly one
|
||||
* listener is attached per SKU while at least one caller is subscribed.
|
||||
*
|
||||
* @param {string} sku Article SKU to watch.
|
||||
* @param {(sku: string) => void} handler Called with the changed SKU.
|
||||
* @returns {() => void} Unsubscribe callback.
|
||||
@@ -389,9 +452,6 @@ const actions = {
|
||||
/**
|
||||
* Global `GET /api/events` connection.
|
||||
*
|
||||
* Owns the single `EventSource` for the tab and dispatches signals into the
|
||||
* domain automats. Reconnects whenever the selected company changes.
|
||||
*
|
||||
* @type {Automat<SseState>}
|
||||
*/
|
||||
sseAutomat = new Automat(initialState, actions, { name: 'sse' });
|
||||
@@ -399,48 +459,26 @@ sseAutomat = new Automat(initialState, actions, { name: 'sse' });
|
||||
/**
|
||||
* Refreshes the cached article plus any open history/supplier view.
|
||||
*
|
||||
* Background refreshes stay silent: an article that is not on screen has no
|
||||
* visible subscriber to notify, so failures are swallowed.
|
||||
*
|
||||
* @param {string} sku Article SKU.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function invalidateArticle(sku) {
|
||||
const key = String(sku);
|
||||
void artikelAutomat.actions.reloadArticle(key).catch(() => {});
|
||||
if (articleHistoryAutomat.has(key)) {
|
||||
void articleHistoryAutomat.reload(key).catch(() => {});
|
||||
const art = Automat.get('artikel');
|
||||
if (art?.actions?.reloadArticle) {
|
||||
void art.actions.reloadArticle(key).catch(() => {});
|
||||
}
|
||||
if (articleSupplierAutomat.has(key)) {
|
||||
void articleSupplierAutomat.reload(key).catch(() => {});
|
||||
const hist = Automat.get('articleHistory');
|
||||
if (hist?.has?.(key)) {
|
||||
void hist.reload(key).catch(() => {});
|
||||
}
|
||||
const sup = Automat.get('articleSupplier');
|
||||
if (sup?.has?.(key)) {
|
||||
void sup.reload(key).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// Reconnect when the selected company changes; disconnect on logout.
|
||||
// On logout the stream must close; on relogin (anon -> authed with a company)
|
||||
// the stream is re-established even if the company id matches the previous one.
|
||||
let wasAuthed = false;
|
||||
authAutomat.subscribe((auth) => {
|
||||
const companyId =
|
||||
auth.status === 'authed' && auth.user?.selectedCompanyId != null
|
||||
? String(auth.user.selectedCompanyId)
|
||||
: null;
|
||||
const isAuthed = companyId != null;
|
||||
const relogin = isAuthed && !wasAuthed;
|
||||
wasAuthed = isAuthed;
|
||||
|
||||
if (!relogin && companyId === sseAutomat.state.companyId) return;
|
||||
|
||||
if (companyId == null) {
|
||||
disconnect();
|
||||
sseAutomat.setState({ companyId: null });
|
||||
return;
|
||||
}
|
||||
|
||||
// A different company (or a fresh login) means a different stream scope.
|
||||
disconnect();
|
||||
sseAutomat.setState({ companyId });
|
||||
connect();
|
||||
});
|
||||
authAutomat.subscribe(handleAuthSync);
|
||||
handleAuthSync(authAutomat.state);
|
||||
|
||||
export default sseAutomat;
|
||||
|
||||
@@ -1,268 +0,0 @@
|
||||
/**
|
||||
* @file supplierAutomat.js
|
||||
* @description Manages cached supplier list (`GET /api/supplier`) for the active company.
|
||||
*
|
||||
* Upstream Triggers:
|
||||
* - `authAutomat`: Subscribes to auth state changes (`syncWithAuth`) — triggers
|
||||
* `actions.load()` on `'authed'` with a `selectedCompanyId`, and `actions.clear()` on `'anon'`.
|
||||
* - `sseAutomat`: Calls `actions.reload()` upon receiving a `'supplier'` SSE event.
|
||||
* - UI / Routes: Calls to `actions.load()`, `actions.reload()`, or `actions.setSuppliers()`.
|
||||
*/
|
||||
|
||||
import { Automat } from 'react-automat';
|
||||
import { supplier as supplierApi } from '../api';
|
||||
import { requireArray, requireDefined, requireObject } from '../utils/apiShape';
|
||||
import authAutomat from './authAutomat';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Supplier
|
||||
* @property {string|number} id Supplier identifier.
|
||||
* @property {string} name Supplier name.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SupplierState
|
||||
* @property {Array<Supplier>} suppliers Company-scoped suppliers.
|
||||
* @property {'idle'|'loading'|'ready'|'failed'} status Load lifecycle state.
|
||||
* @property {Error|null} error Last load error.
|
||||
*/
|
||||
|
||||
/** @type {SupplierState} */
|
||||
const initialState = {
|
||||
suppliers: [],
|
||||
articlesBySupplier: {},
|
||||
articlesLoading: false,
|
||||
status: 'idle',
|
||||
error: null,
|
||||
};
|
||||
|
||||
let supplierAutomat;
|
||||
|
||||
/**
|
||||
* Fetches the company-scoped supplier list.
|
||||
*
|
||||
* @returns {Promise<Array<Supplier>>} Supplier list.
|
||||
*/
|
||||
async function fetchSuppliers() {
|
||||
const data = await supplierApi.list();
|
||||
requireObject(data, 'GET /api/supplier');
|
||||
return requireArray(data.suppliers, 'suppliers');
|
||||
}
|
||||
|
||||
/** In-flight `load()` promise so concurrent consumers share one request. */
|
||||
let inflightLoad = null;
|
||||
|
||||
/**
|
||||
* Loads the supplier list once, deduplicating concurrent callers.
|
||||
*
|
||||
* @returns {Promise<Array<Supplier>>} Loaded suppliers.
|
||||
*/
|
||||
function loadSuppliers() {
|
||||
if (inflightLoad) return inflightLoad;
|
||||
inflightLoad = (async () => {
|
||||
supplierAutomat.setState({ status: 'loading', error: null });
|
||||
try {
|
||||
const suppliers = await fetchSuppliers();
|
||||
supplierAutomat.setState({ suppliers, status: 'ready', error: null });
|
||||
void actions.loadAllSupplierArticles().catch(() => {});
|
||||
return suppliers;
|
||||
} catch (error) {
|
||||
supplierAutomat.setState({ status: 'failed', error });
|
||||
throw error;
|
||||
} finally {
|
||||
inflightLoad = null;
|
||||
}
|
||||
})();
|
||||
return inflightLoad;
|
||||
}
|
||||
|
||||
const actions = {
|
||||
/**
|
||||
* Loads the supplier list, exposing a loading/failed lifecycle.
|
||||
*
|
||||
* @returns {Promise<Array<Supplier>>} Loaded suppliers.
|
||||
*/
|
||||
load() {
|
||||
return loadSuppliers();
|
||||
},
|
||||
|
||||
/**
|
||||
* Silently re-fetches the supplier list while keeping the current state.
|
||||
*
|
||||
* @returns {Promise<Array<Supplier>>} Fresh suppliers.
|
||||
*/
|
||||
async reload() {
|
||||
try {
|
||||
const suppliers = await fetchSuppliers();
|
||||
supplierAutomat.setState({ suppliers, status: 'ready', error: null });
|
||||
void actions.loadAllSupplierArticles().catch(() => {});
|
||||
return suppliers;
|
||||
} catch (error) {
|
||||
supplierAutomat.setState({ error });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads articles for a single supplier, caching them in state.articlesBySupplier.
|
||||
*
|
||||
* @param {string|number} supplierId
|
||||
* @returns {Promise<Array<object>>}
|
||||
*/
|
||||
async loadArticlesForSupplier(supplierId) {
|
||||
const sId = String(supplierId);
|
||||
if (supplierAutomat.state.articlesBySupplier?.[sId]) {
|
||||
return supplierAutomat.state.articlesBySupplier[sId];
|
||||
}
|
||||
const res = await supplierApi.getArticles(supplierId);
|
||||
const articles = Array.isArray(res?.articles) ? res.articles : [];
|
||||
supplierAutomat.setState({
|
||||
articlesBySupplier: {
|
||||
...(supplierAutomat.state.articlesBySupplier || {}),
|
||||
[sId]: articles,
|
||||
},
|
||||
});
|
||||
return articles;
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads articles for all suppliers in the supplier list, caching them in state.articlesBySupplier.
|
||||
*
|
||||
* @returns {Promise<Record<string, Array<object>>>}
|
||||
*/
|
||||
async loadAllSupplierArticles() {
|
||||
const suppliers = supplierAutomat.state.suppliers;
|
||||
if (!suppliers.length) return {};
|
||||
supplierAutomat.setState({ articlesLoading: true });
|
||||
try {
|
||||
const results = await Promise.allSettled(
|
||||
suppliers.map(async (s) => {
|
||||
const res = await supplierApi.getArticles(s.id);
|
||||
return { id: String(s.id), articles: Array.isArray(res?.articles) ? res.articles : [] };
|
||||
}),
|
||||
);
|
||||
const nextMap = { ...(supplierAutomat.state.articlesBySupplier || {}) };
|
||||
for (const r of results) {
|
||||
if (r.status === 'fulfilled') {
|
||||
nextMap[r.value.id] = r.value.articles;
|
||||
}
|
||||
}
|
||||
supplierAutomat.setState({ articlesBySupplier: nextMap, articlesLoading: false });
|
||||
return nextMap;
|
||||
} catch {
|
||||
supplierAutomat.setState({ articlesLoading: false });
|
||||
return supplierAutomat.state.articlesBySupplier || {};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a supplier and refreshes the cached list.
|
||||
*
|
||||
* @param {{name: string}} body Creation payload.
|
||||
* @returns {Promise<Supplier>} Created supplier.
|
||||
*/
|
||||
async create(body) {
|
||||
const data = await supplierApi.create(body);
|
||||
requireObject(data, 'POST /api/supplier');
|
||||
requireObject(data.supplier, 'supplier');
|
||||
requireDefined(data.supplier.id, 'supplier.id');
|
||||
await supplierAutomat.actions.reload();
|
||||
return data.supplier;
|
||||
},
|
||||
|
||||
/**
|
||||
* Renames a supplier and refreshes the cached list.
|
||||
*
|
||||
* @param {string|number} id Supplier identifier.
|
||||
* @param {{name: string}} body Update payload.
|
||||
* @returns {Promise<Supplier>} Updated supplier.
|
||||
*/
|
||||
async update(id, body) {
|
||||
const data = await supplierApi.update(id, body);
|
||||
requireObject(data, 'PATCH /api/supplier/:id');
|
||||
requireObject(data.supplier, 'supplier');
|
||||
requireDefined(data.supplier.id, 'supplier.id');
|
||||
await supplierAutomat.actions.reload();
|
||||
return data.supplier;
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes a supplier and refreshes the cached list.
|
||||
*
|
||||
* @param {string|number} id Supplier identifier.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async remove(id) {
|
||||
await supplierApi.delete(id);
|
||||
await supplierAutomat.actions.reload();
|
||||
},
|
||||
|
||||
/**
|
||||
* Replaces the cached list without a round-trip.
|
||||
*
|
||||
* @param {Array<Supplier>} suppliers Suppliers to cache.
|
||||
* @returns {void}
|
||||
*/
|
||||
setSuppliers(suppliers) {
|
||||
supplierAutomat.setState({
|
||||
suppliers: Array.isArray(suppliers) ? suppliers : [],
|
||||
status: 'ready',
|
||||
error: null,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Forgets the cached list and returns to the initial state.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
clear() {
|
||||
supplierAutomat.setState({ ...initialState });
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Company-scoped supplier cache. Loads automatically once an authenticated
|
||||
* session with a selected company exists and is cleared when auth is gone.
|
||||
*
|
||||
* @type {Automat<SupplierState>}
|
||||
*/
|
||||
supplierAutomat = new Automat(initialState, actions, { name: 'supplier' });
|
||||
|
||||
/**
|
||||
* Derives the cache key from the auth state: the selected company while
|
||||
* authenticated, otherwise `null`.
|
||||
*
|
||||
* @param {import('./authAutomat').AuthState} authState Auth automat state.
|
||||
* @returns {string|null} Cache key.
|
||||
*/
|
||||
function authKey(authState) {
|
||||
if (authState?.status !== 'authed' || !authState.user) return null;
|
||||
const companyId = authState.user.selectedCompanyId;
|
||||
return companyId == null ? null : String(companyId);
|
||||
}
|
||||
|
||||
let lastAuthKey;
|
||||
|
||||
/**
|
||||
* Loads the supplier list when auth/company becomes available and forgets it
|
||||
* when the session ends.
|
||||
*
|
||||
* @param {import('./authAutomat').AuthState} authState Auth automat state.
|
||||
* @returns {void}
|
||||
*/
|
||||
function syncWithAuth(authState) {
|
||||
const key = authKey(authState);
|
||||
if (key === lastAuthKey) return;
|
||||
lastAuthKey = key;
|
||||
if (key == null) {
|
||||
supplierAutomat.actions.clear();
|
||||
} else {
|
||||
void supplierAutomat.actions.load().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
authAutomat.subscribe(syncWithAuth);
|
||||
syncWithAuth(authAutomat.state);
|
||||
|
||||
export default supplierAutomat;
|
||||
@@ -1,105 +0,0 @@
|
||||
/**
|
||||
* @file unitAutomat.js
|
||||
* @description Manages allowable units of measure (Stück, kg, Liter, etc.)
|
||||
* for the active company.
|
||||
*
|
||||
* Upstream Triggers:
|
||||
* - `authAutomat`: Subscribes to auth state changes — calls `actions.load()` on
|
||||
* `'authed'` with a `selectedCompanyId`, and `actions.reset()` on `'anon'`.
|
||||
* - UI / Settings: Mutating actions (`upsert`, `delete`).
|
||||
*/
|
||||
|
||||
import { Automat } from 'react-automat';
|
||||
import { unit as unitApi } from '../api';
|
||||
import authAutomat from './authAutomat';
|
||||
|
||||
/**
|
||||
* @typedef {Object} UnitState
|
||||
* @property {Array<object>} units
|
||||
* @property {'idle'|'loading'|'succeeded'|'failed'} status
|
||||
* @property {string|null} error
|
||||
*/
|
||||
|
||||
/** @type {UnitState} */
|
||||
const initialState = {
|
||||
units: [],
|
||||
status: 'idle',
|
||||
error: null,
|
||||
};
|
||||
|
||||
function sortUnits(units) {
|
||||
return [...units].sort((a, b) => {
|
||||
const left = a.symbol ?? a.si ?? '';
|
||||
const right = b.symbol ?? b.si ?? '';
|
||||
return String(left).localeCompare(String(right), 'de');
|
||||
});
|
||||
}
|
||||
|
||||
let unitAutomat;
|
||||
|
||||
const actions = {
|
||||
/** Load all units for the selected company. */
|
||||
async load() {
|
||||
unitAutomat.setState({ status: 'loading', error: null });
|
||||
try {
|
||||
const res = await unitApi.list();
|
||||
const units = sortUnits(Array.isArray(res.units) ? res.units : []);
|
||||
unitAutomat.setState({ units, status: 'succeeded' });
|
||||
return units;
|
||||
} catch (err) {
|
||||
unitAutomat.setState({ status: 'failed', error: err?.message ?? String(err) });
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Upsert (create or update) a unit.
|
||||
* @param {object} body
|
||||
*/
|
||||
async upsert(body) {
|
||||
const res = await unitApi.upsert(body);
|
||||
const row = res.unit;
|
||||
const units = sortUnits([
|
||||
...unitAutomat.state.units.filter((u) => u.si !== row.si),
|
||||
row,
|
||||
]);
|
||||
unitAutomat.setState({ units });
|
||||
return units;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a unit by SI symbol.
|
||||
* @param {string} si
|
||||
*/
|
||||
async delete(si) {
|
||||
await unitApi.delete(si);
|
||||
unitAutomat.setState({
|
||||
units: unitAutomat.state.units.filter((u) => u.si !== si),
|
||||
});
|
||||
},
|
||||
|
||||
/** Reset to idle state (called on logout / company switch). */
|
||||
reset() {
|
||||
unitAutomat.setState(initialState);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Global unit state container.
|
||||
* Loads automatically when authAutomat reaches 'authed' with a selectedCompanyId.
|
||||
*
|
||||
* @type {Automat<UnitState>}
|
||||
*/
|
||||
unitAutomat = new Automat(initialState, actions, { name: 'unit' });
|
||||
|
||||
// Subscribe to auth changes
|
||||
authAutomat.subscribe((auth) => {
|
||||
const companyId = auth.user?.selectedCompanyId;
|
||||
if (auth.status === 'authed' && companyId) {
|
||||
void unitAutomat.actions.load();
|
||||
} else if (auth.status === 'anon') {
|
||||
unitAutomat.actions.reset();
|
||||
}
|
||||
});
|
||||
|
||||
export default unitAutomat;
|
||||
@@ -1,75 +0,0 @@
|
||||
/**
|
||||
* @file userSettingsAutomat.js
|
||||
* @description Manages client-side user UI layout preferences (attachment viewer
|
||||
* position, chat drawer width). Persisted in IndexedDB.
|
||||
*
|
||||
* Upstream Triggers:
|
||||
* - UI / User layout preferences: Calls to `setViewerPosition(position)` and
|
||||
* `setChatDrawerWidth(width)`.
|
||||
*/
|
||||
|
||||
import { Automat } from 'react-automat';
|
||||
|
||||
/**
|
||||
* @typedef {'top'|'right'|'below'} ViewerPosition
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} UserSettingsState
|
||||
* @property {ViewerPosition} viewerPosition Preferred docking position of the
|
||||
* attachment viewer (text editor / PDF preview) relative to the article list.
|
||||
* @property {number} chatDrawerWidth Preferred width of the chat drawer in pixels.
|
||||
*/
|
||||
|
||||
/** @type {UserSettingsState} */
|
||||
const initialState = {
|
||||
viewerPosition: 'top',
|
||||
chatDrawerWidth: 460,
|
||||
};
|
||||
|
||||
/**
|
||||
* Persisted per-user UI preferences. Unlike the other automats this one is
|
||||
* backed by IndexedDB (`persist: true`) so settings survive reloads and
|
||||
* sessions.
|
||||
*
|
||||
* @type {Automat<UserSettingsState>}
|
||||
*/
|
||||
const userSettingsAutomat = new Automat(
|
||||
initialState,
|
||||
{
|
||||
/**
|
||||
* Set the docking position of the attachment viewer.
|
||||
*
|
||||
* @param {ViewerPosition} position One of `'top'`, `'right'` or `'below'`.
|
||||
* @returns {void}
|
||||
*/
|
||||
setViewerPosition(position) {
|
||||
if (position !== 'top' && position !== 'right' && position !== 'below') {
|
||||
return;
|
||||
}
|
||||
userSettingsAutomat.setState({ viewerPosition: position });
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the width of the chat drawer in pixels.
|
||||
*
|
||||
* @param {number} width
|
||||
* @returns {void}
|
||||
*/
|
||||
setChatDrawerWidth(width) {
|
||||
const num = Number(width);
|
||||
if (Number.isNaN(num) || num < 280) {
|
||||
return;
|
||||
}
|
||||
userSettingsAutomat.setState({ chatDrawerWidth: Math.round(num) });
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'userSettings',
|
||||
persist: true,
|
||||
}
|
||||
);
|
||||
|
||||
export default userSettingsAutomat;
|
||||
export const { setViewerPosition, setChatDrawerWidth } = userSettingsAutomat.actions;
|
||||
export const selectChatDrawerWidth = () => userSettingsAutomat.state.chatDrawerWidth || 460;
|
||||
@@ -1,256 +0,0 @@
|
||||
/**
|
||||
* @file warehouseAutomat.js
|
||||
* @description Manages cached warehouse locations (`GET /api/warehouse`) for the active company.
|
||||
*
|
||||
* Upstream Triggers:
|
||||
* - `authAutomat`: Subscribes to auth state changes (`syncWithAuth`) — triggers
|
||||
* `actions.load()` on `'authed'` with a `selectedCompanyId`, and `actions.clear()` on `'anon'`.
|
||||
* - `sseAutomat`: Calls `actions.reload()` upon receiving a `'warehouse'` SSE event.
|
||||
* - UI / Routes: Calls to `actions.load()`, `actions.reload()`, or `actions.setWarehouses()`.
|
||||
*/
|
||||
|
||||
import { Automat } from 'react-automat';
|
||||
import { warehouse as warehouseApi } from '../api';
|
||||
import { requireArray, requireDefined, requireObject } from '../utils/apiShape';
|
||||
import authAutomat from './authAutomat';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Warehouse
|
||||
* @property {string|number} id Warehouse identifier.
|
||||
* @property {string} name Warehouse name.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} WarehouseState
|
||||
* @property {Array<Warehouse>} warehouses Company-scoped warehouses.
|
||||
* @property {Array<Warehouse>} filteredWarehouses Filtered warehouses according to active filter query.
|
||||
* @property {string} filter Active search/filter string.
|
||||
* @property {'idle'|'loading'|'ready'|'failed'} status Load lifecycle state.
|
||||
* @property {Error|null} error Last load error.
|
||||
*/
|
||||
|
||||
/** @type {WarehouseState} */
|
||||
const initialState = {
|
||||
warehouses: [],
|
||||
filteredWarehouses: [],
|
||||
filter: '',
|
||||
status: 'idle',
|
||||
error: null,
|
||||
};
|
||||
|
||||
let warehouseAutomat;
|
||||
|
||||
const applyFilter = (warehouses, filter) => {
|
||||
const query = String(filter ?? '').trim().toLowerCase();
|
||||
return query ? warehouses.filter((w) => String(w.name ?? '').toLowerCase().includes(query)) : warehouses;
|
||||
};
|
||||
|
||||
const updateStateWithWarehouses = (warehouses, extra = {}) => {
|
||||
const list = Array.isArray(warehouses) ? warehouses : [];
|
||||
const filteredWarehouses = applyFilter(list, warehouseAutomat.state.filter);
|
||||
warehouseAutomat.setState({ warehouses: list, filteredWarehouses, ...extra });
|
||||
return list;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the company-scoped warehouse list.
|
||||
*
|
||||
* @returns {Promise<Array<Warehouse>>} Warehouse list.
|
||||
*/
|
||||
async function fetchWarehouses() {
|
||||
const data = await warehouseApi.list();
|
||||
requireObject(data, 'GET /api/warehouse');
|
||||
return requireArray(data.warehouses, 'warehouses');
|
||||
}
|
||||
|
||||
/** In-flight `load()` promise so concurrent callers share one request. */
|
||||
let inflightLoad = null;
|
||||
|
||||
/**
|
||||
* Loads the warehouse list once, deduplicating concurrent callers.
|
||||
*
|
||||
* @returns {Promise<Array<Warehouse>>} Loaded warehouses.
|
||||
*/
|
||||
function loadWarehouses() {
|
||||
if (inflightLoad) return inflightLoad;
|
||||
inflightLoad = (async () => {
|
||||
warehouseAutomat.setState({ status: 'loading', error: null });
|
||||
try {
|
||||
const warehouses = await fetchWarehouses();
|
||||
updateStateWithWarehouses(warehouses, { status: 'ready', error: null });
|
||||
return warehouses;
|
||||
} catch (error) {
|
||||
warehouseAutomat.setState({ status: 'failed', error });
|
||||
throw error;
|
||||
} finally {
|
||||
inflightLoad = null;
|
||||
}
|
||||
})();
|
||||
return inflightLoad;
|
||||
}
|
||||
|
||||
const actions = {
|
||||
/** Sets the active warehouse search filter and updates filteredWarehouses. */
|
||||
filter(query) {
|
||||
const filter = String(query ?? '');
|
||||
warehouseAutomat.setState({
|
||||
filter,
|
||||
filteredWarehouses: applyFilter(warehouseAutomat.state.warehouses, filter),
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads the warehouse list, exposing a loading/failed lifecycle.
|
||||
*
|
||||
* @returns {Promise<Array<Warehouse>>} Loaded warehouses.
|
||||
*/
|
||||
load() {
|
||||
return loadWarehouses();
|
||||
},
|
||||
|
||||
/**
|
||||
* Silently re-fetches the warehouse list while keeping the current state.
|
||||
*
|
||||
* @returns {Promise<Array<Warehouse>>} Fresh warehouses.
|
||||
*/
|
||||
async reload() {
|
||||
try {
|
||||
const warehouses = await fetchWarehouses();
|
||||
updateStateWithWarehouses(warehouses, { status: 'ready', error: null });
|
||||
return warehouses;
|
||||
} catch (error) {
|
||||
warehouseAutomat.setState({ error });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a warehouse and refreshes the cached list.
|
||||
*
|
||||
* @param {{name: string}} body Creation payload.
|
||||
* @returns {Promise<Warehouse>} Created warehouse.
|
||||
*/
|
||||
async create(body) {
|
||||
const data = await warehouseApi.create(body);
|
||||
requireObject(data, 'POST /api/warehouse');
|
||||
requireObject(data.warehouse, 'warehouse');
|
||||
requireDefined(data.warehouse.id, 'warehouse.id');
|
||||
await warehouseAutomat.actions.reload();
|
||||
return data.warehouse;
|
||||
},
|
||||
|
||||
/**
|
||||
* Renames a warehouse and refreshes the cached list.
|
||||
*
|
||||
* @param {string|number} id Warehouse identifier.
|
||||
* @param {{name: string}} body Update payload.
|
||||
* @returns {Promise<Warehouse>} Updated warehouse.
|
||||
*/
|
||||
async update(id, body) {
|
||||
const data = await warehouseApi.update(id, body);
|
||||
requireObject(data, 'PATCH /api/warehouse/:id');
|
||||
requireObject(data.warehouse, 'warehouse');
|
||||
requireDefined(data.warehouse.id, 'warehouse.id');
|
||||
await warehouseAutomat.actions.reload();
|
||||
return data.warehouse;
|
||||
},
|
||||
|
||||
rename(id, body) {
|
||||
return actions.update(id, body);
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes a warehouse and refreshes the cached list.
|
||||
*
|
||||
* @param {string|number} id Warehouse identifier.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async remove(id) {
|
||||
await warehouseApi.delete(id);
|
||||
await warehouseAutomat.actions.reload();
|
||||
},
|
||||
|
||||
/**
|
||||
* Persists a new sort order for all warehouses.
|
||||
*
|
||||
* @param {Array<string|number>} warehouseIds IDs in the desired order.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async reorder(warehouseIds) {
|
||||
const byId = new Map(warehouseAutomat.state.warehouses.map((w) => [w.id, w]));
|
||||
const reordered = warehouseIds.map((id) => byId.get(id)).filter(Boolean);
|
||||
if (reordered.length === warehouseAutomat.state.warehouses.length) {
|
||||
updateStateWithWarehouses(reordered);
|
||||
}
|
||||
await warehouseApi.order({ warehouseIds });
|
||||
await warehouseAutomat.actions.reload();
|
||||
},
|
||||
|
||||
/**
|
||||
* Replaces the cached list without a round-trip.
|
||||
*
|
||||
* @param {Array<Warehouse>} warehouses Warehouses to cache.
|
||||
* @returns {void}
|
||||
*/
|
||||
setWarehouses(warehouses) {
|
||||
updateStateWithWarehouses(warehouses, {
|
||||
status: 'ready',
|
||||
error: null,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Forgets the cached list and returns to the initial state.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
clear() {
|
||||
warehouseAutomat.setState({ ...initialState });
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Company-scoped warehouse cache. Loads automatically once an authenticated
|
||||
* session with a selected company exists and is cleared when auth is gone.
|
||||
*
|
||||
* @type {Automat<WarehouseState>}
|
||||
*/
|
||||
warehouseAutomat = new Automat(initialState, actions, { name: 'warehouse' });
|
||||
|
||||
/**
|
||||
* Derives the cache key from the auth state: the selected company while
|
||||
* authenticated, otherwise `null`.
|
||||
*
|
||||
* @param {import('./authAutomat').AuthState} authState Auth automat state.
|
||||
* @returns {string|null} Cache key.
|
||||
*/
|
||||
function authKey(authState) {
|
||||
if (authState?.status !== 'authed' || !authState.user) return null;
|
||||
const companyId = authState.user.selectedCompanyId;
|
||||
return companyId == null ? null : String(companyId);
|
||||
}
|
||||
|
||||
let lastAuthKey;
|
||||
|
||||
/**
|
||||
* Loads the warehouse list when auth/company becomes available and forgets it
|
||||
* when the session ends.
|
||||
*
|
||||
* @param {import('./authAutomat').AuthState} authState Auth automat state.
|
||||
* @returns {void}
|
||||
*/
|
||||
function syncWithAuth(authState) {
|
||||
const key = authKey(authState);
|
||||
if (key === lastAuthKey) return;
|
||||
lastAuthKey = key;
|
||||
if (key == null) {
|
||||
warehouseAutomat.actions.clear();
|
||||
} else {
|
||||
void warehouseAutomat.actions.load().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
authAutomat.subscribe(syncWithAuth);
|
||||
syncWithAuth(authAutomat.state);
|
||||
|
||||
export default warehouseAutomat;
|
||||
@@ -12,7 +12,10 @@ import HelpBadge from './HelpBadge';
|
||||
import { NAV_ITEMS, SUB_NAV, resolveActiveNav, resolveActiveSub } from '../nav';
|
||||
import { artikelQueryPath, buildSearchUrl } from './article/artikelQuery';
|
||||
import { defaultCategoryPath } from './category/categoryRoute';
|
||||
import { navigationAutomat } from '../automats';
|
||||
import companyAutomat from '../automats/companyAutomat';
|
||||
import navigationAutomat from '../automats/navigationAutomat';
|
||||
import { initPwa } from '../automats/pwaAutomat';
|
||||
import PwaUpdateDialog from './PwaUpdateDialog';
|
||||
import {
|
||||
NAV_ITEM_ACTIVE_ACCENT,
|
||||
NAV_SUB_ACTIVE,
|
||||
@@ -22,9 +25,16 @@ import {
|
||||
import { ChatFab, ChatDrawer } from './chat';
|
||||
import { PinnedAppletFabs, PinnedAppletDialog } from './applet';
|
||||
|
||||
// Ensure companyAutomat is retained as part of the 1st-level navigation shell
|
||||
void companyAutomat;
|
||||
initPwa();
|
||||
|
||||
const BreakpointOverlay = 0 && import.meta.env.DEV
|
||||
? lazy(() => import('./BreakpointOverlay'))
|
||||
: null;
|
||||
const ChunkCacheMonitor = import.meta.env.DEV
|
||||
? lazy(() => import('../devtools/ChunkCacheMonitor'))
|
||||
: null;
|
||||
|
||||
|
||||
const SIDEBAR_WIDTH = 144;
|
||||
@@ -403,13 +413,21 @@ export default function AppShell() {
|
||||
minWidth: 0,
|
||||
})}
|
||||
>
|
||||
<Outlet />
|
||||
<Suspense fallback={null}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</Box>
|
||||
</Box>
|
||||
<ChatFab />
|
||||
<ChatDrawer />
|
||||
<PinnedAppletFabs pathname={pathname} />
|
||||
<PinnedAppletDialog />
|
||||
<PwaUpdateDialog />
|
||||
{ChunkCacheMonitor ? (
|
||||
<Suspense fallback={null}>
|
||||
<ChunkCacheMonitor />
|
||||
</Suspense>
|
||||
) : null}
|
||||
{BreakpointOverlay && (
|
||||
<Suspense fallback={null}>
|
||||
<BreakpointOverlay />
|
||||
|
||||
@@ -35,10 +35,8 @@ import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
||||
import RequestQuoteIcon from '@mui/icons-material/RequestQuote';
|
||||
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
|
||||
import BackendError from './BackendError';
|
||||
import { company, number as numberApi } from '../api';
|
||||
import { company } from '../api';
|
||||
import authAutomat, { bootstrapAuthMe } from '../automats/authAutomat';
|
||||
import companyAutomat from '../automats/companyAutomat';
|
||||
import numberAutomat from '../automats/numberAutomat';
|
||||
import {
|
||||
defaultIso639,
|
||||
ISO639_OPTIONS,
|
||||
@@ -213,30 +211,10 @@ class CompanySetupDialog extends Component {
|
||||
iso639,
|
||||
taxrate: parsedTaxrate,
|
||||
reducedtaxrate: parsedReducedtaxrate,
|
||||
numbers,
|
||||
});
|
||||
|
||||
// Apply initial number sequences if set
|
||||
if (numbers) {
|
||||
try {
|
||||
const listRes = await numberApi.list();
|
||||
const serverNumbers = listRes?.numbers ?? [];
|
||||
for (const serverRow of serverNumbers) {
|
||||
const userConfig = numbers[serverRow.type];
|
||||
if (userConfig) {
|
||||
const prefix = String(userConfig.prefix ?? '');
|
||||
const padding = Math.max(0, Number(userConfig.padding) || 0);
|
||||
const next = Math.max(1, Number(userConfig.next) || 1);
|
||||
await numberApi.update(serverRow.id, { prefix, padding, next });
|
||||
}
|
||||
}
|
||||
} catch (numErr) {
|
||||
console.warn('Initial number sequences could not be saved:', numErr);
|
||||
}
|
||||
}
|
||||
|
||||
await bootstrapAuthMe();
|
||||
await companyAutomat.actions.load();
|
||||
await numberAutomat.actions.reload().catch(() => { });
|
||||
} catch (err) {
|
||||
this.setState({ error: backendErrorFrom(err) });
|
||||
} finally {
|
||||
|
||||
@@ -7,7 +7,7 @@ import Paper from '@mui/material/Paper';
|
||||
import Popper from '@mui/material/Popper';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { helpAutomat } from '../automats';
|
||||
import helpAutomat from '../automats/helpAutomat';
|
||||
import {
|
||||
RED,
|
||||
RED_HOVER,
|
||||
|
||||
@@ -22,7 +22,7 @@ import { enqueueSnackbar } from 'notistack';
|
||||
import AppDialog from './AppDialog';
|
||||
import { thumb, article as articleApi } from '../api';
|
||||
import artikelAutomat from '../automats/artikelAutomat';
|
||||
import brandAutomat from '../automats/brandAutomat';
|
||||
import companyAutomat from '../automats/companyAutomat';
|
||||
import categoryAutomat from '../automats/categoryAutomat';
|
||||
import chatAutomat from '../automats/chatAutomat';
|
||||
import { requireArray, requireObject } from '../utils/apiShape';
|
||||
@@ -238,7 +238,7 @@ export default class PicturesDialog extends PureComponent {
|
||||
if (onUpload) {
|
||||
await onUpload(file);
|
||||
} else if (brand?.id) {
|
||||
await brandAutomat.actions.addImage(brand.id, file);
|
||||
await companyAutomat.actions.addBrandImage(brand.id, file);
|
||||
enqueueSnackbar(`Markenlogo für „${brand.name || ''}“ gespeichert.`, { variant: 'success' });
|
||||
} else if (article?.sku) {
|
||||
const uploadStarted = performance.now();
|
||||
@@ -318,7 +318,7 @@ export default class PicturesDialog extends PureComponent {
|
||||
if (onDelete) {
|
||||
await onDelete(pic, 0);
|
||||
} else if (brand?.id) {
|
||||
await brandAutomat.actions.deleteImage(brand.id);
|
||||
await companyAutomat.actions.deleteBrandImage(brand.id);
|
||||
enqueueSnackbar('Markenlogo gelöscht.', { variant: 'success' });
|
||||
} else if (category?.id) {
|
||||
await categoryAutomat.actions.deleteCategoryImage(category.id);
|
||||
|
||||
@@ -14,7 +14,8 @@ import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import authAutomat, { bootstrapAuthMe } from '../automats/authAutomat';
|
||||
import { helpAutomat, toggleHelp, pwaAutomat, openPwaDialog } from '../automats';
|
||||
import helpAutomat, { toggleHelp } from '../automats/helpAutomat';
|
||||
import pwaAutomat, { openPwaDialog } from '../automats/pwaAutomat';
|
||||
import SettingsDialog from './settings/SettingsDialog';
|
||||
import { notifyError } from '../utils/notify';
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ import { stock } from '../../api';
|
||||
import { formatStock } from './searchFormat';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import artikelAutomat from '../../automats/artikelAutomat';
|
||||
import warehouseAutomat from '../../automats/warehouseAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import {
|
||||
BORDER,
|
||||
GREEN_BG,
|
||||
@@ -64,7 +64,7 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
reason: '',
|
||||
submitting: false,
|
||||
error: null,
|
||||
warehouses: warehouseAutomat.state.warehouses || [],
|
||||
warehouses: companyAutomat.state.warehouses || [],
|
||||
trackStock: isTrackStock,
|
||||
initialTrackStock: isTrackStock,
|
||||
};
|
||||
@@ -79,14 +79,14 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsubWarehouse = warehouseAutomat.subscribe((state) => {
|
||||
this.unsubWarehouse = companyAutomat.subscribe((state) => {
|
||||
const warehouses = state.warehouses || [];
|
||||
this.setState((prev) => ({
|
||||
warehouses,
|
||||
warehouseId: prev.warehouseId || (warehouses.length > 0 ? warehouses[0].id : ''),
|
||||
}));
|
||||
});
|
||||
void warehouseAutomat.actions.load().catch(() => {});
|
||||
}, ['warehouses']);
|
||||
void companyAutomat.actions.loadWarehouses().catch(() => {});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -95,7 +95,7 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.open && !prevProps.open) {
|
||||
void warehouseAutomat.actions.load().catch(() => {});
|
||||
void companyAutomat.actions.loadWarehouses().catch(() => {});
|
||||
const initialWhId = this.resolveInitialWarehouseId(this.props);
|
||||
const isTrackStock =
|
||||
this.props.article?.track_stock !== false && this.props.article?.track_stock !== 0;
|
||||
@@ -107,7 +107,7 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
reason: '',
|
||||
submitting: false,
|
||||
error: null,
|
||||
warehouses: warehouseAutomat.state.warehouses || [],
|
||||
warehouses: companyAutomat.state.warehouses || [],
|
||||
trackStock: isTrackStock,
|
||||
initialTrackStock: isTrackStock,
|
||||
});
|
||||
@@ -121,7 +121,7 @@ export default class ArticleStockAdjustDialog extends PureComponent {
|
||||
if (articleWarehouses.length > 0) {
|
||||
return articleWarehouses[0].id;
|
||||
}
|
||||
const allWarehouses = warehouseAutomat.state.warehouses || [];
|
||||
const allWarehouses = companyAutomat.state.warehouses || [];
|
||||
return allWarehouses.length > 0 ? allWarehouses[0].id : '';
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import LayersOutlinedIcon from '@mui/icons-material/LayersOutlined';
|
||||
import AppDialog from '../AppDialog';
|
||||
import ArticlePhoto from './ArticlePhoto';
|
||||
import { article as articleApi } from '../../api';
|
||||
import brandAutomat from '../../automats/brandAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import categoryAutomat from '../../automats/categoryAutomat';
|
||||
import { formatPrice } from '../../utils/price';
|
||||
|
||||
@@ -195,7 +195,7 @@ export default class ArticleUnifyDialog extends Component {
|
||||
|
||||
resolveBrandName(article) {
|
||||
if (!article) return '–';
|
||||
const brands = brandAutomat.state.brands || [];
|
||||
const brands = companyAutomat.state.brands || [];
|
||||
if (article.brandId != null) {
|
||||
const b = brands.find((x) => x.id === article.brandId);
|
||||
if (b?.name) return b.name;
|
||||
|
||||
@@ -15,7 +15,7 @@ import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import TextInput from '../TextInput';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import unitAutomat from '../../automats/unitAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { unit as unitApi } from '../../api';
|
||||
import { requireObject } from '../../utils/apiShape';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
@@ -85,7 +85,7 @@ export default class ArticleUnitEditorDialog extends Component {
|
||||
async loadUnits() {
|
||||
this.setState({ unitsLoading: true });
|
||||
try {
|
||||
const units = await unitAutomat.actions.load();
|
||||
const units = await companyAutomat.actions.loadUnits();
|
||||
this.setState({ units, unitsLoading: false });
|
||||
} catch (err) {
|
||||
this.setState({ unitsLoading: false });
|
||||
@@ -125,7 +125,7 @@ export default class ArticleUnitEditorDialog extends Component {
|
||||
});
|
||||
requireObject(data, 'PUT /api/unit');
|
||||
requireObject(data.unit, 'unit');
|
||||
const units = await unitAutomat.actions.load();
|
||||
const units = await companyAutomat.actions.loadUnits();
|
||||
this.setState({
|
||||
busy: false,
|
||||
units,
|
||||
@@ -145,7 +145,7 @@ export default class ArticleUnitEditorDialog extends Component {
|
||||
this.setState({ busy: true });
|
||||
try {
|
||||
await unitApi.delete(selectedSi);
|
||||
const units = await unitAutomat.actions.load();
|
||||
const units = await companyAutomat.actions.loadUnits();
|
||||
this.setState({ busy: false, units, form: emptyForm(), selectedSi: null });
|
||||
this.props.onUnitDeleted?.(selectedSi);
|
||||
} catch (err) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import TextInput from '../../TextInput';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
import { EditablePanelTile, PanelTile } from 'dashgui/panels';
|
||||
import brandAutomat from '../../../automats/brandAutomat';
|
||||
import companyAutomat from '../../../automats/companyAutomat';
|
||||
import artikelAutomat, { getArticle } from '../../../automats/artikelAutomat';
|
||||
import { requireObject } from '../../../utils/apiShape';
|
||||
import { notifyError } from '../../../utils/notify';
|
||||
@@ -119,16 +119,16 @@ export default class ArticleViewBrand extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
brands: brandAutomat.state.brands ?? [],
|
||||
brands: companyAutomat.state.brands ?? [],
|
||||
};
|
||||
this._cancelled = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._cancelled = false;
|
||||
this.unsub = brandAutomat.subscribe(this, (state) => ({
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
brands: state.brands ?? [],
|
||||
}));
|
||||
}), ['brands']);
|
||||
const parentSku = this.props.article?.parentSku || this.props.article?.parent_sku;
|
||||
if (parentSku && !getArticle(parentSku)) {
|
||||
void artikelAutomat.actions.loadArticle(parentSku).catch(() => {});
|
||||
@@ -168,8 +168,8 @@ export default class ArticleViewBrand extends PureComponent {
|
||||
if (existing) {
|
||||
brandId = existing.id;
|
||||
} else {
|
||||
await brandAutomat.actions.create({ name });
|
||||
const nextBrands = await brandAutomat.actions.load();
|
||||
await companyAutomat.actions.createBrand({ name });
|
||||
const nextBrands = await companyAutomat.actions.loadBrands();
|
||||
this.setState({ brands: nextBrands });
|
||||
brandId = nextBrands.find(
|
||||
(row) => String(row.name ?? '').toLowerCase() === name.toLowerCase(),
|
||||
|
||||
@@ -32,9 +32,8 @@ import { PanelTile } from 'dashgui/panels';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { artikelDetailPath } from '../articleRoute';
|
||||
import { article as articleApi } from '../../../api';
|
||||
import attributeAutomat, { selectAttributes } from '../../../automats/attributeAutomat';
|
||||
import artikelAutomat, { getArticle } from '../../../automats/artikelAutomat';
|
||||
import companyAutomat from '../../../automats/companyAutomat';
|
||||
import companyAutomat, { selectAttributes } from '../../../automats/companyAutomat';
|
||||
import { formatStock } from '../searchFormat';
|
||||
import { formatPrice, getArticleGross } from '../../../utils/price';
|
||||
import { notifyError } from '../../../utils/notify';
|
||||
@@ -159,10 +158,7 @@ export default class ArticleViewLists extends PureComponent {
|
||||
|
||||
componentDidMount() {
|
||||
this._cancelled = false;
|
||||
this.unsubCompany = companyAutomat.subscribe(this, (state) => ({
|
||||
company: state.current,
|
||||
}));
|
||||
this.unsubAttr = attributeAutomat.subscribe((state) => {
|
||||
this.unsubCompany = companyAutomat.subscribe((state) => {
|
||||
if (this._cancelled) return;
|
||||
this.setState((prev) => {
|
||||
const assignedIds = new Set(prev.assignedAttributes.map((a) => a.attributeId));
|
||||
@@ -172,19 +168,20 @@ export default class ArticleViewLists extends PureComponent {
|
||||
nextSelectedId = selectable[0].id;
|
||||
}
|
||||
return {
|
||||
company: state.current,
|
||||
attributes: state.attributes,
|
||||
selectedAttributeId: nextSelectedId,
|
||||
};
|
||||
});
|
||||
});
|
||||
}, ['current', 'attributes']);
|
||||
|
||||
this.unsubArtikel = artikelAutomat.subscribe(() => {
|
||||
if (this._cancelled) return;
|
||||
this.forceUpdate();
|
||||
});
|
||||
|
||||
if (attributeAutomat.state.status === 'idle') {
|
||||
void attributeAutomat.actions.load();
|
||||
if (companyAutomat.state.attributesStatus === 'idle') {
|
||||
void companyAutomat.actions.loadAttributes();
|
||||
}
|
||||
|
||||
if (this.props.article?.sku) {
|
||||
@@ -262,7 +259,6 @@ export default class ArticleViewLists extends PureComponent {
|
||||
componentWillUnmount() {
|
||||
this._cancelled = true;
|
||||
this.unsubCompany?.();
|
||||
this.unsubAttr?.();
|
||||
this.unsubArtikel?.();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import Typography from '@mui/material/Typography';
|
||||
import { EditablePanelTile, PanelTile } from 'dashgui/panels';
|
||||
import ArticlePriceTiersDialog from '../ArticlePriceTiersDialog';
|
||||
import ArticleUnitEditorDialog from '../ArticleUnitEditorDialog';
|
||||
import unitAutomat from '../../../automats/unitAutomat';
|
||||
import companyAutomat from '../../../automats/companyAutomat';
|
||||
import artikelAutomat, { getArticle } from '../../../automats/artikelAutomat';
|
||||
import { requireDefined, requireObject } from '../../../utils/apiShape';
|
||||
@@ -333,7 +332,7 @@ export default class ArticleViewPrice extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const company = companyFromStore();
|
||||
const units = unitAutomat.state.units;
|
||||
const units = companyAutomat.state.units;
|
||||
this.state = {
|
||||
tiersDialogOpen: false,
|
||||
unitEditorOpen: false,
|
||||
@@ -345,12 +344,10 @@ export default class ArticleViewPrice extends PureComponent {
|
||||
|
||||
componentDidMount() {
|
||||
this._cancelled = false;
|
||||
this.unsubUnits = unitAutomat.subscribe(this, (state) => ({
|
||||
units: state.units ?? [],
|
||||
}));
|
||||
this.unsubCompany = companyAutomat.subscribe(this, (state) => ({
|
||||
units: state.units ?? [],
|
||||
company: state.current ?? null,
|
||||
}));
|
||||
}), ['units', 'current']);
|
||||
this.unsubArtikel = artikelAutomat.subscribe(this, () => ({}));
|
||||
if (this.props.article?.isParent) {
|
||||
this.ensureChildren(this.props.article.children);
|
||||
@@ -369,8 +366,6 @@ export default class ArticleViewPrice extends PureComponent {
|
||||
|
||||
componentWillUnmount() {
|
||||
this._cancelled = true;
|
||||
this.unsubUnits?.();
|
||||
this.unsubUnits = null;
|
||||
this.unsubCompany?.();
|
||||
this.unsubCompany = null;
|
||||
this.unsubArtikel?.();
|
||||
@@ -462,7 +457,7 @@ export default class ArticleViewPrice extends PureComponent {
|
||||
}
|
||||
|
||||
refreshUnits = async () => {
|
||||
const units = await unitAutomat.actions.load();
|
||||
const units = await companyAutomat.actions.loadUnits();
|
||||
this.setState({ units });
|
||||
};
|
||||
|
||||
|
||||
@@ -10,8 +10,7 @@ import Tooltip from '@mui/material/Tooltip';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import { notifyError } from '../../../utils/notify';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import attributeAutomat, { selectAttributes } from '../../../automats/attributeAutomat';
|
||||
import propertyAutomat, { selectProperties } from '../../../automats/propertyAutomat';
|
||||
import companyAutomat, { selectAttributes, selectProperties } from '../../../automats/companyAutomat';
|
||||
import artikelAutomat, { getArticle } from '../../../automats/artikelAutomat';
|
||||
import { article as articleApi } from '../../../api';
|
||||
|
||||
@@ -81,26 +80,27 @@ export default class ArticleAttributesTab extends PureComponent {
|
||||
|
||||
componentDidMount() {
|
||||
this._cancelled = false;
|
||||
this.unsubAttr = attributeAutomat.subscribe((state) => {
|
||||
this.unsubCompany = companyAutomat.subscribe((state) => {
|
||||
if (this._cancelled) return;
|
||||
this.setState((prev) => {
|
||||
let nextKey = prev.selectedMerkmalKey;
|
||||
let nextItemId = prev.selectedItemId;
|
||||
|
||||
const selectableAttrs = state.attributes;
|
||||
const globalProps = state.properties;
|
||||
|
||||
const isCurrentKeyValid =
|
||||
nextKey &&
|
||||
(selectableAttrs.some((a) => `attr-${a.id}` === nextKey) ||
|
||||
prev.globalProperties.some((p) => `prop-${p.id}` === nextKey));
|
||||
globalProps.some((p) => `prop-${p.id}` === nextKey));
|
||||
|
||||
if (!isCurrentKeyValid) {
|
||||
const first = selectableAttrs[0];
|
||||
if (first) {
|
||||
nextKey = `attr-${first.id}`;
|
||||
nextItemId = first.values?.[0]?.id ?? '';
|
||||
} else if (prev.globalProperties[0]) {
|
||||
nextKey = `prop-${prev.globalProperties[0].id}`;
|
||||
} else if (globalProps[0]) {
|
||||
nextKey = `prop-${globalProps[0].id}`;
|
||||
nextItemId = '';
|
||||
} else {
|
||||
nextKey = '';
|
||||
@@ -108,38 +108,24 @@ export default class ArticleAttributesTab extends PureComponent {
|
||||
}
|
||||
}
|
||||
return {
|
||||
globalAttributes: state.attributes,
|
||||
globalAttributes: selectableAttrs,
|
||||
globalProperties: globalProps,
|
||||
selectedMerkmalKey: nextKey,
|
||||
selectedItemId: nextItemId,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
this.unsubProp = propertyAutomat.subscribe((state) => {
|
||||
if (this._cancelled) return;
|
||||
this.setState((prev) => {
|
||||
const selectableAttrs = prev.globalAttributes;
|
||||
let nextKey = prev.selectedMerkmalKey;
|
||||
if (!nextKey && !selectableAttrs.length && state.properties[0]) {
|
||||
nextKey = `prop-${state.properties[0].id}`;
|
||||
}
|
||||
return {
|
||||
globalProperties: state.properties,
|
||||
selectedMerkmalKey: nextKey,
|
||||
};
|
||||
});
|
||||
});
|
||||
}, ['attributes', 'properties']);
|
||||
|
||||
this.unsubArtikel = artikelAutomat.subscribe(() => {
|
||||
if (this._cancelled) return;
|
||||
this.forceUpdate();
|
||||
});
|
||||
|
||||
if (attributeAutomat.state.status === 'idle') {
|
||||
void attributeAutomat.actions.load();
|
||||
if (companyAutomat.state.attributesStatus === 'idle') {
|
||||
void companyAutomat.actions.loadAttributes();
|
||||
}
|
||||
if (propertyAutomat.state.status === 'idle') {
|
||||
void propertyAutomat.actions.load();
|
||||
if (companyAutomat.state.propertiesStatus === 'idle') {
|
||||
void companyAutomat.actions.loadProperties();
|
||||
}
|
||||
|
||||
void this.loadArticleData(this.props.sku);
|
||||
@@ -153,8 +139,7 @@ export default class ArticleAttributesTab extends PureComponent {
|
||||
|
||||
componentWillUnmount() {
|
||||
this._cancelled = true;
|
||||
this.unsubAttr?.();
|
||||
this.unsubProp?.();
|
||||
this.unsubCompany?.();
|
||||
this.unsubArtikel?.();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import sseAutomat from '../../../automats/sseAutomat';
|
||||
import shipmentAutomat from '../../../automats/shipmentAutomat';
|
||||
import orderAutomat from '../../../automats/orderAutomat';
|
||||
import entityAutomat from '../../../automats/entityAutomat';
|
||||
import brandAutomat from '../../../automats/brandAutomat';
|
||||
import companyAutomat from '../../../automats/companyAutomat';
|
||||
import { shipment as shipmentApi, order as orderApi } from '../../../api';
|
||||
import { ShipmentDetailDialog } from '../../manifest';
|
||||
import SkeleHashImage from '../../settings/SkeleHashImage';
|
||||
@@ -709,8 +709,8 @@ class ArticleHistoryTab extends PureComponent {
|
||||
if (entityAutomat.state.status === 'idle') {
|
||||
void entityAutomat.actions.load().catch(() => {});
|
||||
}
|
||||
if (brandAutomat.state.status === 'idle') {
|
||||
void brandAutomat.actions.load().catch(() => {});
|
||||
if (companyAutomat.state.brandsStatus === 'idle') {
|
||||
void companyAutomat.actions.loadBrands().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -868,7 +868,7 @@ class ArticleHistoryTab extends PureComponent {
|
||||
|
||||
handleOpenBrand(brandName) {
|
||||
if (!brandName) return;
|
||||
const match = (brandAutomat.state.brands || []).find(
|
||||
const match = (companyAutomat.state.brands || []).find(
|
||||
(b) => b && (b.name?.toLowerCase() === brandName.toLowerCase() || String(b.id) === String(brandName)),
|
||||
);
|
||||
const brandParam = match?.id != null ? String(match.id) : brandName;
|
||||
|
||||
@@ -9,7 +9,7 @@ import GoogleIcon from '@mui/icons-material/Google';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
|
||||
import { article as articleApi } from '../../../api';
|
||||
import { artikelAutomat, getArticle } from '../../../automats';
|
||||
import artikelAutomat, { getArticle } from '../../../automats/artikelAutomat';
|
||||
import { notifyError } from '../../../utils/notify';
|
||||
import {
|
||||
articleEans,
|
||||
|
||||
@@ -23,7 +23,6 @@ import { enqueueSnackbar } from 'notistack';
|
||||
import { article as articleApi } from '../../../api';
|
||||
import { notifyError } from '../../../utils/notify';
|
||||
import { formatPrice } from '../../../utils/price';
|
||||
import supplierAutomat from '../../../automats/supplierAutomat';
|
||||
import { articleSupplierAutomat } from '../../../automats/articleDetailAutomat';
|
||||
import sseAutomat from '../../../automats/sseAutomat';
|
||||
import companyAutomat from '../../../automats/companyAutomat';
|
||||
@@ -93,7 +92,7 @@ export default class ArticleSupplierTab extends PureComponent {
|
||||
|
||||
// 2. Fetch available company suppliers
|
||||
try {
|
||||
const companySuppliers = await supplierAutomat.actions.load();
|
||||
const companySuppliers = await companyAutomat.actions.loadSuppliers();
|
||||
this.setState({ companySuppliers: companySuppliers ?? [] });
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
|
||||
@@ -9,7 +9,7 @@ import ImageSearchIcon from '@mui/icons-material/ImageSearch';
|
||||
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import brandAutomat from '../../automats/brandAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import BrandThumb from './BrandThumb';
|
||||
import BrandImageSearchDialog from './BrandImageSearchDialog';
|
||||
@@ -56,7 +56,7 @@ class ArtikelMarke extends PureComponent {
|
||||
|
||||
async renameBrand(brand, name) {
|
||||
try {
|
||||
await brandAutomat.actions.rename(brand.id, { name });
|
||||
await companyAutomat.actions.renameBrand(brand.id, { name });
|
||||
enqueueSnackbar('Marke gespeichert.', { variant: 'success' });
|
||||
} catch (err) {
|
||||
this.setState({ draft: brand?.name ?? '' });
|
||||
@@ -99,7 +99,7 @@ class ArtikelMarke extends PureComponent {
|
||||
if (!window.confirm(`Marke „${brand.name}" wirklich löschen?`)) return;
|
||||
void (async () => {
|
||||
try {
|
||||
await brandAutomat.actions.remove(brand.id);
|
||||
await companyAutomat.actions.removeBrand(brand.id);
|
||||
enqueueSnackbar('Marke gelöscht.', { variant: 'success' });
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
|
||||
@@ -3,7 +3,7 @@ import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import brandAutomat from '../../automats/brandAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
|
||||
// Own component so typing a new brand name never rerenders ArtikelMarkenPage
|
||||
@@ -19,7 +19,7 @@ class ArtikelMarkeNew extends PureComponent {
|
||||
const name = this.state.newName.trim();
|
||||
if (!name) return;
|
||||
try {
|
||||
const row = await brandAutomat.actions.create({ name });
|
||||
const row = await companyAutomat.actions.createBrand({ name });
|
||||
enqueueSnackbar(`Marke „${row.name || name}" angelegt.`, { variant: 'success' });
|
||||
this.setState({ newName: '' });
|
||||
} catch (err) {
|
||||
|
||||
@@ -25,7 +25,7 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { serper } from '../../api';
|
||||
import brandAutomat from '../../automats/brandAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { downloadAndPrepareLogoAvif } from '../../utils/cropLogo';
|
||||
import {
|
||||
CHECKERBOARD,
|
||||
@@ -211,8 +211,8 @@ export default class BrandImageSearchDialog extends PureComponent {
|
||||
|
||||
this.setState({ processingStep: 'Logo wird gespeichert...' });
|
||||
|
||||
// 2. Upload as brandpic and update brandAutomat
|
||||
await brandAutomat.actions.addImage(brand.id, avifFile);
|
||||
// 2. Upload as brandpic and update companyAutomat
|
||||
await companyAutomat.actions.addBrandImage(brand.id, avifFile);
|
||||
|
||||
enqueueSnackbar(`Markenlogo für „${brand.name}“ erfolgreich gespeichert.`, {
|
||||
variant: 'success',
|
||||
|
||||
@@ -9,7 +9,7 @@ import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
||||
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import carrierAutomat from '../../automats/carrierAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
|
||||
class CarrierItem extends PureComponent {
|
||||
@@ -54,7 +54,7 @@ class CarrierItem extends PureComponent {
|
||||
|
||||
async updateCarrier(item, patch) {
|
||||
try {
|
||||
await carrierAutomat.actions.update(item.id, patch);
|
||||
await companyAutomat.actions.updateCarrier(item.id, patch);
|
||||
enqueueSnackbar('Versandart gespeichert.', { variant: 'success' });
|
||||
} catch (err) {
|
||||
this.setState({ draft: item.name ?? '' });
|
||||
@@ -95,7 +95,7 @@ class CarrierItem extends PureComponent {
|
||||
if (!window.confirm(`Versandart „${item.name}“ wirklich löschen?`)) return;
|
||||
void (async () => {
|
||||
try {
|
||||
await carrierAutomat.actions.remove(item.id);
|
||||
await companyAutomat.actions.removeCarrier(item.id);
|
||||
enqueueSnackbar('Versandart gelöscht.', { variant: 'success' });
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
|
||||
@@ -40,7 +40,7 @@ import { setting as settingApi } from '../../api';
|
||||
import chatAutomat, {
|
||||
selectPendingAskUser,
|
||||
} from '../../automats/chatAutomat';
|
||||
import userSettingsAutomat from '../../automats/userSettingsAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import vfsAutomat from '../../automats/vfsAutomat';
|
||||
import { VfsEditorPanel, VfsFileIcon } from '../vfs';
|
||||
import ChatFlowList from './ChatFlowList';
|
||||
@@ -130,7 +130,7 @@ export default class ChatDrawer extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const s = chatAutomat.state;
|
||||
const settings = userSettingsAutomat.state;
|
||||
const settings = companyAutomat.state;
|
||||
const vfs = vfsAutomat.state;
|
||||
this.state = {
|
||||
isOpen: s.isOpen,
|
||||
@@ -293,12 +293,12 @@ export default class ChatDrawer extends PureComponent {
|
||||
return patch;
|
||||
});
|
||||
|
||||
this.unsubSettings = userSettingsAutomat.subscribe(this, (state) => {
|
||||
this.unsubSettings = companyAutomat.subscribe(this, (state) => {
|
||||
if (!this.state.isResizing) {
|
||||
return { drawerWidth: state.chatDrawerWidth || DEFAULT_DRAWER_WIDTH };
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}, ['chatDrawerWidth']);
|
||||
|
||||
this.unsubVfs = vfsAutomat.subscribe(this, (state) => ({
|
||||
isVfsOpen: state.isOpen,
|
||||
@@ -465,7 +465,7 @@ export default class ChatDrawer extends PureComponent {
|
||||
|
||||
this.setState({ isResizing: false });
|
||||
vfsAutomat.actions.setVfsWidth(currentVfsWidth);
|
||||
userSettingsAutomat.actions.setChatDrawerWidth(currentChatWidth);
|
||||
companyAutomat.actions.setChatDrawerWidth(currentChatWidth);
|
||||
};
|
||||
|
||||
window.addEventListener('pointermove', onMove, { passive: false });
|
||||
@@ -531,7 +531,7 @@ export default class ChatDrawer extends PureComponent {
|
||||
if (isVfs) {
|
||||
vfsAutomat.actions.setVfsWidth(currentVfsWidth);
|
||||
} else {
|
||||
userSettingsAutomat.actions.setChatDrawerWidth(currentChatWidth);
|
||||
companyAutomat.actions.setChatDrawerWidth(currentChatWidth);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -546,7 +546,7 @@ export default class ChatDrawer extends PureComponent {
|
||||
|
||||
handleResizeDoubleClick = () => {
|
||||
this.setState({ drawerWidth: DEFAULT_DRAWER_WIDTH });
|
||||
userSettingsAutomat.actions.setChatDrawerWidth(DEFAULT_DRAWER_WIDTH);
|
||||
companyAutomat.actions.setChatDrawerWidth(DEFAULT_DRAWER_WIDTH);
|
||||
};
|
||||
|
||||
cleanupResizeListeners = () => {
|
||||
|
||||
@@ -30,7 +30,8 @@ import EditNoteIcon from '@mui/icons-material/EditNote';
|
||||
import PaidRoundedIcon from '@mui/icons-material/PaidRounded';
|
||||
import Button from '@mui/material/Button';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { deliveryEditorAutomat, setLastDeliveryPath } from '../../automats';
|
||||
import deliveryEditorAutomat from '../../automats/deliveryEditorAutomat';
|
||||
import { setLastDeliveryPath } from '../../automats/navigationAutomat';
|
||||
import HealingMarkdown from './HealingMarkdown';
|
||||
import {
|
||||
VfsAttachmentLink,
|
||||
|
||||
@@ -6,7 +6,7 @@ import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
||||
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import customergroupAutomat from '../../automats/customergroupAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
|
||||
class KundeGruppe extends PureComponent {
|
||||
@@ -54,7 +54,7 @@ class KundeGruppe extends PureComponent {
|
||||
|
||||
async renameGroup(item, name) {
|
||||
try {
|
||||
await customergroupAutomat.actions.update(item.id, { name });
|
||||
await companyAutomat.actions.updateCustomergroup(item.id, { name });
|
||||
enqueueSnackbar('Kundengruppe gespeichert.', { variant: 'success' });
|
||||
} catch (err) {
|
||||
this.setState({ draft: item.name ?? '' });
|
||||
@@ -87,7 +87,7 @@ class KundeGruppe extends PureComponent {
|
||||
if (!window.confirm(`Kundengruppe „${item.name}“ wirklich löschen?`)) return;
|
||||
void (async () => {
|
||||
try {
|
||||
await customergroupAutomat.actions.remove(item.id);
|
||||
await companyAutomat.actions.removeCustomergroup(item.id);
|
||||
enqueueSnackbar('Kundengruppe gelöscht.', { variant: 'success' });
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
} from '../../utils/deliveryLedger';
|
||||
import { formatStock } from '../article/searchFormat';
|
||||
import artikelAutomat, { getArticle } from '../../automats/artikelAutomat';
|
||||
import warehouseAutomat from '../../automats/warehouseAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { supplier as supplierApi } from '../../api';
|
||||
import deliveryEditorAutomat, { buildSupplierArticlesMap } from '../../automats/deliveryEditorAutomat';
|
||||
import {
|
||||
@@ -65,7 +65,7 @@ export default class BookDeliveryDialog extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
const initialWarehouses = warehouseAutomat.state.warehouses || [];
|
||||
const initialWarehouses = companyAutomat.state.warehouses || [];
|
||||
const initialSupplierMap =
|
||||
props.supplierArticlesMap ||
|
||||
(deliveryEditorAutomat?.state?.supplierArticlesMap ?? {});
|
||||
@@ -107,7 +107,7 @@ export default class BookDeliveryDialog extends PureComponent {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsubWarehouse = warehouseAutomat.subscribe((state) => {
|
||||
this.unsubWarehouse = companyAutomat.subscribe((state) => {
|
||||
const warehouses = state.warehouses || [];
|
||||
this.setState((prev) => {
|
||||
const selectedId =
|
||||
@@ -118,11 +118,11 @@ export default class BookDeliveryDialog extends PureComponent {
|
||||
selectedWarehouseId: selectedId,
|
||||
};
|
||||
});
|
||||
});
|
||||
}, ['warehouses']);
|
||||
this.unsubArticle = artikelAutomat.subscribe(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
void warehouseAutomat.actions.load().catch(() => { });
|
||||
void companyAutomat.actions.loadWarehouses().catch(() => { });
|
||||
this.loadSupplierArticlesIfNeeded();
|
||||
|
||||
if (this.props.open) {
|
||||
@@ -205,7 +205,7 @@ export default class BookDeliveryDialog extends PureComponent {
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.open && !prevProps.open) {
|
||||
const currentWarehouses = this.state.warehouses || warehouseAutomat.state.warehouses || [];
|
||||
const currentWarehouses = this.state.warehouses || companyAutomat.state.warehouses || [];
|
||||
const currentSupplierMap =
|
||||
this.props.supplierArticlesMap ||
|
||||
(deliveryEditorAutomat?.state?.supplierArticlesMap ?? this.state.supplierArticlesMap ?? {});
|
||||
@@ -225,7 +225,7 @@ export default class BookDeliveryDialog extends PureComponent {
|
||||
supplierArticlesMap: currentSupplierMap,
|
||||
},
|
||||
() => {
|
||||
void warehouseAutomat.actions.load().catch(() => { });
|
||||
void companyAutomat.actions.loadWarehouses().catch(() => { });
|
||||
this.loadSupplierArticlesIfNeeded();
|
||||
if (this.props.summary) {
|
||||
this.initAmountsFromSummary(this.props.summary, this.props.prefillLineId);
|
||||
|
||||
@@ -23,7 +23,7 @@ import FillSpace from 'dashgui/FillSpace';
|
||||
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||
import BackendError from '../BackendError';
|
||||
import deliveryAutomat, { CHUNK_SIZE } from '../../automats/deliveryAutomat';
|
||||
import numberAutomat from '../../automats/numberAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { delivery as deliveryApi } from '../../api';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
@@ -693,11 +693,15 @@ export class DeliveryList extends PureComponent {
|
||||
supplierId: deliveryAutomat.state.supplierId ?? null,
|
||||
});
|
||||
}
|
||||
this.unsubNumber = numberAutomat.subscribe(this, () => {
|
||||
this.headerItem = createDummyTestDelivery();
|
||||
this.forceUpdate();
|
||||
});
|
||||
void numberAutomat.actions.ensureLoaded().catch(() => { });
|
||||
this.unsubNumber = companyAutomat.subscribe(
|
||||
this,
|
||||
() => {
|
||||
this.headerItem = createDummyTestDelivery();
|
||||
this.forceUpdate();
|
||||
},
|
||||
['numbers'],
|
||||
);
|
||||
void companyAutomat.actions.ensureNumbersLoaded().catch(() => { });
|
||||
void this.loadSuppliers();
|
||||
}
|
||||
|
||||
|
||||
@@ -17,12 +17,9 @@ import OrderDocumentsTiles from './OrderDocumentsTiles';
|
||||
import OrderShipmentDialog from './OrderShipmentDialog';
|
||||
import artikelAutomat, { getArticle } from '../../automats/artikelAutomat';
|
||||
import entityAutomat from '../../automats/entityAutomat';
|
||||
import customergroupAutomat from '../../automats/customergroupAutomat';
|
||||
import supplierAutomat from '../../automats/supplierAutomat';
|
||||
import orderAutomat from '../../automats/orderAutomat';
|
||||
import retoureAutomat from '../../automats/retoureAutomat';
|
||||
import companyAutomat, { selectCompanyTaxrate } from '../../automats/companyAutomat';
|
||||
import warehouseAutomat from '../../automats/warehouseAutomat';
|
||||
import sseAutomat from '../../automats/sseAutomat';
|
||||
import OrderLedgerPanel from './OrderLedgerPanel';
|
||||
import { fetchOrderLedgerEntries } from '../../utils/orderLedger';
|
||||
@@ -67,15 +64,15 @@ class ManifestForm extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.cfg = manifestConfig(props.kind);
|
||||
const groups = customergroupAutomat.state.items ?? [];
|
||||
const groups = companyAutomat.state.customergroups ?? [];
|
||||
const initialParties = props.kind === 'delivery'
|
||||
? (supplierAutomat.state.status === 'ready' ? (supplierAutomat.state.suppliers ?? []) : [])
|
||||
? (companyAutomat.state.suppliersStatus === 'ready' ? (companyAutomat.state.suppliers ?? []) : [])
|
||||
: (entityAutomat.state.status === 'ready'
|
||||
? filterNonSupplierEntities(entityAutomat.state.items ?? [], groups)
|
||||
: []);
|
||||
const partiesReady = props.kind === 'delivery'
|
||||
? supplierAutomat.state.status === 'ready'
|
||||
: (entityAutomat.state.status === 'ready' && customergroupAutomat.state.status === 'ready');
|
||||
? companyAutomat.state.suppliersStatus === 'ready'
|
||||
: (entityAutomat.state.status === 'ready' && companyAutomat.state.customergroupsStatus === 'ready');
|
||||
|
||||
this.draftAutomat = props.mode === 'create' ? getManifestDraftAutomat(props.kind) : null;
|
||||
this.editAutomat = props.mode === 'edit' ? getManifestEditAutomat(props.kind) : null;
|
||||
@@ -184,64 +181,69 @@ class ManifestForm extends PureComponent {
|
||||
|
||||
componentDidMount() {
|
||||
this._cancelled = false;
|
||||
if (this.props.kind === 'delivery') {
|
||||
this.unsubParties = supplierAutomat.subscribe(this, (state) => ({
|
||||
parties: state.status === 'ready' ? (state.suppliers ?? []) : [],
|
||||
partiesLoading: state.status === 'loading',
|
||||
}));
|
||||
} else {
|
||||
const updateParties = () => {
|
||||
const syncPartiesFromState = () => {
|
||||
if (this.props.kind === 'delivery') {
|
||||
this.setState({
|
||||
parties: companyAutomat.state.suppliersStatus === 'ready' ? (companyAutomat.state.suppliers ?? []) : [],
|
||||
partiesLoading: companyAutomat.state.suppliersStatus === 'loading',
|
||||
});
|
||||
} else {
|
||||
const entities = entityAutomat.state.items ?? [];
|
||||
const groups = customergroupAutomat.state.items ?? [];
|
||||
const groups = companyAutomat.state.customergroups ?? [];
|
||||
this.setState({
|
||||
parties: filterNonSupplierEntities(entities, groups),
|
||||
partiesLoading:
|
||||
entityAutomat.state.status === 'loading' ||
|
||||
customergroupAutomat.state.status === 'loading',
|
||||
companyAutomat.state.customergroupsStatus === 'loading',
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
if (this.props.kind !== 'delivery') {
|
||||
this.unsubParties = entityAutomat.subscribe(this, () => {
|
||||
updateParties();
|
||||
syncPartiesFromState();
|
||||
return null;
|
||||
});
|
||||
this.unsubGroups = customergroupAutomat.subscribe(this, () => {
|
||||
updateParties();
|
||||
return null;
|
||||
});
|
||||
void customergroupAutomat.actions.ensureLoaded();
|
||||
void companyAutomat.actions.ensureCustomergroupsLoaded();
|
||||
}
|
||||
|
||||
if (companyAutomat.state.status === 'idle') {
|
||||
void companyAutomat.actions.load();
|
||||
}
|
||||
this.unsubCompany = companyAutomat.subscribe(this, (state) => {
|
||||
const taxrate = state.current?.taxrate;
|
||||
if (taxrate != null) {
|
||||
this.setState((prev) => {
|
||||
if (
|
||||
prev.lines.length === 1 &&
|
||||
!prev.lines[0].sku &&
|
||||
!prev.lines[0].name &&
|
||||
!prev.lines[0].net &&
|
||||
prev.lines[0].tax !== String(taxrate)
|
||||
) {
|
||||
return {
|
||||
lines: [
|
||||
{
|
||||
...prev.lines[0],
|
||||
tax: String(taxrate),
|
||||
taxrate: String(taxrate),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
});
|
||||
this.unsubCompany = companyAutomat.subscribe(
|
||||
this,
|
||||
(state) => {
|
||||
syncPartiesFromState();
|
||||
const taxrate = state.current?.taxrate;
|
||||
if (taxrate != null) {
|
||||
this.setState((prev) => {
|
||||
if (
|
||||
prev.lines.length === 1 &&
|
||||
!prev.lines[0].sku &&
|
||||
!prev.lines[0].name &&
|
||||
!prev.lines[0].net &&
|
||||
prev.lines[0].tax !== String(taxrate)
|
||||
) {
|
||||
return {
|
||||
lines: [
|
||||
{
|
||||
...prev.lines[0],
|
||||
tax: String(taxrate),
|
||||
taxrate: String(taxrate),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
return null;
|
||||
},
|
||||
['current', 'suppliers', 'suppliersStatus', 'customergroups', 'customergroupsStatus'],
|
||||
);
|
||||
|
||||
if (warehouseAutomat.state.status === 'idle') {
|
||||
void warehouseAutomat.actions.load();
|
||||
if (companyAutomat.state.warehousesStatus === 'idle') {
|
||||
void companyAutomat.actions.loadWarehouses();
|
||||
}
|
||||
|
||||
if (this.props.mode === 'edit') {
|
||||
@@ -304,32 +306,29 @@ class ManifestForm extends PureComponent {
|
||||
if (prevProps.kind !== this.props.kind) {
|
||||
this.cfg = manifestConfig(this.props.kind);
|
||||
this.unsubParties?.();
|
||||
this.unsubGroups?.();
|
||||
this.unsubParties = null;
|
||||
if (this.props.kind === 'delivery') {
|
||||
this.unsubParties = supplierAutomat.subscribe(this, (state) => ({
|
||||
parties: state.status === 'ready' ? (state.suppliers ?? []) : [],
|
||||
partiesLoading: state.status === 'loading',
|
||||
}));
|
||||
this.setState({
|
||||
parties: companyAutomat.state.suppliersStatus === 'ready' ? (companyAutomat.state.suppliers ?? []) : [],
|
||||
partiesLoading: companyAutomat.state.suppliersStatus === 'loading',
|
||||
});
|
||||
} else {
|
||||
const updateParties = () => {
|
||||
const entities = entityAutomat.state.items ?? [];
|
||||
const groups = customergroupAutomat.state.items ?? [];
|
||||
const groups = companyAutomat.state.customergroups ?? [];
|
||||
this.setState({
|
||||
parties: filterNonSupplierEntities(entities, groups),
|
||||
partiesLoading:
|
||||
entityAutomat.state.status === 'loading' ||
|
||||
customergroupAutomat.state.status === 'loading',
|
||||
companyAutomat.state.customergroupsStatus === 'loading',
|
||||
});
|
||||
};
|
||||
this.unsubParties = entityAutomat.subscribe(this, () => {
|
||||
updateParties();
|
||||
return null;
|
||||
});
|
||||
this.unsubGroups = customergroupAutomat.subscribe(this, () => {
|
||||
updateParties();
|
||||
return null;
|
||||
});
|
||||
void customergroupAutomat.actions.ensureLoaded();
|
||||
updateParties();
|
||||
void companyAutomat.actions.ensureCustomergroupsLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,9 +25,7 @@ import {
|
||||
} from './manifestConfig';
|
||||
import { notifyError, notifyErrorMessage } from '../../utils/notify';
|
||||
import entityAutomat from '../../automats/entityAutomat';
|
||||
import customergroupAutomat from '../../automats/customergroupAutomat';
|
||||
import supplierAutomat from '../../automats/supplierAutomat';
|
||||
import numberAutomat from '../../automats/numberAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import quoteAutomat from '../../automats/quoteAutomat';
|
||||
import orderAutomat from '../../automats/orderAutomat';
|
||||
import deliveryAutomat from '../../automats/deliveryAutomat';
|
||||
@@ -42,7 +40,7 @@ class ManifestHeader extends PureComponent {
|
||||
const selected =
|
||||
(props.parties || []).find((row) => row.id === props.partyId) ??
|
||||
(props.kind === 'delivery'
|
||||
? (supplierAutomat.state.suppliers || []).find((row) => row.id === props.partyId)
|
||||
? (companyAutomat.state.suppliers || []).find((row) => row.id === props.partyId)
|
||||
: (entityAutomat.state.items || []).find((row) => row.id === props.partyId)) ??
|
||||
null;
|
||||
this.state = {
|
||||
@@ -68,7 +66,7 @@ class ManifestHeader extends PureComponent {
|
||||
(this.props.parties || []).find((row) => row.id === this.props.partyId) ??
|
||||
(this.state.createdParty?.id === this.props.partyId ? this.state.createdParty : null) ??
|
||||
(this.props.kind === 'delivery'
|
||||
? (supplierAutomat.state.suppliers || []).find((row) => row.id === this.props.partyId)
|
||||
? (companyAutomat.state.suppliers || []).find((row) => row.id === this.props.partyId)
|
||||
: (entityAutomat.state.items || []).find((row) => row.id === this.props.partyId));
|
||||
if (selected) {
|
||||
this.setState({ partyInput: partyDisplayName(this.props.kind, selected) });
|
||||
@@ -147,14 +145,14 @@ class ManifestHeader extends PureComponent {
|
||||
try {
|
||||
const { kind, onPartyChange } = this.props;
|
||||
if (kind === 'delivery') {
|
||||
const row = await supplierAutomat.actions.create({ name: raw });
|
||||
const row = await companyAutomat.actions.createSupplier({ name: raw });
|
||||
const displayName = row?.name || raw;
|
||||
enqueueSnackbar(`Lieferant „${displayName}“ angelegt.`, { variant: 'success' });
|
||||
this.setState({ creatingParty: false, createdParty: row, partyInput: displayName });
|
||||
onPartyChange?.(row.id);
|
||||
} else {
|
||||
await customergroupAutomat.actions.ensureLoaded();
|
||||
const groups = customergroupAutomat.state.items ?? [];
|
||||
await companyAutomat.actions.ensureCustomergroupsLoaded();
|
||||
const groups = companyAutomat.state.customergroups ?? [];
|
||||
const targetGroup = getLowestSortNonSupplierCustomerGroup(groups) ?? groups[0];
|
||||
if (!targetGroup) {
|
||||
throw new Error('Keine Kundengruppe vorhanden.');
|
||||
@@ -257,11 +255,15 @@ class ManifestHeader extends PureComponent {
|
||||
|
||||
componentDidMount() {
|
||||
this._cancelled = false;
|
||||
this.unsubNumber = numberAutomat.subscribe(this, () => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
this.unsubNumber = companyAutomat.subscribe(
|
||||
this,
|
||||
() => {
|
||||
this.forceUpdate();
|
||||
},
|
||||
['numbers'],
|
||||
);
|
||||
this.subscribeListAutomat(this.props.kind);
|
||||
void numberAutomat.actions.ensureLoaded().catch(() => {});
|
||||
void companyAutomat.actions.ensureNumbersLoaded().catch(() => {});
|
||||
if (!this.props.number && this.props.docId == null) {
|
||||
void ensureDocumentNumber(this.props.kind)
|
||||
.then(() => {
|
||||
|
||||
@@ -26,7 +26,7 @@ import orderAutomat, {
|
||||
isOrderPaid,
|
||||
isOrderDelivered,
|
||||
} from '../../automats/orderAutomat';
|
||||
import numberAutomat from '../../automats/numberAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { order as orderApi } from '../../api';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
@@ -595,11 +595,15 @@ export class OrderList extends PureComponent {
|
||||
filterKey: orderAutomat.getFilterKey(state),
|
||||
entityId: state.entityId ?? null,
|
||||
}));
|
||||
this.unsubNumber = numberAutomat.subscribe(this, () => {
|
||||
this.headerItem = createDummyTestOrder();
|
||||
this.forceUpdate();
|
||||
});
|
||||
void numberAutomat.actions.ensureLoaded().catch(() => {});
|
||||
this.unsubNumber = companyAutomat.subscribe(
|
||||
this,
|
||||
() => {
|
||||
this.headerItem = createDummyTestOrder();
|
||||
this.forceUpdate();
|
||||
},
|
||||
['numbers'],
|
||||
);
|
||||
void companyAutomat.actions.ensureNumbersLoaded().catch(() => {});
|
||||
void this.loadParties();
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import FillSpace from 'dashgui/FillSpace';
|
||||
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||
import BackendError from '../BackendError';
|
||||
import orderAutomat from '../../automats/orderAutomat';
|
||||
import accountAutomat from '../../automats/accountAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { payment as paymentApi } from '../../api';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
@@ -184,14 +184,18 @@ export class OrderPaymentList extends PureComponent {
|
||||
status: state.status,
|
||||
error: state.error ? backendErrorFrom(state.error) : null,
|
||||
}));
|
||||
this.unsubAccount = accountAutomat.subscribe(this, (state) => ({
|
||||
accountsById: Object.fromEntries((state.items || []).map((a) => [a.id, a.name])),
|
||||
}));
|
||||
this.unsubAccount = companyAutomat.subscribe(
|
||||
this,
|
||||
(state) => ({
|
||||
accountsById: Object.fromEntries((state.accounts || []).map((a) => [a.id, a.name])),
|
||||
}),
|
||||
['accounts'],
|
||||
);
|
||||
|
||||
if (orderAutomat.state.status === 'idle') {
|
||||
void orderAutomat.actions.load().catch(() => {});
|
||||
}
|
||||
void accountAutomat.actions.ensureLoaded().catch(() => {});
|
||||
void companyAutomat.actions.ensureAccountsLoaded().catch(() => {});
|
||||
void this.loadPartiesData();
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||
import BackendError from '../BackendError';
|
||||
import SkeleHashImage from '../settings/SkeleHashImage';
|
||||
import quoteAutomat, { CHUNK_SIZE } from '../../automats/quoteAutomat';
|
||||
import numberAutomat from '../../automats/numberAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { quote as quoteApi } from '../../api';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
@@ -593,11 +593,15 @@ export class QuoteList extends PureComponent {
|
||||
filterKey: quoteAutomat.getFilterKey(state),
|
||||
entityId: state.entityId ?? null,
|
||||
}));
|
||||
this.unsubNumber = numberAutomat.subscribe(this, () => {
|
||||
this.headerItem = createDummyTestQuote();
|
||||
this.forceUpdate();
|
||||
});
|
||||
void numberAutomat.actions.ensureLoaded().catch(() => { });
|
||||
this.unsubNumber = companyAutomat.subscribe(
|
||||
this,
|
||||
() => {
|
||||
this.headerItem = createDummyTestQuote();
|
||||
this.forceUpdate();
|
||||
},
|
||||
['numbers'],
|
||||
);
|
||||
void companyAutomat.actions.ensureNumbersLoaded().catch(() => { });
|
||||
void this.loadParties();
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import retoureAutomat, {
|
||||
isRetourePaid,
|
||||
isRetoureShipped,
|
||||
} from '../../automats/retoureAutomat';
|
||||
import numberAutomat from '../../automats/numberAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { retoure as retoureApi } from '../../api';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
@@ -600,10 +600,14 @@ export class RetoureList extends PureComponent {
|
||||
filterKey: retoureAutomat.getFilterKey(state),
|
||||
entityId: state.entityId ?? null,
|
||||
}));
|
||||
this.unsubNumber = numberAutomat.subscribe(this, () => {
|
||||
this.headerItem = createDummyTestRetoure();
|
||||
this.forceUpdate();
|
||||
});
|
||||
this.unsubNumber = companyAutomat.subscribe(
|
||||
this,
|
||||
() => {
|
||||
this.headerItem = createDummyTestRetoure();
|
||||
this.forceUpdate();
|
||||
},
|
||||
['numbers'],
|
||||
);
|
||||
void this.loadParties();
|
||||
}
|
||||
|
||||
|
||||
@@ -32,14 +32,14 @@ import { notifyError } from '../../utils/notify';
|
||||
import { formatDate, formatDateTime } from './manifestConfig';
|
||||
import orderAutomat from '../../automats/orderAutomat';
|
||||
import shipmentAutomat from '../../automats/shipmentAutomat';
|
||||
import carrierAutomat from '../../automats/carrierAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { SURFACE_RAISED, WHITE } from '../../colors';
|
||||
|
||||
// MARK: <AddPackageDialog/>
|
||||
class AddPackageDialog extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const carriers = (carrierAutomat.state.carriers || []).filter((c) => !c.isReverse);
|
||||
const carriers = (companyAutomat.state.carriers || []).filter((c) => !c.isReverse);
|
||||
this.state = {
|
||||
carriers,
|
||||
carrierId: carriers[0]?.id ?? '',
|
||||
@@ -50,17 +50,21 @@ class AddPackageDialog extends PureComponent {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsub = carrierAutomat.subscribe(this, (state) => {
|
||||
const carriers = (state.carriers || []).filter((c) => !c.isReverse);
|
||||
const currentId = this.state.carrierId;
|
||||
const valid = carriers.some((c) => c.id === currentId);
|
||||
return {
|
||||
carriers,
|
||||
carrierId: valid ? currentId : (carriers[0]?.id ?? ''),
|
||||
};
|
||||
});
|
||||
if (carrierAutomat.state.status !== 'ready') {
|
||||
void carrierAutomat.actions.load().catch(() => { });
|
||||
this.unsub = companyAutomat.subscribe(
|
||||
this,
|
||||
(state) => {
|
||||
const carriers = (state.carriers || []).filter((c) => !c.isReverse);
|
||||
const currentId = this.state.carrierId;
|
||||
const valid = carriers.some((c) => c.id === currentId);
|
||||
return {
|
||||
carriers,
|
||||
carrierId: valid ? currentId : (carriers[0]?.id ?? ''),
|
||||
};
|
||||
},
|
||||
['carriers'],
|
||||
);
|
||||
if (companyAutomat.state.carriersStatus !== 'ready') {
|
||||
void companyAutomat.actions.loadCarriers().catch(() => { });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,7 +418,7 @@ export default class ShipmentDetailDialog extends PureComponent {
|
||||
|
||||
const shLines = Array.isArray(shipment.lines) ? shipment.lines : [];
|
||||
const shPackages = Array.isArray(shipment.shippings) ? shipment.shippings : [];
|
||||
const carriers = carrierAutomat.state.carriers || [];
|
||||
const carriers = companyAutomat.state.carriers || [];
|
||||
const isShipmentShipped = Boolean(
|
||||
shipment.state === 'delivered' ||
|
||||
shipment.state === 'shipped' ||
|
||||
|
||||
@@ -24,7 +24,7 @@ import BackendError from '../BackendError';
|
||||
import AppDialog from '../AppDialog';
|
||||
import shipmentAutomat, { CHUNK_SIZE } from '../../automats/shipmentAutomat';
|
||||
import orderAutomat from '../../automats/orderAutomat';
|
||||
import numberAutomat from '../../automats/numberAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { shipment as shipmentApi } from '../../api';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
@@ -949,11 +949,15 @@ export class ShipmentList extends PureComponent {
|
||||
orders: state.items || [],
|
||||
}));
|
||||
|
||||
this.unsubNumber = numberAutomat.subscribe(this, () => {
|
||||
this.headerItem = createDummyTestShipment();
|
||||
this.forceUpdate();
|
||||
});
|
||||
void numberAutomat.actions.ensureLoaded().catch(() => {});
|
||||
this.unsubNumber = companyAutomat.subscribe(
|
||||
this,
|
||||
() => {
|
||||
this.headerItem = createDummyTestShipment();
|
||||
this.forceUpdate();
|
||||
},
|
||||
['numbers'],
|
||||
);
|
||||
void companyAutomat.actions.ensureNumbersLoaded().catch(() => {});
|
||||
|
||||
void shipmentAutomat.actions.load().catch(() => {});
|
||||
void orderAutomat.actions.load().catch(() => {});
|
||||
|
||||
@@ -4,15 +4,13 @@ import {
|
||||
order as orderApi,
|
||||
retoure as retoureApi,
|
||||
} from '../../api';
|
||||
import supplierAutomat from '../../automats/supplierAutomat';
|
||||
import entityAutomat from '../../automats/entityAutomat';
|
||||
import customergroupAutomat from '../../automats/customergroupAutomat';
|
||||
import deliveryAutomat from '../../automats/deliveryAutomat';
|
||||
import quoteAutomat from '../../automats/quoteAutomat';
|
||||
import orderAutomat from '../../automats/orderAutomat';
|
||||
import retoureAutomat from '../../automats/retoureAutomat';
|
||||
import { getArticle } from '../../automats/artikelAutomat';
|
||||
import { selectCompanyTaxrate } from '../../automats/companyAutomat';
|
||||
import companyAutomat, { selectCompanyTaxrate } from '../../automats/companyAutomat';
|
||||
import { netFromGross } from '../../utils/price';
|
||||
import { ensureDocumentNumber, getNextDocumentNumber } from '../../utils/documentNumber';
|
||||
import {
|
||||
@@ -646,18 +644,18 @@ export function apiFor(cfgOrKind) {
|
||||
export async function loadParties(cfgOrKind) {
|
||||
const kind = typeof cfgOrKind === 'string' ? cfgOrKind : cfgOrKind?.kind;
|
||||
if (kind === 'delivery') {
|
||||
if (supplierAutomat.state.status === 'ready') {
|
||||
return sortSuppliers(supplierAutomat.state.suppliers);
|
||||
if (companyAutomat.state.suppliersStatus === 'ready') {
|
||||
return sortSuppliers(companyAutomat.state.suppliers);
|
||||
}
|
||||
const list = await supplierAutomat.actions.load();
|
||||
const list = await companyAutomat.actions.loadSuppliers();
|
||||
return sortSuppliers(list);
|
||||
}
|
||||
const [entities, groups] = await Promise.all([
|
||||
entityAutomat.actions.ensureLoaded(),
|
||||
customergroupAutomat.actions.ensureLoaded(),
|
||||
companyAutomat.actions.ensureCustomergroupsLoaded(),
|
||||
]);
|
||||
const entityList = entities ?? entityAutomat.state.items ?? [];
|
||||
const groupList = groups ?? customergroupAutomat.state.items ?? [];
|
||||
const groupList = groups ?? companyAutomat.state.customergroups ?? [];
|
||||
return filterNonSupplierEntities(entityList, groupList);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
||||
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import attributeAutomat from '../../automats/attributeAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import {
|
||||
SELECT_BG,
|
||||
@@ -56,7 +56,7 @@ class ArtikelMerkmalAuswahl extends PureComponent {
|
||||
async renameAttribute(attribute, name) {
|
||||
this.setState({ busy: true });
|
||||
try {
|
||||
await attributeAutomat.actions.updateAttribute(attribute.id, { name });
|
||||
await companyAutomat.actions.updateAttribute(attribute.id, { name });
|
||||
enqueueSnackbar('Attribut gespeichert.', { variant: 'success' });
|
||||
} catch (err) {
|
||||
this.setState({ draft: attribute.name ?? '' });
|
||||
@@ -87,7 +87,7 @@ class ArtikelMerkmalAuswahl extends PureComponent {
|
||||
const nextExclusive = !attribute.exclusive;
|
||||
this.setState({ busy: true });
|
||||
try {
|
||||
await attributeAutomat.actions.updateAttribute(attribute.id, { exclusive: nextExclusive });
|
||||
await companyAutomat.actions.updateAttribute(attribute.id, { exclusive: nextExclusive });
|
||||
enqueueSnackbar(
|
||||
`Attribut „${attribute.name}“ auf ${nextExclusive ? 'Einzelauswahl' : 'Mehrfachauswahl'} gestellt.`,
|
||||
{ variant: 'info' }
|
||||
@@ -111,7 +111,7 @@ class ArtikelMerkmalAuswahl extends PureComponent {
|
||||
this.setState({ busy: true });
|
||||
void (async () => {
|
||||
try {
|
||||
await attributeAutomat.actions.deleteAttribute(attribute.id);
|
||||
await companyAutomat.actions.deleteAttribute(attribute.id);
|
||||
enqueueSnackbar('Attribut gelöscht.', { variant: 'success' });
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
|
||||
@@ -7,7 +7,7 @@ import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
||||
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import propertyAutomat from '../../automats/propertyAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
|
||||
class ArtikelMerkmalText extends PureComponent {
|
||||
@@ -54,7 +54,7 @@ class ArtikelMerkmalText extends PureComponent {
|
||||
async renameProperty(property, name) {
|
||||
this.setState({ busy: true });
|
||||
try {
|
||||
await propertyAutomat.actions.updateProperty(property.id, { name });
|
||||
await companyAutomat.actions.updateProperty(property.id, { name });
|
||||
enqueueSnackbar('Merkmal gespeichert.', { variant: 'success' });
|
||||
} catch (err) {
|
||||
this.setState({ draft: property.name ?? '' });
|
||||
@@ -85,7 +85,7 @@ class ArtikelMerkmalText extends PureComponent {
|
||||
const nextExclusive = !item.exclusive;
|
||||
this.setState({ busy: true });
|
||||
try {
|
||||
await propertyAutomat.actions.updateProperty(item.id, { exclusive: nextExclusive });
|
||||
await companyAutomat.actions.updateProperty(item.id, { exclusive: nextExclusive });
|
||||
enqueueSnackbar(
|
||||
`Merkmal „${item.name}“ auf ${nextExclusive ? 'Einzelauswahl' : 'Mehrfachauswahl'} gestellt.`,
|
||||
{ variant: 'info' }
|
||||
@@ -108,7 +108,7 @@ class ArtikelMerkmalText extends PureComponent {
|
||||
this.setState({ busy: true });
|
||||
void (async () => {
|
||||
try {
|
||||
await propertyAutomat.actions.deleteProperty(item.id);
|
||||
await companyAutomat.actions.deleteProperty(item.id);
|
||||
enqueueSnackbar('Merkmal gelöscht.', { variant: 'success' });
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
|
||||
@@ -7,7 +7,7 @@ import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
||||
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import attributeAutomat from '../../automats/attributeAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
|
||||
class ArtikelMerkmalWert extends PureComponent {
|
||||
@@ -54,7 +54,7 @@ class ArtikelMerkmalWert extends PureComponent {
|
||||
async renameValue(item, val) {
|
||||
this.setState({ busy: true });
|
||||
try {
|
||||
await attributeAutomat.actions.updateValue(item.id, { value: val });
|
||||
await companyAutomat.actions.updateValue(item.id, { value: val });
|
||||
enqueueSnackbar('Wert gespeichert.', { variant: 'success' });
|
||||
} catch (err) {
|
||||
this.setState({ draft: item.value ?? '' });
|
||||
@@ -88,7 +88,7 @@ class ArtikelMerkmalWert extends PureComponent {
|
||||
this.setState({ busy: true });
|
||||
void (async () => {
|
||||
try {
|
||||
await attributeAutomat.actions.deleteValue(item.id);
|
||||
await companyAutomat.actions.deleteValue(item.id);
|
||||
enqueueSnackbar('Wert gelöscht.', { variant: 'success' });
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
|
||||
@@ -14,7 +14,7 @@ import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||
import BackendError from '../BackendError';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import attributeAutomat, { selectAttributes } from '../../automats/attributeAutomat';
|
||||
import companyAutomat, { selectAttributes } from '../../automats/companyAutomat';
|
||||
import ArtikelMerkmalAuswahl from './ArtikelMerkmalAuswahl';
|
||||
import ArtikelMerkmalWert from './ArtikelMerkmalWert';
|
||||
import { BLUE_PANEL, PURPLE_PANEL } from '../../colors';
|
||||
@@ -30,8 +30,8 @@ class AuswahlMerkmaleTab extends PureComponent {
|
||||
const initialSelectedAttrId = initialAttributes[0]?.id ?? null;
|
||||
|
||||
this.state = {
|
||||
attrStatus: attributeAutomat.state.status === 'succeeded' ? 'ready' : 'loading',
|
||||
error: attributeAutomat.state.error || null,
|
||||
attrStatus: companyAutomat.state.attributesStatus === 'succeeded' ? 'ready' : 'loading',
|
||||
error: companyAutomat.state.attributesError || null,
|
||||
attributes: initialAttributes,
|
||||
selectedAttributeId: initialSelectedAttrId,
|
||||
|
||||
@@ -64,9 +64,9 @@ class AuswahlMerkmaleTab extends PureComponent {
|
||||
componentDidMount() {
|
||||
this._cancelled = false;
|
||||
|
||||
this.unsubAttr = attributeAutomat.subscribe((state) => {
|
||||
this.unsubAttr = companyAutomat.subscribe((state) => {
|
||||
if (this._cancelled) return;
|
||||
if (state.status === 'succeeded') {
|
||||
if (state.attributesStatus === 'succeeded') {
|
||||
let selectedId = this.state.selectedAttributeId;
|
||||
if (!selectedId || !state.attributes.some((a) => a.id === selectedId)) {
|
||||
selectedId = state.attributes[0]?.id ?? null;
|
||||
@@ -77,12 +77,12 @@ class AuswahlMerkmaleTab extends PureComponent {
|
||||
attributes: state.attributes,
|
||||
selectedAttributeId: selectedId,
|
||||
});
|
||||
} else if (state.status === 'failed') {
|
||||
this.setState({ attrStatus: 'failed', error: backendErrorFrom(state.error) });
|
||||
} else if (state.attributesStatus === 'failed') {
|
||||
this.setState({ attrStatus: 'failed', error: backendErrorFrom(state.attributesError) });
|
||||
}
|
||||
});
|
||||
}, ['attributes', 'attributesStatus', 'attributesError']);
|
||||
|
||||
if (attributeAutomat.state.status !== 'succeeded') {
|
||||
if (companyAutomat.state.attributesStatus !== 'succeeded') {
|
||||
void this.load();
|
||||
} else {
|
||||
this._initAttrSortable();
|
||||
@@ -216,21 +216,21 @@ class AuswahlMerkmaleTab extends PureComponent {
|
||||
|
||||
async _persistAttrOrder(ids) {
|
||||
try {
|
||||
await attributeAutomat.actions.reorderAttributes(ids);
|
||||
await companyAutomat.actions.reorderAttributes(ids);
|
||||
} catch (err) {
|
||||
if (this._cancelled) return;
|
||||
notifyError(err);
|
||||
void attributeAutomat.actions.load();
|
||||
void companyAutomat.actions.loadAttributes();
|
||||
}
|
||||
}
|
||||
|
||||
async _persistValOrder(attributeId, ids) {
|
||||
try {
|
||||
await attributeAutomat.actions.reorderValues(attributeId, ids);
|
||||
await companyAutomat.actions.reorderValues(attributeId, ids);
|
||||
} catch (err) {
|
||||
if (this._cancelled) return;
|
||||
notifyError(err);
|
||||
void attributeAutomat.actions.load();
|
||||
void companyAutomat.actions.loadAttributes();
|
||||
}
|
||||
}
|
||||
// #endregion
|
||||
@@ -238,11 +238,11 @@ class AuswahlMerkmaleTab extends PureComponent {
|
||||
// #region Actions & Handlers
|
||||
async load() {
|
||||
this.setState({
|
||||
attrStatus: attributeAutomat.state.status === 'succeeded' ? 'ready' : 'loading',
|
||||
attrStatus: companyAutomat.state.attributesStatus === 'succeeded' ? 'ready' : 'loading',
|
||||
error: null,
|
||||
});
|
||||
try {
|
||||
await attributeAutomat.actions.load();
|
||||
await companyAutomat.actions.loadAttributes();
|
||||
} catch (err) {
|
||||
if (this._cancelled) return;
|
||||
this.setState({ error: backendErrorFrom(err) });
|
||||
@@ -263,7 +263,7 @@ class AuswahlMerkmaleTab extends PureComponent {
|
||||
|
||||
this.setState({ attrSubmitting: true });
|
||||
try {
|
||||
const row = await attributeAutomat.actions.createAttribute({
|
||||
const row = await companyAutomat.actions.createAttribute({
|
||||
name,
|
||||
exclusive: true,
|
||||
});
|
||||
@@ -288,7 +288,7 @@ class AuswahlMerkmaleTab extends PureComponent {
|
||||
|
||||
this.setState({ valSubmitting: true });
|
||||
try {
|
||||
await attributeAutomat.actions.createValue(selectedAttributeId, { value });
|
||||
await companyAutomat.actions.createValue(selectedAttributeId, { value });
|
||||
if (this._cancelled) return;
|
||||
this.setState({ valFilter: '', valSubmitting: false });
|
||||
enqueueSnackbar(`Wert „${value}“ angelegt.`, { variant: 'success' });
|
||||
@@ -311,7 +311,7 @@ class AuswahlMerkmaleTab extends PureComponent {
|
||||
|
||||
this.setState({ attrDeletingUnused: true });
|
||||
try {
|
||||
await attributeAutomat.actions.deleteAttributes(unused.map((a) => a.id));
|
||||
await companyAutomat.actions.deleteAttributes(unused.map((a) => a.id));
|
||||
enqueueSnackbar(`${unused.length} ungenutzte Auswahl-Merkmale gelöscht.`, { variant: 'success' });
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
@@ -336,7 +336,7 @@ class AuswahlMerkmaleTab extends PureComponent {
|
||||
|
||||
this.setState({ valDeletingUnused: true });
|
||||
try {
|
||||
await attributeAutomat.actions.deleteValues(unused.map((v) => v.id));
|
||||
await companyAutomat.actions.deleteValues(unused.map((v) => v.id));
|
||||
enqueueSnackbar(`${unused.length} ungenutzte Werte gelöscht.`, { variant: 'success' });
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
|
||||
@@ -13,7 +13,7 @@ import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||
import BackendError from '../BackendError';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import propertyAutomat, { selectProperties } from '../../automats/propertyAutomat';
|
||||
import companyAutomat, { selectProperties } from '../../automats/companyAutomat';
|
||||
import ArtikelMerkmalText from './ArtikelMerkmalText';
|
||||
import { ORANGE_PANEL } from '../../colors';
|
||||
|
||||
@@ -27,8 +27,8 @@ class TextMerkmaleTab extends PureComponent {
|
||||
const initialProperties = selectProperties() || [];
|
||||
|
||||
this.state = {
|
||||
propStatus: propertyAutomat.state.status === 'succeeded' ? 'ready' : 'loading',
|
||||
error: propertyAutomat.state.error || null,
|
||||
propStatus: companyAutomat.state.propertiesStatus === 'succeeded' ? 'ready' : 'loading',
|
||||
error: companyAutomat.state.propertiesError || null,
|
||||
properties: initialProperties,
|
||||
|
||||
propFilter: '',
|
||||
@@ -52,20 +52,20 @@ class TextMerkmaleTab extends PureComponent {
|
||||
this._cancelled = false;
|
||||
console.log('TextMerkmaleTab mounted', this.props);
|
||||
|
||||
this.unsubProp = propertyAutomat.subscribe((state) => {
|
||||
this.unsubProp = companyAutomat.subscribe((state) => {
|
||||
if (this._cancelled) return;
|
||||
if (state.status === 'succeeded') {
|
||||
if (state.propertiesStatus === 'succeeded') {
|
||||
this.setState({
|
||||
propStatus: 'ready',
|
||||
error: null,
|
||||
properties: state.properties,
|
||||
});
|
||||
} else if (state.status === 'failed') {
|
||||
this.setState({ propStatus: 'failed', error: backendErrorFrom(state.error) });
|
||||
} else if (state.propertiesStatus === 'failed') {
|
||||
this.setState({ propStatus: 'failed', error: backendErrorFrom(state.propertiesError) });
|
||||
}
|
||||
});
|
||||
}, ['properties', 'propertiesStatus', 'propertiesError']);
|
||||
|
||||
if (propertyAutomat.state.status !== 'succeeded') {
|
||||
if (companyAutomat.state.propertiesStatus !== 'succeeded') {
|
||||
void this.load();
|
||||
} else {
|
||||
this._initPropSortable();
|
||||
@@ -139,11 +139,11 @@ class TextMerkmaleTab extends PureComponent {
|
||||
|
||||
async _persistPropOrder(ids) {
|
||||
try {
|
||||
await propertyAutomat.actions.reorderProperties(ids);
|
||||
await companyAutomat.actions.reorderProperties(ids);
|
||||
} catch (err) {
|
||||
if (this._cancelled) return;
|
||||
notifyError(err);
|
||||
void propertyAutomat.actions.load();
|
||||
void companyAutomat.actions.loadProperties();
|
||||
}
|
||||
}
|
||||
// #endregion
|
||||
@@ -151,11 +151,11 @@ class TextMerkmaleTab extends PureComponent {
|
||||
// #region Actions & Handlers
|
||||
async load() {
|
||||
this.setState({
|
||||
propStatus: propertyAutomat.state.status === 'succeeded' ? 'ready' : 'loading',
|
||||
propStatus: companyAutomat.state.propertiesStatus === 'succeeded' ? 'ready' : 'loading',
|
||||
error: null,
|
||||
});
|
||||
try {
|
||||
await propertyAutomat.actions.load();
|
||||
await companyAutomat.actions.loadProperties();
|
||||
} catch (err) {
|
||||
if (this._cancelled) return;
|
||||
this.setState({ error: backendErrorFrom(err) });
|
||||
@@ -172,7 +172,7 @@ class TextMerkmaleTab extends PureComponent {
|
||||
|
||||
this.setState({ propSubmitting: true });
|
||||
try {
|
||||
const row = await propertyAutomat.actions.createProperty({
|
||||
const row = await companyAutomat.actions.createProperty({
|
||||
name,
|
||||
exclusive: true,
|
||||
});
|
||||
@@ -198,7 +198,7 @@ class TextMerkmaleTab extends PureComponent {
|
||||
|
||||
this.setState({ propDeletingUnused: true });
|
||||
try {
|
||||
await propertyAutomat.actions.deleteProperties(unused.map((p) => p.id));
|
||||
await companyAutomat.actions.deleteProperties(unused.map((p) => p.id));
|
||||
enqueueSnackbar(`${unused.length} ungenutzte Text-Merkmale gelöscht.`, { variant: 'success' });
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
|
||||
@@ -17,7 +17,7 @@ import { enqueueSnackbar } from 'notistack';
|
||||
import { company } from '../../api';
|
||||
import categoryAutomat from '../../automats/categoryAutomat';
|
||||
import articleIndexAutomat from '../../automats/articleIndexAutomat';
|
||||
import brandAutomat from '../../automats/brandAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { orderAutomat } from '../../automats/orderAutomat';
|
||||
import SkeleHashImage from './SkeleHashImage';
|
||||
|
||||
@@ -240,7 +240,7 @@ export default class ImportExportSettingsPanel extends Component {
|
||||
enqueueSnackbar('Import erfolgreich abgeschlossen.', { variant: 'success' });
|
||||
void categoryAutomat.actions.loadTree();
|
||||
void articleIndexAutomat.actions.reload?.();
|
||||
void brandAutomat.actions.load?.();
|
||||
void companyAutomat.actions.loadBrands?.();
|
||||
if (stats?.ordersImported) {
|
||||
void orderAutomat.actions.reload?.();
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import BackendError from '../BackendError';
|
||||
import { number as numberApi } from '../../api';
|
||||
import numberAutomat from '../../automats/numberAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
@@ -53,7 +53,7 @@ export default class NumberSettingsPanel extends Component {
|
||||
this.setState({ status: 'loading', error: null });
|
||||
try {
|
||||
const numbers = requireArray(
|
||||
await numberAutomat.actions.reload(),
|
||||
await companyAutomat.actions.reloadNumbers(),
|
||||
'numbers',
|
||||
);
|
||||
const drafts = Object.fromEntries(
|
||||
@@ -146,7 +146,7 @@ export default class NumberSettingsPanel extends Component {
|
||||
const updated = requireDefined(data.number, 'number');
|
||||
|
||||
const numbers = this.state.numbers.map((n) => (n.id === id ? { ...n, ...body } : n));
|
||||
await this.context.updateNumber();
|
||||
await companyAutomat.actions.reloadNumbers();
|
||||
this.setState({
|
||||
numbers,
|
||||
drafts: {
|
||||
|
||||
@@ -17,7 +17,7 @@ import PointOfSaleIcon from '@mui/icons-material/PointOfSale';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import BackendError from '../BackendError';
|
||||
import CategoryPickerDialog from '../article/CategoryPickerDialog';
|
||||
import posterminalAutomat from '../../automats/posterminalAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import categoryAutomat, { whenCategoryTreeReady } from '../../automats/categoryAutomat';
|
||||
import { categoryDisplayName } from '../../utils/categoryUtils';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
@@ -43,9 +43,9 @@ function formatDateTime(val) {
|
||||
export default class PosTerminalSettingsPanel extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const terminals = posterminalAutomat.state.terminals ?? [];
|
||||
const terminals = companyAutomat.state.terminals ?? [];
|
||||
this.state = {
|
||||
status: posterminalAutomat.state.status === 'succeeded' ? 'ready' : 'loading',
|
||||
status: companyAutomat.state.terminalsStatus === 'succeeded' ? 'ready' : 'loading',
|
||||
error: null,
|
||||
terminals,
|
||||
categories: categoryAutomat.state.categories,
|
||||
@@ -67,9 +67,9 @@ export default class PosTerminalSettingsPanel extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsub = posterminalAutomat.subscribe(this, (state) => ({
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
terminals: Array.isArray(state.terminals) ? state.terminals : [],
|
||||
status: state.status === 'succeeded' ? 'ready' : state.status === 'failed' ? 'failed' : this.state.status,
|
||||
status: state.terminalsStatus === 'succeeded' ? 'ready' : state.terminalsStatus === 'failed' ? 'failed' : this.state.status,
|
||||
}));
|
||||
this.unsubCategory = categoryAutomat.subscribe(this, (state) => ({
|
||||
categories: state.categories,
|
||||
@@ -89,7 +89,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
||||
async loadTerminals() {
|
||||
this.setState({ error: null });
|
||||
try {
|
||||
const terminals = await posterminalAutomat.actions.load();
|
||||
const terminals = await companyAutomat.actions.loadTerminals();
|
||||
this.setState({
|
||||
status: 'ready',
|
||||
terminals: Array.isArray(terminals) ? terminals : [],
|
||||
@@ -111,14 +111,14 @@ export default class PosTerminalSettingsPanel extends Component {
|
||||
|
||||
this.setState({ creatingTerminal: true });
|
||||
try {
|
||||
const created = await posterminalAutomat.actions.create({ name: trimmed });
|
||||
const created = await companyAutomat.actions.createTerminal({ name: trimmed });
|
||||
|
||||
if (enableAllForNew && created?.id != null) {
|
||||
await posterminalAutomat.actions.enableAll({ terminalId: created.id });
|
||||
await companyAutomat.actions.enableAllTerminalCategories({ terminalId: created.id });
|
||||
await whenCategoryTreeReady();
|
||||
}
|
||||
|
||||
await posterminalAutomat.actions.load();
|
||||
await companyAutomat.actions.loadTerminals();
|
||||
this.setState({ newTerminalName: '' });
|
||||
enqueueSnackbar('Terminal hinzugefügt.', { variant: 'success' });
|
||||
} catch (err) {
|
||||
@@ -131,8 +131,8 @@ export default class PosTerminalSettingsPanel extends Component {
|
||||
async handleDeleteTerminal(terminal) {
|
||||
this.setState({ deletingId: terminal.id });
|
||||
try {
|
||||
await posterminalAutomat.actions.delete(terminal.id);
|
||||
await posterminalAutomat.actions.load();
|
||||
await companyAutomat.actions.deleteTerminal(terminal.id);
|
||||
await companyAutomat.actions.loadTerminals();
|
||||
enqueueSnackbar('Terminal gelöscht.', { variant: 'info' });
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
@@ -144,7 +144,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
||||
async handleSetRootCategory(terminal, categoryId) {
|
||||
this.setState({ patchingTerminalId: terminal.id });
|
||||
try {
|
||||
await posterminalAutomat.actions.update(terminal.id, {
|
||||
await companyAutomat.actions.updateTerminal(terminal.id, {
|
||||
rootCategoryId: categoryId != null ? Number(categoryId) : null,
|
||||
});
|
||||
enqueueSnackbar('Root-Kategorie aktualisiert.', { variant: 'success' });
|
||||
@@ -158,7 +158,7 @@ export default class PosTerminalSettingsPanel extends Component {
|
||||
async handleToggleSyncRootArticles(terminal, nextValue) {
|
||||
this.setState({ patchingTerminalId: terminal.id });
|
||||
try {
|
||||
await posterminalAutomat.actions.update(terminal.id, {
|
||||
await companyAutomat.actions.updateTerminal(terminal.id, {
|
||||
syncRootArticles: Boolean(nextValue),
|
||||
});
|
||||
enqueueSnackbar('Synchronisierungseinstellung gespeichert.', { variant: 'success' });
|
||||
|
||||
@@ -6,7 +6,7 @@ import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
||||
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import warehouseAutomat from '../../automats/warehouseAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
|
||||
class ArtikelLager extends PureComponent {
|
||||
@@ -51,7 +51,7 @@ class ArtikelLager extends PureComponent {
|
||||
|
||||
async renameWarehouse(item, name) {
|
||||
try {
|
||||
await warehouseAutomat.actions.update(item.id, { name });
|
||||
await companyAutomat.actions.updateWarehouse(item.id, { name });
|
||||
enqueueSnackbar('Lager gespeichert.', { variant: 'success' });
|
||||
} catch (err) {
|
||||
this.setState({ draft: item.name ?? '' });
|
||||
@@ -78,7 +78,7 @@ class ArtikelLager extends PureComponent {
|
||||
if (!window.confirm(`Lager „${item.name}" wirklich löschen?`)) return;
|
||||
void (async () => {
|
||||
try {
|
||||
await warehouseAutomat.actions.remove(item.id);
|
||||
await companyAutomat.actions.removeWarehouse(item.id);
|
||||
enqueueSnackbar('Lager gelöscht.', { variant: 'success' });
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
|
||||
28
src/main.jsx
28
src/main.jsx
@@ -9,17 +9,11 @@ import App from './App';
|
||||
import { getActiveTheme } from './theme';
|
||||
import { installCssVars } from './colors';
|
||||
import { disableTouchAutofocus } from './utils/touch';
|
||||
import './index.css';
|
||||
|
||||
// Globally suppress automatic focus on touch devices to avoid virtual keyboard popups
|
||||
disableTouchAutofocus();
|
||||
|
||||
// Side-effect imports: each module registers its automat and subscribes to
|
||||
// authAutomat, so the stores must be evaluated before the session bootstraps.
|
||||
import './automats';
|
||||
import { initPwa } from './automats';
|
||||
import PwaUpdateDialog from './components/PwaUpdateDialog';
|
||||
import './index.css';
|
||||
|
||||
// Publish the color tokens as CSS custom properties so index.css (and any
|
||||
// other plain-CSS stylesheet) reads the same palette as the React components.
|
||||
installCssVars();
|
||||
@@ -29,12 +23,6 @@ installCssVars();
|
||||
const AutomatInspector = import.meta.env.DEV
|
||||
? lazy(() => import('./devtools/AutomatInspector'))
|
||||
: null;
|
||||
const SseMonitor = import.meta.env.DEV
|
||||
? lazy(() => import('./devtools/SseMonitor'))
|
||||
: null;
|
||||
const ChunkCacheMonitor = import.meta.env.DEV
|
||||
? lazy(() => import('./devtools/ChunkCacheMonitor'))
|
||||
: null;
|
||||
|
||||
async function clearDevServiceWorker() {
|
||||
if (!import.meta.env.DEV || !('serviceWorker' in navigator)) return;
|
||||
@@ -50,9 +38,6 @@ if (import.meta.env.DEV) {
|
||||
void clearDevServiceWorker();
|
||||
}
|
||||
|
||||
// Initialize PWA service worker registration and update listener in production
|
||||
initPwa();
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<ThemeProvider theme={getActiveTheme()}>
|
||||
<CssBaseline />
|
||||
@@ -74,17 +59,6 @@ createRoot(document.getElementById('root')).render(
|
||||
</Suspense>
|
||||
) : null}
|
||||
</Box>
|
||||
{SseMonitor ? (
|
||||
<Suspense fallback={null}>
|
||||
<SseMonitor />
|
||||
</Suspense>
|
||||
) : null}
|
||||
{ChunkCacheMonitor ? (
|
||||
<Suspense fallback={null}>
|
||||
<ChunkCacheMonitor />
|
||||
</Suspense>
|
||||
) : null}
|
||||
<PwaUpdateDialog />
|
||||
</AuthBootstrap>
|
||||
</SnackbarProvider>
|
||||
</ThemeProvider>,
|
||||
|
||||
@@ -40,16 +40,13 @@ import { preserveSearchFocus } from '../components/article/searchFocus';
|
||||
import { artikelDetailPath } from '../components/article/articleRoute';
|
||||
import { buildCategoryPath } from '../components/category/categoryRoute';
|
||||
import withSearchParams from '../components/article/withSearchParams';
|
||||
import {
|
||||
import navigationAutomat, {
|
||||
setLastArticleSku,
|
||||
setLastSearchTerm,
|
||||
setLastCategoryId,
|
||||
navigationAutomat,
|
||||
categoryAutomat,
|
||||
companyAutomat,
|
||||
brandAutomat,
|
||||
supplierAutomat,
|
||||
} from '../automats';
|
||||
} from '../automats/navigationAutomat';
|
||||
import categoryAutomat from '../automats/categoryAutomat';
|
||||
import companyAutomat from '../automats/companyAutomat';
|
||||
import { BrandLabel, BrandThumb } from '../components/brand';
|
||||
import { SCRIM_LABEL } from '../colors';
|
||||
// #endregion
|
||||
@@ -358,12 +355,14 @@ class ArtikelSearchPage extends Component {
|
||||
|
||||
// Push search results into local component state.
|
||||
this.unsubSearch = articleSearchAutomat.subscribe(this, (state) => state.result);
|
||||
this.unsubBrand = brandAutomat.subscribe(this, () => ({}));
|
||||
this.unsubSupplier = supplierAutomat.subscribe(this, () => ({}));
|
||||
this.unsubCompany = companyAutomat.subscribe(this, (state) => ({
|
||||
_brands: state.brands,
|
||||
_suppliers: state.suppliers,
|
||||
}));
|
||||
|
||||
void brandAutomat.actions.load().catch(() => {});
|
||||
void supplierAutomat.actions.load().catch(() => {});
|
||||
void supplierAutomat.actions.loadAllSupplierArticles().catch(() => {});
|
||||
void companyAutomat.actions.loadBrands().catch(() => {});
|
||||
void companyAutomat.actions.loadSuppliers().catch(() => {});
|
||||
void companyAutomat.actions.loadAllSupplierArticles().catch(() => {});
|
||||
|
||||
// Initial search: kicks off index loading if idle and derives current items
|
||||
const {
|
||||
@@ -410,10 +409,8 @@ class ArtikelSearchPage extends Component {
|
||||
document.removeEventListener('mouseup', preserveSearchFocus, true);
|
||||
this.unsubSearch?.();
|
||||
this.unsubSearch = null;
|
||||
this.unsubBrand?.();
|
||||
this.unsubBrand = null;
|
||||
this.unsubSupplier?.();
|
||||
this.unsubSupplier = null;
|
||||
this.unsubCompany?.();
|
||||
this.unsubCompany = null;
|
||||
this.persistQuery();
|
||||
const query = queryFromParams(this.props.searchParams);
|
||||
const target = this.state.input;
|
||||
@@ -711,7 +708,7 @@ class ArtikelSearchPage extends Component {
|
||||
{selectedBrands.map((brandId) => {
|
||||
const label =
|
||||
brandFacets?.[brandId]?.name ||
|
||||
brandAutomat.state.brands?.find((b) => String(b.id) === String(brandId) || b.name === brandId)?.name ||
|
||||
companyAutomat.state.brands?.find((b) => String(b.id) === String(brandId) || b.name === brandId)?.name ||
|
||||
brandId;
|
||||
return (
|
||||
<Chip
|
||||
@@ -737,7 +734,7 @@ class ArtikelSearchPage extends Component {
|
||||
{selectedSuppliers.map((supplierId) => {
|
||||
const label =
|
||||
supplierFacets?.[supplierId]?.name ||
|
||||
supplierAutomat.state.suppliers?.find((s) => String(s.id) === String(supplierId))?.name ||
|
||||
companyAutomat.state.suppliers?.find((s) => String(s.id) === String(supplierId))?.name ||
|
||||
`Lieferant #${supplierId}`;
|
||||
return (
|
||||
<Chip
|
||||
|
||||
@@ -6,12 +6,11 @@ import companyAutomat from '../../automats/companyAutomat';
|
||||
import { bootstrapAuthMe } from '../../automats/authAutomat';
|
||||
import BackendError from '../../components/BackendError';
|
||||
import { ArticleView } from '../../components/article';
|
||||
import {
|
||||
artikelAutomat,
|
||||
import artikelAutomat, {
|
||||
createArticleSelector,
|
||||
getArticle,
|
||||
setLastArticleSku,
|
||||
} from '../../automats';
|
||||
} from '../../automats/artikelAutomat';
|
||||
import { setLastArticleSku } from '../../automats/navigationAutomat';
|
||||
import withSkuRoute from '../../components/article/withSkuRoute';
|
||||
|
||||
/** Full-screen loading state while the shared article cache resolves. */
|
||||
|
||||
@@ -26,9 +26,8 @@ import {
|
||||
ROOT_CATEGORY_NAME,
|
||||
} from '../../components/category/categoryRoute';
|
||||
import categoryAutomat from '../../automats/categoryAutomat';
|
||||
import posterminalAutomat from '../../automats/posterminalAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { setLastArticleSku, setLastCategorySlug, setLastCategoryId } from '../../automats';
|
||||
import { setLastArticleSku, setLastCategorySlug, setLastCategoryId } from '../../automats/navigationAutomat';
|
||||
import {
|
||||
categoryDisplayName,
|
||||
getCategoryBreadcrumbs,
|
||||
@@ -86,7 +85,7 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
loaded: view?.loaded ?? false,
|
||||
loading: view?.loading ?? false,
|
||||
routeError: view?.error ?? null,
|
||||
...selectTerminals(posterminalAutomat.state),
|
||||
...selectTerminals(companyAutomat.state),
|
||||
companyId: companyAutomat.state.current?.id ?? null,
|
||||
moveCategory: null,
|
||||
};
|
||||
@@ -100,8 +99,8 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
setLastArticleSku(null);
|
||||
setLastCategorySlug(this.props.categorySlugPath);
|
||||
this._unsubCat = categoryAutomat.subscribe(this, selectFromCategory);
|
||||
this._unsubTerm = posterminalAutomat.subscribe(this, selectTerminals);
|
||||
this._unsubCompany = companyAutomat.subscribe(this, (s) => ({
|
||||
terminals: s.terminals,
|
||||
companyId: s.current?.id ?? null,
|
||||
}));
|
||||
this.ensureTreeLoaded();
|
||||
@@ -138,7 +137,6 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
componentWillUnmount() {
|
||||
setLastCategorySlug(this.props.categorySlugPath);
|
||||
this._unsubCat?.();
|
||||
this._unsubTerm?.();
|
||||
this._unsubCompany?.();
|
||||
this._unsubHandle?.();
|
||||
this.sortableModule?.destroy();
|
||||
@@ -197,7 +195,7 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
ensurePosterminalsLoaded = () => {
|
||||
const { companyId } = this.state;
|
||||
if (!companyId) return;
|
||||
void posterminalAutomat.actions.load();
|
||||
void companyAutomat.actions.loadTerminals();
|
||||
};
|
||||
|
||||
syncCategoryFromUrl = () => {
|
||||
@@ -472,8 +470,8 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
categoryPosterminalIds: item.posterminalIds || [],
|
||||
onTerminalToggle: (terminalId, enabled) => {
|
||||
const action = enabled
|
||||
? posterminalAutomat.actions.enableCategory
|
||||
: posterminalAutomat.actions.disableCategory;
|
||||
? companyAutomat.actions.enableCategory
|
||||
: companyAutomat.actions.disableCategory;
|
||||
void action({ terminalId, categoryId: item.id })
|
||||
.then(() => {
|
||||
categoryAutomat.actions.patchCategoryPosterminalIds({
|
||||
|
||||
@@ -13,8 +13,8 @@ import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||
import withNavigate from '../../components/withNavigate';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import { setLastArticleSku } from '../../automats';
|
||||
import warehouseAutomat from '../../automats/warehouseAutomat';
|
||||
import { setLastArticleSku } from '../../automats/navigationAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { ArtikelLager } from '../../components/warehouse';
|
||||
import { GREEN_PANEL } from '../../colors';
|
||||
|
||||
@@ -37,20 +37,20 @@ function getInitialParams(props) {
|
||||
class ArtikelLagerSearchField extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const initial = warehouseAutomat.state.filter || props.prefill || '';
|
||||
const initial = companyAutomat.state.warehouseFilter || props.prefill || '';
|
||||
this.state = {
|
||||
input: initial,
|
||||
submitting: false,
|
||||
};
|
||||
if (props.prefill && !warehouseAutomat.state.filter) {
|
||||
warehouseAutomat.actions.filter(props.prefill);
|
||||
if (props.prefill && !companyAutomat.state.warehouseFilter) {
|
||||
companyAutomat.actions.filterWarehouses(props.prefill);
|
||||
}
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsub = warehouseAutomat.subscribe(this, (state) => ({
|
||||
input: state.filter,
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
input: state.warehouseFilter,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ class ArtikelLagerSearchField extends PureComponent {
|
||||
|
||||
onInputChange(input) {
|
||||
this.setState({ input });
|
||||
warehouseAutomat.actions.filter(input);
|
||||
companyAutomat.actions.filterWarehouses(input);
|
||||
}
|
||||
|
||||
async handleSubmit() {
|
||||
@@ -69,10 +69,10 @@ class ArtikelLagerSearchField extends PureComponent {
|
||||
|
||||
this.setState({ submitting: true });
|
||||
try {
|
||||
const row = await warehouseAutomat.actions.create({ name });
|
||||
const row = await companyAutomat.actions.createWarehouse({ name });
|
||||
enqueueSnackbar(`Lager „${row.name || name}“ angelegt.`, { variant: 'success' });
|
||||
this.setState({ input: '', submitting: false });
|
||||
warehouseAutomat.actions.filter('');
|
||||
companyAutomat.actions.filterWarehouses('');
|
||||
if (this.props.returnTo && this.props.navigate) {
|
||||
this.props.navigate(this.props.returnTo);
|
||||
}
|
||||
@@ -157,12 +157,12 @@ class ArtikelLagerPage extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const initialWarehouses =
|
||||
warehouseAutomat.state.filteredWarehouses ?? warehouseAutomat.state.warehouses ?? [];
|
||||
companyAutomat.state.filteredWarehouses ?? companyAutomat.state.warehouses ?? [];
|
||||
this.state = {
|
||||
status: warehouseAutomat.state.status === 'ready' ? 'ready' : 'loading',
|
||||
error: warehouseAutomat.state.error ? backendErrorFrom(warehouseAutomat.state.error) : null,
|
||||
status: companyAutomat.state.warehousesStatus === 'ready' ? 'ready' : 'loading',
|
||||
error: companyAutomat.state.warehousesError ? backendErrorFrom(companyAutomat.state.warehousesError) : null,
|
||||
warehouses: initialWarehouses,
|
||||
filter: warehouseAutomat.state.filter ?? '',
|
||||
filter: companyAutomat.state.warehouseFilter ?? '',
|
||||
};
|
||||
this.searchRef = createRef();
|
||||
this.listRef = createRef();
|
||||
@@ -177,15 +177,15 @@ class ArtikelLagerPage extends PureComponent {
|
||||
setLastArticleSku(null);
|
||||
this._cancelled = false;
|
||||
|
||||
this.unsub = warehouseAutomat.subscribe(this, (state) => ({
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
warehouses: state.filteredWarehouses ?? state.warehouses,
|
||||
filter: state.filter ?? '',
|
||||
status: state.status,
|
||||
error: state.error ? backendErrorFrom(state.error) : null,
|
||||
filter: state.warehouseFilter ?? '',
|
||||
status: state.warehousesStatus,
|
||||
error: state.warehousesError ? backendErrorFrom(state.warehousesError) : null,
|
||||
}));
|
||||
|
||||
if (warehouseAutomat.state.status !== 'ready') {
|
||||
void warehouseAutomat.actions.load().catch((err) => notifyError(err));
|
||||
if (companyAutomat.state.warehousesStatus !== 'ready') {
|
||||
void companyAutomat.actions.loadWarehouses().catch((err) => notifyError(err));
|
||||
} else {
|
||||
this._initSortable();
|
||||
}
|
||||
@@ -249,16 +249,16 @@ class ArtikelLagerPage extends PureComponent {
|
||||
|
||||
async _persistOrder(warehouseIds) {
|
||||
try {
|
||||
await warehouseAutomat.actions.reorder(warehouseIds);
|
||||
await companyAutomat.actions.reorderWarehouses(warehouseIds);
|
||||
} catch (err) {
|
||||
if (this._cancelled) return;
|
||||
notifyError(err);
|
||||
void warehouseAutomat.actions.reload();
|
||||
void companyAutomat.actions.reloadWarehouses();
|
||||
}
|
||||
}
|
||||
|
||||
handleRetry() {
|
||||
void warehouseAutomat.actions.load().catch((err) => notifyError(err));
|
||||
void companyAutomat.actions.loadWarehouses().catch((err) => notifyError(err));
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -8,8 +8,8 @@ import FillSpace from 'dashgui/FillSpace';
|
||||
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import { setLastArticleSku } from '../../automats';
|
||||
import brandAutomat from '../../automats/brandAutomat';
|
||||
import { setLastArticleSku } from '../../automats/navigationAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import {
|
||||
ArtikelMarke,
|
||||
@@ -22,9 +22,9 @@ import {
|
||||
class ArtikelMarkenSearchField extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const brands = brandAutomat.state.brands || [];
|
||||
const brands = companyAutomat.state.brands || [];
|
||||
this.state = {
|
||||
input: brandAutomat.state.filter ?? '',
|
||||
input: companyAutomat.state.brandFilter ?? '',
|
||||
submitting: false,
|
||||
deletingUnused: false,
|
||||
unusedCount: brands.filter((b) => (b.articleCount ?? 0) === 0).length,
|
||||
@@ -34,8 +34,8 @@ class ArtikelMarkenSearchField extends PureComponent {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsub = brandAutomat.subscribe(this, (state) => ({
|
||||
input: state.filter,
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
input: state.brandFilter,
|
||||
unusedCount: (state.brands || []).filter((b) => (b.articleCount ?? 0) === 0).length,
|
||||
}));
|
||||
}
|
||||
@@ -46,7 +46,7 @@ class ArtikelMarkenSearchField extends PureComponent {
|
||||
|
||||
onInputChange(input) {
|
||||
this.setState({ input });
|
||||
brandAutomat.actions.filter(input);
|
||||
companyAutomat.actions.filterBrands(input);
|
||||
}
|
||||
|
||||
async handleSubmit() {
|
||||
@@ -55,10 +55,10 @@ class ArtikelMarkenSearchField extends PureComponent {
|
||||
|
||||
this.setState({ submitting: true });
|
||||
try {
|
||||
const row = await brandAutomat.actions.create({ name });
|
||||
const row = await companyAutomat.actions.createBrand({ name });
|
||||
enqueueSnackbar(`Marke „${row.name || name}“ angelegt.`, { variant: 'success' });
|
||||
this.setState({ input: '', submitting: false });
|
||||
brandAutomat.actions.filter('');
|
||||
companyAutomat.actions.filterBrands('');
|
||||
} catch (err) {
|
||||
this.setState({ submitting: false });
|
||||
notifyError(err);
|
||||
@@ -67,7 +67,7 @@ class ArtikelMarkenSearchField extends PureComponent {
|
||||
|
||||
async handleDeleteUnused() {
|
||||
if (this.state.deletingUnused) return;
|
||||
const allBrands = brandAutomat.state.brands || [];
|
||||
const allBrands = companyAutomat.state.brands || [];
|
||||
const unused = allBrands.filter((b) => (b.articleCount ?? 0) === 0);
|
||||
if (!unused.length) return;
|
||||
|
||||
@@ -77,7 +77,7 @@ class ArtikelMarkenSearchField extends PureComponent {
|
||||
|
||||
this.setState({ deletingUnused: true });
|
||||
try {
|
||||
await brandAutomat.actions.removeBrands(unused.map((b) => b.id));
|
||||
await companyAutomat.actions.removeBrands(unused.map((b) => b.id));
|
||||
enqueueSnackbar(`${unused.length} ungenutzte Marken gelöscht.`, { variant: 'success' });
|
||||
} catch (err) {
|
||||
notifyError(err);
|
||||
@@ -148,10 +148,10 @@ class ArtikelMarkenPage extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
status: brandAutomat.state.status,
|
||||
error: brandAutomat.state.error ? backendErrorFrom({ message: brandAutomat.state.error }) : null,
|
||||
brands: brandAutomat.state.filteredBrands ?? brandAutomat.state.brands,
|
||||
totalCount: brandAutomat.state.brands?.length ?? 0,
|
||||
status: companyAutomat.state.brandsStatus,
|
||||
error: companyAutomat.state.brandsError ? backendErrorFrom({ message: companyAutomat.state.brandsError }) : null,
|
||||
brands: companyAutomat.state.filteredBrands ?? companyAutomat.state.brands,
|
||||
totalCount: companyAutomat.state.brands?.length ?? 0,
|
||||
};
|
||||
this.searchRef = createRef();
|
||||
this.handleRetry = this.handleRetry.bind(this);
|
||||
@@ -160,11 +160,11 @@ class ArtikelMarkenPage extends PureComponent {
|
||||
componentDidMount() {
|
||||
setLastArticleSku(null);
|
||||
|
||||
this.unsub = brandAutomat.subscribe(this, (state) => ({
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
brands: state.filteredBrands,
|
||||
totalCount: state.brands?.length ?? 0,
|
||||
status: state.status,
|
||||
error: state.error ? backendErrorFrom({ message: state.error }) : null,
|
||||
status: state.brandsStatus,
|
||||
error: state.brandsError ? backendErrorFrom({ message: state.brandsError }) : null,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ class ArtikelMarkenPage extends PureComponent {
|
||||
}
|
||||
|
||||
handleRetry() {
|
||||
void brandAutomat.actions.load().catch((err) => notifyError(err));
|
||||
void companyAutomat.actions.loadBrands().catch((err) => notifyError(err));
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -4,7 +4,7 @@ import TuneIcon from '@mui/icons-material/Tune';
|
||||
import StyleIcon from '@mui/icons-material/Style';
|
||||
import { TabbedPanelTileGrid } from 'dashgui/panels';
|
||||
import withNavigate from '../../components/withNavigate';
|
||||
import { setLastArticleSku } from '../../automats';
|
||||
import { setLastArticleSku } from '../../automats/navigationAutomat';
|
||||
import {
|
||||
AuswahlMerkmaleTab,
|
||||
TextMerkmaleTab,
|
||||
|
||||
@@ -22,7 +22,7 @@ import BackendError from '../../components/BackendError';
|
||||
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import { setLastArticleSku } from '../../automats';
|
||||
import { setLastArticleSku } from '../../automats/navigationAutomat';
|
||||
import { labeltemplate } from '../../api';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import {
|
||||
|
||||
@@ -12,14 +12,14 @@ import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||
import withNavigate from '../../components/withNavigate';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import carrierAutomat from '../../automats/carrierAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { CarrierItem } from '../../components/carrier';
|
||||
import { GREEN_PANEL } from '../../colors';
|
||||
|
||||
class VersandartenSearchField extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const initial = carrierAutomat.state.filter || '';
|
||||
const initial = companyAutomat.state.carrierFilter || '';
|
||||
this.state = {
|
||||
input: initial,
|
||||
submitting: false,
|
||||
@@ -28,8 +28,8 @@ class VersandartenSearchField extends PureComponent {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsub = carrierAutomat.subscribe(this, (state) => ({
|
||||
input: state.filter,
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
input: state.carrierFilter,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class VersandartenSearchField extends PureComponent {
|
||||
|
||||
onInputChange(input) {
|
||||
this.setState({ input });
|
||||
carrierAutomat.actions.filter(input);
|
||||
companyAutomat.actions.filterCarriers(input);
|
||||
}
|
||||
|
||||
async handleSubmit() {
|
||||
@@ -48,14 +48,14 @@ class VersandartenSearchField extends PureComponent {
|
||||
|
||||
this.setState({ submitting: true });
|
||||
try {
|
||||
const row = await carrierAutomat.actions.create({
|
||||
const row = await companyAutomat.actions.createCarrier({
|
||||
name,
|
||||
hasPackages: true,
|
||||
isReverse: false,
|
||||
});
|
||||
enqueueSnackbar(`Versandart „${row.name || name}“ angelegt.`, { variant: 'success' });
|
||||
this.setState({ input: '', submitting: false });
|
||||
carrierAutomat.actions.filter('');
|
||||
companyAutomat.actions.filterCarriers('');
|
||||
} catch (err) {
|
||||
this.setState({ submitting: false });
|
||||
notifyError(err);
|
||||
@@ -118,12 +118,12 @@ class AuftragVersandartenPage extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const initialCarriers =
|
||||
carrierAutomat.state.filteredCarriers ?? carrierAutomat.state.carriers ?? [];
|
||||
companyAutomat.state.filteredCarriers ?? companyAutomat.state.carriers ?? [];
|
||||
this.state = {
|
||||
status: carrierAutomat.state.status === 'ready' ? 'ready' : 'loading',
|
||||
error: carrierAutomat.state.error ? backendErrorFrom(carrierAutomat.state.error) : null,
|
||||
status: companyAutomat.state.carriersStatus === 'ready' ? 'ready' : 'loading',
|
||||
error: companyAutomat.state.carriersError ? backendErrorFrom(companyAutomat.state.carriersError) : null,
|
||||
carriers: initialCarriers,
|
||||
filter: carrierAutomat.state.filter ?? '',
|
||||
filter: companyAutomat.state.carrierFilter ?? '',
|
||||
};
|
||||
this.searchRef = createRef();
|
||||
this.listRef = createRef();
|
||||
@@ -137,15 +137,15 @@ class AuftragVersandartenPage extends PureComponent {
|
||||
componentDidMount() {
|
||||
this._cancelled = false;
|
||||
|
||||
this.unsub = carrierAutomat.subscribe(this, (state) => ({
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
carriers: state.filteredCarriers ?? state.carriers,
|
||||
filter: state.filter ?? '',
|
||||
status: state.status,
|
||||
error: state.error ? backendErrorFrom(state.error) : null,
|
||||
filter: state.carrierFilter ?? '',
|
||||
status: state.carriersStatus,
|
||||
error: state.carriersError ? backendErrorFrom(state.carriersError) : null,
|
||||
}));
|
||||
|
||||
if (carrierAutomat.state.status !== 'ready') {
|
||||
void carrierAutomat.actions.load().catch((err) => notifyError(err));
|
||||
if (companyAutomat.state.carriersStatus !== 'ready') {
|
||||
void companyAutomat.actions.loadCarriers().catch((err) => notifyError(err));
|
||||
} else {
|
||||
this._initSortable();
|
||||
}
|
||||
@@ -209,16 +209,16 @@ class AuftragVersandartenPage extends PureComponent {
|
||||
|
||||
async _persistOrder(carrierIds) {
|
||||
try {
|
||||
await carrierAutomat.actions.reorder(carrierIds);
|
||||
await companyAutomat.actions.reorderCarriers(carrierIds);
|
||||
} catch (err) {
|
||||
if (this._cancelled) return;
|
||||
notifyError(err);
|
||||
void carrierAutomat.actions.reload();
|
||||
void companyAutomat.actions.reloadCarriers();
|
||||
}
|
||||
}
|
||||
|
||||
handleRetry() {
|
||||
void carrierAutomat.actions.load().catch((err) => notifyError(err));
|
||||
void companyAutomat.actions.loadCarriers().catch((err) => notifyError(err));
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -13,7 +13,7 @@ import withNavigate from '../components/withNavigate';
|
||||
import { backendErrorFrom } from '../utils/backendError';
|
||||
import { notifyError } from '../utils/notify';
|
||||
import entityAutomat from '../automats/entityAutomat';
|
||||
import customergroupAutomat from '../automats/customergroupAutomat';
|
||||
import companyAutomat from '../automats/companyAutomat';
|
||||
import { getLowestSortNonSupplierCustomerGroup } from '../components/manifest/manifestConfig';
|
||||
import { Kunde } from '../components/kunde';
|
||||
import { BLUE_PANEL } from '../colors';
|
||||
@@ -69,8 +69,8 @@ class KundeSearchField extends PureComponent {
|
||||
|
||||
this.setState({ submitting: true });
|
||||
try {
|
||||
await customergroupAutomat.actions.ensureLoaded();
|
||||
const groups = customergroupAutomat.state.items ?? [];
|
||||
await companyAutomat.actions.ensureCustomergroupsLoaded();
|
||||
const groups = companyAutomat.state.customergroups ?? [];
|
||||
const defaultGroup = getLowestSortNonSupplierCustomerGroup(groups) ?? groups[0];
|
||||
if (!defaultGroup) {
|
||||
throw new Error('Keine Kundengruppe vorhanden.');
|
||||
@@ -190,7 +190,7 @@ class KundeListePage extends PureComponent {
|
||||
status: entityAutomat.state.status === 'ready' ? 'ready' : 'loading',
|
||||
error: entityAutomat.state.error ? backendErrorFrom(entityAutomat.state.error) : null,
|
||||
entities: initialEntities,
|
||||
groups: customergroupAutomat.state.items ?? [],
|
||||
groups: companyAutomat.state.customergroups ?? [],
|
||||
filter: entityAutomat.state.filter ?? '',
|
||||
};
|
||||
this.searchRef = createRef();
|
||||
@@ -206,15 +206,15 @@ class KundeListePage extends PureComponent {
|
||||
error: state.error ? backendErrorFrom(state.error) : null,
|
||||
}));
|
||||
|
||||
this.unsubGroups = customergroupAutomat.subscribe(this, (state) => ({
|
||||
groups: state.items,
|
||||
this.unsubGroups = companyAutomat.subscribe(this, (state) => ({
|
||||
groups: state.customergroups,
|
||||
}));
|
||||
|
||||
if (entityAutomat.state.status !== 'ready') {
|
||||
void entityAutomat.actions.load().catch((err) => notifyError(err));
|
||||
}
|
||||
if (customergroupAutomat.state.status !== 'ready') {
|
||||
void customergroupAutomat.actions.load().catch(() => { });
|
||||
if (companyAutomat.state.customergroupsStatus !== 'ready') {
|
||||
void companyAutomat.actions.loadCustomergroups().catch(() => { });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||
import { entity as entityApi } from '../../api';
|
||||
import entityAutomat from '../../automats/entityAutomat';
|
||||
import customergroupAutomat from '../../automats/customergroupAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
@@ -137,7 +137,7 @@ class KundeEditPage extends PureComponent {
|
||||
entityApi.get(entityId),
|
||||
entityApi.listAddresses(entityId),
|
||||
entityApi.listContacts(entityId),
|
||||
customergroupAutomat.actions.ensureLoaded(),
|
||||
companyAutomat.actions.ensureCustomergroupsLoaded(),
|
||||
]);
|
||||
if (this._cancelled) return;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||
import withNavigate from '../../components/withNavigate';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import customergroupAutomat from '../../automats/customergroupAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { KundeGruppe } from '../../components/kunde';
|
||||
import { PURPLE_BG } from '../../colors';
|
||||
|
||||
@@ -36,20 +36,20 @@ function getInitialParams(props) {
|
||||
class KundeGruppenSearchField extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const initial = customergroupAutomat.state.filter || props.prefill || '';
|
||||
const initial = companyAutomat.state.customergroupFilter || props.prefill || '';
|
||||
this.state = {
|
||||
input: initial,
|
||||
submitting: false,
|
||||
};
|
||||
if (props.prefill && !customergroupAutomat.state.filter) {
|
||||
customergroupAutomat.actions.filter(props.prefill);
|
||||
if (props.prefill && !companyAutomat.state.customergroupFilter) {
|
||||
companyAutomat.actions.filterCustomergroups(props.prefill);
|
||||
}
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsub = customergroupAutomat.subscribe(this, (state) => ({
|
||||
input: state.filter,
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
input: state.customergroupFilter,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ class KundeGruppenSearchField extends PureComponent {
|
||||
|
||||
onInputChange(input) {
|
||||
this.setState({ input });
|
||||
customergroupAutomat.actions.filter(input);
|
||||
companyAutomat.actions.filterCustomergroups(input);
|
||||
}
|
||||
|
||||
async handleSubmit() {
|
||||
@@ -68,10 +68,10 @@ class KundeGruppenSearchField extends PureComponent {
|
||||
|
||||
this.setState({ submitting: true });
|
||||
try {
|
||||
const row = await customergroupAutomat.actions.create({ name });
|
||||
const row = await companyAutomat.actions.createCustomergroup({ name });
|
||||
enqueueSnackbar(`Kundengruppe „${row.name || name}“ angelegt.`, { variant: 'success' });
|
||||
this.setState({ input: '', submitting: false });
|
||||
customergroupAutomat.actions.filter('');
|
||||
companyAutomat.actions.filterCustomergroups('');
|
||||
if (this.props.returnTo && this.props.navigate) {
|
||||
this.props.navigate(this.props.returnTo);
|
||||
}
|
||||
@@ -157,12 +157,12 @@ class KundeGruppenPage extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const initialGroups =
|
||||
customergroupAutomat.state.filteredItems ?? customergroupAutomat.state.items ?? [];
|
||||
companyAutomat.state.filteredCustomergroups ?? companyAutomat.state.customergroups ?? [];
|
||||
this.state = {
|
||||
status: customergroupAutomat.state.status === 'ready' ? 'ready' : 'loading',
|
||||
error: customergroupAutomat.state.error ? backendErrorFrom(customergroupAutomat.state.error) : null,
|
||||
status: companyAutomat.state.customergroupsStatus === 'ready' ? 'ready' : 'loading',
|
||||
error: companyAutomat.state.customergroupsError ? backendErrorFrom(companyAutomat.state.customergroupsError) : null,
|
||||
groups: initialGroups,
|
||||
filter: customergroupAutomat.state.filter ?? '',
|
||||
filter: companyAutomat.state.customergroupFilter ?? '',
|
||||
};
|
||||
this.searchRef = createRef();
|
||||
this.listRef = createRef();
|
||||
@@ -176,15 +176,15 @@ class KundeGruppenPage extends PureComponent {
|
||||
componentDidMount() {
|
||||
this._cancelled = false;
|
||||
|
||||
this.unsub = customergroupAutomat.subscribe(this, (state) => ({
|
||||
groups: state.filteredItems ?? state.items,
|
||||
filter: state.filter ?? '',
|
||||
status: state.status,
|
||||
error: state.error ? backendErrorFrom(state.error) : null,
|
||||
this.unsub = companyAutomat.subscribe(this, (state) => ({
|
||||
groups: state.filteredCustomergroups ?? state.customergroups,
|
||||
filter: state.customergroupFilter ?? '',
|
||||
status: state.customergroupsStatus,
|
||||
error: state.customergroupsError ? backendErrorFrom(state.customergroupsError) : null,
|
||||
}));
|
||||
|
||||
if (customergroupAutomat.state.status !== 'ready') {
|
||||
void customergroupAutomat.actions.load().catch((err) => notifyError(err));
|
||||
if (companyAutomat.state.customergroupsStatus !== 'ready') {
|
||||
void companyAutomat.actions.loadCustomergroups().catch((err) => notifyError(err));
|
||||
} else {
|
||||
this._initSortable();
|
||||
}
|
||||
@@ -248,16 +248,16 @@ class KundeGruppenPage extends PureComponent {
|
||||
|
||||
async _persistOrder(customergroupIds) {
|
||||
try {
|
||||
await customergroupAutomat.actions.reorder(customergroupIds);
|
||||
await companyAutomat.actions.reorderCustomergroups(customergroupIds);
|
||||
} catch (err) {
|
||||
if (this._cancelled) return;
|
||||
notifyError(err);
|
||||
void customergroupAutomat.actions.reload();
|
||||
void companyAutomat.actions.reloadCustomergroups();
|
||||
}
|
||||
}
|
||||
|
||||
handleRetry() {
|
||||
void customergroupAutomat.actions.load().catch((err) => notifyError(err));
|
||||
void companyAutomat.actions.loadCustomergroups().catch((err) => notifyError(err));
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -10,7 +10,7 @@ import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||
import { entity as entityApi } from '../../api';
|
||||
import entityAutomat from '../../automats/entityAutomat';
|
||||
import customergroupAutomat from '../../automats/customergroupAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { getLowestSortNonSupplierCustomerGroup } from '../../components/manifest/manifestConfig';
|
||||
import { router } from '../../appRouter';
|
||||
import { requireDefined, requireObject } from '../../utils/apiShape';
|
||||
@@ -45,7 +45,7 @@ function getInitialPrefill(props) {
|
||||
class KundeNeuPage extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const groups = customergroupAutomat.state.items ?? [];
|
||||
const groups = companyAutomat.state.customergroups ?? [];
|
||||
const defaultGroup = getLowestSortNonSupplierCustomerGroup(groups) ?? groups[0];
|
||||
const prefill = getInitialPrefill(props);
|
||||
this.state = {
|
||||
@@ -62,7 +62,7 @@ class KundeNeuPage extends PureComponent {
|
||||
componentDidMount() {
|
||||
void this.loadNumber();
|
||||
if (this.state.customergroupId == null) {
|
||||
void customergroupAutomat.actions.ensureLoaded().then((groups) => {
|
||||
void companyAutomat.actions.ensureCustomergroupsLoaded().then((groups) => {
|
||||
const defaultGroup = getLowestSortNonSupplierCustomerGroup(groups) ?? groups?.[0];
|
||||
if (defaultGroup && this.state.customergroupId == null) {
|
||||
this.setState({ customergroupId: defaultGroup.id });
|
||||
@@ -125,7 +125,7 @@ class KundeNeuPage extends PureComponent {
|
||||
|
||||
render() {
|
||||
const { customergroupId, number, name, firstName, saving } = this.state;
|
||||
const groups = customergroupAutomat.state.items ?? [];
|
||||
const groups = companyAutomat.state.customergroups ?? [];
|
||||
|
||||
const canSave =
|
||||
customergroupId != null && name.trim() !== '' && !saving;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PureComponent } from 'react';
|
||||
import { DeliveryList } from '../components/manifest';
|
||||
import { setLastDeliveryPath } from '../automats';
|
||||
import { setLastDeliveryPath } from '../automats/navigationAutomat';
|
||||
|
||||
class LieferungListePage extends PureComponent {
|
||||
componentDidMount() {
|
||||
|
||||
@@ -18,8 +18,8 @@ import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||
import withNavigate from '../../components/withNavigate';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import { setLastDeliveryPath } from '../../automats';
|
||||
import supplierAutomat from '../../automats/supplierAutomat';
|
||||
import { setLastDeliveryPath } from '../../automats/navigationAutomat';
|
||||
import companyAutomat from '../../automats/companyAutomat';
|
||||
import { deliveryDraftAutomat } from '../../automats/deliveryEditorAutomat';
|
||||
import {
|
||||
GREEN_PANEL,
|
||||
@@ -109,7 +109,7 @@ class LieferungLieferantenPage extends PureComponent {
|
||||
async loadSuppliers() {
|
||||
this.setState({ status: 'loading', error: null });
|
||||
try {
|
||||
const list = await supplierAutomat.actions.load();
|
||||
const list = await companyAutomat.actions.loadSuppliers();
|
||||
if (this._cancelled) return;
|
||||
this.applySuppliers(list);
|
||||
} catch (err) {
|
||||
@@ -123,7 +123,7 @@ class LieferungLieferantenPage extends PureComponent {
|
||||
}
|
||||
|
||||
async refreshFromServer() {
|
||||
const list = await supplierAutomat.actions.reload();
|
||||
const list = await companyAutomat.actions.reloadSuppliers();
|
||||
if (this._cancelled) return;
|
||||
this.applySuppliers(list);
|
||||
}
|
||||
@@ -136,7 +136,7 @@ class LieferungLieferantenPage extends PureComponent {
|
||||
|
||||
this.setState({ busyKey: 'new' });
|
||||
try {
|
||||
const newSupplier = await supplierAutomat.actions.create({ name });
|
||||
const newSupplier = await companyAutomat.actions.createSupplier({ name });
|
||||
|
||||
// Assign the new supplier to the active delivery draft & clear unmatched name
|
||||
deliveryDraftAutomat.actions.setDeliveryEntity(newSupplier.id);
|
||||
@@ -170,7 +170,7 @@ class LieferungLieferantenPage extends PureComponent {
|
||||
|
||||
this.setState({ busyKey: supplier.id });
|
||||
try {
|
||||
await supplierAutomat.actions.update(supplier.id, { name: draft });
|
||||
await companyAutomat.actions.updateSupplier(supplier.id, { name: draft });
|
||||
await this.refreshFromServer();
|
||||
if (this._cancelled) return;
|
||||
this.setState({ busyKey: null });
|
||||
@@ -193,7 +193,7 @@ class LieferungLieferantenPage extends PureComponent {
|
||||
|
||||
this.setState({ busyKey: supplier.id });
|
||||
try {
|
||||
await supplierAutomat.actions.remove(supplier.id);
|
||||
await companyAutomat.actions.removeSupplier(supplier.id);
|
||||
await this.refreshFromServer();
|
||||
if (this._cancelled) return;
|
||||
this.setState({ busyKey: null });
|
||||
|
||||
@@ -38,8 +38,7 @@ import {
|
||||
BookDeliveryDialog,
|
||||
} from '../../components/manifest';
|
||||
import { loadParties, apiFor, resolveLineSkus, sortSuppliers } from '../../components/manifest/manifestConfig';
|
||||
import supplierAutomat from '../../automats/supplierAutomat';
|
||||
import warehouseAutomat from '../../automats/warehouseAutomat';
|
||||
import companyAutomat, { setViewerPosition } from '../../automats/companyAutomat';
|
||||
import { article as articleApi, delivery as deliveryApi } from '../../api';
|
||||
import { getCategoryState } from '../../automats/categoryAutomat';
|
||||
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
||||
@@ -49,7 +48,7 @@ import { generatePdfThumbnail } from '../../utils/pdfThumbnail';
|
||||
import { generateTextThumbnail } from '../../utils/textThumbnail';
|
||||
import { extractPdfText } from '../../utils/pdfText';
|
||||
import { getDeliveryEditorAutomat } from '../../automats/deliveryEditorAutomat';
|
||||
import { setLastDeliveryPath, userSettingsAutomat, setViewerPosition } from '../../automats';
|
||||
import { setLastDeliveryPath } from '../../automats/navigationAutomat';
|
||||
import chatAutomat from '../../automats/chatAutomat';
|
||||
|
||||
import { fetchDeliveryLedgerSummary } from '../../utils/deliveryLedger';
|
||||
@@ -110,10 +109,10 @@ class LieferungNeuPage extends PureComponent {
|
||||
this.cfg = manifestConfig('delivery');
|
||||
this.automat = getDeliveryEditorAutomat(props.docId);
|
||||
if (
|
||||
supplierAutomat.state.status === 'ready' &&
|
||||
companyAutomat.state.suppliersStatus === 'ready' &&
|
||||
(!this.automat.state.deliveryEntities || this.automat.state.deliveryEntities.length === 0)
|
||||
) {
|
||||
this.automat.actions.setDeliveryEntities(sortSuppliers(supplierAutomat.state.suppliers));
|
||||
this.automat.actions.setDeliveryEntities(sortSuppliers(companyAutomat.state.suppliers));
|
||||
}
|
||||
const initialDate = this.automat.state.date || (props.docId == null ? todayInputValue() : null);
|
||||
if (props.docId == null && !this.automat.state.date) {
|
||||
@@ -129,7 +128,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
pdfUploading: false,
|
||||
textEditorOpen: false,
|
||||
textEditorAttachment: null,
|
||||
viewerPosition: userSettingsAutomat.state.viewerPosition,
|
||||
viewerPosition: companyAutomat.state.viewerPosition,
|
||||
pdfViewerOpen: false,
|
||||
pdfViewerAttachment: null,
|
||||
articleSearchDialogOpen: false,
|
||||
@@ -139,7 +138,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
bookDeliveryDialogOpen: false,
|
||||
bookDeliveryPrefillLineId: null,
|
||||
pdfCreating: false,
|
||||
warehouses: warehouseAutomat.state.warehouses || [],
|
||||
warehouses: companyAutomat.state.warehouses || [],
|
||||
};
|
||||
this._cancelled = false;
|
||||
this.handleRetry = this.handleRetry.bind(this);
|
||||
@@ -183,18 +182,16 @@ class LieferungNeuPage extends PureComponent {
|
||||
|
||||
componentDidMount() {
|
||||
this.unsubscribe = this.automat.subscribe(this);
|
||||
this._settingsUnsub = userSettingsAutomat.subscribe(this, (state) => ({
|
||||
viewerPosition: state.viewerPosition,
|
||||
}));
|
||||
this._warehouseUnsub = warehouseAutomat.subscribe(this, (state) => ({
|
||||
warehouses: state.warehouses,
|
||||
}));
|
||||
this._supplierUnsub = supplierAutomat.subscribe(this, (state) => {
|
||||
if (state.status === 'ready' && Array.isArray(state.suppliers)) {
|
||||
this._companyUnsub = companyAutomat.subscribe(this, (state) => {
|
||||
if (state.suppliersStatus === 'ready' && Array.isArray(state.suppliers)) {
|
||||
this.automat.actions.setDeliveryEntities(sortSuppliers(state.suppliers));
|
||||
}
|
||||
return {
|
||||
viewerPosition: state.viewerPosition,
|
||||
warehouses: state.warehouses,
|
||||
};
|
||||
});
|
||||
void warehouseAutomat.actions.load().catch(() => { });
|
||||
void companyAutomat.actions.loadWarehouses().catch(() => { });
|
||||
this._cancelled = false;
|
||||
if (this.props.docId != null) {
|
||||
setLastDeliveryPath(`/lieferung/${this.props.docId}`);
|
||||
@@ -237,9 +234,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
this._skuDebounceTimers = null;
|
||||
}
|
||||
this.unsubscribe?.();
|
||||
this._settingsUnsub?.();
|
||||
this._warehouseUnsub?.();
|
||||
this._supplierUnsub?.();
|
||||
this._companyUnsub?.();
|
||||
}
|
||||
|
||||
async loadPartiesOnly() {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
netFromGross,
|
||||
parsePriceInput,
|
||||
} from './price';
|
||||
import brandAutomat from '../automats/brandAutomat';
|
||||
import companyAutomat from '../automats/companyAutomat';
|
||||
|
||||
export function sortedPhotos(article) {
|
||||
if (!Array.isArray(article?.photos)) return [];
|
||||
@@ -176,7 +176,7 @@ export function getArticleBrand(article) {
|
||||
return String(article.brandName).trim();
|
||||
}
|
||||
if (article.brandId != null) {
|
||||
const brands = brandAutomat?.state?.brands || [];
|
||||
const brands = companyAutomat?.state?.brands || [];
|
||||
const b = brands.find((x) => x.id === article.brandId);
|
||||
if (b?.name) return String(b.name).trim();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import numberAutomat from '../automats/numberAutomat';
|
||||
import companyAutomat from '../automats/companyAutomat';
|
||||
import quoteAutomat from '../automats/quoteAutomat';
|
||||
import orderAutomat from '../automats/orderAutomat';
|
||||
import deliveryAutomat from '../automats/deliveryAutomat';
|
||||
@@ -41,7 +41,7 @@ export function formatNumberPattern(prefix, padding, next) {
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getNextDocumentNumber(kind) {
|
||||
const numbers = numberAutomat.state.items ?? [];
|
||||
const numbers = companyAutomat.state.numbers ?? [];
|
||||
const seq = numbers.find((r) => r.type === kind);
|
||||
|
||||
let prefix = '';
|
||||
@@ -135,7 +135,7 @@ export function getNextDocumentNumber(kind) {
|
||||
export async function ensureDocumentNumber(kind) {
|
||||
const listAutomat = kind === 'quote' ? quoteAutomat : (kind === 'order' ? orderAutomat : (kind === 'delivery' ? deliveryAutomat : (kind === 'retoure' ? retoureAutomat : null)));
|
||||
await Promise.all([
|
||||
numberAutomat.actions.ensureLoaded().catch(() => {}),
|
||||
companyAutomat.actions.ensureNumbersLoaded().catch(() => {}),
|
||||
listAutomat?.actions?.ensureLoaded ? listAutomat.actions.ensureLoaded().catch(() => {}) : Promise.resolve(),
|
||||
]);
|
||||
|
||||
@@ -156,7 +156,7 @@ export async function ensureDocumentNumber(kind) {
|
||||
}
|
||||
|
||||
if (recentDocs.length > 0) {
|
||||
const numbers = numberAutomat.state.items ?? [];
|
||||
const numbers = companyAutomat.state.numbers ?? [];
|
||||
const seq = numbers.find((r) => r.type === kind);
|
||||
let prefix = seq?.prefix;
|
||||
if (prefix == null) {
|
||||
@@ -197,7 +197,7 @@ export async function ensureDocumentNumber(kind) {
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getMaxDocumentNumber(kind = 'quote') {
|
||||
const numbers = numberAutomat.state.items ?? [];
|
||||
const numbers = companyAutomat.state.numbers ?? [];
|
||||
const seq = numbers.find((r) => r.type === kind);
|
||||
if (seq) {
|
||||
const pad = Math.max(1, Number(seq.padding) || 6);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ledger as defaultLedgerApi, shipment as defaultShipmentApi } from '../api';
|
||||
import warehouseAutomat from '../automats/warehouseAutomat';
|
||||
import companyAutomat from '../automats/companyAutomat';
|
||||
|
||||
/**
|
||||
* Fetches ledger entries for an order by inspecting:
|
||||
@@ -100,7 +100,7 @@ export async function fetchOrderLedgerEntries(orderId, {
|
||||
})
|
||||
);
|
||||
|
||||
const warehouses = warehouseAutomat.state.warehouses || [];
|
||||
const warehouses = companyAutomat.state.warehouses || [];
|
||||
const allEntries = [];
|
||||
|
||||
for (const { target, entries } of results) {
|
||||
|
||||
Reference in New Issue
Block a user