/** * @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} items Loaded records. * @property {Array} 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} */ 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;