Files
dashlibstorage/src/store/ledger/listHistory.js
2026-10-18 03:15:20 +02:00

181 lines
6.2 KiB
JavaScript

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