automat
This commit is contained in:
180
src/automats/authAutomat.js
Normal file
180
src/automats/authAutomat.js
Normal file
@@ -0,0 +1,180 @@
|
||||
import { Automat } from 'react-automat';
|
||||
import { auth, company } from '../api';
|
||||
|
||||
/**
|
||||
* @typedef {Object} AuthUser
|
||||
* @property {string|number|null} [selectedCompanyId] Currently selected company.
|
||||
* @property {*} [key] Additional user properties returned by the API.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} AuthState
|
||||
* @property {AuthUser|null} user Authenticated user, or `null` for anonymous sessions.
|
||||
* @property {string|null} googleClientId Google Identity Services client ID.
|
||||
* @property {'loading'|'anon'|'authed'} status Current authentication lifecycle state.
|
||||
*/
|
||||
|
||||
/** Emitted after an authenticated session has been established. */
|
||||
export const AUTH_SESSION_ESTABLISHED = 'auth/sessionEstablished';
|
||||
/** Emitted after the active company has changed. */
|
||||
export const AUTH_COMPANY_CHANGED = 'auth/companyChanged';
|
||||
/** Emitted after the current user has logged out. */
|
||||
export const AUTH_LOGGED_OUT = 'auth/loggedOut';
|
||||
|
||||
/** @type {AuthState} */
|
||||
const initialState = {
|
||||
user: null,
|
||||
googleClientId: null,
|
||||
status: 'loading',
|
||||
};
|
||||
|
||||
let emit = () => { };
|
||||
|
||||
/**
|
||||
* Registers the application-level dispatcher used for authentication events.
|
||||
*
|
||||
* @param {(event: {type: string}) => void} dispatch Event dispatcher.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function setAuthEventDispatcher(dispatch) {
|
||||
emit = dispatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges API user data while retaining the current company selection when the
|
||||
* response does not contain one.
|
||||
*
|
||||
* @param {AuthUser|null} current Current user.
|
||||
* @param {AuthUser|null|undefined} incoming User data returned by the API.
|
||||
* @returns {AuthUser|null} Merged user.
|
||||
*/
|
||||
function mergeUser(current, incoming) {
|
||||
if (!incoming) return current;
|
||||
const selectedCompanyId =
|
||||
incoming.selectedCompanyId ?? current?.selectedCompanyId ?? null;
|
||||
return { ...current, ...incoming, selectedCompanyId };
|
||||
}
|
||||
|
||||
let authAutomat;
|
||||
|
||||
const actions = {
|
||||
/**
|
||||
* Restores an existing session or requests it from the authentication API.
|
||||
*
|
||||
* @param {{user?: AuthUser|null, googleClientId?: string|null}|null} [session]
|
||||
* Optional preloaded session.
|
||||
* @returns {Promise<Object>} Resolved session response.
|
||||
*/
|
||||
async bootstrapSession(session) {
|
||||
try {
|
||||
const nextSession = session ?? await auth.me();
|
||||
const googleClientId =
|
||||
nextSession.googleClientId ?? authAutomat.state.googleClientId;
|
||||
|
||||
if (!nextSession.user) {
|
||||
if (authAutomat.state.status !== 'authed') {
|
||||
authAutomat.setState({ user: null, googleClientId, status: 'anon' });
|
||||
} else {
|
||||
authAutomat.setState({ googleClientId });
|
||||
}
|
||||
return nextSession;
|
||||
}
|
||||
|
||||
const user = mergeUser(authAutomat.state.user, nextSession.user);
|
||||
authAutomat.setState({ user, googleClientId, status: 'authed' });
|
||||
emit({ type: AUTH_SESSION_ESTABLISHED });
|
||||
return nextSession;
|
||||
} catch (error) {
|
||||
if (authAutomat.state.status === 'loading') {
|
||||
authAutomat.setState({ user: null, status: 'anon' });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Exchanges a Google credential for an authenticated application session.
|
||||
*
|
||||
* @param {string} credential Google Identity Services credential.
|
||||
* @returns {Promise<Object>} Authenticated session.
|
||||
*/
|
||||
async loginWithGoogle(credential) {
|
||||
try {
|
||||
const { session } = await auth.loginWithGoogle(credential);
|
||||
if (!session?.user) {
|
||||
throw new Error('Login ok but session cookie not accepted (/api/auth/me still unauthorized)');
|
||||
}
|
||||
authAutomat.setState({
|
||||
user: session.user,
|
||||
googleClientId: session.googleClientId ?? authAutomat.state.googleClientId,
|
||||
status: 'authed',
|
||||
});
|
||||
emit({ type: AUTH_SESSION_ESTABLISHED });
|
||||
return session;
|
||||
} catch (error) {
|
||||
authAutomat.setState({ user: null, status: 'anon' });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Selects the company used by the current session.
|
||||
*
|
||||
* @param {string|number} companyId Company identifier.
|
||||
* @returns {Promise<AuthUser>} Updated user.
|
||||
*/
|
||||
async selectCompany(companyId) {
|
||||
const data = await company.select(companyId);
|
||||
const user = mergeUser(
|
||||
authAutomat.state.user,
|
||||
data.user ?? { selectedCompanyId: companyId },
|
||||
);
|
||||
authAutomat.setState({ user, status: 'authed' });
|
||||
emit({ type: AUTH_COMPANY_CHANGED });
|
||||
return user;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a company and selects it for the current user.
|
||||
*
|
||||
* @param {Object} payload Company creation payload.
|
||||
* @returns {Promise<AuthUser>} Updated user.
|
||||
*/
|
||||
async createCompany(payload) {
|
||||
const data = await company.create(payload);
|
||||
let incoming = data.user;
|
||||
if (!incoming && data.company?.id) {
|
||||
const selected = await company.select(data.company.id);
|
||||
incoming = selected.user ?? { selectedCompanyId: data.company.id };
|
||||
}
|
||||
if (!incoming) {
|
||||
throw new Error('Unternehmen angelegt, aber keine Benutzerdaten erhalten.');
|
||||
}
|
||||
|
||||
const user = mergeUser(authAutomat.state.user, incoming);
|
||||
authAutomat.setState({ user, status: 'authed' });
|
||||
emit({ type: AUTH_COMPANY_CHANGED });
|
||||
return user;
|
||||
},
|
||||
|
||||
/**
|
||||
* Ends the server session, clears the current user, and returns to `/`.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async logout() {
|
||||
await auth.logout();
|
||||
window.history.replaceState(null, '', '/');
|
||||
authAutomat.setState({ user: null, status: 'anon' });
|
||||
emit({ type: AUTH_LOGGED_OUT });
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Global authentication state container.
|
||||
*
|
||||
* @type {Automat<AuthState>}
|
||||
*/
|
||||
authAutomat = new Automat(initialState, actions, { name: 'auth' });
|
||||
|
||||
export default authAutomat;
|
||||
@@ -1,6 +1,58 @@
|
||||
import { Automat } from 'react-automat';
|
||||
import { supplier as supplierApi } from '../api';
|
||||
|
||||
/**
|
||||
* @typedef {Object} DeliveryLine
|
||||
* @property {string|number} key Stable editor key.
|
||||
* @property {string|null} name Article name.
|
||||
* @property {string|number|null} sku Supplier or internal article SKU.
|
||||
* @property {number|null} amount Ordered amount.
|
||||
* @property {number|null} pu Price unit.
|
||||
* @property {string|null} unit Unit of measure.
|
||||
* @property {number|null} netprice Net price.
|
||||
* @property {number|null} taxrate Tax rate.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SupplierArticle
|
||||
* @property {string|number} [id] Supplier-article identifier.
|
||||
* @property {string|number|null} [sku] Supplier SKU.
|
||||
* @property {{sku?: string|number|null, eans?: Array<string|number>, articleean?: Array<string|number>}} [article]
|
||||
* Linked internal article.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} DeliveryEditorState
|
||||
* @property {Array<Object>} deliveryEntities Available suppliers or delivery parties.
|
||||
* @property {string|number|null} id Delivery identifier.
|
||||
* @property {string|number|null} deliveryId Alias of `id`.
|
||||
* @property {string|null} date Delivery date.
|
||||
* @property {string|number|null} deliveryEntity Selected delivery party.
|
||||
* @property {string|number|null} partyId Alias of `deliveryEntity`.
|
||||
* @property {string|null} externalNumber External delivery number.
|
||||
* @property {string|null} external_number API-compatible alias of `externalNumber`.
|
||||
* @property {string|null} number Internal delivery number.
|
||||
* @property {string|null} type Delivery type.
|
||||
* @property {Array<DeliveryLine>} lines Editable lines.
|
||||
* @property {Array<DeliveryLine>} lineitems Alias of `lines`.
|
||||
* @property {Array<*>} notes Delivery notes.
|
||||
* @property {Array<Object>} attachments Delivery attachments.
|
||||
* @property {Array<Object>} files Alias of `attachments`.
|
||||
* @property {Array<SupplierArticle>} supplierArticles Loaded supplier articles.
|
||||
* @property {Object<string, SupplierArticle>} supplierArticlesMap Articles indexed by SKU or EAN.
|
||||
* @property {Object<string, SupplierArticle|null>} supplierArticlesByLineKey Matched article per line.
|
||||
* @property {boolean} supplierArticlesLoading Whether any supplier article request is pending.
|
||||
* @property {Object<string, boolean>} loadingLineKeys Pending lookup state per line.
|
||||
* @property {string|null} supplierArticlesError Last supplier article error.
|
||||
* @property {string|null} unmatchedSupplierName Parsed supplier name without a matching entity.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Normalizes a partial line into an editor line with a stable key.
|
||||
*
|
||||
* @param {Partial<DeliveryLine>} [line] Initial line values.
|
||||
* @returns {DeliveryLine} Normalized line.
|
||||
*/
|
||||
export function createEmptyLineitem(line = {}) {
|
||||
return {
|
||||
key: line.key ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
@@ -14,6 +66,14 @@ export function createEmptyLineitem(line = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Indexes supplier articles by supplier SKU, internal SKU, and EAN. Text keys
|
||||
* are stored both verbatim and lower-cased for case-insensitive lookup.
|
||||
*
|
||||
* @param {Array<SupplierArticle>} [articles] Articles to index.
|
||||
* @param {Object<string, SupplierArticle>} [existingMap] Existing index to extend.
|
||||
* @returns {Object<string, SupplierArticle>} Article lookup map.
|
||||
*/
|
||||
export function buildSupplierArticlesMap(articles = [], existingMap = {}) {
|
||||
const map = { ...existingMap };
|
||||
for (const item of articles) {
|
||||
@@ -49,6 +109,7 @@ export function buildSupplierArticlesMap(articles = [], existingMap = {}) {
|
||||
return map;
|
||||
}
|
||||
|
||||
/** @type {Omit<DeliveryEditorState, 'deliveryEntities'>} */
|
||||
export const initialDelivery = {
|
||||
id: null,
|
||||
deliveryId: null,
|
||||
@@ -74,11 +135,19 @@ export const initialDelivery = {
|
||||
};
|
||||
|
||||
|
||||
/** @type {DeliveryEditorState} */
|
||||
export const initialState = {
|
||||
deliveryEntities: [],
|
||||
...initialDelivery,
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a delivery editor automat or returns the already registered named
|
||||
* instance. Named instances allow independent state for multiple deliveries.
|
||||
*
|
||||
* @param {{name?: string|null}|string} [options] Automat options or name.
|
||||
* @returns {Automat<DeliveryEditorState>} Delivery editor automat.
|
||||
*/
|
||||
export function createDeliveryEditorAutomat(options = {}) {
|
||||
const name = typeof options === 'string' ? options : options?.name ?? null;
|
||||
if (name) {
|
||||
@@ -88,14 +157,21 @@ export function createDeliveryEditorAutomat(options = {}) {
|
||||
|
||||
let automat;
|
||||
const actions = {
|
||||
/** @param {string|number|null} id @returns {void} */
|
||||
setId(id) {
|
||||
automat.setState({ id, deliveryId: id });
|
||||
},
|
||||
|
||||
/** @param {Array<Object>} deliveryEntities @returns {void} */
|
||||
setDeliveryEntities(deliveryEntities) {
|
||||
automat.setState({ deliveryEntities: deliveryEntities ?? [] });
|
||||
},
|
||||
|
||||
/**
|
||||
* Replaces the edited delivery and normalizes API aliases.
|
||||
* @param {Partial<DeliveryEditorState>} [delivery]
|
||||
* @returns {void}
|
||||
*/
|
||||
setDelivery(delivery = {}) {
|
||||
const rawLines = delivery.lines ?? delivery.lineitems ?? [];
|
||||
const lines = Array.isArray(rawLines) ? rawLines : [];
|
||||
@@ -129,10 +205,16 @@ export function createDeliveryEditorAutomat(options = {}) {
|
||||
}
|
||||
},
|
||||
|
||||
/** @param {string|null} date @returns {void} */
|
||||
setDate(date) {
|
||||
automat.setState({ date });
|
||||
},
|
||||
|
||||
/**
|
||||
* Selects a delivery party and refreshes its supplier articles.
|
||||
* @param {string|number|null} entityId
|
||||
* @returns {void}
|
||||
*/
|
||||
setDeliveryEntity(entityId) {
|
||||
const prevId = automat.state.deliveryEntity ?? automat.state.partyId;
|
||||
const idChanged = prevId !== entityId;
|
||||
@@ -155,15 +237,18 @@ export function createDeliveryEditorAutomat(options = {}) {
|
||||
}
|
||||
},
|
||||
|
||||
/** @param {string|null} name @returns {void} */
|
||||
setUnmatchedSupplierName(name) {
|
||||
automat.setState({ unmatchedSupplierName: name ?? null });
|
||||
},
|
||||
|
||||
|
||||
/** @param {string|number|null} partyId @returns {void} */
|
||||
setPartyId(partyId) {
|
||||
automat.actions.setDeliveryEntity(partyId);
|
||||
},
|
||||
|
||||
/** @param {string|null} externalNumber @returns {void} */
|
||||
setExternalNumber(externalNumber) {
|
||||
automat.setState({
|
||||
externalNumber,
|
||||
@@ -171,24 +256,29 @@ export function createDeliveryEditorAutomat(options = {}) {
|
||||
});
|
||||
},
|
||||
|
||||
/** @param {string|null} number @returns {void} */
|
||||
setNumber(number) {
|
||||
automat.setState({ number });
|
||||
},
|
||||
|
||||
/** @param {string|null} type @returns {void} */
|
||||
setType(type) {
|
||||
automat.setState({ type });
|
||||
},
|
||||
|
||||
/** @param {Array<DeliveryLine>} lines @returns {void} */
|
||||
setLines(lines) {
|
||||
const cleanLines = lines ?? [];
|
||||
automat.setState({ lines: cleanLines, lineitems: cleanLines });
|
||||
},
|
||||
|
||||
/** @param {Array<DeliveryLine>} lines @returns {void} */
|
||||
setLineitems(lines) {
|
||||
const cleanLines = lines ?? [];
|
||||
automat.setState({ lines: cleanLines, lineitems: cleanLines });
|
||||
},
|
||||
|
||||
/** @param {Array<*>|*} notes @returns {void} */
|
||||
setNotes(notes) {
|
||||
const cleanNotes = Array.isArray(notes)
|
||||
? notes
|
||||
@@ -196,16 +286,23 @@ export function createDeliveryEditorAutomat(options = {}) {
|
||||
automat.setState({ notes: cleanNotes });
|
||||
},
|
||||
|
||||
/** @param {Partial<DeliveryLine>} [line] @returns {void} */
|
||||
addLineitem(line = {}) {
|
||||
const item = createEmptyLineitem(line);
|
||||
const nextLines = [...(automat.state.lines ?? []), item];
|
||||
automat.setState({ lines: nextLines, lineitems: nextLines });
|
||||
},
|
||||
|
||||
/** @param {Partial<DeliveryLine>} [line] @returns {void} */
|
||||
addLine(line = {}) {
|
||||
automat.actions.addLineitem(line);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates a line selected by index or key.
|
||||
* @param {{index?: number, key?: string|number, [key: string]: *}} payload
|
||||
* @returns {void}
|
||||
*/
|
||||
updateLineitem(payload) {
|
||||
const { index, key, ...fields } = payload || {};
|
||||
const currentLines = automat.state.lines ?? [];
|
||||
@@ -223,10 +320,12 @@ export function createDeliveryEditorAutomat(options = {}) {
|
||||
}
|
||||
},
|
||||
|
||||
/** @param {{index?: number, key?: string|number, [key: string]: *}} payload @returns {void} */
|
||||
updateLine(payload) {
|
||||
automat.actions.updateLineitem(payload);
|
||||
},
|
||||
|
||||
/** @param {number|string|{key: string|number}} payload @returns {void} */
|
||||
removeLineitem(payload) {
|
||||
const currentLines = automat.state.lines ?? [];
|
||||
let index = typeof payload === 'number' && !Number.isNaN(payload) ? payload : -1;
|
||||
@@ -251,10 +350,17 @@ export function createDeliveryEditorAutomat(options = {}) {
|
||||
}
|
||||
},
|
||||
|
||||
/** @param {number|string|{key: string|number}} payload @returns {void} */
|
||||
removeLine(payload) {
|
||||
automat.actions.removeLineitem(payload);
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads supplier articles for all current SKUs and rebuilds line matches.
|
||||
* @param {string|number|null} [targetEntityId] Supplier to query.
|
||||
* @param {Array<DeliveryLine>} [targetLines] Lines whose SKUs should be queried.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async fetchSupplierArticles(targetEntityId, targetLines) {
|
||||
const entityId = targetEntityId ?? automat.state.deliveryEntity ?? automat.state.partyId;
|
||||
if (!entityId) {
|
||||
@@ -311,6 +417,12 @@ export function createDeliveryEditorAutomat(options = {}) {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads and matches a supplier article for one line SKU.
|
||||
* @param {string|number|null} sku SKU to query.
|
||||
* @param {string|number} [lineKey] Editor line key receiving the match.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async fetchSupplierArticleForSku(sku, lineKey) {
|
||||
const entityId = automat.state.deliveryEntity ?? automat.state.partyId;
|
||||
const trimmed = sku != null ? String(sku).trim() : '';
|
||||
@@ -370,6 +482,7 @@ export function createDeliveryEditorAutomat(options = {}) {
|
||||
}
|
||||
},
|
||||
|
||||
/** @param {Object} attachment @returns {void} */
|
||||
addAttachment(attachment) {
|
||||
const id = attachment.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const item = { ...attachment, id };
|
||||
@@ -377,11 +490,13 @@ export function createDeliveryEditorAutomat(options = {}) {
|
||||
automat.setState({ attachments: next, files: next });
|
||||
},
|
||||
|
||||
/** @param {Array<Object>} attachments @returns {void} */
|
||||
setAttachments(attachments) {
|
||||
const clean = Array.isArray(attachments) ? attachments : [];
|
||||
automat.setState({ attachments: clean, files: clean });
|
||||
},
|
||||
|
||||
/** @param {{id: string|number, [key: string]: *}} payload @returns {void} */
|
||||
updateAttachment(payload) {
|
||||
const { id, ...fields } = payload || {};
|
||||
const current = automat.state.attachments ?? [];
|
||||
@@ -393,6 +508,7 @@ export function createDeliveryEditorAutomat(options = {}) {
|
||||
}
|
||||
},
|
||||
|
||||
/** @param {string|number|{id?: string|number, fileId?: string|number}} idOrPayload @returns {void} */
|
||||
removeAttachment(idOrPayload) {
|
||||
const current = automat.state.attachments ?? [];
|
||||
const targetId =
|
||||
@@ -411,12 +527,14 @@ export function createDeliveryEditorAutomat(options = {}) {
|
||||
automat.setState({ attachments: next, files: next });
|
||||
},
|
||||
|
||||
/** Resets delivery fields while retaining the available entities. @returns {void} */
|
||||
resetDelivery() {
|
||||
automat.setState({
|
||||
...initialDelivery,
|
||||
});
|
||||
},
|
||||
|
||||
/** Resets the complete editor state. @returns {void} */
|
||||
resetDeliveryEditor() {
|
||||
automat.setState({
|
||||
...initialState,
|
||||
@@ -432,9 +550,17 @@ export function createDeliveryEditorAutomat(options = {}) {
|
||||
return automat;
|
||||
}
|
||||
|
||||
/** Shared automat used for new, unsaved deliveries. */
|
||||
export const deliveryDraftAutomat = createDeliveryEditorAutomat({ name: 'delivery-draft' });
|
||||
/** Backward-compatible alias of {@link deliveryDraftAutomat}. */
|
||||
export const deliveryEditorAutomat = deliveryDraftAutomat;
|
||||
|
||||
/**
|
||||
* Returns the draft automat or the named automat belonging to a saved delivery.
|
||||
*
|
||||
* @param {string|number|null} docId Delivery identifier; `null` selects the draft.
|
||||
* @returns {Automat<DeliveryEditorState>} Delivery-specific editor automat.
|
||||
*/
|
||||
export function getDeliveryEditorAutomat(docId) {
|
||||
if (docId == null) {
|
||||
return deliveryDraftAutomat;
|
||||
@@ -478,22 +604,38 @@ export const {
|
||||
} = deliveryEditorAutomat.actions;
|
||||
|
||||
// Selectors
|
||||
/** @param {DeliveryEditorState} [state] @returns {string|number|null} */
|
||||
export const selectDeliveryId = (state = deliveryEditorAutomat.state) => state.deliveryId ?? state.id;
|
||||
/** @param {DeliveryEditorState} [state] @returns {Array<Object>} */
|
||||
export const selectDeliveryEntities = (state = deliveryEditorAutomat.state) => state.deliveryEntities;
|
||||
/** @param {DeliveryEditorState} [state] @returns {string|null} */
|
||||
export const selectDeliveryDate = (state = deliveryEditorAutomat.state) => state.date;
|
||||
/** @param {DeliveryEditorState} [state] @returns {string|number|null} */
|
||||
export const selectDeliveryEntity = (state = deliveryEditorAutomat.state) => state.deliveryEntity ?? state.partyId;
|
||||
/** @param {DeliveryEditorState} [state] @returns {string|null} */
|
||||
export const selectUnmatchedSupplierName = (state = deliveryEditorAutomat.state) => state.unmatchedSupplierName;
|
||||
/** @param {DeliveryEditorState} [state] @returns {string|null} */
|
||||
export const selectDeliveryExternalNumber = (state = deliveryEditorAutomat.state) => state.externalNumber ?? state.external_number;
|
||||
|
||||
/** @param {DeliveryEditorState} [state] @returns {string|null} */
|
||||
export const selectDeliveryNumber = (state = deliveryEditorAutomat.state) => state.number;
|
||||
/** @param {DeliveryEditorState} [state] @returns {string|null} */
|
||||
export const selectDeliveryType = (state = deliveryEditorAutomat.state) => state.type;
|
||||
/** @param {DeliveryEditorState} [state] @returns {Array<DeliveryLine>} */
|
||||
export const selectDeliveryLineitems = (state = deliveryEditorAutomat.state) => state.lines ?? state.lineitems;
|
||||
/** @param {DeliveryEditorState} [state] @returns {Array<*>} */
|
||||
export const selectDeliveryNotes = (state = deliveryEditorAutomat.state) => state.notes;
|
||||
/** @param {DeliveryEditorState} [state] @returns {Array<Object>} */
|
||||
export const selectDeliveryAttachments = (state = deliveryEditorAutomat.state) => state.attachments;
|
||||
/** @param {DeliveryEditorState} [state] @returns {Array<SupplierArticle>} */
|
||||
export const selectSupplierArticles = (state = deliveryEditorAutomat.state) => state.supplierArticles;
|
||||
/** @param {DeliveryEditorState} [state] @returns {Object<string, SupplierArticle>} */
|
||||
export const selectSupplierArticlesMap = (state = deliveryEditorAutomat.state) => state.supplierArticlesMap;
|
||||
/** @param {DeliveryEditorState} [state] @returns {Object<string, SupplierArticle|null>} */
|
||||
export const selectSupplierArticlesByLineKey = (state = deliveryEditorAutomat.state) => state.supplierArticlesByLineKey;
|
||||
/** @param {DeliveryEditorState} [state] @returns {boolean} */
|
||||
export const selectSupplierArticlesLoading = (state = deliveryEditorAutomat.state) => state.supplierArticlesLoading;
|
||||
/** @param {DeliveryEditorState} [state] @returns {Object<string, boolean>} */
|
||||
export const selectLoadingLineKeys = (state = deliveryEditorAutomat.state) => state.loadingLineKeys;
|
||||
|
||||
export default deliveryEditorAutomat;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './deliveryEditorAutomat';
|
||||
export { default as authAutomat } from './authAutomat';
|
||||
export * from './authAutomat';
|
||||
export { default as navigationAutomat } from './navigationAutomat';
|
||||
export { setLastDeliveryPath, setLastArticleSku, clearNavigation } from './navigationAutomat';
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
import { Automat } from 'react-automat';
|
||||
|
||||
/**
|
||||
* Navigation automat to store last visited delivery and article paths.
|
||||
* Used to return users to their previous context when clicking main menu items.
|
||||
*
|
||||
* State shape:
|
||||
* {
|
||||
* lastDeliveryPath: string | null, // Last visited delivery path (e.g., '/lieferung/19' or '/lieferung/neu')
|
||||
* lastArticleSku: string | null, // Last visited article SKU
|
||||
* }
|
||||
* @typedef {Object} NavigationState
|
||||
* @property {string|null} lastDeliveryPath Last delivery path visited by the user.
|
||||
* @property {string|null} lastArticleSku Last article SKU visited by the user.
|
||||
*/
|
||||
|
||||
/** @type {NavigationState} */
|
||||
const initialState = {
|
||||
lastDeliveryPath: null,
|
||||
lastArticleSku: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Stores the user's most recently visited delivery and article so main-menu
|
||||
* navigation can restore the previous context.
|
||||
*
|
||||
* @type {Automat<NavigationState>}
|
||||
*/
|
||||
const navigationAutomat = new Automat(
|
||||
initialState,
|
||||
{
|
||||
/**
|
||||
* Set the last visited delivery path.
|
||||
* @param {string|null} path - Full path like '/lieferung/19' or '/lieferung/neu'
|
||||
*
|
||||
* @param {string|null} path Full path such as `/lieferung/19` or `/lieferung/neu`.
|
||||
* @returns {void}
|
||||
*/
|
||||
setLastDeliveryPath(path) {
|
||||
navigationAutomat.setState({ lastDeliveryPath: path });
|
||||
@@ -28,7 +33,9 @@ const navigationAutomat = new Automat(
|
||||
|
||||
/**
|
||||
* Set the last visited article SKU.
|
||||
*
|
||||
* @param {string|null} sku
|
||||
* @returns {void}
|
||||
*/
|
||||
setLastArticleSku(sku) {
|
||||
navigationAutomat.setState({ lastArticleSku: sku });
|
||||
@@ -36,6 +43,8 @@ const navigationAutomat = new Automat(
|
||||
|
||||
/**
|
||||
* Clear all navigation history.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
clearNavigation() {
|
||||
navigationAutomat.setState({
|
||||
|
||||
Reference in New Issue
Block a user