198 lines
6.0 KiB
JavaScript
198 lines
6.0 KiB
JavaScript
/**
|
|
* @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;
|