This commit is contained in:
seb
2026-10-18 03:15:20 +02:00
parent fda895f75a
commit c687553eea
6 changed files with 402 additions and 0 deletions

View File

@@ -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 = {

View 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,
};
};
}

View 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,
};
};
}