/** * @file attributeAutomat.js * @description Manages catalog product attributes and their discrete values * (e.g. Color, Size) for the active company. * * Upstream Triggers: * - `authAutomat`: Subscribes to auth state changes — calls `actions.load()` on * `'authed'` with a `selectedCompanyId`, and `actions.reset()` on `'anon'`. * - `sseAutomat`: Calls `actions.load()` upon receiving an `'attributename'` SSE event. * - UI / Dialogs: Mutating actions (`createAttribute`, `updateAttribute`, * `deleteAttribute`, `addValue`, `deleteValue`, `reorderValues`). */ import { Automat } from 'react-automat'; import { attributename as attributenameApi, attributevalue as attributevalueApi } from '../api'; import authAutomat from './authAutomat'; /** * @typedef {Object} AttributeValue * @property {number} id * @property {number} attributeId * @property {string} value * @property {number} sort */ /** * @typedef {Object} AttributeName * @property {number} id * @property {string} name * @property {number} sort * @property {boolean} exclusive * @property {AttributeValue[]} values */ /** * @typedef {Object} AttributeState * @property {AttributeName[]} attributes * @property {'idle'|'loading'|'succeeded'|'failed'} status * @property {string|null} error */ /** @type {AttributeState} */ const initialState = { attributes: [], status: 'idle', error: null, }; function sortValues(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: sortValues(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); }); } let attributeAutomat; const actions = { /** Load all attribute names and their values for the selected company. */ async load() { attributeAutomat.setState({ status: 'loading', error: null }); try { const res = await attributenameApi.list(); const attributes = normalizeAttributes(res.attributenames); attributeAutomat.setState({ attributes, status: 'succeeded' }); return attributes; } catch (err) { attributeAutomat.setState({ status: 'failed', error: err?.message ?? String(err) }); throw err; } }, /** * Create an attribute name. * @param {{ name: string, sort?: number, exclusive?: boolean }} body */ async createAttribute(body) { const res = await attributenameApi.create(body); await actions.load(); return res.attributename; }, /** * Update an attribute name. * @param {number} id * @param {{ name?: string, sort?: number, exclusive?: boolean }} body */ async updateAttribute(id, body) { const res = await attributenameApi.update(id, body); await actions.load(); return res.attributename; }, /** * Delete an attribute name. * @param {number} id */ async deleteAttribute(id) { await attributenameApi.delete(id); await actions.load(); }, /** * Delete multiple attribute names. * @param {number[]} ids */ async deleteAttributes(ids) { for (const id of ids) { await attributenameApi.delete(id); } await actions.load(); }, /** * Reorder attribute names. * @param {number[]} attributenameIds */ async reorderAttributes(attributenameIds) { await attributenameApi.order(attributenameIds); await actions.load(); }, /** * Create a value for an attribute. * @param {number} attributeId * @param {{ value: string, sort?: number }} body */ async createValue(attributeId, body) { const res = await attributevalueApi.create(attributeId, body); await actions.load(); return res.attributevalue; }, /** * Update an attribute value. * @param {number} id * @param {{ value?: string, sort?: number }} body */ async updateValue(id, body) { const res = await attributevalueApi.update(id, body); await actions.load(); return res.attributevalue; }, /** * Delete an attribute value. * @param {number} id */ async deleteValue(id) { await attributevalueApi.delete(id); await actions.load(); }, /** * Delete multiple attribute values. * @param {number[]} ids */ async deleteValues(ids) { for (const id of ids) { await attributevalueApi.delete(id); } await actions.load(); }, /** * Reorder values of an attribute. * @param {number} attributeId * @param {number[]} attributevalueIds */ async reorderValues(attributeId, attributevalueIds) { await attributevalueApi.order(attributeId, attributevalueIds); await actions.load(); }, /** Reset to idle state (called on logout / company switch). */ reset() { attributeAutomat.setState(initialState); }, }; /** * Global attribute state container. * Loads automatically when authAutomat reaches 'authed' with a selectedCompanyId. * * @type {Automat} */ attributeAutomat = new Automat(initialState, actions, { name: 'attribute' }); // Selectors export function selectAttributes() { return attributeAutomat.state.attributes; } export function selectAttributeStatus() { return attributeAutomat.state.status; } // Subscribe to auth changes authAutomat.subscribe((auth) => { const companyId = auth.user?.selectedCompanyId; if (auth.status === 'authed' && companyId) { void attributeAutomat.actions.load(); } else if (auth.status === 'anon') { attributeAutomat.actions.reset(); } }); export default attributeAutomat;