u
This commit is contained in:
@@ -1,39 +1,236 @@
|
||||
/**
|
||||
* @file orderAutomat.js
|
||||
* @description Manages cached company sales orders (`GET /api/order`).
|
||||
* Built via `createListAutomat`.
|
||||
* @description Manages cached and paginated company sales orders (`GET /api/order`).
|
||||
*
|
||||
* Upstream Triggers:
|
||||
* - `authAutomat`: Via `createListAutomat` — triggers `load()` on `'authed'` with
|
||||
* a `selectedCompanyId`, and `reset()` on `'anon'`.
|
||||
* - UI / Routes: Manual calls to `load()` or `reload()`.
|
||||
* - `authAutomat`: Triggers `actions.load()` on `'authed'` with a `selectedCompanyId`,
|
||||
* and `actions.reset()` on `'anon'`.
|
||||
* - UI / Routes: Calls to `load({ offset, limit })`, `setPage(page)`, or `reload()`.
|
||||
*/
|
||||
|
||||
import { Automat } from 'react-automat';
|
||||
import { order as orderApi } from '../api';
|
||||
import { createListAutomat } from './createListAutomat';
|
||||
import authAutomat from './authAutomat';
|
||||
|
||||
/**
|
||||
* @typedef {Object} OrderSummary
|
||||
* @property {string|number} id Order id.
|
||||
* @typedef {Object} OrderLine
|
||||
* @property {number} id
|
||||
* @property {string|null} sku
|
||||
* @property {string|null} name
|
||||
* @property {number} pu
|
||||
* @property {number} amount
|
||||
* @property {number} net
|
||||
* @property {number} rebate
|
||||
* @property {number} taxrate
|
||||
* @property {string|null} comment
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Order
|
||||
* @property {number} id Order id.
|
||||
* @property {string} number Order number.
|
||||
* @property {string} type Always `'order'`.
|
||||
* @property {string|number|null} entityId Customer id.
|
||||
* @property {'order'} type Always `'order'`.
|
||||
* @property {number|null} entityId Customer id.
|
||||
* @property {string|null} externalNumber External reference.
|
||||
* @property {string} date Delivery date (ISO).
|
||||
* @property {string} createdAt Creation timestamp (ISO).
|
||||
* @property {OrderLine[]} lines Order line items.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Company sales orders (`GET /api/order`).
|
||||
*
|
||||
* @type {import('react-automat').Automat<import('./createListAutomat').ListAutomatState>}
|
||||
* @typedef {Object} OrderAutomatState
|
||||
* @property {Order[]} items Loaded orders for the current page.
|
||||
* @property {number} total Total unpaged count for pagination calculation.
|
||||
* @property {number} offset Current 0-based offset.
|
||||
* @property {number} limit Current page limit.
|
||||
* @property {'idle'|'loading'|'ready'|'failed'} status Load lifecycle state.
|
||||
* @property {Error|string|null} error Last load error.
|
||||
*/
|
||||
export const orderAutomat = createListAutomat({
|
||||
name: 'order',
|
||||
fetch: async () => {
|
||||
const data = await orderApi.list();
|
||||
return data.orders ?? [];
|
||||
|
||||
const initialState = {
|
||||
items: [],
|
||||
total: 0,
|
||||
offset: 0,
|
||||
limit: 20,
|
||||
status: 'idle',
|
||||
error: null,
|
||||
};
|
||||
|
||||
let automat;
|
||||
let inflightLoad = null;
|
||||
|
||||
const actions = {
|
||||
/**
|
||||
* Loads paginated orders, deduplicating concurrent callers.
|
||||
*
|
||||
* @param {{ offset?: number, limit?: number }} [params]
|
||||
* @returns {Promise<Order[]>}
|
||||
*/
|
||||
load({ offset, limit } = {}) {
|
||||
const currentOffset = offset ?? automat.state.offset ?? 0;
|
||||
const currentLimit = limit ?? automat.state.limit ?? 20;
|
||||
|
||||
if (inflightLoad) return inflightLoad;
|
||||
inflightLoad = (async () => {
|
||||
automat.setState({ status: 'loading', error: null });
|
||||
try {
|
||||
const res = await orderApi.list({ offset: currentOffset, limit: currentLimit });
|
||||
const orders = res?.orders ?? [];
|
||||
automat.setState({
|
||||
items: orders,
|
||||
total: Number(res?.total ?? orders.length),
|
||||
offset: Number(res?.offset ?? currentOffset),
|
||||
limit: Number(res?.limit ?? currentLimit),
|
||||
status: 'ready',
|
||||
error: null,
|
||||
});
|
||||
return orders;
|
||||
} catch (error) {
|
||||
automat.setState({ status: 'failed', error });
|
||||
throw error;
|
||||
} finally {
|
||||
inflightLoad = null;
|
||||
}
|
||||
})();
|
||||
return inflightLoad;
|
||||
},
|
||||
sort: (items) =>
|
||||
[...items].sort((a, b) => String(b.date).localeCompare(String(a.date))),
|
||||
|
||||
/**
|
||||
* Silently re-fetches the current page of orders without flashing loading state.
|
||||
*
|
||||
* @returns {Promise<Order[]>}
|
||||
*/
|
||||
async reload() {
|
||||
const { offset, limit } = automat.state;
|
||||
try {
|
||||
const res = await orderApi.list({ offset, limit });
|
||||
const orders = res?.orders ?? [];
|
||||
automat.setState({
|
||||
items: orders,
|
||||
total: Number(res?.total ?? orders.length),
|
||||
offset: Number(res?.offset ?? offset),
|
||||
limit: Number(res?.limit ?? limit),
|
||||
status: 'ready',
|
||||
error: null,
|
||||
});
|
||||
return orders;
|
||||
} catch (error) {
|
||||
automat.setState({ error });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Navigates to a 1-based page using formula: offset = (page - 1) * limit
|
||||
*
|
||||
* @param {number} page 1-based page number.
|
||||
* @returns {Promise<Order[]>}
|
||||
*/
|
||||
setPage(page) {
|
||||
const limit = automat.state.limit || 20;
|
||||
const offset = Math.max(0, (page - 1) * limit);
|
||||
return actions.load({ offset, limit });
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets pagination offset directly.
|
||||
*
|
||||
* @param {number} offset 0-based offset.
|
||||
* @returns {Promise<Order[]>}
|
||||
*/
|
||||
setOffset(offset) {
|
||||
return actions.load({ offset, limit: automat.state.limit });
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets page limit (records to return) and resets offset to 0.
|
||||
*
|
||||
* @param {number} limit Records per page (1..100).
|
||||
* @returns {Promise<Order[]>}
|
||||
*/
|
||||
setLimit(limit) {
|
||||
return actions.load({ offset: 0, limit });
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensures orders are loaded once the selected company is known.
|
||||
*
|
||||
* @returns {Promise<Order[]>}
|
||||
*/
|
||||
ensureLoaded() {
|
||||
if (automat.state.status === 'ready') {
|
||||
return Promise.resolve(automat.state.items);
|
||||
}
|
||||
return actions.load();
|
||||
},
|
||||
|
||||
/**
|
||||
* Replaces cached orders without network round-trip.
|
||||
*
|
||||
* @param {Order[]} items
|
||||
* @returns {Order[]}
|
||||
*/
|
||||
replace(items) {
|
||||
automat.setState({
|
||||
items,
|
||||
total: items.length,
|
||||
status: 'ready',
|
||||
error: null,
|
||||
});
|
||||
return items;
|
||||
},
|
||||
|
||||
/**
|
||||
* Resets to idle state (called on logout).
|
||||
*/
|
||||
reset() {
|
||||
inflightLoad = null;
|
||||
automat.setState(initialState);
|
||||
},
|
||||
};
|
||||
|
||||
export const orderAutomat = new Automat(initialState, actions, { name: 'order' });
|
||||
automat = orderAutomat;
|
||||
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
|
||||
/** Selects current orders */
|
||||
export function selectOrders(state = automat.state) {
|
||||
return state.items;
|
||||
}
|
||||
|
||||
/** Selects total unpaged count */
|
||||
export function selectOrderTotal(state = automat.state) {
|
||||
return state.total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates client pagination metadata.
|
||||
* Current Page: Math.floor(offset / limit) + 1
|
||||
* Total Pages: Math.ceil(total / limit)
|
||||
* Next Offset: (page - 1) * limit
|
||||
*/
|
||||
export function selectOrderPagination(state = automat.state) {
|
||||
const total = state.total ?? 0;
|
||||
const offset = state.offset ?? 0;
|
||||
const limit = state.limit || 20;
|
||||
const currentPage = Math.floor(offset / limit) + 1;
|
||||
const pageCount = Math.max(1, Math.ceil(total / limit));
|
||||
return {
|
||||
total,
|
||||
offset,
|
||||
limit,
|
||||
currentPage,
|
||||
pageCount,
|
||||
};
|
||||
}
|
||||
|
||||
export default orderAutomat;
|
||||
|
||||
Reference in New Issue
Block a user