1878 lines
58 KiB
JavaScript
1878 lines
58 KiB
JavaScript
/**
|
|
* @file companyAutomat.js
|
|
* @description Central company state container managing the active company profile
|
|
* and all company-coupled base data:
|
|
* - Company profile (`current`, tax rates, ISO language, root category ID)
|
|
* - Document number sequences (`numbers`)
|
|
* - POS terminals (`terminals`)
|
|
* - Suppliers & supplier article catalogs (`suppliers`, `articlesBySupplier`)
|
|
* - Units of measure (`units`)
|
|
* - User UI preferences (`viewerPosition`, `chatDrawerWidth`)
|
|
* - Warehouses (`warehouses`, `filteredWarehouses`, `warehouseFilter`)
|
|
* - Product properties (`properties`)
|
|
* - Customer groups (`customergroups`, `filteredCustomergroups`, `customergroupFilter`)
|
|
* - Shipping carriers (`carriers`, `filteredCarriers`, `carrierFilter`)
|
|
* - Brands (`brands`, `filteredBrands`, `brandFilter`)
|
|
* - Product attributes & values (`attributes`)
|
|
* - Payment accounts (`accounts`)
|
|
*
|
|
* Lives with the 1st-level navigation shell (`AppShell`) and triggers when
|
|
* `authAutomat.state.hasCompany` is true (`hasCompany` requires `hasAuth`).
|
|
*/
|
|
|
|
import { Automat } from 'react-automat';
|
|
import {
|
|
company as companyApi,
|
|
number as numberApi,
|
|
posterminal as posterminalApi,
|
|
printer as printerApi,
|
|
printeragent as printeragentApi,
|
|
agentprinter as agentprinterApi,
|
|
supplier as supplierApi,
|
|
unit as unitApi,
|
|
warehouse as warehouseApi,
|
|
propertyname as propertynameApi,
|
|
customergroup as customergroupApi,
|
|
carrier as carrierApi,
|
|
brand as brandApi,
|
|
attributename as attributenameApi,
|
|
attributevalue as attributevalueApi,
|
|
account as accountApi,
|
|
} from '../api';
|
|
import { requireArray, requireDefined, requireObject } from '../utils/apiShape';
|
|
import { normalizeCompany } from '../utils/companyForm';
|
|
import authAutomat from './authAutomat';
|
|
import sseAutomat from './sseAutomat';
|
|
|
|
// #region userSettings persistence helpers
|
|
const USER_SETTINGS_STORAGE_KEY = 'dash.userSettings';
|
|
const IDB_NAME = 'automat_db';
|
|
const IDB_STORE = 'states';
|
|
const IDB_USER_SETTINGS_KEY = 'userSettings';
|
|
|
|
function readInitialUserSettings() {
|
|
const defaults = { viewerPosition: 'top', chatDrawerWidth: 460 };
|
|
if (typeof window === 'undefined') return defaults;
|
|
try {
|
|
const raw = window.localStorage.getItem(USER_SETTINGS_STORAGE_KEY);
|
|
if (!raw) return defaults;
|
|
const parsed = JSON.parse(raw);
|
|
const viewerPosition =
|
|
parsed?.viewerPosition === 'top' ||
|
|
parsed?.viewerPosition === 'right' ||
|
|
parsed?.viewerPosition === 'below'
|
|
? parsed.viewerPosition
|
|
: defaults.viewerPosition;
|
|
const width = Number(parsed?.chatDrawerWidth);
|
|
const chatDrawerWidth = !Number.isNaN(width) && width >= 280 ? Math.round(width) : defaults.chatDrawerWidth;
|
|
return { viewerPosition, chatDrawerWidth };
|
|
} catch {
|
|
return defaults;
|
|
}
|
|
}
|
|
|
|
function persistUserSettings(settings) {
|
|
if (typeof window === 'undefined') return;
|
|
try {
|
|
window.localStorage.setItem(USER_SETTINGS_STORAGE_KEY, JSON.stringify(settings));
|
|
} catch {}
|
|
if (typeof indexedDB !== 'undefined') {
|
|
try {
|
|
const req = indexedDB.open(IDB_NAME, 1);
|
|
req.onupgradeneeded = () => {
|
|
const db = req.result;
|
|
if (!db.objectStoreNames.contains(IDB_STORE)) {
|
|
db.createObjectStore(IDB_STORE);
|
|
}
|
|
};
|
|
req.onsuccess = () => {
|
|
try {
|
|
const db = req.result;
|
|
const tx = db.transaction(IDB_STORE, 'readwrite');
|
|
tx.objectStore(IDB_STORE).put(settings, IDB_USER_SETTINGS_KEY);
|
|
} catch {}
|
|
};
|
|
} catch {}
|
|
}
|
|
}
|
|
|
|
function hydrateUserSettingsFromIdb(automat) {
|
|
if (typeof indexedDB === 'undefined') return;
|
|
try {
|
|
const req = indexedDB.open(IDB_NAME, 1);
|
|
req.onupgradeneeded = () => {
|
|
const db = req.result;
|
|
if (!db.objectStoreNames.contains(IDB_STORE)) {
|
|
db.createObjectStore(IDB_STORE);
|
|
}
|
|
};
|
|
req.onsuccess = () => {
|
|
try {
|
|
const db = req.result;
|
|
const tx = db.transaction(IDB_STORE, 'readonly');
|
|
const getReq = tx.objectStore(IDB_STORE).get(IDB_USER_SETTINGS_KEY);
|
|
getReq.onsuccess = () => {
|
|
const saved = getReq.result;
|
|
if (!saved || typeof saved !== 'object') return;
|
|
const patch = {};
|
|
if (
|
|
saved.viewerPosition === 'top' ||
|
|
saved.viewerPosition === 'right' ||
|
|
saved.viewerPosition === 'below'
|
|
) {
|
|
patch.viewerPosition = saved.viewerPosition;
|
|
}
|
|
const num = Number(saved.chatDrawerWidth);
|
|
if (!Number.isNaN(num) && num >= 280) {
|
|
patch.chatDrawerWidth = Math.round(num);
|
|
}
|
|
if (Object.keys(patch).length > 0) {
|
|
automat.setState(patch);
|
|
try {
|
|
window.localStorage.setItem(
|
|
USER_SETTINGS_STORAGE_KEY,
|
|
JSON.stringify({
|
|
viewerPosition: patch.viewerPosition ?? automat.state.viewerPosition,
|
|
chatDrawerWidth: patch.chatDrawerWidth ?? automat.state.chatDrawerWidth,
|
|
}),
|
|
);
|
|
} catch {}
|
|
}
|
|
};
|
|
} catch {}
|
|
};
|
|
} catch {}
|
|
}
|
|
// #endregion
|
|
|
|
// #region sorting & filtering helpers
|
|
const sortNumbers = (items) =>
|
|
[...items].sort((a, b) => String(a.type ?? '').localeCompare(String(b.type ?? ''), 'de'));
|
|
|
|
const sortUnits = (units) =>
|
|
[...units].sort((a, b) => {
|
|
const left = a.symbol ?? a.si ?? '';
|
|
const right = b.symbol ?? b.si ?? '';
|
|
return String(left).localeCompare(String(right), 'de');
|
|
});
|
|
|
|
const sortBrands = (brands) =>
|
|
[...brands].sort((a, b) => String(a.name ?? '').localeCompare(String(b.name ?? ''), 'de'));
|
|
|
|
const sortGroups = (items) =>
|
|
[...items].sort((a, b) => (Number(a.sort) || 0) - (Number(b.sort) || 0));
|
|
|
|
const sortTerminals = (terminals) => {
|
|
if (!Array.isArray(terminals)) return [];
|
|
return [...terminals].sort((a, b) => {
|
|
const sDiff = (Number(a.sort) || 0) - (Number(b.sort) || 0);
|
|
if (sDiff !== 0) return sDiff;
|
|
return (Number(a.id) || 0) - (Number(b.id) || 0);
|
|
});
|
|
};
|
|
|
|
const sortPrinterAgents = (agents) => {
|
|
if (!Array.isArray(agents)) return [];
|
|
return [...agents].sort((a, b) => {
|
|
const sDiff = (Number(a.sort) || 0) - (Number(b.sort) || 0);
|
|
if (sDiff !== 0) return sDiff;
|
|
return (Number(a.id) || 0) - (Number(b.id) || 0);
|
|
});
|
|
};
|
|
|
|
const sortAgentPrinters = (printers) => {
|
|
if (!Array.isArray(printers)) return [];
|
|
return [...printers].sort((a, b) => {
|
|
const sDiff = (Number(a.sort) || 0) - (Number(b.sort) || 0);
|
|
if (sDiff !== 0) return sDiff;
|
|
return (Number(a.id) || 0) - (Number(b.id) || 0);
|
|
});
|
|
};
|
|
|
|
const sortAccounts = (items) =>
|
|
[...items].sort((a, b) => String(a.name ?? '').localeCompare(String(b.name ?? ''), 'de'));
|
|
|
|
const filterByName = (items, filter) => {
|
|
const query = String(filter ?? '').trim().toLowerCase();
|
|
return query
|
|
? items.filter((item) => String(item.name ?? '').toLowerCase().includes(query))
|
|
: items;
|
|
};
|
|
|
|
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);
|
|
});
|
|
}
|
|
|
|
function sortAttributeValues(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: sortAttributeValues(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);
|
|
});
|
|
}
|
|
// #endregion
|
|
|
|
const initialUserSettings = readInitialUserSettings();
|
|
|
|
const initialState = {
|
|
// 1. Company profile
|
|
companies: [],
|
|
current: null,
|
|
status: 'idle',
|
|
error: null,
|
|
|
|
// 2. Number sequences
|
|
numbers: [],
|
|
numbersStatus: 'idle',
|
|
numbersError: null,
|
|
|
|
// 3. POS terminals
|
|
terminals: [],
|
|
terminalsCompanyId: null,
|
|
terminalsStatus: 'idle',
|
|
terminalsError: null,
|
|
|
|
// Printer agents & agent printers
|
|
printeragents: [],
|
|
printeragentsCompanyId: null,
|
|
printeragentsStatus: 'idle',
|
|
printeragentsError: null,
|
|
|
|
agentprinters: [],
|
|
agentprintersCompanyId: null,
|
|
agentprintersStatus: 'idle',
|
|
agentprintersError: null,
|
|
|
|
livePrinters: [], // LivePrinter[] from /api/printer/state or SSE printer/printerstate
|
|
|
|
// 4. Suppliers
|
|
suppliers: [],
|
|
articlesBySupplier: {},
|
|
articlesLoading: false,
|
|
suppliersStatus: 'idle',
|
|
suppliersError: null,
|
|
|
|
// 5. Units
|
|
units: [],
|
|
unitsStatus: 'idle',
|
|
unitsError: null,
|
|
|
|
// 6. User settings
|
|
viewerPosition: initialUserSettings.viewerPosition,
|
|
chatDrawerWidth: initialUserSettings.chatDrawerWidth,
|
|
|
|
// 7. Warehouses
|
|
warehouses: [],
|
|
filteredWarehouses: [],
|
|
warehouseFilter: '',
|
|
warehousesStatus: 'idle',
|
|
warehousesError: null,
|
|
|
|
// 8. Properties
|
|
properties: [],
|
|
propertiesStatus: 'idle',
|
|
propertiesError: null,
|
|
|
|
// 9. Customer groups
|
|
customergroups: [],
|
|
filteredCustomergroups: [],
|
|
customergroupFilter: '',
|
|
customergroupsStatus: 'idle',
|
|
customergroupsError: null,
|
|
|
|
// 10. Carriers
|
|
carriers: [],
|
|
filteredCarriers: [],
|
|
carrierFilter: '',
|
|
carriersStatus: 'idle',
|
|
carriersError: null,
|
|
|
|
// 11. Brands
|
|
brands: [],
|
|
filteredBrands: [],
|
|
brandFilter: '',
|
|
brandsStatus: 'idle',
|
|
brandsError: null,
|
|
|
|
// 12. Attributes & values
|
|
attributes: [],
|
|
attributesStatus: 'idle',
|
|
attributesError: null,
|
|
|
|
// 13. Payment accounts
|
|
accounts: [],
|
|
accountsStatus: 'idle',
|
|
accountsError: null,
|
|
};
|
|
|
|
let companyAutomat;
|
|
|
|
// #region in-flight deduplication promises
|
|
let inflightCompany = null;
|
|
let inflightNumbers = null;
|
|
let inflightTerminals = null;
|
|
let inflightPrinterAgents = null;
|
|
let inflightAgentPrinters = null;
|
|
let inflightLivePrinters = null;
|
|
let inflightSuppliers = null;
|
|
let inflightUnits = null;
|
|
let inflightWarehouses = null;
|
|
let inflightProperties = null;
|
|
let inflightCustomergroups = null;
|
|
let inflightCarriers = null;
|
|
let inflightBrands = null;
|
|
let inflightAttributes = null;
|
|
let inflightAccounts = null;
|
|
// #endregion
|
|
|
|
// #region internal state updaters & fetchers
|
|
const BRAND_FETCH_PAGE_SIZE = 100;
|
|
|
|
async function fetchAllBrands() {
|
|
let offset = 0;
|
|
let total = Infinity;
|
|
const collected = [];
|
|
while (offset === 0 || collected.length < total) {
|
|
const res = await brandApi.list({ offset, limit: BRAND_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) };
|
|
}
|
|
|
|
function updateStateWithBrands(brands, extra = {}) {
|
|
const list = Array.isArray(brands) ? brands : [];
|
|
const filteredBrands = filterByName(list, companyAutomat.state.brandFilter);
|
|
companyAutomat.setState({ brands: list, filteredBrands, ...extra });
|
|
return list;
|
|
}
|
|
|
|
function updateStateWithWarehouses(warehouses, extra = {}) {
|
|
const list = Array.isArray(warehouses) ? warehouses : [];
|
|
const filteredWarehouses = filterByName(list, companyAutomat.state.warehouseFilter);
|
|
companyAutomat.setState({ warehouses: list, filteredWarehouses, ...extra });
|
|
return list;
|
|
}
|
|
|
|
function updateStateWithCustomergroups(groups, extra = {}) {
|
|
const list = sortGroups(Array.isArray(groups) ? groups : []);
|
|
const filteredCustomergroups = filterByName(list, companyAutomat.state.customergroupFilter);
|
|
companyAutomat.setState({ customergroups: list, filteredCustomergroups, ...extra });
|
|
return list;
|
|
}
|
|
|
|
function updateStateWithCarriers(carriers, extra = {}) {
|
|
const list = Array.isArray(carriers) ? carriers : [];
|
|
const filteredCarriers = filterByName(list, companyAutomat.state.carrierFilter);
|
|
companyAutomat.setState({ carriers: list, filteredCarriers, ...extra });
|
|
return list;
|
|
}
|
|
|
|
function patchTerminalCategories(terminalId, categoryIds) {
|
|
companyAutomat.setState({
|
|
terminals: companyAutomat.state.terminals.map((t) =>
|
|
t.id === terminalId ? { ...t, categoryIds } : t,
|
|
),
|
|
});
|
|
}
|
|
|
|
async function fetchSuppliersList() {
|
|
const data = await supplierApi.list();
|
|
requireObject(data, 'GET /api/supplier');
|
|
return requireArray(data.suppliers, 'suppliers');
|
|
}
|
|
|
|
async function fetchWarehousesList() {
|
|
const data = await warehouseApi.list();
|
|
requireObject(data, 'GET /api/warehouse');
|
|
return requireArray(data.warehouses, 'warehouses');
|
|
}
|
|
|
|
async function fetchCustomergroupsList() {
|
|
const data = await customergroupApi.list();
|
|
requireObject(data, 'GET /api/customergroup');
|
|
return requireArray(data.customergroups, 'customergroups');
|
|
}
|
|
|
|
async function fetchCarriersList() {
|
|
const data = await carrierApi.list();
|
|
if (Array.isArray(data)) return data;
|
|
if (data && Array.isArray(data.carriers)) return data.carriers;
|
|
return [];
|
|
}
|
|
// #endregion
|
|
|
|
const actions = {
|
|
// #region 1. Company profile & orchestration
|
|
// MARK: calls company.list from ../api/index.js
|
|
async load() {
|
|
const hasAuth = Boolean(
|
|
authAutomat.state.hasAuth ??
|
|
(authAutomat.state.status === 'authed' && authAutomat.state.user),
|
|
);
|
|
if (!hasAuth) return null;
|
|
if (inflightCompany) return inflightCompany;
|
|
companyAutomat.setState({ status: 'loading', error: null });
|
|
inflightCompany = (async () => {
|
|
try {
|
|
const data = await companyApi.list();
|
|
const companies = Array.isArray(data?.companies)
|
|
? data.companies.map((item) => normalizeCompany(item)).filter(Boolean)
|
|
: [];
|
|
const companyId = authAutomat.state.user?.selectedCompanyId;
|
|
const current =
|
|
companyId != null
|
|
? (companies.find(
|
|
(item) => item.id === companyId || String(item.id) === String(companyId),
|
|
) ?? null)
|
|
: null;
|
|
if (companyId != null && !current) {
|
|
throw new Error('Unternehmen nicht gefunden.');
|
|
}
|
|
companyAutomat.setState({ companies, current, status: 'succeeded', error: null });
|
|
return current;
|
|
} catch (err) {
|
|
companyAutomat.setState({ status: 'failed', error: err?.message ?? String(err) });
|
|
throw err;
|
|
} finally {
|
|
inflightCompany = null;
|
|
}
|
|
})();
|
|
return inflightCompany;
|
|
},
|
|
|
|
// MARK: calls company.list from ../api/index.js
|
|
async reload() {
|
|
const hasAuth = Boolean(
|
|
authAutomat.state.hasAuth ??
|
|
(authAutomat.state.status === 'authed' && authAutomat.state.user),
|
|
);
|
|
if (!hasAuth) return null;
|
|
try {
|
|
const data = await companyApi.list();
|
|
const companies = Array.isArray(data?.companies)
|
|
? data.companies.map((item) => normalizeCompany(item)).filter(Boolean)
|
|
: [];
|
|
const companyId = authAutomat.state.user?.selectedCompanyId;
|
|
const current =
|
|
companyId != null
|
|
? (companies.find(
|
|
(item) => item.id === companyId || String(item.id) === String(companyId),
|
|
) ?? companyAutomat.state.current)
|
|
: null;
|
|
companyAutomat.setState({ companies, current, status: 'succeeded', error: null });
|
|
return current;
|
|
} catch (err) {
|
|
companyAutomat.setState({ error: err?.message ?? String(err) });
|
|
throw err;
|
|
}
|
|
},
|
|
|
|
// MARK: calls company.update and company.list from ../api/index.js
|
|
async update(payload) {
|
|
const companyId = authAutomat.state.user?.selectedCompanyId;
|
|
if (!companyId) throw new Error('Kein Unternehmen ausgewählt.');
|
|
companyAutomat.setState({ status: 'loading', error: null });
|
|
try {
|
|
await companyApi.update(companyId, payload);
|
|
const data = await companyApi.list();
|
|
const companies = Array.isArray(data?.companies)
|
|
? data.companies
|
|
.map((item) =>
|
|
item.id === companyId || String(item.id) === String(companyId)
|
|
? normalizeCompany(item, payload)
|
|
: normalizeCompany(item),
|
|
)
|
|
.filter(Boolean)
|
|
: [];
|
|
const normalized =
|
|
companies.find((item) => item.id === companyId || String(item.id) === String(companyId)) ??
|
|
null;
|
|
if (!normalized) throw new Error('Unternehmen nicht gefunden.');
|
|
companyAutomat.setState({ companies, current: normalized, status: 'succeeded', error: null });
|
|
return normalized;
|
|
} catch (err) {
|
|
companyAutomat.setState({ status: 'failed', error: err?.message ?? String(err) });
|
|
throw err;
|
|
}
|
|
},
|
|
|
|
// MARK: loads all company-coupled base data in parallel
|
|
async loadAll() {
|
|
if (!authAutomat.state.hasCompany) return;
|
|
await Promise.allSettled([
|
|
actions.load(),
|
|
actions.loadNumbers(),
|
|
actions.loadTerminals(),
|
|
actions.loadPrinterAgents(),
|
|
actions.loadAgentPrinters(),
|
|
actions.loadLivePrinters(),
|
|
actions.loadSuppliers(),
|
|
actions.loadUnits(),
|
|
actions.loadWarehouses(),
|
|
actions.loadProperties(),
|
|
actions.loadCustomergroups(),
|
|
actions.loadCarriers(),
|
|
actions.loadBrands(),
|
|
actions.loadAttributes(),
|
|
actions.loadAccounts(),
|
|
]);
|
|
},
|
|
|
|
reset() {
|
|
inflightCompany = null;
|
|
inflightNumbers = null;
|
|
inflightTerminals = null;
|
|
inflightPrinterAgents = null;
|
|
inflightAgentPrinters = null;
|
|
inflightLivePrinters = null;
|
|
inflightSuppliers = null;
|
|
inflightUnits = null;
|
|
inflightWarehouses = null;
|
|
inflightProperties = null;
|
|
inflightCustomergroups = null;
|
|
inflightCarriers = null;
|
|
inflightBrands = null;
|
|
inflightAttributes = null;
|
|
inflightAccounts = null;
|
|
companyAutomat.setState({
|
|
...initialState,
|
|
viewerPosition: companyAutomat.state.viewerPosition,
|
|
chatDrawerWidth: companyAutomat.state.chatDrawerWidth,
|
|
});
|
|
},
|
|
// #endregion
|
|
|
|
// #region 2. Number sequences (number)
|
|
// MARK: calls number.list from ../api/index.js
|
|
loadNumbers() {
|
|
if (inflightNumbers) return inflightNumbers;
|
|
inflightNumbers = (async () => {
|
|
companyAutomat.setState({ numbersStatus: 'loading', numbersError: null });
|
|
try {
|
|
const data = await numberApi.list();
|
|
const numbers = sortNumbers(data.numbers ?? []);
|
|
companyAutomat.setState({ numbers, numbersStatus: 'ready', numbersError: null });
|
|
return numbers;
|
|
} catch (error) {
|
|
companyAutomat.setState({ numbersStatus: 'failed', numbersError: error });
|
|
throw error;
|
|
} finally {
|
|
inflightNumbers = null;
|
|
}
|
|
})();
|
|
return inflightNumbers;
|
|
},
|
|
|
|
async reloadNumbers() {
|
|
try {
|
|
const data = await numberApi.list();
|
|
const numbers = sortNumbers(data.numbers ?? []);
|
|
companyAutomat.setState({ numbers, numbersStatus: 'ready', numbersError: null });
|
|
return numbers;
|
|
} catch (error) {
|
|
companyAutomat.setState({ numbersError: error });
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
ensureNumbersLoaded() {
|
|
if (companyAutomat.state.numbersStatus === 'ready') {
|
|
return Promise.resolve(companyAutomat.state.numbers);
|
|
}
|
|
return actions.loadNumbers();
|
|
},
|
|
|
|
replaceNumbers(items) {
|
|
const numbers = sortNumbers(Array.isArray(items) ? items : []);
|
|
companyAutomat.setState({ numbers, numbersStatus: 'ready', numbersError: null });
|
|
return numbers;
|
|
},
|
|
// #endregion
|
|
|
|
// #region 3. POS terminals (posterminal)
|
|
// MARK: calls posterminal.list from ../api/index.js
|
|
async loadTerminals() {
|
|
const companyId =
|
|
authAutomat.state.user?.selectedCompanyId ?? companyAutomat.state.current?.id;
|
|
if (!companyId) return [];
|
|
if (inflightTerminals) return inflightTerminals;
|
|
companyAutomat.setState({ terminalsStatus: 'loading', terminalsError: null });
|
|
inflightTerminals = (async () => {
|
|
try {
|
|
const res = await posterminalApi.list();
|
|
const terminals = sortTerminals(Array.isArray(res.posterminals) ? res.posterminals : []);
|
|
companyAutomat.setState({
|
|
terminals,
|
|
terminalsCompanyId: companyId,
|
|
terminalsStatus: 'succeeded',
|
|
terminalsError: null,
|
|
});
|
|
return terminals;
|
|
} catch (err) {
|
|
companyAutomat.setState({
|
|
terminalsStatus: 'failed',
|
|
terminalsError: err?.message ?? String(err),
|
|
});
|
|
throw err;
|
|
} finally {
|
|
inflightTerminals = null;
|
|
}
|
|
})();
|
|
return inflightTerminals;
|
|
},
|
|
|
|
loadPosterminals() {
|
|
return actions.loadTerminals();
|
|
},
|
|
|
|
reloadTerminals() {
|
|
return actions.loadTerminals();
|
|
},
|
|
|
|
async createTerminal(payload) {
|
|
const res = await posterminalApi.create(payload);
|
|
const terminal = res.posterminal;
|
|
companyAutomat.setState({
|
|
terminals: sortTerminals([...companyAutomat.state.terminals, terminal]),
|
|
});
|
|
return terminal;
|
|
},
|
|
|
|
createPosterminal(payload) {
|
|
return actions.createTerminal(payload);
|
|
},
|
|
|
|
async updateTerminal(terminalId, patch) {
|
|
const res = await posterminalApi.update(terminalId, patch);
|
|
const updated = res.posterminal;
|
|
companyAutomat.setState({
|
|
terminals: sortTerminals(
|
|
companyAutomat.state.terminals.map((t) =>
|
|
t.id === terminalId ? { ...t, ...updated } : t,
|
|
),
|
|
),
|
|
});
|
|
return updated;
|
|
},
|
|
|
|
updatePosterminal(terminalId, patch) {
|
|
return actions.updateTerminal(terminalId, patch);
|
|
},
|
|
|
|
async deleteTerminal(terminalId) {
|
|
await posterminalApi.delete(terminalId);
|
|
companyAutomat.setState({
|
|
terminals: companyAutomat.state.terminals.filter((t) => t.id !== terminalId),
|
|
});
|
|
},
|
|
|
|
deletePosterminal(terminalId) {
|
|
return actions.deleteTerminal(terminalId);
|
|
},
|
|
|
|
async enableAllTerminalCategories({ terminalId }) {
|
|
const res = await posterminalApi.enableAllCategories(terminalId);
|
|
patchTerminalCategories(terminalId, res.categoryIds);
|
|
return res;
|
|
},
|
|
|
|
enableAll(payload) {
|
|
return actions.enableAllTerminalCategories(payload);
|
|
},
|
|
|
|
async disableAllTerminalCategories({ terminalId }) {
|
|
const res = await posterminalApi.disableAllCategories(terminalId);
|
|
patchTerminalCategories(terminalId, res.categoryIds);
|
|
return res;
|
|
},
|
|
|
|
disableAll(payload) {
|
|
return actions.disableAllTerminalCategories(payload);
|
|
},
|
|
|
|
async enableTerminalCategory({ terminalId, categoryId }) {
|
|
const res = await posterminalApi.enableCategory(terminalId, categoryId);
|
|
patchTerminalCategories(terminalId, res.categoryIds);
|
|
return res;
|
|
},
|
|
|
|
enableCategory(payload) {
|
|
return actions.enableTerminalCategory(payload);
|
|
},
|
|
|
|
async disableTerminalCategory({ terminalId, categoryId }) {
|
|
const res = await posterminalApi.disableCategory(terminalId, categoryId);
|
|
patchTerminalCategories(terminalId, res.categoryIds);
|
|
return res;
|
|
},
|
|
|
|
disableCategory(payload) {
|
|
return actions.disableTerminalCategory(payload);
|
|
},
|
|
// #endregion
|
|
|
|
// #region Printer agents (printeragent)
|
|
// MARK: calls printeragent.list from ../api/index.js
|
|
async loadPrinterAgents() {
|
|
const companyId =
|
|
authAutomat.state.user?.selectedCompanyId ?? companyAutomat.state.current?.id;
|
|
if (!companyId) return [];
|
|
if (inflightPrinterAgents) return inflightPrinterAgents;
|
|
companyAutomat.setState({ printeragentsStatus: 'loading', printeragentsError: null });
|
|
inflightPrinterAgents = (async () => {
|
|
try {
|
|
const res = await printeragentApi.list();
|
|
const printeragents = sortPrinterAgents(Array.isArray(res.printeragents) ? res.printeragents : []);
|
|
companyAutomat.setState({
|
|
printeragents,
|
|
printeragentsCompanyId: companyId,
|
|
printeragentsStatus: 'succeeded',
|
|
printeragentsError: null,
|
|
});
|
|
return printeragents;
|
|
} catch (err) {
|
|
companyAutomat.setState({
|
|
printeragentsStatus: 'failed',
|
|
printeragentsError: err?.message ?? String(err),
|
|
});
|
|
throw err;
|
|
} finally {
|
|
inflightPrinterAgents = null;
|
|
}
|
|
})();
|
|
return inflightPrinterAgents;
|
|
},
|
|
|
|
reloadPrinterAgents() {
|
|
return actions.loadPrinterAgents();
|
|
},
|
|
|
|
async createPrinterAgent(payload) {
|
|
const res = await printeragentApi.create(payload);
|
|
const agent = res.printeragent;
|
|
companyAutomat.setState({
|
|
printeragents: sortPrinterAgents([...companyAutomat.state.printeragents, agent]),
|
|
});
|
|
return agent;
|
|
},
|
|
|
|
async updatePrinterAgent(agentId, patch) {
|
|
const res = await printeragentApi.update(agentId, patch);
|
|
const updated = res.printeragent;
|
|
companyAutomat.setState({
|
|
printeragents: sortPrinterAgents(
|
|
companyAutomat.state.printeragents.map((a) =>
|
|
a.id === agentId ? { ...a, ...updated } : a,
|
|
),
|
|
),
|
|
});
|
|
return updated;
|
|
},
|
|
|
|
async deletePrinterAgent(agentId) {
|
|
await printeragentApi.delete(agentId);
|
|
companyAutomat.setState({
|
|
printeragents: companyAutomat.state.printeragents.filter((a) => a.id !== agentId),
|
|
agentprinters: companyAutomat.state.agentprinters.filter((p) => p.printeragentId !== agentId),
|
|
});
|
|
},
|
|
// #endregion
|
|
|
|
// #region Agent printers (agentprinter) & live telemetry
|
|
// MARK: calls agentprinter.list from ../api/index.js
|
|
async loadAgentPrinters(printeragentId) {
|
|
const companyId =
|
|
authAutomat.state.user?.selectedCompanyId ?? companyAutomat.state.current?.id;
|
|
if (!companyId) return [];
|
|
if (inflightAgentPrinters) return inflightAgentPrinters;
|
|
companyAutomat.setState({ agentprintersStatus: 'loading', agentprintersError: null });
|
|
inflightAgentPrinters = (async () => {
|
|
try {
|
|
const res = await agentprinterApi.list(printeragentId);
|
|
const agentprinters = sortAgentPrinters(Array.isArray(res.agentprinters) ? res.agentprinters : []);
|
|
companyAutomat.setState({
|
|
agentprinters,
|
|
agentprintersCompanyId: companyId,
|
|
agentprintersStatus: 'succeeded',
|
|
agentprintersError: null,
|
|
});
|
|
return agentprinters;
|
|
} catch (err) {
|
|
companyAutomat.setState({
|
|
agentprintersStatus: 'failed',
|
|
agentprintersError: err?.message ?? String(err),
|
|
});
|
|
throw err;
|
|
} finally {
|
|
inflightAgentPrinters = null;
|
|
}
|
|
})();
|
|
return inflightAgentPrinters;
|
|
},
|
|
|
|
reloadAgentPrinters(printeragentId) {
|
|
return actions.loadAgentPrinters(printeragentId);
|
|
},
|
|
|
|
async updateAgentPrinter(id, patch) {
|
|
const res = await agentprinterApi.update(id, patch);
|
|
const updated = res.agentprinter;
|
|
companyAutomat.setState({
|
|
agentprinters: sortAgentPrinters(
|
|
companyAutomat.state.agentprinters.map((p) =>
|
|
p.id === id ? { ...p, ...updated } : p,
|
|
),
|
|
),
|
|
});
|
|
return updated;
|
|
},
|
|
|
|
async reorderAgentPrinters({ printeragentId, agentprinterIds }) {
|
|
await agentprinterApi.order({ printeragentId, agentprinterIds });
|
|
const idOrder = new Map(agentprinterIds.map((id, index) => [id, index]));
|
|
companyAutomat.setState({
|
|
agentprinters: sortAgentPrinters(
|
|
companyAutomat.state.agentprinters.map((p) =>
|
|
idOrder.has(p.id) ? { ...p, sort: idOrder.get(p.id) } : p,
|
|
),
|
|
),
|
|
});
|
|
},
|
|
|
|
async deleteAgentPrinter(id) {
|
|
await agentprinterApi.delete(id);
|
|
companyAutomat.setState({
|
|
agentprinters: companyAutomat.state.agentprinters.filter((p) => p.id !== id),
|
|
});
|
|
},
|
|
|
|
async loadLivePrinters() {
|
|
const companyId =
|
|
authAutomat.state.user?.selectedCompanyId ?? companyAutomat.state.current?.id;
|
|
if (!companyId) return [];
|
|
if (inflightLivePrinters) return inflightLivePrinters;
|
|
inflightLivePrinters = (async () => {
|
|
try {
|
|
const res = await printerApi.state();
|
|
const livePrinters = Array.isArray(res.printers) ? res.printers : [];
|
|
companyAutomat.setState({ livePrinters });
|
|
return livePrinters;
|
|
} catch (err) {
|
|
return [];
|
|
} finally {
|
|
inflightLivePrinters = null;
|
|
}
|
|
})();
|
|
return inflightLivePrinters;
|
|
},
|
|
|
|
setLivePrinters(printers) {
|
|
companyAutomat.setState({
|
|
livePrinters: Array.isArray(printers) ? printers : [],
|
|
});
|
|
},
|
|
// #endregion
|
|
|
|
// #region 4. Suppliers (supplier)
|
|
// MARK: calls supplier.list from ../api/index.js
|
|
loadSuppliers() {
|
|
if (inflightSuppliers) return inflightSuppliers;
|
|
inflightSuppliers = (async () => {
|
|
companyAutomat.setState({ suppliersStatus: 'loading', suppliersError: null });
|
|
try {
|
|
const suppliers = await fetchSuppliersList();
|
|
companyAutomat.setState({ suppliers, suppliersStatus: 'ready', suppliersError: null });
|
|
void actions.loadAllSupplierArticles().catch(() => {});
|
|
return suppliers;
|
|
} catch (error) {
|
|
companyAutomat.setState({ suppliersStatus: 'failed', suppliersError: error });
|
|
throw error;
|
|
} finally {
|
|
inflightSuppliers = null;
|
|
}
|
|
})();
|
|
return inflightSuppliers;
|
|
},
|
|
|
|
async reloadSuppliers() {
|
|
if (inflightSuppliers) return inflightSuppliers;
|
|
inflightSuppliers = (async () => {
|
|
try {
|
|
const suppliers = await fetchSuppliersList();
|
|
companyAutomat.setState({ suppliers, suppliersStatus: 'ready', suppliersError: null });
|
|
void actions.loadAllSupplierArticles().catch(() => {});
|
|
return suppliers;
|
|
} catch (error) {
|
|
companyAutomat.setState({ suppliersError: error });
|
|
throw error;
|
|
} finally {
|
|
inflightSuppliers = null;
|
|
}
|
|
})();
|
|
return inflightSuppliers;
|
|
},
|
|
|
|
async loadArticlesForSupplier(supplierId) {
|
|
const sId = String(supplierId);
|
|
if (companyAutomat.state.articlesBySupplier?.[sId]) {
|
|
return companyAutomat.state.articlesBySupplier[sId];
|
|
}
|
|
const res = await supplierApi.getArticles(supplierId);
|
|
const articles = Array.isArray(res?.articles) ? res.articles : [];
|
|
companyAutomat.setState({
|
|
articlesBySupplier: {
|
|
...(companyAutomat.state.articlesBySupplier || {}),
|
|
[sId]: articles,
|
|
},
|
|
});
|
|
return articles;
|
|
},
|
|
|
|
async loadAllSupplierArticles() {
|
|
const suppliers = companyAutomat.state.suppliers;
|
|
if (!suppliers.length) return {};
|
|
companyAutomat.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 = { ...(companyAutomat.state.articlesBySupplier || {}) };
|
|
for (const r of results) {
|
|
if (r.status === 'fulfilled') {
|
|
nextMap[r.value.id] = r.value.articles;
|
|
}
|
|
}
|
|
companyAutomat.setState({ articlesBySupplier: nextMap, articlesLoading: false });
|
|
return nextMap;
|
|
} catch {
|
|
companyAutomat.setState({ articlesLoading: false });
|
|
return companyAutomat.state.articlesBySupplier || {};
|
|
}
|
|
},
|
|
|
|
syncArticleSuppliers(sku, suppliers) {
|
|
const artSku = String(sku).trim().toLowerCase();
|
|
const assignedSupplierIds = new Set(
|
|
(suppliers || []).map((s) => String(s.entityId ?? s.supplierId ?? s.id))
|
|
);
|
|
const prevMap = companyAutomat.state.articlesBySupplier || {};
|
|
let changed = false;
|
|
const nextMap = { ...prevMap };
|
|
|
|
for (const sId of assignedSupplierIds) {
|
|
const currentList = nextMap[sId] ? [...nextMap[sId]] : [];
|
|
const hasSku = currentList.some(
|
|
(item) => String(item.article?.sku ?? item.sku ?? '').toLowerCase() === artSku
|
|
);
|
|
if (!hasSku) {
|
|
currentList.push({ sku, article: { sku } });
|
|
nextMap[sId] = currentList;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
for (const [sId, list] of Object.entries(nextMap)) {
|
|
if (!assignedSupplierIds.has(sId) && Array.isArray(list)) {
|
|
const filtered = list.filter(
|
|
(item) => String(item.article?.sku ?? item.sku ?? '').toLowerCase() !== artSku
|
|
);
|
|
if (filtered.length !== list.length) {
|
|
nextMap[sId] = filtered;
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
companyAutomat.setState({ articlesBySupplier: nextMap });
|
|
}
|
|
},
|
|
|
|
async createSupplier(body) {
|
|
const data = await supplierApi.create(body);
|
|
requireObject(data, 'POST /api/supplier');
|
|
requireObject(data.supplier, 'supplier');
|
|
requireDefined(data.supplier.id, 'supplier.id');
|
|
await actions.reloadSuppliers();
|
|
return data.supplier;
|
|
},
|
|
|
|
async updateSupplier(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 actions.reloadSuppliers();
|
|
return data.supplier;
|
|
},
|
|
|
|
async removeSupplier(id) {
|
|
await supplierApi.delete(id);
|
|
await actions.reloadSuppliers();
|
|
},
|
|
|
|
setSuppliers(suppliers) {
|
|
companyAutomat.setState({
|
|
suppliers: Array.isArray(suppliers) ? suppliers : [],
|
|
suppliersStatus: 'ready',
|
|
suppliersError: null,
|
|
});
|
|
},
|
|
// #endregion
|
|
|
|
// #region 5. Units (unit)
|
|
// MARK: calls unit.list from ../api/index.js
|
|
loadUnits() {
|
|
if (inflightUnits) return inflightUnits;
|
|
companyAutomat.setState({ unitsStatus: 'loading', unitsError: null });
|
|
inflightUnits = (async () => {
|
|
try {
|
|
const res = await unitApi.list();
|
|
const units = sortUnits(Array.isArray(res.units) ? res.units : []);
|
|
companyAutomat.setState({ units, unitsStatus: 'succeeded', unitsError: null });
|
|
return units;
|
|
} catch (err) {
|
|
companyAutomat.setState({
|
|
unitsStatus: 'failed',
|
|
unitsError: err?.message ?? String(err),
|
|
});
|
|
throw err;
|
|
} finally {
|
|
inflightUnits = null;
|
|
}
|
|
})();
|
|
return inflightUnits;
|
|
},
|
|
|
|
async upsertUnit(body) {
|
|
const res = await unitApi.upsert(body);
|
|
const row = res.unit;
|
|
const units = sortUnits([
|
|
...companyAutomat.state.units.filter((u) => u.si !== row.si),
|
|
row,
|
|
]);
|
|
companyAutomat.setState({ units });
|
|
return units;
|
|
},
|
|
|
|
async deleteUnit(si) {
|
|
await unitApi.delete(si);
|
|
companyAutomat.setState({
|
|
units: companyAutomat.state.units.filter((u) => u.si !== si),
|
|
});
|
|
},
|
|
// #endregion
|
|
|
|
// #region 6. User settings (userSetting)
|
|
setViewerPosition(position) {
|
|
if (position !== 'top' && position !== 'right' && position !== 'below') {
|
|
return;
|
|
}
|
|
companyAutomat.setState({ viewerPosition: position });
|
|
persistUserSettings({
|
|
viewerPosition: position,
|
|
chatDrawerWidth: companyAutomat.state.chatDrawerWidth,
|
|
});
|
|
},
|
|
|
|
setChatDrawerWidth(width) {
|
|
const num = Number(width);
|
|
if (Number.isNaN(num) || num < 280) {
|
|
return;
|
|
}
|
|
const rounded = Math.round(num);
|
|
companyAutomat.setState({ chatDrawerWidth: rounded });
|
|
persistUserSettings({
|
|
viewerPosition: companyAutomat.state.viewerPosition,
|
|
chatDrawerWidth: rounded,
|
|
});
|
|
},
|
|
// #endregion
|
|
|
|
// #region 7. Warehouses (warehouse)
|
|
// MARK: calls warehouse.list from ../api/index.js
|
|
filterWarehouses(query) {
|
|
const warehouseFilter = String(query ?? '');
|
|
companyAutomat.setState({
|
|
warehouseFilter,
|
|
filteredWarehouses: filterByName(companyAutomat.state.warehouses, warehouseFilter),
|
|
});
|
|
},
|
|
|
|
loadWarehouses() {
|
|
if (inflightWarehouses) return inflightWarehouses;
|
|
inflightWarehouses = (async () => {
|
|
companyAutomat.setState({ warehousesStatus: 'loading', warehousesError: null });
|
|
try {
|
|
const warehouses = await fetchWarehousesList();
|
|
updateStateWithWarehouses(warehouses, {
|
|
warehousesStatus: 'ready',
|
|
warehousesError: null,
|
|
});
|
|
return warehouses;
|
|
} catch (error) {
|
|
companyAutomat.setState({ warehousesStatus: 'failed', warehousesError: error });
|
|
throw error;
|
|
} finally {
|
|
inflightWarehouses = null;
|
|
}
|
|
})();
|
|
return inflightWarehouses;
|
|
},
|
|
|
|
async reloadWarehouses() {
|
|
try {
|
|
const warehouses = await fetchWarehousesList();
|
|
updateStateWithWarehouses(warehouses, {
|
|
warehousesStatus: 'ready',
|
|
warehousesError: null,
|
|
});
|
|
return warehouses;
|
|
} catch (error) {
|
|
companyAutomat.setState({ warehousesError: error });
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
async createWarehouse(body) {
|
|
const data = await warehouseApi.create(body);
|
|
requireObject(data, 'POST /api/warehouse');
|
|
requireObject(data.warehouse, 'warehouse');
|
|
requireDefined(data.warehouse.id, 'warehouse.id');
|
|
await actions.reloadWarehouses();
|
|
return data.warehouse;
|
|
},
|
|
|
|
async updateWarehouse(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 actions.reloadWarehouses();
|
|
return data.warehouse;
|
|
},
|
|
|
|
renameWarehouse(id, body) {
|
|
return actions.updateWarehouse(id, body);
|
|
},
|
|
|
|
async removeWarehouse(id) {
|
|
await warehouseApi.delete(id);
|
|
await actions.reloadWarehouses();
|
|
},
|
|
|
|
async reorderWarehouses(warehouseIds) {
|
|
const byId = new Map(companyAutomat.state.warehouses.map((w) => [w.id, w]));
|
|
const reordered = warehouseIds.map((id) => byId.get(id)).filter(Boolean);
|
|
if (reordered.length === companyAutomat.state.warehouses.length) {
|
|
updateStateWithWarehouses(reordered);
|
|
}
|
|
await warehouseApi.order({ warehouseIds });
|
|
await actions.reloadWarehouses();
|
|
},
|
|
|
|
setWarehouses(warehouses) {
|
|
updateStateWithWarehouses(warehouses, {
|
|
warehousesStatus: 'ready',
|
|
warehousesError: null,
|
|
});
|
|
},
|
|
// #endregion
|
|
|
|
// #region 8. Properties (property)
|
|
// MARK: calls propertyname.list from ../api/index.js
|
|
loadProperties() {
|
|
if (inflightProperties) return inflightProperties;
|
|
companyAutomat.setState({ propertiesStatus: 'loading', propertiesError: null });
|
|
inflightProperties = (async () => {
|
|
try {
|
|
const res = await propertynameApi.list();
|
|
const properties = normalizeProperties(res.propertynames);
|
|
companyAutomat.setState({
|
|
properties,
|
|
propertiesStatus: 'succeeded',
|
|
propertiesError: null,
|
|
});
|
|
return properties;
|
|
} catch (err) {
|
|
companyAutomat.setState({
|
|
propertiesStatus: 'failed',
|
|
propertiesError: err?.message ?? String(err),
|
|
});
|
|
throw err;
|
|
} finally {
|
|
inflightProperties = null;
|
|
}
|
|
})();
|
|
return inflightProperties;
|
|
},
|
|
|
|
async createProperty(body) {
|
|
const res = await propertynameApi.create(body);
|
|
await actions.loadProperties();
|
|
return res.propertyname;
|
|
},
|
|
|
|
async updateProperty(id, body) {
|
|
const res = await propertynameApi.update(id, body);
|
|
await actions.loadProperties();
|
|
return res.propertyname;
|
|
},
|
|
|
|
async deleteProperty(id) {
|
|
await propertynameApi.delete(id);
|
|
await actions.loadProperties();
|
|
},
|
|
|
|
async deleteProperties(ids) {
|
|
for (const id of ids) {
|
|
await propertynameApi.delete(id);
|
|
}
|
|
await actions.loadProperties();
|
|
},
|
|
|
|
async reorderProperties(propertynameIds) {
|
|
await propertynameApi.order(propertynameIds);
|
|
await actions.loadProperties();
|
|
},
|
|
// #endregion
|
|
|
|
// #region 9. Customer groups (customergroup)
|
|
// MARK: calls customergroup.list from ../api/index.js
|
|
filterCustomergroups(query) {
|
|
const customergroupFilter = String(query ?? '');
|
|
companyAutomat.setState({
|
|
customergroupFilter,
|
|
filteredCustomergroups: filterByName(
|
|
companyAutomat.state.customergroups,
|
|
customergroupFilter,
|
|
),
|
|
});
|
|
},
|
|
|
|
loadCustomergroups() {
|
|
if (inflightCustomergroups) return inflightCustomergroups;
|
|
inflightCustomergroups = (async () => {
|
|
companyAutomat.setState({ customergroupsStatus: 'loading', customergroupsError: null });
|
|
try {
|
|
const groups = await fetchCustomergroupsList();
|
|
updateStateWithCustomergroups(groups, {
|
|
customergroupsStatus: 'ready',
|
|
customergroupsError: null,
|
|
});
|
|
return companyAutomat.state.customergroups;
|
|
} catch (error) {
|
|
companyAutomat.setState({
|
|
customergroupsStatus: 'failed',
|
|
customergroupsError: error,
|
|
});
|
|
throw error;
|
|
} finally {
|
|
inflightCustomergroups = null;
|
|
}
|
|
})();
|
|
return inflightCustomergroups;
|
|
},
|
|
|
|
async reloadCustomergroups() {
|
|
try {
|
|
const groups = await fetchCustomergroupsList();
|
|
updateStateWithCustomergroups(groups, {
|
|
customergroupsStatus: 'ready',
|
|
customergroupsError: null,
|
|
});
|
|
return companyAutomat.state.customergroups;
|
|
} catch (error) {
|
|
companyAutomat.setState({ customergroupsError: error });
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
replaceCustomergroups(items) {
|
|
return updateStateWithCustomergroups(items, {
|
|
customergroupsStatus: 'ready',
|
|
customergroupsError: null,
|
|
});
|
|
},
|
|
|
|
ensureCustomergroupsLoaded() {
|
|
if (companyAutomat.state.customergroupsStatus === 'ready') {
|
|
return Promise.resolve(companyAutomat.state.customergroups);
|
|
}
|
|
return actions.loadCustomergroups();
|
|
},
|
|
|
|
async createCustomergroup(body) {
|
|
const data = await customergroupApi.create(body);
|
|
requireObject(data, 'POST /api/customergroup');
|
|
requireObject(data.customergroup, 'customergroup');
|
|
requireDefined(data.customergroup.id, 'customergroup.id');
|
|
await actions.reloadCustomergroups();
|
|
return data.customergroup;
|
|
},
|
|
|
|
async updateCustomergroup(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 actions.reloadCustomergroups();
|
|
return data.customergroup;
|
|
},
|
|
|
|
renameCustomergroup(id, body) {
|
|
return actions.updateCustomergroup(id, body);
|
|
},
|
|
|
|
async removeCustomergroup(id) {
|
|
await customergroupApi.delete(id);
|
|
await actions.reloadCustomergroups();
|
|
},
|
|
|
|
async reorderCustomergroups(customergroupIds) {
|
|
const data = await customergroupApi.order({ customergroupIds });
|
|
requireObject(data, 'PUT /api/customergroup/order');
|
|
await actions.reloadCustomergroups();
|
|
},
|
|
// #endregion
|
|
|
|
// #region 10. Carriers (carrier)
|
|
// MARK: calls carrier.list from ../api/index.js
|
|
filterCarriers(query) {
|
|
const carrierFilter = String(query ?? '');
|
|
companyAutomat.setState({
|
|
carrierFilter,
|
|
filteredCarriers: filterByName(companyAutomat.state.carriers, carrierFilter),
|
|
});
|
|
},
|
|
|
|
loadCarriers() {
|
|
if (inflightCarriers) return inflightCarriers;
|
|
inflightCarriers = (async () => {
|
|
companyAutomat.setState({ carriersStatus: 'loading', carriersError: null });
|
|
try {
|
|
const carriers = await fetchCarriersList();
|
|
updateStateWithCarriers(carriers, {
|
|
carriersStatus: 'ready',
|
|
carriersError: null,
|
|
});
|
|
return carriers;
|
|
} catch (error) {
|
|
companyAutomat.setState({ carriersStatus: 'failed', carriersError: error });
|
|
throw error;
|
|
} finally {
|
|
inflightCarriers = null;
|
|
}
|
|
})();
|
|
return inflightCarriers;
|
|
},
|
|
|
|
async reloadCarriers() {
|
|
try {
|
|
const carriers = await fetchCarriersList();
|
|
updateStateWithCarriers(carriers, {
|
|
carriersStatus: 'ready',
|
|
carriersError: null,
|
|
});
|
|
return carriers;
|
|
} catch (error) {
|
|
companyAutomat.setState({ carriersError: error });
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
async createCarrier(body) {
|
|
const data = await carrierApi.create(body);
|
|
const created = data?.carrier || data;
|
|
await actions.reloadCarriers();
|
|
return created;
|
|
},
|
|
|
|
async updateCarrier(id, body) {
|
|
const data = await carrierApi.update(id, body);
|
|
const updated = data?.carrier || data;
|
|
await actions.reloadCarriers();
|
|
return updated;
|
|
},
|
|
|
|
async removeCarrier(id) {
|
|
await carrierApi.delete(id);
|
|
await actions.reloadCarriers();
|
|
},
|
|
|
|
async reorderCarriers(carrierIds) {
|
|
const byId = new Map(companyAutomat.state.carriers.map((c) => [c.id, c]));
|
|
const reordered = carrierIds.map((id) => byId.get(id)).filter(Boolean);
|
|
if (reordered.length === companyAutomat.state.carriers.length) {
|
|
updateStateWithCarriers(reordered);
|
|
}
|
|
await carrierApi.order({ carrierIds });
|
|
await actions.reloadCarriers();
|
|
},
|
|
|
|
setCarriers(carriers) {
|
|
updateStateWithCarriers(carriers, {
|
|
carriersStatus: 'ready',
|
|
carriersError: null,
|
|
});
|
|
},
|
|
// #endregion
|
|
|
|
// #region 11. Brands (brand)
|
|
// MARK: calls brand.list from ../api/index.js
|
|
filterBrands(query) {
|
|
const brandFilter = String(query ?? '');
|
|
companyAutomat.setState({
|
|
brandFilter,
|
|
filteredBrands: filterByName(companyAutomat.state.brands, brandFilter),
|
|
});
|
|
},
|
|
|
|
async loadBrands(silent = false) {
|
|
if (inflightBrands) return inflightBrands;
|
|
if (!silent) {
|
|
companyAutomat.setState({ brandsStatus: 'loading', brandsError: null });
|
|
}
|
|
inflightBrands = fetchAllBrands()
|
|
.then(({ brands }) =>
|
|
updateStateWithBrands(brands, { brandsStatus: 'succeeded', brandsError: null }),
|
|
)
|
|
.catch((err) => {
|
|
const brandsError = err?.message ?? String(err);
|
|
companyAutomat.setState(
|
|
silent ? { brandsError } : { brandsStatus: 'failed', brandsError },
|
|
);
|
|
throw err;
|
|
})
|
|
.finally(() => {
|
|
inflightBrands = null;
|
|
});
|
|
return inflightBrands;
|
|
},
|
|
|
|
async createBrand(body) {
|
|
const res = await brandApi.create(body);
|
|
const row = res.brand;
|
|
updateStateWithBrands(
|
|
sortBrands([...companyAutomat.state.brands.filter((b) => b.id !== row.id), row]),
|
|
);
|
|
return row;
|
|
},
|
|
|
|
async renameBrand(id, body) {
|
|
const res = await brandApi.rename(id, body);
|
|
const updated = res.brand ?? { id: res.id, name: res.name, gsprpinfo: res.gsprpinfo };
|
|
updateStateWithBrands(
|
|
sortBrands(
|
|
companyAutomat.state.brands.map((b) => (b.id === id ? { ...b, ...updated } : b)),
|
|
),
|
|
);
|
|
return res;
|
|
},
|
|
|
|
async removeBrand(id) {
|
|
await brandApi.delete(id);
|
|
updateStateWithBrands(companyAutomat.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(companyAutomat.state.brands.filter((b) => !idSet.has(b.id)));
|
|
await actions.loadBrands(true);
|
|
},
|
|
|
|
async addBrandImage(id, file) {
|
|
const res = await brandApi.addImage(id, file);
|
|
await actions.loadBrands(true);
|
|
return res.brand;
|
|
},
|
|
|
|
async deleteBrandImage(id) {
|
|
const res = await brandApi.deleteImage(id);
|
|
const updated = res.brand ?? { pics: [] };
|
|
updateStateWithBrands(
|
|
companyAutomat.state.brands.map((b) =>
|
|
b.id === id ? { ...b, ...updated, pics: [] } : b,
|
|
),
|
|
);
|
|
return res.brand;
|
|
},
|
|
// #endregion
|
|
|
|
// #region 12. Attributes & values (attribute)
|
|
// MARK: calls attributename.list from ../api/index.js
|
|
loadAttributes() {
|
|
if (inflightAttributes) return inflightAttributes;
|
|
companyAutomat.setState({ attributesStatus: 'loading', attributesError: null });
|
|
inflightAttributes = (async () => {
|
|
try {
|
|
const res = await attributenameApi.list();
|
|
const attributes = normalizeAttributes(res.attributenames);
|
|
companyAutomat.setState({
|
|
attributes,
|
|
attributesStatus: 'succeeded',
|
|
attributesError: null,
|
|
});
|
|
return attributes;
|
|
} catch (err) {
|
|
companyAutomat.setState({
|
|
attributesStatus: 'failed',
|
|
attributesError: err?.message ?? String(err),
|
|
});
|
|
throw err;
|
|
} finally {
|
|
inflightAttributes = null;
|
|
}
|
|
})();
|
|
return inflightAttributes;
|
|
},
|
|
|
|
async createAttribute(body) {
|
|
const res = await attributenameApi.create(body);
|
|
await actions.loadAttributes();
|
|
return res.attributename;
|
|
},
|
|
|
|
async updateAttribute(id, body) {
|
|
const res = await attributenameApi.update(id, body);
|
|
await actions.loadAttributes();
|
|
return res.attributename;
|
|
},
|
|
|
|
async deleteAttribute(id) {
|
|
await attributenameApi.delete(id);
|
|
await actions.loadAttributes();
|
|
},
|
|
|
|
async deleteAttributes(ids) {
|
|
for (const id of ids) {
|
|
await attributenameApi.delete(id);
|
|
}
|
|
await actions.loadAttributes();
|
|
},
|
|
|
|
async reorderAttributes(attributenameIds) {
|
|
await attributenameApi.order(attributenameIds);
|
|
await actions.loadAttributes();
|
|
},
|
|
|
|
async createValue(attributeId, body) {
|
|
const res = await attributevalueApi.create(attributeId, body);
|
|
await actions.loadAttributes();
|
|
return res.attributevalue;
|
|
},
|
|
|
|
createAttributeValue(attributeId, body) {
|
|
return actions.createValue(attributeId, body);
|
|
},
|
|
|
|
async updateValue(id, body) {
|
|
const res = await attributevalueApi.update(id, body);
|
|
await actions.loadAttributes();
|
|
return res.attributevalue;
|
|
},
|
|
|
|
updateAttributeValue(id, body) {
|
|
return actions.updateValue(id, body);
|
|
},
|
|
|
|
async deleteValue(id) {
|
|
await attributevalueApi.delete(id);
|
|
await actions.loadAttributes();
|
|
},
|
|
|
|
deleteAttributeValue(id) {
|
|
return actions.deleteValue(id);
|
|
},
|
|
|
|
async deleteValues(ids) {
|
|
for (const id of ids) {
|
|
await attributevalueApi.delete(id);
|
|
}
|
|
await actions.loadAttributes();
|
|
},
|
|
|
|
deleteAttributeValues(ids) {
|
|
return actions.deleteValues(ids);
|
|
},
|
|
|
|
async reorderValues(attributeId, attributevalueIds) {
|
|
await attributevalueApi.order(attributeId, attributevalueIds);
|
|
await actions.loadAttributes();
|
|
},
|
|
|
|
reorderAttributeValues(attributeId, attributevalueIds) {
|
|
return actions.reorderValues(attributeId, attributevalueIds);
|
|
},
|
|
// #endregion
|
|
|
|
// #region 13. Payment accounts (account)
|
|
// MARK: calls account.list from ../api/index.js
|
|
loadAccounts() {
|
|
if (inflightAccounts) return inflightAccounts;
|
|
inflightAccounts = (async () => {
|
|
companyAutomat.setState({ accountsStatus: 'loading', accountsError: null });
|
|
try {
|
|
const res = await accountApi.list();
|
|
const accounts = sortAccounts(Array.isArray(res?.accounts) ? res.accounts : []);
|
|
companyAutomat.setState({
|
|
accounts,
|
|
accountsStatus: 'ready',
|
|
accountsError: null,
|
|
});
|
|
return accounts;
|
|
} catch (error) {
|
|
companyAutomat.setState({ accountsStatus: 'failed', accountsError: error });
|
|
throw error;
|
|
} finally {
|
|
inflightAccounts = null;
|
|
}
|
|
})();
|
|
return inflightAccounts;
|
|
},
|
|
|
|
async reloadAccounts() {
|
|
try {
|
|
const res = await accountApi.list();
|
|
const accounts = sortAccounts(Array.isArray(res?.accounts) ? res.accounts : []);
|
|
companyAutomat.setState({
|
|
accounts,
|
|
accountsStatus: 'ready',
|
|
accountsError: null,
|
|
});
|
|
return accounts;
|
|
} catch (error) {
|
|
companyAutomat.setState({ accountsError: error });
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
ensureAccountsLoaded() {
|
|
if (companyAutomat.state.accountsStatus === 'ready') {
|
|
return Promise.resolve(companyAutomat.state.accounts);
|
|
}
|
|
return actions.loadAccounts();
|
|
},
|
|
|
|
replaceAccounts(items) {
|
|
const accounts = sortAccounts(Array.isArray(items) ? items : []);
|
|
companyAutomat.setState({ accounts, accountsStatus: 'ready', accountsError: null });
|
|
return accounts;
|
|
},
|
|
// #endregion
|
|
};
|
|
|
|
/**
|
|
* Global company state container.
|
|
* Instantiated with the 1st-level navigation shell (`AppShell`) and loads all
|
|
* company-coupled base data when `authAutomat.state.hasCompany` is true.
|
|
*
|
|
* @type {Automat<typeof initialState>}
|
|
*/
|
|
companyAutomat = new Automat(initialState, actions, { name: 'company' });
|
|
|
|
// Hydrate user layout settings from IndexedDB if present
|
|
hydrateUserSettingsFromIdb(companyAutomat);
|
|
|
|
// #region authAutomat synchronization
|
|
let lastAuthKey;
|
|
|
|
function syncWithAuth(auth) {
|
|
const hasAuth = Boolean(
|
|
auth?.hasAuth ?? (auth?.status === 'authed' && auth?.user),
|
|
);
|
|
const hasCompany = Boolean(
|
|
auth?.hasCompany ?? (hasAuth && auth?.user?.selectedCompanyId != null),
|
|
);
|
|
const companyId = hasCompany ? String(auth.user.selectedCompanyId) : null;
|
|
const authKey = hasAuth ? `authed:${companyId ?? ''}` : 'anon';
|
|
if (authKey === lastAuthKey) return;
|
|
lastAuthKey = authKey;
|
|
if (!hasAuth) {
|
|
companyAutomat.actions.reset();
|
|
} else if (companyId == null) {
|
|
companyAutomat.actions.reset();
|
|
void companyAutomat.actions.load().catch(() => {});
|
|
} else {
|
|
companyAutomat.actions.reset();
|
|
void companyAutomat.actions.loadAll();
|
|
}
|
|
}
|
|
|
|
authAutomat.subscribe(syncWithAuth);
|
|
syncWithAuth(authAutomat.state);
|
|
// #endregion
|
|
|
|
// #region sseAutomat synchronization
|
|
sseAutomat.actions.on('supplier', () => {
|
|
void companyAutomat.actions.reloadSuppliers().catch(() => {});
|
|
});
|
|
sseAutomat.actions.on('warehouse', () => {
|
|
void companyAutomat.actions.reloadWarehouses().catch(() => {});
|
|
});
|
|
sseAutomat.actions.on('brand', () => {
|
|
void companyAutomat.actions.loadBrands(true).catch(() => {});
|
|
});
|
|
sseAutomat.actions.on('unit', () => {
|
|
void companyAutomat.actions.loadUnits().catch(() => {});
|
|
});
|
|
sseAutomat.actions.on('carrier', () => {
|
|
void companyAutomat.actions.reloadCarriers().catch(() => {});
|
|
});
|
|
sseAutomat.actions.on('posterminal', () => {
|
|
void companyAutomat.actions.loadTerminals().catch(() => {});
|
|
});
|
|
sseAutomat.actions.on('printeragent', () => {
|
|
void companyAutomat.actions.loadPrinterAgents().catch(() => {});
|
|
});
|
|
sseAutomat.actions.on('agentprinter', () => {
|
|
void companyAutomat.actions.loadAgentPrinters().catch(() => {});
|
|
});
|
|
sseAutomat.actions.on('printerstate', (ev) => {
|
|
try {
|
|
const payload = typeof ev?.data === 'string' ? JSON.parse(ev.data || '{}') : (ev?.data || {});
|
|
if (Array.isArray(payload.printers)) {
|
|
companyAutomat.actions.setLivePrinters(payload.printers);
|
|
return;
|
|
}
|
|
} catch {}
|
|
void companyAutomat.actions.loadLivePrinters().catch(() => {});
|
|
});
|
|
sseAutomat.actions.on('printer', (ev) => {
|
|
try {
|
|
const payload = typeof ev?.data === 'string' ? JSON.parse(ev.data || '{}') : (ev?.data || {});
|
|
if (Array.isArray(payload.printers)) {
|
|
companyAutomat.actions.setLivePrinters(payload.printers);
|
|
}
|
|
} catch {}
|
|
});
|
|
sseAutomat.actions.on('customergroup', () => {
|
|
void companyAutomat.actions.reloadCustomergroups().catch(() => {});
|
|
});
|
|
sseAutomat.actions.on('propertyname', () => {
|
|
void companyAutomat.actions.loadProperties().catch(() => {});
|
|
});
|
|
sseAutomat.actions.on('attributename', () => {
|
|
void companyAutomat.actions.loadAttributes().catch(() => {});
|
|
});
|
|
sseAutomat.actions.on('account', () => {
|
|
void companyAutomat.actions.reloadAccounts().catch(() => {});
|
|
});
|
|
sseAutomat.actions.on('company', () => {
|
|
void companyAutomat.actions.reload().catch(() => {});
|
|
});
|
|
// #endregion
|
|
|
|
// #region selectors & exported action helpers
|
|
// Company profile selectors
|
|
export const selectCompanies = () => companyAutomat.state.companies;
|
|
export const selectCurrentCompany = () => companyAutomat.state.current;
|
|
export const selectCompanyId = () => companyAutomat.state.current?.id ?? null;
|
|
export const selectCompanyStatus = () => companyAutomat.state.status;
|
|
export const selectCompanyName = () => companyAutomat.state.current?.name;
|
|
export const selectCompanyIso639 = () => companyAutomat.state.current?.iso639;
|
|
export const selectCompanyLanguageId = () => companyAutomat.state.current?.languageId ?? null;
|
|
export const selectCompanyTaxrate = () => companyAutomat.state.current?.taxrate ?? null;
|
|
export const selectCompanyReducedTaxrate = () => companyAutomat.state.current?.reducedtaxrate ?? null;
|
|
export const selectCompanyTaxfreerate = () => companyAutomat.state.current?.taxfreerate ?? null;
|
|
export const selectCompanyRootCategoryId = () => companyAutomat.state.current?.rootCategoryId ?? null;
|
|
|
|
// Numbers selectors
|
|
export const selectNumbers = () => companyAutomat.state.numbers;
|
|
export const selectNumbersStatus = () => companyAutomat.state.numbersStatus;
|
|
|
|
// POS terminals selectors
|
|
export const selectPosterminals = () => companyAutomat.state.terminals;
|
|
export const selectPosterminalStatus = () => companyAutomat.state.terminalsStatus;
|
|
export const selectHasPosterminals = () => companyAutomat.state.terminals.length > 0;
|
|
|
|
// Printer agents selectors
|
|
export const selectPrinterAgents = () => companyAutomat.state.printeragents;
|
|
export const selectPrinterAgentStatus = () => companyAutomat.state.printeragentsStatus;
|
|
export const selectHasPrinterAgents = () => companyAutomat.state.printeragents.length > 0;
|
|
|
|
// Agent printers selectors
|
|
export const selectAgentPrinters = () => companyAutomat.state.agentprinters;
|
|
export const selectAgentPrinterStatus = () => companyAutomat.state.agentprintersStatus;
|
|
export const selectLivePrinters = () => companyAutomat.state.livePrinters;
|
|
|
|
// Suppliers selectors
|
|
export const selectSuppliers = () => companyAutomat.state.suppliers;
|
|
export const selectSuppliersStatus = () => companyAutomat.state.suppliersStatus;
|
|
|
|
// Units selectors
|
|
export const selectUnits = () => companyAutomat.state.units;
|
|
export const selectUnitsStatus = () => companyAutomat.state.unitsStatus;
|
|
|
|
// User settings helpers & selectors
|
|
export const setViewerPosition = (position) => companyAutomat.actions.setViewerPosition(position);
|
|
export const setChatDrawerWidth = (width) => companyAutomat.actions.setChatDrawerWidth(width);
|
|
export const selectChatDrawerWidth = () => companyAutomat.state.chatDrawerWidth || 460;
|
|
|
|
// Warehouses selectors
|
|
export const selectWarehouses = () => companyAutomat.state.warehouses;
|
|
export const selectWarehousesStatus = () => companyAutomat.state.warehousesStatus;
|
|
|
|
// Properties selectors
|
|
export const selectProperties = () => companyAutomat.state.properties;
|
|
export const selectPropertyStatus = () => companyAutomat.state.propertiesStatus;
|
|
|
|
// Customer groups selectors
|
|
export const selectCustomergroups = () => companyAutomat.state.customergroups;
|
|
export const selectCustomergroupsStatus = () => companyAutomat.state.customergroupsStatus;
|
|
|
|
// Carriers selectors
|
|
export const selectCarriers = () => companyAutomat.state.carriers;
|
|
export const selectCarriersStatus = () => companyAutomat.state.carriersStatus;
|
|
|
|
// Brands selectors
|
|
export const selectBrands = () => companyAutomat.state.brands;
|
|
export const selectBrandsStatus = () => companyAutomat.state.brandsStatus;
|
|
|
|
// Attributes selectors
|
|
export const selectAttributes = () => companyAutomat.state.attributes;
|
|
export const selectAttributeStatus = () => companyAutomat.state.attributesStatus;
|
|
|
|
// Accounts selectors
|
|
export const selectAccounts = () => companyAutomat.state.accounts;
|
|
export const selectAccountsStatus = () => companyAutomat.state.accountsStatus;
|
|
// #endregion
|
|
|
|
export default companyAutomat;
|