This commit is contained in:
seb
2026-07-31 05:54:13 +02:00
parent 6db7d4d75c
commit db8b192094
32 changed files with 100 additions and 2048 deletions

View File

@@ -1,15 +0,0 @@
export function enrichSupplierAnmerkungRow(row) {
const enriched = { ...row };
if (typeof row.cAnmerkung === 'string' && row.cAnmerkung.trim().startsWith('{')) {
try {
enriched.anmerkung = JSON.parse(row.cAnmerkung);
} catch {
enriched.anmerkung = row.cAnmerkung;
}
} else if (row.cAnmerkung != null) {
enriched.anmerkung = row.cAnmerkung;
}
return enriched;
}

View File

@@ -1,193 +0,0 @@
import env from '../../config/env.js';
import { getPool } from '../../db/mssqlClient.js';
import { enrichSupplierAnmerkungRow } from './enrichSupplier.js';
import { bruttoFromNetto, loadSteuerklassen } from '../misc/getSteuer.js';
import * as Q from '../../db/mssql/getArticle.js';
async function query(pool, sql, inputs = {}) {
const request = pool.request();
for (const [name, value] of Object.entries(inputs)) {
request.input(name, value);
}
const result = await request.query(sql);
return result.recordset;
}
function buildKatSkipClause() {
if (!env.article.katSkip.length) {
return '';
}
return env.article.katSkip.map((id) => `kKategorie != ${id}`).join(' AND ') + ' AND ';
}
async function findArticle(pool, target) {
const rows = await query(pool, Q.FIND_ARTICLE, { target });
return rows[0] ?? null;
}
async function loadChildren(pool, kArtikel) {
return query(pool, Q.CHILDREN_BY_VATER, { kArtikel });
}
async function loadVater(pool, kVaterArtikel) {
const rows = await query(pool, Q.VATER_BY_ID, { kArtikel: kVaterArtikel });
return rows[0] ?? null;
}
async function loadStueckliste(pool, kStueckliste) {
return query(pool, Q.STUECKLISTE, { kStueckliste });
}
async function loadWarehouseExtras(pool, kArtikel) {
const [fAndereLager, EANs, KATs] = await Promise.all([
query(pool, Q.ANDERE_LAGER, { kArtikel }),
query(pool, Q.EANS, { kArtikel }),
query(pool, Q.categoriesQuery(buildKatSkipClause(), env.article.katLists), { kArtikel }),
]);
return { fAndereLager, EANs, KATs };
}
async function loadSuppliers(pool, kArtikel) {
const rows = await query(pool, Q.SUPPLIERS, { kArtikel });
return rows.map(enrichSupplierAnmerkungRow);
}
async function loadIncomingOrders(pool, kArtikel) {
return query(pool, Q.INCOMING_ORDERS, { kArtikel });
}
async function loadOpenIncidents(pool, kArtikel) {
return query(pool, Q.OPEN_INCIDENTS, { kArtikel });
}
async function loadMerkmale(pool, kArtikel) {
return query(pool, Q.MERKMALE, { kArtikel });
}
async function loadAttributes(pool, kArtikel) {
return query(pool, Q.ATTRIBUTES, { kArtikel });
}
async function loadStaffel(pool, kArtikel, kSteuerklasse) {
const rows = await query(pool, Q.STAFFEL, { kArtikel });
const steuerklassen = await loadSteuerklassen();
return rows.map((item) => ({
menge: item.nAnzahlAb.toString(),
fVKBrutto: bruttoFromNetto(item.fNEttoPreis, kSteuerklasse, steuerklassen)?.toFixed(2),
}));
}
async function loadExtraLager(pool, kArtikel, nLager) {
const rows = await query(pool, Q.EXTRA_LAGER, { kArtikel, nLager });
return rows[0]?.nMenge;
}
async function loadShopSum(pool, kArtikel) {
const rows = await query(pool, Q.SHOP_SUM, { kArtikel });
return rows[0]?.kShop;
}
async function loadHerstellerGpsr(pool, kArtikel) {
const rows = await query(pool, Q.HERSTELLER_GPSR, { kArtikel });
const row = rows[0];
if (!row) {
return { cHerstellerGPSR: null, herstellerBild: null };
}
const herstellerBild = row.herstellerBild
? Buffer.from(row.herstellerBild).toString('base64')
: null;
return {
cHerstellerGPSR: row.cHerstellerGPSR ?? null,
herstellerBild,
};
}
async function enrichArticle(pool, article) {
const { kArtikel, kSteuerklasse, nIstVater, kVaterArtikel, kStueckliste } = article;
const parallel = [];
if (nIstVater === 1) {
parallel.push(
loadChildren(pool, kArtikel).then((children) => {
article.children = children;
})
);
}
if (kVaterArtikel) {
parallel.push(
loadVater(pool, kVaterArtikel).then((vater) => {
article.vater = vater;
})
);
}
if (kStueckliste !== 0) {
parallel.push(
loadStueckliste(pool, kStueckliste).then((liste) => {
article.liste = liste;
})
);
}
parallel.push(
loadWarehouseExtras(pool, kArtikel).then(({ fAndereLager, EANs, KATs }) => {
article.fAndereLager = fAndereLager;
article.EANs = EANs;
article.KATs = KATs;
}),
loadSuppliers(pool, kArtikel).then((lief) => {
article.lief = lief;
}),
loadIncomingOrders(pool, kArtikel).then((incoming) => {
article.incoming = incoming;
}),
loadOpenIncidents(pool, kArtikel).then((orders) => {
article.orders = orders;
}),
loadMerkmale(pool, kArtikel).then((merkmale) => {
article.merkmale = merkmale;
}),
loadAttributes(pool, kArtikel).then((attribute) => {
article.attribute = attribute;
}),
loadStaffel(pool, kArtikel, kSteuerklasse).then((staffel) => {
article.staffel = staffel;
}),
loadExtraLager(pool, kArtikel, 0).then((garage) => {
article.garage = garage;
}),
loadExtraLager(pool, kArtikel, 1).then((keller) => {
article.keller = keller;
}),
loadShopSum(pool, kArtikel).then((shops) => {
article.shops = shops;
}),
loadHerstellerGpsr(pool, kArtikel).then(({ cHerstellerGPSR, herstellerBild }) => {
article.cHerstellerGPSR = cHerstellerGPSR;
article.herstellerBild = herstellerBild;
})
);
await Promise.all(parallel);
return article;
}
export async function getArticle(target) {
const trimmed = target?.trim();
if (!trimmed) {
return null;
}
const pool = await getPool();
const article = await findArticle(pool, trimmed);
if (!article) {
return null;
}
return enrichArticle(pool, article);
}

View File

@@ -1,229 +0,0 @@
import { getPool } from '../../db/mssqlClient.js';
import { LOAD_CATEGORIES } from '../../db/mssql/categories.js';
import * as Q from '../../db/mssql/searchArticles.js';
import { bruttoFromNetto, loadSteuersatz } from '../misc/getSteuer.js';
let categoryCache;
export function parseFilter(raw) {
if (raw === undefined || raw === null || raw === '') {
return [''];
}
const parts = String(raw).split(',').map((s) => s.trim()).filter(Boolean);
return parts.length ? parts : [''];
}
function getUpPath(kat, parentMap) {
const list = [kat];
if (parentMap[kat]) {
for (const item of getUpPath(parentMap[kat], parentMap)) {
list.push(item);
}
}
return list;
}
async function loadCategoryCache(pool) {
if (categoryCache) {
return categoryCache;
}
const result = await pool.request().query(LOAD_CATEGORIES);
const byId = {};
const parentMap = {};
for (const item of result.recordset) {
byId[item.kKategorie] = {
name: item.cName ?? null,
parent: item.kOberKategorie,
};
parentMap[item.kKategorie] = item.kOberKategorie;
}
categoryCache = { byId, parentMap };
return categoryCache;
}
function buildKatAgg(recordset, parentMap) {
const counts = {};
for (const item of recordset) {
if (!item.katList) {
continue;
}
for (const kat of item.katList.split(',')) {
for (const katpart of getUpPath(kat, parentMap)) {
if (!counts[katpart]) {
counts[katpart] = 0;
}
counts[katpart]++;
}
}
}
return counts;
}
function shapeCategories(counts, byId) {
const categories = {};
for (const [id, count] of Object.entries(counts)) {
const meta = byId[id] ?? {};
categories[id] = {
count,
name: meta.name ?? null,
parent: meta.parent ?? null,
};
}
return categories;
}
/**
* Ensure every applied filter id and its ancestors appear in categories
* (count 0 if no hits), so the frontend can render hierarchy via parent links.
*/
function ensureFilterCategories(categories, filter, byId, parentMap) {
for (const id of filter) {
if (id === '' || id === null) {
continue;
}
for (const ancestorId of getUpPath(id, parentMap)) {
const key = String(ancestorId);
if (categories[key]) {
continue;
}
const meta = byId[ancestorId] ?? byId[key] ?? {};
categories[key] = {
count: 0,
name: meta.name ?? null,
parent: meta.parent ?? null,
};
}
}
return categories;
}
function shapeArticle(row, steuersaetze) {
const taxRow = steuersaetze.find((item) => item.kSteuerklasse === row.kSteuerklasse);
const brutto = bruttoFromNetto(row.fVKNEtto, row.kSteuerklasse, steuersaetze);
return {
categories: row.katList,
available: row.fVerfuegbar,
id: row.kArtikel,
stock: row.fBestand,
sku: row.cArtNr,
name: row.cName,
price: brutto == null ? null : Math.round(brutto * 10) / 10,
tax: taxRow?.fSteuersatz ?? null,
};
}
function buildTargetClause(request, target) {
const parts = [];
let i = 0;
const terms = target
.toString()
.trim()
.replace(/ {2,}/g, ' ')
.split(' ')
.filter(Boolean);
for (const part of terms) {
request.input(`target_${i}`, `%${part}%`);
parts.push(
'tGebinde.cEAN LIKE @target_' + i +
' OR tLiefArtikel.cLiefArtNr LIKE @target_' + i +
' OR tArtikelBeschreibung.cName LIKE @target_' + i +
' OR tArtikel.cArtNr LIKE @target_' + i +
' OR tArtikel.cBarcode LIKE @target_' + i
);
i++;
}
return { parts, nextIndex: i };
}
function buildCategoryClause(request, filter, startIndex) {
const parts = [];
let i = startIndex;
for (const part of filter) {
request.input(`target_${i}`, part);
parts.push(`tkategorieartikel.kKategorie = @target_${i}`);
i++;
}
return {
clause: '(' + parts.join(') OR (') + ')',
nextIndex: i,
};
}
function buildWhereClause(parts, filter) {
const aktivClause = filter?.[0] == '751' ? '' : " AND (tArtikel.cAktiv = 'Y')";
if (!parts.length) {
return `1=1${aktivClause}`;
}
return '(' + parts.join(') AND (') + ')' + aktivClause;
}
function buildQuery(filter, categoryFilter) {
const select = filter[0] === '' || filter[0] === null
? `SELECT TOP (2000) ${Q.SELECT_COLUMNS}`
: `SELECT ${Q.SELECT_COLUMNS}`;
const categoryJoin = categoryFilter
? Q.CATEGORY_JOIN.replace('{categoryFilter}', categoryFilter)
: '';
return `${select}
${Q.FROM_JOINS}
${categoryJoin}
WHERE `;
}
export async function searchArticles(target, filter = [''], offset = 0, maxCount = 10) {
const trimmed = target?.toString().trim();
if (!trimmed) {
return { categories: {}, length: 0, recordset: [] };
}
const start = Math.max(0, Number(offset) || 0);
const count = Math.max(0, Number(maxCount) || 10);
const pool = await getPool();
const request = pool.request();
const { parts } = buildTargetClause(request, trimmed);
const whereClause = buildWhereClause(parts, filter);
let sql = buildQuery(filter, null) + whereClause + Q.GROUP_ORDER;
if (filter[0] !== '' && filter[0] !== null) {
const { clause: categoryFilter } = buildCategoryClause(request, filter, parts.length);
sql = buildQuery(filter, categoryFilter) + whereClause + Q.GROUP_ORDER;
}
const result = await request.query(sql);
const [categoryData, steuersaetze] = await Promise.all([
loadCategoryCache(pool),
loadSteuersatz(),
]);
const categories = ensureFilterCategories(
shapeCategories(
buildKatAgg(result.recordset, categoryData.parentMap),
categoryData.byId
),
filter,
categoryData.byId,
categoryData.parentMap
);
const length = result.recordset.length;
const recordset = result.recordset
.slice(start, start + count)
.map((row) => shapeArticle(row, steuersaetze));
console.dir({ categories, length, recordset }, { depth: null });
return { categories, length, recordset };
}

View File

@@ -1,77 +0,0 @@
import { getPool } from '../../db/mssqlClient.js';
import {
CATEGORY_ARTICLE_COUNTS,
LOAD_CATEGORIES,
} from '../../db/mssql/categories.js';
function sortCategories(list) {
list.sort((a, b) => {
const sortDiff = (a.nSort ?? 0) - (b.nSort ?? 0);
if (sortDiff !== 0) {
return sortDiff;
}
return String(a.cName ?? '').localeCompare(String(b.cName ?? ''), 'de');
});
for (const node of list) {
sortCategories(node.children);
}
}
function buildCategoryTree(rows) {
const nodes = new Map();
for (const row of rows) {
nodes.set(row.kKategorie, {
kKategorie: row.kKategorie,
kOberKategorie: row.kOberKategorie,
nSort: row.nSort,
cName: row.cName ?? null,
count: row.count ?? 0,
children: [],
});
}
const roots = [];
for (const node of nodes.values()) {
const parent = nodes.get(node.kOberKategorie);
if (parent && node.kOberKategorie !== node.kKategorie) {
parent.children.push(node);
} else {
roots.push(node);
}
}
sortCategories(roots);
return roots;
}
/**
* Loads the full JTL category list with per-category article counts and a nested tree.
* @returns {Promise<{ categories: Array<{ kKategorie: number, kOberKategorie: number, nSort: number, cName: string|null, count: number }>, tree: object[] }>}
*/
export async function getCategories() {
const pool = await getPool();
const [categoriesResult, countsResult] = await Promise.all([
pool.request().query(LOAD_CATEGORIES),
pool.request().query(CATEGORY_ARTICLE_COUNTS),
]);
const countByCategory = new Map(
countsResult.recordset.map((row) => [row.kKategorie, row.count])
);
const categories = categoriesResult.recordset.map((row) => ({
kKategorie: row.kKategorie,
kOberKategorie: row.kOberKategorie,
nSort: row.nSort,
cName: row.cName ?? null,
count: countByCategory.get(row.kKategorie) ?? 0,
}));
return {
categories,
tree: buildCategoryTree(categories),
};
}

View File

@@ -1,63 +0,0 @@
import { getPool } from '../../db/mssqlClient.js';
import { LOAD_STEUERSATZ, LOAD_STEUERKLASSEN } from '../../db/mssql/steuer.js';
import env from '../../config/env.js';
let steuersatzCache;
let steuerklassenCache;
/**
* Loads VAT rates per tax class for the configured steuer zone (`tSteuersatz` only).
* Results are cached for the process lifetime.
* @returns {Promise<Array<{ kSteuerklasse: number, fSteuersatz: number }>>}
*/
export async function loadSteuersatz() {
if (steuersatzCache) {
return steuersatzCache;
}
const pool = await getPool();
const result = await pool.request()
.input('steuerZone', env.steuerZone)
.query(LOAD_STEUERSATZ);
steuersatzCache = result.recordset.map((row) => ({
kSteuerklasse: row.kSteuerklasse,
fSteuersatz: row.fSteuersatz,
}));
return steuersatzCache;
}
/**
* Loads tax classes with display metadata for the configured steuer zone.
* Results are cached for the process lifetime.
* @returns {Promise<Array<{ kSteuerklasse: number, cName: string, nStandard: number, fSteuersatz: number }>>}
*/
export async function loadSteuerklassen() {
if (steuerklassenCache) {
return steuerklassenCache;
}
const pool = await getPool();
const result = await pool.request()
.input('steuerZone', env.steuerZone)
.query(LOAD_STEUERKLASSEN);
steuerklassenCache = result.recordset.map((row) => ({
kSteuerklasse: row.kSteuerklasse,
cName: row.cName,
nStandard: row.nStandard,
fSteuersatz: row.fSteuersatz,
}));
return steuerklassenCache;
}
export function bruttoFromNetto(netto, kSteuerklasse, steuersaetze) {
const row = steuersaetze.find((item) => item.kSteuerklasse === kSteuerklasse);
if (row == null || netto == null) {
return null;
}
return netto * (1 + row.fSteuersatz / 100);
}
export async function getSteuer() {
const steuerklassen = await loadSteuerklassen();
return { steuerklassen };
}