204 lines
5.3 KiB
JavaScript
204 lines
5.3 KiB
JavaScript
/**
|
|
* @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();
|
|
},
|
|
|
|
/**
|
|
* 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();
|
|
},
|
|
|
|
/**
|
|
* 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<AttributeState>}
|
|
*/
|
|
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;
|