This commit is contained in:
seb
2026-10-16 22:09:12 +02:00
parent 796f6a3272
commit 7635821dad
38 changed files with 6191 additions and 3350 deletions

View File

@@ -10,30 +10,317 @@ export default function list(provider) {
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteQueries : mssqlQueries;
return async (googleAuthId) => {
const { companyId } = await access(googleAuthId);
const runner = makeRunner(provider);
const sqlCountManifests = isSqlite
? 'SELECT COUNT(*) AS total FROM manifest WHERE company_id = @companyId AND "type" = @type'
: 'SELECT COUNT(*) AS total FROM manifest WHERE company_id = @companyId AND [type] = @type';
const result = await runner(queries.LIST_DELIVERIES, {
companyId,
type: queries.DELIVERY_TYPE,
});
/**
* Lists the deliveries of the user's selected company.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {object} [opts] - Optional pagination and filter options.
* @param {number} [opts.offset=0] - Number of deliveries to skip.
* @param {number} [opts.limit=50] - Number of deliveries to return.
* @param {string} [opts.search] - Free-text search matching delivery number, external number, or supplier name.
* @param {boolean} [opts.onlyUnbooked=false] - When true, returns only deliveries with unbooked stock items.
* @param {number} [opts.supplierId] - Filter by supplier entity id.
* @param {number} [opts.seekMonths] - Negative integer for cutoff months ago (e.g. -1).
* @param {object} [tx] - Optional transaction.
* @returns {Promise<{deliveries: Array<object>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>}
*/
return async (googleAuthId, opts, tx) => {
let offset = null;
let limit = null;
let isPaged = false;
let resolvedTx = tx;
const filesResult = await runner(queries.LIST_DELIVERY_FILES, {
companyId,
type: queries.DELIVERY_TYPE,
});
const hasFilter = opts != null && typeof opts === 'object' && !opts._isTx && (
opts.offset !== undefined ||
opts.limit !== undefined ||
(typeof opts.search === 'string' && opts.search.trim() !== '') ||
opts.onlyUnbooked === true ||
opts.supplierId !== undefined ||
opts.seekMonths !== undefined
);
const filesByManifest = new Map();
for (const row of filesResult.rows) {
const list = filesByManifest.get(row.manifestId) ?? [];
list.push(shapeAttachmentMeta(row));
filesByManifest.set(row.manifestId, list);
if (opts != null && typeof opts === 'object' && !opts._isTx) {
if (hasFilter || opts.offset !== undefined || opts.limit !== undefined) {
isPaged = true;
const parsedOffset = Number(opts.offset ?? 0);
const parsedLimit = Number(opts.limit ?? 50);
if (!Number.isInteger(parsedOffset) || parsedOffset < 0) {
throw new Error('Invalid offset');
}
if (!Number.isInteger(parsedLimit) || parsedLimit < 1) {
throw new Error('Invalid limit');
}
offset = parsedOffset;
limit = parsedLimit;
}
} else if (opts != null) {
resolvedTx = opts;
}
return result.rows.map((row) => ({
const runner = makeRunner(provider, resolvedTx);
const { companyId } = await access(googleAuthId, runner);
const conditions = ['m.company_id = @companyId', `m.${isSqlite ? '"type"' : '[type]'} = @type`];
const params = {
companyId,
type: queries.DELIVERY_TYPE,
};
// 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(`m.${isSqlite ? '"date"' : '[date]'} <= @dateTo`);
}
// Filter by supplierId
if (opts?.supplierId !== undefined && opts.supplierId !== null) {
const supplierId = Number(opts.supplierId);
if (!Number.isInteger(supplierId) || supplierId <= 0) {
throw new Error('Invalid supplierId');
}
params.supplierId = supplierId;
conditions.push('m.entity_id = @supplierId');
}
// Free-text search
if (typeof opts?.search === 'string' && opts.search.trim() !== '') {
params.searchPattern = `%${opts.search.trim()}%`;
if (isSqlite) {
conditions.push(`(
m."number" LIKE @searchPattern
OR m.external_number LIKE @searchPattern
OR e.name LIKE @searchPattern
OR e.first_name LIKE @searchPattern
OR (IFNULL(e.first_name, '') || ' ' || IFNULL(e.name, '')) LIKE @searchPattern
OR (IFNULL(e.name, '') || ' ' || IFNULL(e.first_name, '')) LIKE @searchPattern
OR ds.delivery_number LIKE @searchPattern
OR ds.supplier_name LIKE @searchPattern
)`);
} else {
conditions.push(`(
m.[number] LIKE @searchPattern
OR m.external_number LIKE @searchPattern
OR e.name LIKE @searchPattern
OR e.first_name LIKE @searchPattern
OR (ISNULL(e.first_name, '') + ' ' + ISNULL(e.name, '')) LIKE @searchPattern
OR (ISNULL(e.name, '') + ' ' + ISNULL(e.first_name, '')) LIKE @searchPattern
OR ds.delivery_number LIKE @searchPattern
OR ds.supplier_name LIKE @searchPattern
)`);
}
}
// Only unbooked deliveries (has stock positions that are not yet booked)
if (opts?.onlyUnbooked === true) {
conditions.push('(COALESCE(ds.unbooked_positions, 0) > 0 OR (COALESCE(ds.is_booked, 0) = 0 AND COALESCE(ds.stock_positions, 0) > 0))');
}
const whereClause = conditions.join('\n AND ');
const sqlCount = `
SELECT COUNT(*) AS total
FROM manifest m
LEFT JOIN deliverysummary ds ON ds.manifest_id = m.id
LEFT JOIN entity e ON e.id = m.entity_id
WHERE ${whereClause}
`;
let sqlList;
if (isSqlite) {
sqlList = `
SELECT
m.id,
m."number",
m."type",
m.entity_id AS supplierId,
m.external_number AS externalNumber,
m."date",
m.created_at AS createdAt,
ds.positions,
ds.gross_total AS grossTotal,
ds.net_total AS netTotal,
ds.total_quantity AS totalQuantity,
ds.stock_positions AS stockPositions,
ds.booked_positions AS bookedPositions,
ds.unbooked_positions AS unbookedPositions,
ds.stock_quantity AS stockQuantity,
ds.booked_quantity AS bookedQuantity,
ds.unbooked_quantity AS unbookedQuantity,
ds.is_booked AS isBooked,
ds.supplier_name AS supplierName
FROM manifest m
LEFT JOIN deliverysummary ds ON ds.manifest_id = m.id
LEFT JOIN entity e ON e.id = m.entity_id
WHERE ${whereClause}
ORDER BY m."date" DESC, m.id DESC
${isPaged ? 'LIMIT @limit OFFSET @offset' : ''}
`;
} else {
sqlList = `
SELECT
m.id,
m.[number],
m.[type],
m.entity_id AS supplierId,
m.external_number AS externalNumber,
m.[date],
m.created_at AS createdAt,
ds.positions,
ds.gross_total AS grossTotal,
ds.net_total AS netTotal,
ds.total_quantity AS totalQuantity,
ds.stock_positions AS stockPositions,
ds.booked_positions AS bookedPositions,
ds.unbooked_positions AS unbookedPositions,
ds.stock_quantity AS stockQuantity,
ds.booked_quantity AS bookedQuantity,
ds.unbooked_quantity AS unbookedQuantity,
ds.is_booked AS isBooked,
ds.supplier_name AS supplierName
FROM manifest m
LEFT JOIN deliverysummary ds ON ds.manifest_id = m.id
LEFT JOIN entity e ON e.id = m.entity_id
WHERE ${whereClause}
ORDER BY m.[date] DESC, m.id DESC
${isPaged ? 'OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY' : ''}
`;
}
const hasUserFilters = (
opts?.seekMonths !== undefined ||
(typeof opts?.search === 'string' && opts.search.trim() !== '') ||
opts?.onlyUnbooked === true ||
opts?.supplierId !== undefined
);
let manifestRows;
let total = 0;
let unfilteredTotal = 0;
if (isPaged) {
params.offset = offset;
params.limit = limit;
const [countResult, listResult, unfilteredCountResult] = await Promise.all([
runner.query(sqlCount, params),
runner.query(sqlList, params),
hasUserFilters ? runner.query(sqlCountManifests, { companyId, type: queries.DELIVERY_TYPE }) : null,
]);
total = Number(countResult.rows[0]?.total ?? 0);
unfilteredTotal = hasUserFilters
? Number(unfilteredCountResult?.rows[0]?.total ?? 0)
: total;
manifestRows = listResult.rows;
} else {
const result = await runner.query(sqlList, params);
manifestRows = result.rows;
total = manifestRows.length;
if (hasUserFilters) {
const countRes = await runner.query(sqlCountManifests, { companyId, type: queries.DELIVERY_TYPE });
unfilteredTotal = Number(countRes.rows[0]?.total ?? 0);
} else {
unfilteredTotal = total;
}
}
const manifestIds = manifestRows.map((r) => r.id);
const filesByManifest = new Map();
if (manifestIds.length > 0) {
const sqlFiles = isSqlite
? `SELECT
ma.id,
ma.manifest_id AS manifestId,
ma.file_id AS fileId,
ma.sort,
f.hash,
f.mimetype,
f.name,
f.size,
f.created,
f.modified,
f.thumbx,
f.thumby,
f.meta
FROM manifestattachment ma
INNER JOIN "file" f ON f.id = ma.file_id
WHERE ma.manifest_id IN (SELECT value FROM json_each(@manifestIdsJson))
ORDER BY ma.manifest_id, ma.sort ASC, ma.id ASC`
: `SELECT
ma.id,
ma.manifest_id AS manifestId,
ma.file_id AS fileId,
ma.sort,
f.hash,
f.mimetype,
f.name,
f.size,
f.created,
f.modified,
f.thumbavif,
f.thumbx,
f.thumby,
f.meta
FROM manifestattachment ma
INNER JOIN [file] f ON f.id = ma.file_id
WHERE ma.manifest_id IN (SELECT CAST(value AS INT) FROM OPENJSON(@manifestIdsJson))
ORDER BY ma.manifest_id, ma.sort ASC, ma.id ASC`;
const filesResult = await runner.query(sqlFiles, {
manifestIdsJson: JSON.stringify(manifestIds),
});
for (const row of filesResult.rows) {
const list = filesByManifest.get(row.manifestId) ?? [];
list.push(shapeAttachmentMeta(row));
filesByManifest.set(row.manifestId, list);
}
}
const deliveries = manifestRows.map((row) => ({
...shapeSummary(row),
positions: row.positions !== undefined && row.positions !== null ? Number(row.positions) : 0,
grossTotal: row.grossTotal !== undefined && row.grossTotal !== null ? Number(row.grossTotal) : 0,
netTotal: row.netTotal !== undefined && row.netTotal !== null ? Number(row.netTotal) : 0,
totalQuantity: row.totalQuantity !== undefined && row.totalQuantity !== null ? Number(row.totalQuantity) : 0,
stockPositions: row.stockPositions !== undefined && row.stockPositions !== null ? Number(row.stockPositions) : 0,
bookedPositions: row.bookedPositions !== undefined && row.bookedPositions !== null ? Number(row.bookedPositions) : 0,
unbookedPositions: row.unbookedPositions !== undefined && row.unbookedPositions !== null ? Number(row.unbookedPositions) : 0,
stockQuantity: row.stockQuantity !== undefined && row.stockQuantity !== null ? Number(row.stockQuantity) : 0,
bookedQuantity: row.bookedQuantity !== undefined && row.bookedQuantity !== null ? Number(row.bookedQuantity) : 0,
unbookedQuantity: row.unbookedQuantity !== undefined && row.unbookedQuantity !== null ? Number(row.unbookedQuantity) : 0,
isBooked: row.isBooked !== undefined && row.isBooked !== null ? Boolean(row.isBooked) : false,
supplierName: row.supplierName ?? undefined,
ledger: {
totalPositions: Number(row.stockPositions ?? row.totalPositions ?? 0),
completelyOnLedgerPositions: Number(row.bookedPositions ?? row.completelyOnLedgerPositions ?? 0),
bookedPositions: Number(row.bookedPositions ?? 0),
unbookedPositions: Number(row.unbookedPositions ?? 0),
totalQuantity: Number(row.stockQuantity ?? 0),
bookedQuantity: Number(row.bookedQuantity ?? 0),
unbookedQuantity: Number(row.unbookedQuantity ?? 0),
isBooked: Boolean(row.isBooked),
},
files: filesByManifest.get(row.id) ?? [],
}));
if (isPaged) {
return {
deliveries,
total,
unfilteredTotal,
offset,
limit,
};
}
return deliveries;
};
}