supplierAutomat
This commit is contained in:
@@ -382,7 +382,6 @@ export class StorageManager extends ApiClient {
|
|||||||
const companyCache = createCache('GET /company', () => this.#loadCompanies());
|
const companyCache = createCache('GET /company', () => this.#loadCompanies());
|
||||||
const categoryTreeCache = createCache('GET /category/tree', () => this.#loadCategoryTree());
|
const categoryTreeCache = createCache('GET /category/tree', () => this.#loadCategoryTree());
|
||||||
const posterminalCache = createCache('GET /posterminal', () => this.#loadPosterminals());
|
const posterminalCache = createCache('GET /posterminal', () => this.#loadPosterminals());
|
||||||
const supplierCache = createCache('GET /supplier', () => this.#loadSuppliers());
|
|
||||||
const deliveryCache = createCache('GET /delivery', () => this.#loadDeliveries());
|
const deliveryCache = createCache('GET /delivery', () => this.#loadDeliveries());
|
||||||
const customergroupCache = createCache('GET /customergroup', () =>
|
const customergroupCache = createCache('GET /customergroup', () =>
|
||||||
this.#loadCustomergroups(),
|
this.#loadCustomergroups(),
|
||||||
@@ -424,11 +423,6 @@ export class StorageManager extends ApiClient {
|
|||||||
posterminalPromise: { get: () => posterminalCache.promise },
|
posterminalPromise: { get: () => posterminalCache.promise },
|
||||||
updatePosterminal: { value: posterminalCache.update },
|
updatePosterminal: { value: posterminalCache.update },
|
||||||
|
|
||||||
supplierPromise: { get: () => supplierCache.promise },
|
|
||||||
updateSupplier: { value: supplierCache.update },
|
|
||||||
/** Settled GET /supplier list, or undefined. */
|
|
||||||
suppliers: { get: () => supplierCache.value },
|
|
||||||
|
|
||||||
deliveryPromise: { get: () => deliveryCache.promise },
|
deliveryPromise: { get: () => deliveryCache.promise },
|
||||||
updateDelivery: { value: deliveryCache.update },
|
updateDelivery: { value: deliveryCache.update },
|
||||||
/** Settled GET /delivery list, or undefined. */
|
/** Settled GET /delivery list, or undefined. */
|
||||||
@@ -580,12 +574,6 @@ export class StorageManager extends ApiClient {
|
|||||||
return requireArray(data.posterminals, 'posterminals');
|
return requireArray(data.posterminals, 'posterminals');
|
||||||
}
|
}
|
||||||
|
|
||||||
async #loadSuppliers() {
|
|
||||||
const data = await this.supplier.list();
|
|
||||||
requireObject(data, 'GET /api/supplier');
|
|
||||||
return requireArray(data.suppliers, 'suppliers');
|
|
||||||
}
|
|
||||||
|
|
||||||
async #loadDeliveries() {
|
async #loadDeliveries() {
|
||||||
const data = await this.delivery.list();
|
const data = await this.delivery.list();
|
||||||
requireObject(data, 'GET /api/delivery');
|
requireObject(data, 'GET /api/delivery');
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ export { default as authAutomat } from './authAutomat';
|
|||||||
export * from './authAutomat';
|
export * from './authAutomat';
|
||||||
export { default as navigationAutomat } from './navigationAutomat';
|
export { default as navigationAutomat } from './navigationAutomat';
|
||||||
export { setLastDeliveryPath, setLastArticleSku, clearNavigation } from './navigationAutomat';
|
export { setLastDeliveryPath, setLastArticleSku, clearNavigation } from './navigationAutomat';
|
||||||
|
export { default as supplierAutomat } from './supplierAutomat';
|
||||||
|
|||||||
201
src/automats/supplierAutomat.js
Normal file
201
src/automats/supplierAutomat.js
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
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: [],
|
||||||
|
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 });
|
||||||
|
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 });
|
||||||
|
return suppliers;
|
||||||
|
} catch (error) {
|
||||||
|
supplierAutomat.setState({ error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
@@ -23,6 +23,7 @@ import { enqueueSnackbar } from 'notistack';
|
|||||||
import { StorageManagerContext } from '../../../api/storageManagerContext';
|
import { StorageManagerContext } from '../../../api/storageManagerContext';
|
||||||
import { notifyError } from '../../../utils/notify';
|
import { notifyError } from '../../../utils/notify';
|
||||||
import { formatPrice } from '../../../utils/price';
|
import { formatPrice } from '../../../utils/price';
|
||||||
|
import supplierAutomat from '../../../automats/supplierAutomat';
|
||||||
import ArticleSupplierDialog from '../ArticleSupplierDialog';
|
import ArticleSupplierDialog from '../ArticleSupplierDialog';
|
||||||
|
|
||||||
export default class ArticleSupplierTab extends PureComponent {
|
export default class ArticleSupplierTab extends PureComponent {
|
||||||
@@ -80,7 +81,7 @@ export default class ArticleSupplierTab extends PureComponent {
|
|||||||
// 2. Fetch available company suppliers and company metadata
|
// 2. Fetch available company suppliers and company metadata
|
||||||
try {
|
try {
|
||||||
const [companySuppliers, company] = await Promise.all([
|
const [companySuppliers, company] = await Promise.all([
|
||||||
this.context.suppliers ?? this.context.supplierPromise,
|
supplierAutomat.actions.load(),
|
||||||
this.context.company ?? this.context.companyPromise,
|
this.context.company ?? this.context.companyPromise,
|
||||||
]);
|
]);
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|||||||
@@ -346,13 +346,13 @@ class ManifestFormLine extends PureComponent {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
||||||
{/* Row 2: Matched Supplier Article Details */}
|
{/* Row 2: Matched Supplier Article Details */}
|
||||||
<TableRow sx={{ '& > td': { pt: 0, pb: 0.75, borderBottom: '1px solid', borderColor: 'divider' } }}>
|
<TableRow sx={{ '& > td': { pt: 0, pb: 0.75, borderBottom: 'none' } }}>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={columns.length + 1}
|
colSpan={columns.length + 1}
|
||||||
sx={{
|
sx={{
|
||||||
py: 0.4,
|
py: 0.4,
|
||||||
px: 2,
|
px: 2,
|
||||||
bgcolor: 'rgba(248, 250, 252, 0.75)',
|
bgcolor: 'transparent',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{this.renderSecondLine()}
|
{this.renderSecondLine()}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class ManifestLines extends PureComponent {
|
|||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table size="small" stickyHeader>
|
<Table size="small" stickyHeader>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow sx={{ '& th': { borderBottom: 'none', fontWeight: 700 } }}>
|
||||||
{columns.map((col) => (
|
{columns.map((col) => (
|
||||||
<TableCell
|
<TableCell
|
||||||
key={col.id}
|
key={col.id}
|
||||||
@@ -50,12 +50,13 @@ class ManifestLines extends PureComponent {
|
|||||||
width: col.width,
|
width: col.width,
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
bgcolor: linesColor,
|
bgcolor: linesColor,
|
||||||
|
fontWeight: 700,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{col.label}
|
{col.label}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
<TableCell padding="checkbox" sx={{ bgcolor: linesColor }} />
|
<TableCell padding="checkbox" sx={{ bgcolor: linesColor, borderBottom: 'none' }} />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import supplierAutomat from '../../automats/supplierAutomat';
|
||||||
|
|
||||||
/** Groups that are not sales customers (filter client-side). */
|
/** Groups that are not sales customers (filter client-side). */
|
||||||
export const EXCLUDED_CUSTOMER_GROUP_NAMES = new Set(['Lieferanten', 'POS']);
|
export const EXCLUDED_CUSTOMER_GROUP_NAMES = new Set(['Lieferanten', 'POS']);
|
||||||
|
|
||||||
@@ -424,7 +426,10 @@ export const DELIVERY_LINE_COLUMNS = [
|
|||||||
*/
|
*/
|
||||||
export async function loadParties(kind, ctx) {
|
export async function loadParties(kind, ctx) {
|
||||||
if (kind === 'delivery') {
|
if (kind === 'delivery') {
|
||||||
const list = ctx.suppliers ?? (await ctx.supplierPromise);
|
if (supplierAutomat.state.status === 'ready') {
|
||||||
|
return sortSuppliers(supplierAutomat.state.suppliers);
|
||||||
|
}
|
||||||
|
const list = await supplierAutomat.actions.load();
|
||||||
return sortSuppliers(list);
|
return sortSuppliers(list);
|
||||||
}
|
}
|
||||||
const [entities, groups] = await Promise.all([
|
const [entities, groups] = await Promise.all([
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { StorageManagerProvider } from './api/storageManagerContext';
|
|||||||
import App from './App';
|
import App from './App';
|
||||||
import { store } from './store';
|
import { store } from './store';
|
||||||
import { getActiveTheme } from './theme';
|
import { getActiveTheme } from './theme';
|
||||||
|
import './automats/supplierAutomat';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
// Dev-only Automat inspector. The `import.meta.env.DEV` branch is statically
|
// Dev-only Automat inspector. The `import.meta.env.DEV` branch is statically
|
||||||
|
|||||||
@@ -16,11 +16,10 @@ import BackendError from '../../components/BackendError';
|
|||||||
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||||
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||||
import withNavigate from '../../components/withNavigate';
|
import withNavigate from '../../components/withNavigate';
|
||||||
import { StorageManagerContext } from '../../api/storageManagerContext';
|
|
||||||
import { requireDefined, requireObject } from '../../utils/apiShape';
|
|
||||||
import { backendErrorFrom } from '../../utils/backendError';
|
import { backendErrorFrom } from '../../utils/backendError';
|
||||||
import { notifyError } from '../../utils/notify';
|
import { notifyError } from '../../utils/notify';
|
||||||
import { setLastDeliveryPath } from '../../automats';
|
import { setLastDeliveryPath } from '../../automats';
|
||||||
|
import supplierAutomat from '../../automats/supplierAutomat';
|
||||||
import { deliveryDraftAutomat } from '../../automats/deliveryEditorAutomat';
|
import { deliveryDraftAutomat } from '../../automats/deliveryEditorAutomat';
|
||||||
|
|
||||||
|
|
||||||
@@ -51,8 +50,6 @@ function getInitialParams(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class LieferungLieferantenPage extends PureComponent {
|
class LieferungLieferantenPage extends PureComponent {
|
||||||
static contextType = StorageManagerContext;
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
const { prefill } = getInitialParams(props);
|
const { prefill } = getInitialParams(props);
|
||||||
@@ -109,7 +106,7 @@ class LieferungLieferantenPage extends PureComponent {
|
|||||||
async loadSuppliers() {
|
async loadSuppliers() {
|
||||||
this.setState({ status: 'loading', error: null });
|
this.setState({ status: 'loading', error: null });
|
||||||
try {
|
try {
|
||||||
const list = this.context.suppliers ?? (await this.context.supplierPromise);
|
const list = await supplierAutomat.actions.load();
|
||||||
if (this._cancelled) return;
|
if (this._cancelled) return;
|
||||||
this.applySuppliers(list);
|
this.applySuppliers(list);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -123,7 +120,7 @@ class LieferungLieferantenPage extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async refreshFromServer() {
|
async refreshFromServer() {
|
||||||
const list = await this.context.updateSupplier();
|
const list = await supplierAutomat.actions.reload();
|
||||||
if (this._cancelled) return;
|
if (this._cancelled) return;
|
||||||
this.applySuppliers(list);
|
this.applySuppliers(list);
|
||||||
}
|
}
|
||||||
@@ -136,11 +133,7 @@ class LieferungLieferantenPage extends PureComponent {
|
|||||||
|
|
||||||
this.setState({ busyKey: 'new' });
|
this.setState({ busyKey: 'new' });
|
||||||
try {
|
try {
|
||||||
const data = await this.context.supplier.create({ name });
|
const newSupplier = await supplierAutomat.actions.create({ name });
|
||||||
requireObject(data, 'POST /api/supplier');
|
|
||||||
requireObject(data.supplier, 'supplier');
|
|
||||||
requireDefined(data.supplier.id, 'supplier.id');
|
|
||||||
const newSupplier = data.supplier;
|
|
||||||
|
|
||||||
// Assign the new supplier to the active delivery draft & clear unmatched name
|
// Assign the new supplier to the active delivery draft & clear unmatched name
|
||||||
deliveryDraftAutomat.actions.setDeliveryEntity(newSupplier.id);
|
deliveryDraftAutomat.actions.setDeliveryEntity(newSupplier.id);
|
||||||
@@ -174,10 +167,7 @@ class LieferungLieferantenPage extends PureComponent {
|
|||||||
|
|
||||||
this.setState({ busyKey: supplier.id });
|
this.setState({ busyKey: supplier.id });
|
||||||
try {
|
try {
|
||||||
const data = await this.context.supplier.update(supplier.id, { name: draft });
|
await supplierAutomat.actions.update(supplier.id, { name: draft });
|
||||||
requireObject(data, 'PATCH /api/supplier/:id');
|
|
||||||
requireObject(data.supplier, 'supplier');
|
|
||||||
requireDefined(data.supplier.id, 'supplier.id');
|
|
||||||
await this.refreshFromServer();
|
await this.refreshFromServer();
|
||||||
if (this._cancelled) return;
|
if (this._cancelled) return;
|
||||||
this.setState({ busyKey: null });
|
this.setState({ busyKey: null });
|
||||||
@@ -200,7 +190,7 @@ class LieferungLieferantenPage extends PureComponent {
|
|||||||
|
|
||||||
this.setState({ busyKey: supplier.id });
|
this.setState({ busyKey: supplier.id });
|
||||||
try {
|
try {
|
||||||
await this.context.supplier.delete(supplier.id);
|
await supplierAutomat.actions.remove(supplier.id);
|
||||||
await this.refreshFromServer();
|
await this.refreshFromServer();
|
||||||
if (this._cancelled) return;
|
if (this._cancelled) return;
|
||||||
this.setState({ busyKey: null });
|
this.setState({ busyKey: null });
|
||||||
|
|||||||
Reference in New Issue
Block a user