This commit is contained in:
seb
2026-06-24 09:37:34 +02:00
parent 977ece3099
commit 185b68fe52
16 changed files with 1312 additions and 89 deletions

View File

@@ -0,0 +1,34 @@
import { getArticle } from '../../../services/article/getArticle.js';
/**
* Registers the full article lookup route backed by MSSQL.
* @param {import('express').Application} app - Express app
* @param {string} [basePath=''] - Optional URL prefix for all routes in this module
*/
export function register(app, basePath = '') {
/**
* @route GET /article/get
* @description Look up a single article by exact article number, EAN, or barcode.
* @query {string} target - Article number, packaging EAN, or barcode
* @returns {Object} 200 - `{ article }` with enriched article data
* @returns {Object} 400 - `{ error }` when target is missing
* @returns {Object} 404 - `{ error }` when no article matches
*/
app.get(`${basePath}/article/get`, async (req, res) => {
const target = req.query.target;
if (!target || !String(target).trim()) {
return res.status(400).json({ error: 'Query parameter "target" is required' });
}
try {
const article = await getArticle(String(target));
if (!article) {
return res.status(404).json({ error: 'Article not found' });
}
return res.status(200).json({ article });
} catch (error) {
return res.status(500).json({ error: error.message });
}
});
}

View File

@@ -0,0 +1,32 @@
import { parseFilter, searchArticles } from '../../../services/article/searchArticles.js';
/**
* Registers the article search route backed by MSSQL.
* @param {import('express').Application} app - Express app
* @param {string} [basePath=''] - Optional URL prefix for all routes in this module
*/
export function register(app, basePath = '') {
/**
* @route GET /article/search
* @description Search articles by partial match on name, number, EAN, barcode, or supplier number.
* @query {string} target - Space-separated search terms
* @query {string} [filter] - Optional comma-separated category filter IDs (empty = all)
* @returns {Object} 200 - `{ katagg, recordset }`
* @returns {Object} 400 - `{ error }` when target is missing
*/
app.get(`${basePath}/article/search`, async (req, res) => {
const target = req.query.target;
if (!target || !String(target).trim()) {
return res.status(400).json({ error: 'Query parameter "target" is required' });
}
try {
const filter = parseFilter(req.query.filter);
const result = await searchArticles(String(target), filter);
return res.status(200).json(result);
} catch (error) {
return res.status(500).json({ error: error.message });
}
});
}

View File

@@ -3,7 +3,14 @@ import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: resolve(__dirname, '../../.env') });
dotenv.config({ path: resolve(__dirname, '../../.env'), quiet: true });
function parseIntList(value) {
return (value || '')
.split(',')
.map((s) => Number(s.trim()))
.filter((n) => Number.isFinite(n));
}
export default {
port: Number(process.env.PORT) || 4208,
@@ -14,4 +21,17 @@ export default {
googleClientId: process.env.GOOGLE_CLIENT_ID,
jwtSecret: process.env.JWT_SECRET || 'fallback_secret_please_change_in_production',
sqlitePath: process.env.SQLITE_PATH,
mssql: {
server: process.env.MSSQL_SERVER,
database: process.env.MSSQL_DATABASE,
user: process.env.MSSQL_USER,
password: process.env.MSSQL_PASSWORD,
port: Number(process.env.MSSQL_PORT) || 1433,
encrypt: process.env.MSSQL_ENCRYPT === 'true',
trustServerCertificate: process.env.MSSQL_TRUST_SERVER_CERTIFICATE === 'true',
},
article: {
katSkip: parseIntList(process.env.KAT_SKIP),
katLists: Number(process.env.KAT_LISTS) || 0,
},
};

View File

@@ -0,0 +1,4 @@
export const LOAD_KAT_MAP = `
SELECT kKategorie, kOberKategorie
FROM dbo.tkategorie
`;

248
src/db/mssql/getArticle.js Normal file
View File

@@ -0,0 +1,248 @@
export const FIND_ARTICLE = `
SELECT TOP 1
tArtikelBeschreibung.cBeschreibung,
tArtikelBeschreibung.cKurzBeschreibung,
b.kVersandklasse,
(SELECT cName FROM dbo.tVersandklasse WHERE tVersandklasse.kVersandklasse = b.kVersandklasse) AS tVersandklassecName,
nIstVater,
kStueckliste,
kVaterArtikel,
b.kEigenschaftKombi,
b.kArtikel,
fUVP,
fGewicht,
tHersteller.cName AS cHersteller,
cBarcode,
fBestand,
b.cArtNr,
b.cAktiv,
tArtikelBeschreibung.cName,
b.fVKNEtto,
b.kSteuerklasse,
nVPE,
kMassEinheit,
fMassMenge,
kGrundPreisEinheit,
cAnmerkung,
tEigenschaftSprache.kEigenschaft,
tEigenschaftSprache.cName AS cEigenschaftName,
tEigenschaftWertSprache.kEigenschaftWert,
tEigenschaftWertSprache.cName AS cEigenschaftWertName,
(
SELECT c.cArtNr
FROM tArtikel c
WHERE c.kArtikel = (
SELECT tArtikel_kArtikel
FROM tliefartikel
WHERE cLiefArtNr = b.cArtNr AND tlieferant_klieferant = 96
)
) AS cLiefArtNr96
FROM tArtikelBeschreibung
INNER JOIN tArtikel b ON b.kArtikel = tArtikelBeschreibung.kArtikel
LEFT JOIN dbo.vLagerbestandProLager ON (
dbo.vLagerbestandProLager.kArtikel = b.kArtikel
AND vLagerbestandProLager.kWarenlager = 1
)
LEFT JOIN tGebinde ON b.kArtikel = tGebinde.kArtikel
LEFT JOIN tHersteller ON b.kHersteller = tHersteller.kHersteller
LEFT JOIN tEigenschaftKombiWert ON b.kEigenschaftKombi = tEigenschaftKombiWert.kEigenschaftKombi
LEFT JOIN tEigenschaftSprache ON (
tEigenschaftKombiWert.kEigenschaft = tEigenschaftSprache.kEigenschaft
AND tEigenschaftSprache.kSprache = 1
)
LEFT JOIN tEigenschaftWertSprache ON (
tEigenschaftWertSprache.kEigenschaftWert = tEigenschaftKombiWert.kEigenschaftWert
AND tEigenschaftWertSprache.kSprache = 1
)
WHERE b.cArtNr = @target OR tGebinde.cEAN = @target OR b.cBarcode = @target
`;
export const CHILDREN_BY_VATER = `
SELECT
tArtikel.cArtNr,
tArtikelBeschreibung.cName,
tArtikel.kEigenschaftKombi,
tEigenschaftSprache.kEigenschaft,
tEigenschaftSprache.cName AS cEigenschaftName,
tEigenschaftWertSprache.kEigenschaftWert,
tEigenschaftWertSprache.cName AS cEigenschaftWertName
FROM tArtikelBeschreibung
INNER JOIN tArtikel ON tArtikel.kArtikel = tArtikelBeschreibung.kArtikel
LEFT JOIN tEigenschaftKombiWert ON tArtikel.kEigenschaftKombi = tEigenschaftKombiWert.kEigenschaftKombi
LEFT JOIN tEigenschaftSprache ON (
tEigenschaftKombiWert.kEigenschaft = tEigenschaftSprache.kEigenschaft
AND tEigenschaftSprache.kSprache = 1
)
LEFT JOIN tEigenschaftWertSprache ON (
tEigenschaftWertSprache.kEigenschaftWert = tEigenschaftKombiWert.kEigenschaftWert
AND tEigenschaftWertSprache.kSprache = 1
)
WHERE tArtikel.kVaterArtikel = @kArtikel
`;
export const VATER_BY_ID = `
SELECT tArtikel.cArtNr
FROM tArtikelBeschreibung
INNER JOIN tArtikel ON tArtikel.kArtikel = tArtikelBeschreibung.kArtikel
WHERE tArtikel.kArtikel = @kArtikel
`;
export const STUECKLISTE = `
SELECT
tArtikelBeschreibung.cName,
tArtikel.cArtNr,
tStueckliste.fAnzahl,
tArtikel.fVKNEtto,
tArtikel.kSteuerklasse,
vLagerbestandProLager.fBestand
FROM dbo.tStueckliste
INNER JOIN dbo.tArtikelBeschreibung ON tArtikelBeschreibung.kArtikel = tStueckliste.kArtikel
LEFT JOIN dbo.vLagerbestandProLager ON (
vLagerbestandProLager.kArtikel = tStueckliste.kArtikel
AND vLagerbestandProLager.kWarenlager = 1
)
INNER JOIN dbo.tArtikel ON tArtikel.kArtikel = tStueckliste.kArtikel
WHERE tStueckliste.kStueckliste = @kStueckliste
ORDER BY tStueckliste.nSort
`;
export const ANDERE_LAGER = `
SELECT SUM(fBestand) AS fSumme
FROM dbo.vLagerbestandProLager
WHERE dbo.vLagerbestandProLager.kArtikel = @kArtikel
AND vLagerbestandProLager.kWarenlager != 1
GROUP BY dbo.vLagerbestandProLager.kArtikel
`;
export const EANS = `
SELECT cEAN
FROM tGebinde
WHERE kArtikel = @kArtikel
`;
export function categoriesQuery(katSkipClause, katLists) {
return `
SELECT kKategorie
FROM dbo.tkategorieartikel
WHERE kArtikel = @kArtikel
AND ${katSkipClause}kKategorie NOT IN (
SELECT kKategorie
FROM tKategorie
WHERE kOberKategorie = ${katLists}
)
`;
}
export const SUPPLIERS = `
SELECT
tlieferant.kLieferant,
cFirma,
tliefartikel.cLiefArtNr,
nStandard,
fLagerbestand,
cVPEEinheit,
nVPEMenge,
fMwSt,
fEKNetto,
tShopKorb.nMenge,
tlieferant.cEmail,
tlieferant.cAnmerkung
FROM tliefartikel
INNER JOIN tlieferant ON tlieferant.kLieferant = tliefartikel.tLieferant_kLieferant
LEFT JOIN tShopKorb ON (
tShopKorb.kLieferant = tlieferant.kLieferant
AND tliefartikel.cLiefArtNr = tShopKorb.cLiefArtNr
)
WHERE tArtikel_kArtikel = @kArtikel
ORDER BY nStandard DESC, tlieferant.kLieferant
`;
export const INCOMING_ORDERS = `
SELECT
tLieferantenBestellung.kLieferant,
tLieferantenBestellungPos.kLieferantenBestellungPos,
tLieferantenBestellungPos.kLieferantenBestellung,
cEigeneBestellnummer,
tLieferantenBestellungPos.kLieferantenBestellung + 1 AS kLieferantenBestellung2,
cFirma,
tLieferantenBestellungPos.dLieferdatum,
cFremdbelegnummer,
cInternerKommentar,
fAnzahlOffen
FROM tLieferantenBestellung
INNER JOIN tlieferant ON tlieferant.kLieferant = tLieferantenBestellung.kLieferant
INNER JOIN tLieferantenBestellungPos ON (
tLieferantenBestellungPos.kLieferantenBestellung = tLieferantenBestellung.kLieferantenBestellung
)
WHERE fAnzahlOffen > 0 AND kArtikel = @kArtikel
ORDER BY tLieferantenBestellungPos.kLieferantenBestellung
`;
export const OPEN_INCIDENTS = `
SELECT cType, cNote, kIncident, cEmployeeIssuer, cEmployeeMap
FROM dbo.tIncident
WHERE kRef1 = @kArtikel
AND cState = 'new'
AND cType IN ('order_count', 'order_label', 'order_free')
`;
export const MERKMALE = `
SELECT
tArtikelMerkmal.kMerkmal,
tArtikelMerkmal.kMerkmalWert,
tMerkmalSprache.cName,
tMerkmalWertSprache.cWert
FROM dbo.tArtikelMerkmal
INNER JOIN dbo.tMerkmalSprache ON tMerkmalSprache.kMerkmal = tArtikelMerkmal.kMerkmal
INNER JOIN dbo.tMerkmalWertSprache ON tMerkmalWertSprache.kMerkmalWert = tArtikelMerkmal.kMerkmalWert
WHERE tArtikelMerkmal.kArtikel = @kArtikel
ORDER BY tArtikelMerkmal.kMerkmal
`;
export const ATTRIBUTES = `
SELECT
tArtikelAttribut.kAttribut,
tArtikelAttributSprache.cWertVarchar,
tArtikelAttributSprache.nWertInt,
tArtikelAttributSprache.fWertDecimal
FROM dbo.tArtikelAttribut
INNER JOIN dbo.tArtikelAttributSprache ON (
tArtikelAttributSprache.kArtikelAttribut = tArtikelAttribut.kArtikelAttribut
AND kSprache = 0
)
WHERE kShop = 0 AND tArtikelAttribut.kArtikel = @kArtikel
`;
export const STAFFEL = `
SELECT nAnzahlAb, fNEttoPreis
FROM tPreis
INNER JOIN tPreisDetail ON tPreisDetail.kPreis = tPreis.kPreis
WHERE kArtikel = @kArtikel AND kKundenGruppe = 2 AND kShop = 0
`;
export const EXTRA_LAGER = `
SELECT nMenge
FROM tExtraLager
WHERE kArtikel = @kArtikel AND nLager = @nLager
`;
export const SHOP_SUM = `
SELECT SUM(kShop) AS kShop
FROM tArtikelShop
WHERE kArtikel = @kArtikel
`;
export const HERSTELLER_GPSR = `
SELECT
tHersteller.cBeschreibung AS cHerstellerGPSR,
b.bVorschauBild AS herstellerBild
FROM tArtikel
INNER JOIN tHersteller ON tArtikel.kHersteller = tHersteller.kHersteller
LEFT JOIN tHerstellerBildPlattform hbp ON (
hbp.kHersteller = tHersteller.kHersteller
AND hbp.kPlattform = 1
AND hbp.kShop = 0
)
LEFT JOIN tBild b ON b.kBild = hbp.kBild
WHERE tArtikel.kArtikel = @kArtikel
`;

View File

@@ -0,0 +1,37 @@
export const SELECT_COLUMNS = `
(SELECT STRING_AGG(tk1.kKategorie, ',')
FROM dbo.tkategorieartikel AS tk1
WHERE tk1.kArtikel = tArtikel.kArtikel
GROUP BY tk1.kArtikel) AS katList,
MIN(fVerfuegbar) AS fVerfuegbar,
tArtikel.kArtikel,
MIN(fBestand) AS fBestand,
MIN(tArtikel.cArtNr) AS cArtNr,
MIN(tArtikelBeschreibung.cName) AS cName,
MIN(tArtikel.fVKNEtto) AS fVKNEtto,
MIN(tArtikel.kSteuerklasse) AS kSteuerklasse
`;
export const FROM_JOINS = `
FROM tArtikelBeschreibung
INNER JOIN tArtikel ON tArtikel.kArtikel = tArtikelBeschreibung.kArtikel
LEFT JOIN tLiefArtikel ON tArtikel.kArtikel = tLiefArtikel.tArtikel_kArtikel
LEFT JOIN tGebinde ON tArtikel.kArtikel = tGebinde.kArtikel
LEFT JOIN dbo.tlagerbestand ON dbo.tlagerbestand.kArtikel = tArtikel.kArtikel
LEFT JOIN dbo.vLagerbestandProLager ON (
dbo.vLagerbestandProLager.kArtikel = tArtikel.kArtikel
AND vLagerbestandProLager.kWarenlager = 1
)
`;
export const GROUP_ORDER = `
GROUP BY tArtikel.kArtikel
ORDER BY MIN(tArtikelBeschreibung.cName)
`;
export const CATEGORY_JOIN = `
INNER JOIN dbo.tkategorieartikel ON (
tkategorieartikel.kArtikel = tArtikel.kArtikel
AND ({categoryFilter})
)
`;

64
src/db/mssqlClient.js Normal file
View File

@@ -0,0 +1,64 @@
import sql from 'mssql';
import env from '../config/env.js';
let pool;
let connecting;
function getMssqlConfig() {
const { server, database, user, password, port, encrypt, trustServerCertificate } = env.mssql;
if (!server || !database || !user || !password) {
throw new Error('MSSQL is not configured (MSSQL_SERVER, MSSQL_DATABASE, MSSQL_USER, MSSQL_PASSWORD required)');
}
return {
server,
database,
user,
password,
port,
pool: {
max: 10,
min: 0,
idleTimeoutMillis: 30_000,
},
options: {
encrypt,
trustServerCertificate,
},
};
}
export async function getPool() {
if (pool) {
return pool;
}
if (!connecting) {
connecting = new sql.ConnectionPool(getMssqlConfig())
.connect()
.then((connectedPool) => {
pool = connectedPool;
return pool;
})
.finally(() => {
connecting = undefined;
});
}
return connecting;
}
export async function ensureConnection() {
const activePool = await getPool();
await activePool.request().query('SELECT 1 AS ok');
}
export async function closePool() {
if (pool) {
await pool.close();
pool = undefined;
}
}
export { sql };

View File

@@ -1,11 +1,13 @@
import './config/env.js';
import { startApi, stopApi } from './api/index.js';
import { closeDb } from './db/client.js';
import { closePool } from './db/mssqlClient.js';
async function shutdown() {
await stopApi();
try {
closeDb();
await closePool();
} catch (error) {
console.error('Failed to close database:', error);
process.exitCode = 1;

View File

@@ -0,0 +1,15 @@
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

@@ -0,0 +1,192 @@
import env from '../../config/env.js';
import { getPool } from '../../db/mssqlClient.js';
import { getBrutto } from '../../utils/tax.js';
import { enrichSupplierAnmerkungRow } from './enrichSupplier.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 });
return rows.map((item) => ({
menge: item.nAnzahlAb.toString(),
fVKBrutto: getBrutto(item.fNEttoPreis, kSteuerklasse).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

@@ -0,0 +1,146 @@
import { getPool } from '../../db/mssqlClient.js';
import { LOAD_KAT_MAP } from '../../db/mssql/categories.js';
import * as Q from '../../db/mssql/searchArticles.js';
let katMapCache;
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, katMap) {
const list = [kat];
if (katMap[kat]) {
for (const item of getUpPath(katMap[kat], katMap)) {
list.push(item);
}
}
return list;
}
async function loadKatMap(pool) {
if (katMapCache) {
return katMapCache;
}
const result = await pool.request().query(LOAD_KAT_MAP);
katMapCache = {};
for (const item of result.recordset) {
katMapCache[item.kKategorie] = item.kOberKategorie;
}
return katMapCache;
}
function buildKatAgg(recordset, katMap) {
const katagg = {};
for (const item of recordset) {
if (!item.katList) {
continue;
}
for (const kat of item.katList.split(',')) {
for (const katpart of getUpPath(kat, katMap)) {
if (!katagg[katpart]) {
katagg[katpart] = 0;
}
katagg[katpart]++;
}
}
}
return katagg;
}
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 = ['']) {
const trimmed = target?.toString().trim();
if (!trimmed) {
return { katagg: {}, recordset: [] };
}
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 katMap = await loadKatMap(pool);
const katagg = buildKatAgg(result.recordset, katMap);
return { katagg, recordset: result.recordset };
}

14
src/utils/tax.js Normal file
View File

@@ -0,0 +1,14 @@
const STEUER_RATES = {
1: 19,
2: 7,
3: 0,
};
export function getSteuer(kSteuerklasse) {
return STEUER_RATES[kSteuerklasse] ?? 19;
}
export function getBrutto(netto, kSteuerklasse) {
const rate = getSteuer(kSteuerklasse);
return netto * (1 + rate / 100);
}