u
This commit is contained in:
@@ -1,16 +1,19 @@
|
||||
/**
|
||||
* @file customergroupAutomat.js
|
||||
* @description Manages cached customer groups (`GET /api/customergroup`) for
|
||||
* the active company. Built via `createListAutomat`.
|
||||
* the active company. Follows the brand/warehouse automat pattern with
|
||||
* filtering, deduplicated loads, silent reloads, and CRUD actions.
|
||||
*
|
||||
* Upstream Triggers:
|
||||
* - `authAutomat`: Via `createListAutomat` — triggers `load()` on `'authed'` with
|
||||
* a `selectedCompanyId`, and `reset()` on `'anon'`.
|
||||
* - UI / Routes: Manual calls to `load()` or `reload()`.
|
||||
* - `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 { createListAutomat } from './createListAutomat';
|
||||
import { requireArray, requireDefined, requireObject } from '../utils/apiShape';
|
||||
import authAutomat from './authAutomat';
|
||||
|
||||
/**
|
||||
* @typedef {Object} CustomerGroup
|
||||
@@ -20,22 +23,175 @@ import { createListAutomat } from './createListAutomat';
|
||||
* @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 {import('react-automat').Automat<import('./createListAutomat').ListAutomatState>}
|
||||
* @type {Automat<CustomerGroupState>}
|
||||
*/
|
||||
export const customergroupAutomat = createListAutomat({
|
||||
customergroupAutomat = new Automat(initialState, actions, {
|
||||
name: 'customergroup',
|
||||
fetch: async () => {
|
||||
const data = await customergroupApi.list();
|
||||
return data.customergroups ?? [];
|
||||
},
|
||||
sort: (items) =>
|
||||
[...items].sort((a, b) => (Number(a.sort) || 0) - (Number(b.sort) || 0)),
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user