u
This commit is contained in:
@@ -320,6 +320,29 @@ const articles = await store.article.listExport(42, null);
|
||||
// => [{ id, sku, name, slug, unitSi, net, taxRate, eans, categoryBreadcrumbs, fullHashes, ... }]
|
||||
```
|
||||
|
||||
## `article.listHistory`
|
||||
|
||||
*Source: `src/store/article/listHistory.js`*
|
||||
|
||||
Lists article history entries for the user's selected company, supporting pagination, date cutoff via seekMonths, and search.
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `provider` | `import('../../providers/provider.js').Provider` | |
|
||||
|
||||
**Returns**
|
||||
|
||||
`(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, seekMonths?: number}, tx?: object) => Promise<{history: Array<{id: number, date: string, sku: string, name: string|null, historytext: string}>, total: number, unfilteredTotal: number, offset: number, limit: number}>`
|
||||
|
||||
**Example**
|
||||
|
||||
```js
|
||||
const res = await store.article.listHistory(googleAuthId, { offset: 0, limit: 50, search: 'hjg', seekMonths: -13 });
|
||||
// => { history: [...], total, unfilteredTotal, offset, limit }
|
||||
```
|
||||
|
||||
## `article.listSearchIndex`
|
||||
|
||||
*Source: `src/store/article/listSearchIndex.js`*
|
||||
|
||||
@@ -84,3 +84,26 @@ Lists ledger entries for a given article SKU, optionally filtered by transaction
|
||||
const entries = await store.ledger.listBySku(googleAuthId, 'SKU-001', 'DELIVERY');
|
||||
// => [{ id, manifestarticle_id, warehouse_id, sku, transaction_type, amount, transaction_text, hash_version, prev_hash, created_at }]
|
||||
```
|
||||
|
||||
## `ledger.listHistory`
|
||||
|
||||
*Source: `src/store/ledger/listHistory.js`*
|
||||
|
||||
Lists ledger entries for the user's selected company across all articles, supporting pagination, date cutoff via seekMonths, and search.
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `provider` | `import('../../providers/provider.js').Provider` | |
|
||||
|
||||
**Returns**
|
||||
|
||||
`(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, seekMonths?: number}, tx?: object) => Promise<{ledger: Array<{id: number, date: string, sku: string|null, name: string|null, transaction_type: string, transaction_text: string, amount: number}>, total: number, unfilteredTotal: number, offset: number, limit: number}>`
|
||||
|
||||
**Example**
|
||||
|
||||
```js
|
||||
const res = await store.ledger.listHistory(googleAuthId, { offset: 0, limit: 50, search: 'kjh', seekMonths: -1 });
|
||||
// => { ledger: [...], total, unfilteredTotal, offset, limit }
|
||||
```
|
||||
|
||||
2
API.md
2
API.md
@@ -116,6 +116,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
||||
- `importRows(input: {googleAuthId: number, rows: Array<object>}, tx?: object): Promise<{imported: Array<object>, skipped: Array<object>, errors: Array<object>}>`
|
||||
- `listChildren(googleAuthId: number, parentSku: string, tx?: object): Promise<Array<{id: number, sku: string, parentsort: number, trackStock: boolean, unitamount: number, weight: number, name: string|null, slug: string|null, attributeValueId: number|null, attributeValue: string|null}>>`
|
||||
- `listExport(googleAuthId: number, categoryId?: number|null, tx?: object): Promise<Array<object>>`
|
||||
- `listHistory(provider: import('../../providers/provider.js').Provider): (googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, seekMonths?: number}, tx?: object) => Promise<{history: Array<{id: number, date: string, sku: string, name: string|null, historytext: string}>, total: number, unfilteredTotal: number, offset: number, limit: number}>`
|
||||
- `listSearchIndex(googleAuthId: number, tx?: object): Promise<Array<{id: number, sku: string, name: string, eans: Array<string>, categoryIds: Array<number>}>>`
|
||||
- `orderImages(googleAuthId: number, sku: string, input: {fullHashes: Array<string>}, tx?: object): Promise<{fullHashes: Array<string>}>`
|
||||
- `removeChild(googleAuthId: number, parentSku: string, childSku: string, options?: {deleteArticle?: boolean, cascade?: boolean}, tx?: object): Promise<{success: boolean, parentSku: string, childSku: string, deleted: boolean}>`
|
||||
@@ -238,6 +239,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
||||
- `add(googleAuthId: number, input: {manifestarticle_id: number, warehouse_id?: number|null, transaction_type: string, amount: number, transaction_text: string}, tx?: object): Promise<{id: number, manifestarticle_id: number, warehouse_id: number|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>`
|
||||
- `listByManifestarticleId(googleAuthId: number, manifestarticleId: number, optionsOrType?: string|object, tx?: object): Promise<Array<{id: number, manifestarticle_id: number, warehouse_id: number|null, sku: string|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>>`
|
||||
- `listBySku(googleAuthId: number, sku: string, optionsOrType?: string|object, tx?: object): Promise<Array<{id: number, manifestarticle_id: number, warehouse_id: number|null, sku: string|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>>`
|
||||
- `listHistory(provider: import('../../providers/provider.js').Provider): (googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, seekMonths?: number}, tx?: object) => Promise<{ledger: Array<{id: number, date: string, sku: string|null, name: string|null, transaction_type: string, transaction_text: string, amount: number}>, total: number, unfilteredTotal: number, offset: number, limit: number}>`
|
||||
- [warehouse](API.warehouse.md)
|
||||
- `create(googleAuthId: number, input: {name: string, sort?: number}, tx?: object): Promise<{id: number, name: string, sort: number}>`
|
||||
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
|
||||
|
||||
@@ -25,6 +25,7 @@ import articleAddImage from './store/article/addImage.js';
|
||||
import articleCreateUnderParent from './store/article/createUnderParent.js';
|
||||
import articleGetBySku from './store/article/getBySku.js';
|
||||
import articleGetHistoryBySku from './store/article/getHistoryBySku.js';
|
||||
import articleListHistory from './store/article/listHistory.js';
|
||||
import articleListSearchIndex from './store/article/listSearchIndex.js';
|
||||
import articleListExport from './store/article/listExport.js';
|
||||
import articleUpdateBySku from './store/article/updateBySku.js';
|
||||
@@ -159,6 +160,7 @@ import manifestattachmentOrder from './store/manifestattachment/order.js';
|
||||
import ledgerAdd from './store/ledger/add.js';
|
||||
import ledgerListByManifestarticleId from './store/ledger/listByManifestarticleId.js';
|
||||
import ledgerListBySku from './store/ledger/listBySku.js';
|
||||
import ledgerListHistory from './store/ledger/listHistory.js';
|
||||
import warehouseCreate from './store/warehouse/create.js';
|
||||
import warehouseGetById from './store/warehouse/getById.js';
|
||||
import warehouseList from './store/warehouse/list.js';
|
||||
@@ -320,6 +322,7 @@ export default class LibDashStore {
|
||||
createUnderParent: articleCreateUnderParent(provider),
|
||||
getBySku: articleGetBySku(provider),
|
||||
getHistoryBySku: articleGetHistoryBySku(provider),
|
||||
listHistory: articleListHistory(provider),
|
||||
listSearchIndex: articleListSearchIndex(provider),
|
||||
listExport: articleListExport(provider),
|
||||
updateBySku: articleUpdateBySku(provider),
|
||||
@@ -555,6 +558,7 @@ export default class LibDashStore {
|
||||
add: ledgerAdd(provider),
|
||||
listByManifestarticleId: ledgerListByManifestarticleId(provider),
|
||||
listBySku: ledgerListBySku(provider),
|
||||
listHistory: ledgerListHistory(provider),
|
||||
};
|
||||
|
||||
this.warehouse = {
|
||||
|
||||
170
src/store/article/listHistory.js
Normal file
170
src/store/article/listHistory.js
Normal file
@@ -0,0 +1,170 @@
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
/**
|
||||
* Lists article history entries for the user's selected company,
|
||||
* supporting pagination, date cutoff via seekMonths, and search.
|
||||
*
|
||||
* @param {import('../../providers/provider.js').Provider} provider
|
||||
* @returns {(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, seekMonths?: number}, tx?: object) => Promise<{history: Array<{id: number, date: string, sku: string, name: string|null, historytext: string}>, total: number, unfilteredTotal: number, offset: number, limit: number}>}
|
||||
*
|
||||
* @example
|
||||
* const res = await store.article.listHistory(googleAuthId, { offset: 0, limit: 50, search: 'hjg', seekMonths: -13 });
|
||||
* // => { history: [...], total, unfilteredTotal, offset, limit }
|
||||
*/
|
||||
export default function listHistory(provider) {
|
||||
const access = resolveSelectedCompanyAccess(provider);
|
||||
const isSqlite = provider?.dialect === 'sqlite3';
|
||||
|
||||
return async (googleAuthId, opts, tx) => {
|
||||
let offset = 0;
|
||||
let limit = 50;
|
||||
let isPaged = true;
|
||||
let resolvedTx = tx;
|
||||
|
||||
if (opts != null && typeof opts === 'object' && !opts._isTx) {
|
||||
if (opts.offset !== undefined) {
|
||||
const parsedOffset = Number(opts.offset);
|
||||
if (!Number.isInteger(parsedOffset) || parsedOffset < 0) {
|
||||
throw new Error('Invalid offset');
|
||||
}
|
||||
offset = parsedOffset;
|
||||
}
|
||||
if (opts.limit !== undefined) {
|
||||
const parsedLimit = Number(opts.limit);
|
||||
if (!Number.isInteger(parsedLimit) || parsedLimit < 1) {
|
||||
throw new Error('Invalid limit');
|
||||
}
|
||||
limit = parsedLimit;
|
||||
}
|
||||
} else if (opts != null) {
|
||||
resolvedTx = opts;
|
||||
}
|
||||
|
||||
const runner = makeRunner(provider, resolvedTx);
|
||||
const { companyId, languageId } = await access(googleAuthId, runner);
|
||||
|
||||
const conditions = ['a.company_id = @companyId'];
|
||||
const params = {
|
||||
companyId,
|
||||
languageId,
|
||||
};
|
||||
|
||||
// Date cutoff via seekMonths (strictly integer)
|
||||
if (opts?.seekMonths !== undefined) {
|
||||
if (typeof opts.seekMonths !== 'number' || !Number.isInteger(opts.seekMonths)) {
|
||||
throw new Error('Invalid seekMonths');
|
||||
}
|
||||
const d = new Date();
|
||||
d.setMonth(d.getMonth() - Math.abs(opts.seekMonths));
|
||||
params.dateTo = d.toISOString();
|
||||
conditions.push(`ah.${isSqlite ? '"date"' : '[date]'} <= @dateTo`);
|
||||
}
|
||||
|
||||
// Free-text search
|
||||
if (typeof opts?.search === 'string' && opts.search.trim() !== '') {
|
||||
params.searchPattern = `%${opts.search.trim()}%`;
|
||||
if (isSqlite) {
|
||||
conditions.push(`(
|
||||
a.sku LIKE @searchPattern
|
||||
OR at.name LIKE @searchPattern
|
||||
OR ah."text" LIKE @searchPattern
|
||||
)`);
|
||||
} else {
|
||||
conditions.push(`(
|
||||
a.sku LIKE @searchPattern
|
||||
OR at.name LIKE @searchPattern
|
||||
OR ah.[text] LIKE @searchPattern
|
||||
)`);
|
||||
}
|
||||
}
|
||||
|
||||
const whereClause = conditions.join('\n AND ');
|
||||
|
||||
const sqlCount = `
|
||||
SELECT COUNT(*) AS total
|
||||
FROM articlehistory ah
|
||||
INNER JOIN article a ON a.id = ah.article_id
|
||||
LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId
|
||||
WHERE ${whereClause}
|
||||
`;
|
||||
|
||||
const sqlCountBase = `
|
||||
SELECT COUNT(*) AS total
|
||||
FROM articlehistory ah
|
||||
INNER JOIN article a ON a.id = ah.article_id
|
||||
WHERE a.company_id = @companyId
|
||||
`;
|
||||
|
||||
let sqlList;
|
||||
if (isSqlite) {
|
||||
sqlList = `
|
||||
SELECT
|
||||
ah.id,
|
||||
ah."date",
|
||||
a.sku,
|
||||
at.name,
|
||||
ah."text" AS historytext
|
||||
FROM articlehistory ah
|
||||
INNER JOIN article a ON a.id = ah.article_id
|
||||
LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId
|
||||
WHERE ${whereClause}
|
||||
ORDER BY ah."date" DESC, ah.id DESC
|
||||
${isPaged ? 'LIMIT @limit OFFSET @offset' : ''}
|
||||
`;
|
||||
} else {
|
||||
sqlList = `
|
||||
SELECT
|
||||
ah.id,
|
||||
ah.[date],
|
||||
a.sku,
|
||||
at.name,
|
||||
ah.[text] AS historytext
|
||||
FROM articlehistory ah
|
||||
INNER JOIN article a ON a.id = ah.article_id
|
||||
LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId
|
||||
WHERE ${whereClause}
|
||||
ORDER BY ah.[date] DESC, ah.id DESC
|
||||
${isPaged ? 'OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY' : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
const hasUserFilters = (
|
||||
opts?.seekMonths !== undefined ||
|
||||
(typeof opts?.search === 'string' && opts.search.trim() !== '')
|
||||
);
|
||||
|
||||
params.offset = offset;
|
||||
params.limit = limit;
|
||||
|
||||
const [countResult, listResult, unfilteredCountResult] = await Promise.all([
|
||||
runner.query(sqlCount, params),
|
||||
runner.query(sqlList, params),
|
||||
hasUserFilters ? runner.query(sqlCountBase, { companyId }) : null,
|
||||
]);
|
||||
|
||||
const total = Number(countResult.rows[0]?.total ?? 0);
|
||||
const unfilteredTotal = hasUserFilters
|
||||
? Number(unfilteredCountResult?.rows[0]?.total ?? 0)
|
||||
: total;
|
||||
|
||||
return {
|
||||
history: listResult.rows.map((row) => ({
|
||||
id: row.id,
|
||||
date:
|
||||
row.date instanceof Date
|
||||
? row.date.toISOString()
|
||||
: row.date
|
||||
? new Date(row.date).toISOString()
|
||||
: null,
|
||||
sku: row.sku,
|
||||
name: row.name ?? null,
|
||||
historytext: row.historytext,
|
||||
})),
|
||||
total,
|
||||
unfilteredTotal,
|
||||
offset,
|
||||
limit,
|
||||
};
|
||||
};
|
||||
}
|
||||
180
src/store/ledger/listHistory.js
Normal file
180
src/store/ledger/listHistory.js
Normal file
@@ -0,0 +1,180 @@
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
/**
|
||||
* Lists ledger entries for the user's selected company across all articles,
|
||||
* supporting pagination, date cutoff via seekMonths, and search.
|
||||
*
|
||||
* @param {import('../../providers/provider.js').Provider} provider
|
||||
* @returns {(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, seekMonths?: number}, tx?: object) => Promise<{ledger: Array<{id: number, date: string, sku: string|null, name: string|null, transaction_type: string, transaction_text: string, amount: number}>, total: number, unfilteredTotal: number, offset: number, limit: number}>}
|
||||
*
|
||||
* @example
|
||||
* const res = await store.ledger.listHistory(googleAuthId, { offset: 0, limit: 50, search: 'kjh', seekMonths: -1 });
|
||||
* // => { ledger: [...], total, unfilteredTotal, offset, limit }
|
||||
*/
|
||||
export default function listHistory(provider) {
|
||||
const access = resolveSelectedCompanyAccess(provider);
|
||||
const isSqlite = provider?.dialect === 'sqlite3';
|
||||
|
||||
return async (googleAuthId, opts, tx) => {
|
||||
let offset = 0;
|
||||
let limit = 50;
|
||||
let isPaged = true;
|
||||
let resolvedTx = tx;
|
||||
|
||||
if (opts != null && typeof opts === 'object' && !opts._isTx) {
|
||||
if (opts.offset !== undefined) {
|
||||
const parsedOffset = Number(opts.offset);
|
||||
if (!Number.isInteger(parsedOffset) || parsedOffset < 0) {
|
||||
throw new Error('Invalid offset');
|
||||
}
|
||||
offset = parsedOffset;
|
||||
}
|
||||
if (opts.limit !== undefined) {
|
||||
const parsedLimit = Number(opts.limit);
|
||||
if (!Number.isInteger(parsedLimit) || parsedLimit < 1) {
|
||||
throw new Error('Invalid limit');
|
||||
}
|
||||
limit = parsedLimit;
|
||||
}
|
||||
} else if (opts != null) {
|
||||
resolvedTx = opts;
|
||||
}
|
||||
|
||||
const runner = makeRunner(provider, resolvedTx);
|
||||
const { companyId, languageId } = await access(googleAuthId, runner);
|
||||
|
||||
const conditions = ['m.company_id = @companyId'];
|
||||
const params = {
|
||||
companyId,
|
||||
languageId,
|
||||
};
|
||||
|
||||
// Date cutoff via seekMonths (strictly integer)
|
||||
if (opts?.seekMonths !== undefined) {
|
||||
if (typeof opts.seekMonths !== 'number' || !Number.isInteger(opts.seekMonths)) {
|
||||
throw new Error('Invalid seekMonths');
|
||||
}
|
||||
const d = new Date();
|
||||
d.setMonth(d.getMonth() - Math.abs(opts.seekMonths));
|
||||
params.dateTo = d.toISOString();
|
||||
conditions.push(`l.created_at <= @dateTo`);
|
||||
}
|
||||
|
||||
// Free-text search
|
||||
if (typeof opts?.search === 'string' && opts.search.trim() !== '') {
|
||||
params.searchPattern = `%${opts.search.trim()}%`;
|
||||
const numCol = isSqlite ? 'm."number"' : 'm.[number]';
|
||||
conditions.push(`(
|
||||
ma.sku LIKE @searchPattern
|
||||
OR ma.name LIKE @searchPattern
|
||||
OR at.name LIKE @searchPattern
|
||||
OR ${numCol} LIKE @searchPattern
|
||||
OR m.external_number LIKE @searchPattern
|
||||
OR l.transaction_type LIKE @searchPattern
|
||||
OR l.transaction_text LIKE @searchPattern
|
||||
)`);
|
||||
}
|
||||
|
||||
const whereClause = conditions.join('\n AND ');
|
||||
|
||||
const sqlCount = `
|
||||
SELECT COUNT(*) AS total
|
||||
FROM ledger l
|
||||
INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id
|
||||
INNER JOIN manifest m ON m.id = ma.manifest_id
|
||||
LEFT JOIN article a ON a.sku = ma.sku AND a.company_id = m.company_id
|
||||
LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId
|
||||
WHERE ${whereClause}
|
||||
`;
|
||||
|
||||
const sqlCountBase = `
|
||||
SELECT COUNT(*) AS total
|
||||
FROM ledger l
|
||||
INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id
|
||||
INNER JOIN manifest m ON m.id = ma.manifest_id
|
||||
WHERE m.company_id = @companyId
|
||||
`;
|
||||
|
||||
let sqlList;
|
||||
if (isSqlite) {
|
||||
sqlList = `
|
||||
SELECT
|
||||
l.id,
|
||||
l.created_at AS "date",
|
||||
ma.sku,
|
||||
COALESCE(at.name, ma.name) AS name,
|
||||
l.transaction_type,
|
||||
l.transaction_text,
|
||||
l.amount
|
||||
FROM ledger l
|
||||
INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id
|
||||
INNER JOIN manifest m ON m.id = ma.manifest_id
|
||||
LEFT JOIN article a ON a.sku = ma.sku AND a.company_id = m.company_id
|
||||
LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId
|
||||
WHERE ${whereClause}
|
||||
ORDER BY l.created_at DESC, l.id DESC
|
||||
${isPaged ? 'LIMIT @limit OFFSET @offset' : ''}
|
||||
`;
|
||||
} else {
|
||||
sqlList = `
|
||||
SELECT
|
||||
l.id,
|
||||
l.created_at AS [date],
|
||||
ma.sku,
|
||||
COALESCE(at.name, ma.name) AS name,
|
||||
l.transaction_type,
|
||||
l.transaction_text,
|
||||
l.amount
|
||||
FROM ledger l
|
||||
INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id
|
||||
INNER JOIN manifest m ON m.id = ma.manifest_id
|
||||
LEFT JOIN article a ON a.sku = ma.sku AND a.company_id = m.company_id
|
||||
LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId
|
||||
WHERE ${whereClause}
|
||||
ORDER BY l.created_at DESC, l.id DESC
|
||||
${isPaged ? 'OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY' : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
const hasUserFilters = (
|
||||
opts?.seekMonths !== undefined ||
|
||||
(typeof opts?.search === 'string' && opts.search.trim() !== '')
|
||||
);
|
||||
|
||||
params.offset = offset;
|
||||
params.limit = limit;
|
||||
|
||||
const [countResult, listResult, unfilteredCountResult] = await Promise.all([
|
||||
runner.query(sqlCount, params),
|
||||
runner.query(sqlList, params),
|
||||
hasUserFilters ? runner.query(sqlCountBase, { companyId }) : null,
|
||||
]);
|
||||
|
||||
const total = Number(countResult.rows[0]?.total ?? 0);
|
||||
const unfilteredTotal = hasUserFilters
|
||||
? Number(unfilteredCountResult?.rows[0]?.total ?? 0)
|
||||
: total;
|
||||
|
||||
return {
|
||||
ledger: listResult.rows.map((row) => ({
|
||||
id: row.id,
|
||||
date:
|
||||
row.date instanceof Date
|
||||
? row.date.toISOString()
|
||||
: row.date
|
||||
? new Date(row.date).toISOString()
|
||||
: null,
|
||||
sku: row.sku ?? null,
|
||||
name: row.name ?? null,
|
||||
transaction_type: row.transaction_type,
|
||||
transaction_text: row.transaction_text,
|
||||
amount: row.amount,
|
||||
})),
|
||||
total,
|
||||
unfilteredTotal,
|
||||
offset,
|
||||
limit,
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user