/** * @file propertyAutomat.js * @description Manages catalog product property names (custom metadata definitions) * 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 a `'propertyname'` SSE event. * - UI / Settings: Mutating actions (`createProperty`, `updateProperty`, * `deleteProperty`, `reorderProperties`). */ import { Automat } from 'react-automat'; import { propertyname as propertynameApi } from '../api'; import authAutomat from './authAutomat'; /** * @typedef {Object} PropertyName * @property {number} id * @property {string} name * @property {number} sort */ /** * @typedef {Object} PropertyState * @property {PropertyName[]} properties * @property {'idle'|'loading'|'succeeded'|'failed'} status * @property {string|null} error */ /** @type {PropertyState} */ const initialState = { properties: [], status: 'idle', error: null, }; function normalizeProperties(properties) { if (!Array.isArray(properties)) return []; return [...properties] .map((p) => ({ ...p, exclusive: Boolean(p.exclusive), })) .sort((a, b) => { const sDiff = (a.sort ?? 0) - (b.sort ?? 0); if (sDiff !== 0) return sDiff; return (a.id ?? 0) - (b.id ?? 0); }); } let propertyAutomat; const actions = { /** Load all property names for the selected company. */ async load() { propertyAutomat.setState({ status: 'loading', error: null }); try { const res = await propertynameApi.list(); const properties = normalizeProperties(res.propertynames); propertyAutomat.setState({ properties, status: 'succeeded' }); return properties; } catch (err) { propertyAutomat.setState({ status: 'failed', error: err?.message ?? String(err) }); throw err; } }, /** * Create a property name. * @param {{ name: string, sort?: number, exclusive?: boolean }} body */ async createProperty(body) { const res = await propertynameApi.create(body); await actions.load(); return res.propertyname; }, /** * Update a property name. * @param {number} id * @param {{ name?: string, sort?: number, exclusive?: boolean }} body */ async updateProperty(id, body) { const res = await propertynameApi.update(id, body); await actions.load(); return res.propertyname; }, /** * Delete a property name. * @param {number} id */ async deleteProperty(id) { await propertynameApi.delete(id); await actions.load(); }, /** * Delete multiple property names. * @param {number[]} ids */ async deleteProperties(ids) { for (const id of ids) { await propertynameApi.delete(id); } await actions.load(); }, /** * Reorder property names. * @param {number[]} propertynameIds */ async reorderProperties(propertynameIds) { await propertynameApi.order(propertynameIds); await actions.load(); }, /** Reset to idle state (called on logout / company switch). */ reset() { propertyAutomat.setState(initialState); }, }; /** * Global property state container. * Loads automatically when authAutomat reaches 'authed' with a selectedCompanyId. * * @type {Automat} */ propertyAutomat = new Automat(initialState, actions, { name: 'property' }); // Selectors export function selectProperties() { return propertyAutomat.state.properties; } export function selectPropertyStatus() { return propertyAutomat.state.status; } // Subscribe to auth changes authAutomat.subscribe((auth) => { const companyId = auth.user?.selectedCompanyId; if (auth.status === 'authed' && companyId) { void propertyAutomat.actions.load(); } else if (auth.status === 'anon') { propertyAutomat.actions.reset(); } }); export default propertyAutomat;