reshape search
This commit is contained in:
@@ -23,3 +23,6 @@ MSSQL_TRUST_SERVER_CERTIFICATE=false
|
||||
# Article search — category filters (comma-separated IDs)
|
||||
KAT_SKIP=
|
||||
KAT_LISTS=0
|
||||
|
||||
# Tax zone name for VAT rates (tSteuerzone.cName)
|
||||
STEUER_ZONE=Zone-EU
|
||||
|
||||
@@ -193,14 +193,14 @@ function truncateForDisplay(value, max = 80) {
|
||||
return `${value.slice(0, max)}… (${value.length} chars)`;
|
||||
}
|
||||
|
||||
function formatListRow(row, steuerklassen) {
|
||||
const stock = row.fBestand ?? '—';
|
||||
const price = formatPrice(row, steuerklassen);
|
||||
const name = row.cName ?? '—';
|
||||
return `${row.cArtNr ?? '—'} ${stock} ${price} ${name}`;
|
||||
function formatListRow(row) {
|
||||
const stock = row.stock ?? '—';
|
||||
const price = row.price == null ? '—' : eur(row.price);
|
||||
const name = row.name ?? '—';
|
||||
return `${row.sku ?? '—'} ${stock} ${price} ${name}`;
|
||||
}
|
||||
|
||||
const ResultList = ({ rows, onSelect, onBack, steuerklassen }) => {
|
||||
const ResultList = ({ rows, onSelect, onBack }) => {
|
||||
const [selected, setSelected] = useState(0);
|
||||
const windowStart = Math.max(0, Math.min(selected - Math.floor(VISIBLE_ROWS / 2), rows.length - VISIBLE_ROWS));
|
||||
const visible = rows.slice(windowStart, windowStart + VISIBLE_ROWS);
|
||||
@@ -227,9 +227,9 @@ const ResultList = ({ rows, onSelect, onBack, steuerklassen }) => {
|
||||
const absoluteIndex = windowStart + index;
|
||||
const prefix = absoluteIndex === selected ? chalk.cyan('> ') : ' ';
|
||||
const label = absoluteIndex === selected
|
||||
? chalk.cyan.bold(formatListRow(row, steuerklassen))
|
||||
: formatListRow(row, steuerklassen);
|
||||
return e(Text, { key: row.kArtikel ?? absoluteIndex }, prefix, label);
|
||||
? chalk.cyan.bold(formatListRow(row))
|
||||
: formatListRow(row);
|
||||
return e(Text, { key: row.id ?? absoluteIndex }, prefix, label);
|
||||
}),
|
||||
rows.length > VISIBLE_ROWS
|
||||
? e(Text, null, chalk.gray(`Showing ${windowStart + 1}-${windowStart + visible.length} of ${rows.length}`))
|
||||
@@ -296,7 +296,7 @@ const App = ({ steuerklassen }) => {
|
||||
return;
|
||||
}
|
||||
if (recordset.length === 1) {
|
||||
await loadArticle(recordset[0].cArtNr);
|
||||
await loadArticle(recordset[0].sku);
|
||||
return;
|
||||
}
|
||||
setRows(recordset);
|
||||
@@ -334,9 +334,8 @@ const App = ({ steuerklassen }) => {
|
||||
if (mode === MODES.LIST) {
|
||||
return e(ResultList, {
|
||||
rows,
|
||||
onSelect: (row) => loadArticle(row.cArtNr),
|
||||
onSelect: (row) => loadArticle(row.sku),
|
||||
onBack: backToSearch,
|
||||
steuerklassen,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@ export function register(app, basePath = '') {
|
||||
* @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 }`
|
||||
* @query {number} [offset=0] - Number of results to skip
|
||||
* @query {number} [maxCount=10] - Maximum number of results to return
|
||||
* @returns {Object} 200 - `{ katagg, length, recordset }`
|
||||
* @returns {Object} 400 - `{ error }` when target is missing
|
||||
*/
|
||||
app.get(`${basePath}/article/search`, async (req, res) => {
|
||||
@@ -23,7 +25,9 @@ export function register(app, basePath = '') {
|
||||
|
||||
try {
|
||||
const filter = parseFilter(req.query.filter);
|
||||
const result = await searchArticles(String(target), filter);
|
||||
const offset = req.query.offset !== undefined ? Number(req.query.offset) : 0;
|
||||
const maxCount = req.query.maxCount !== undefined ? Number(req.query.maxCount) : 10;
|
||||
const result = await searchArticles(String(target), filter, offset, maxCount);
|
||||
return res.status(200).json(result);
|
||||
} catch (error) {
|
||||
return res.status(500).json({ error: error.message });
|
||||
|
||||
@@ -34,4 +34,5 @@ export default {
|
||||
katSkip: parseIntList(process.env.KAT_SKIP),
|
||||
katLists: Number(process.env.KAT_LISTS) || 0,
|
||||
},
|
||||
steuerZone: process.env.STEUER_ZONE || 'Zone-EU',
|
||||
};
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
export const LOAD_STEUERSATZ = `
|
||||
SELECT
|
||||
kSteuerklasse,
|
||||
fSteuersatz
|
||||
FROM dbo.tSteuersatz
|
||||
WHERE kSteuerzone IN (
|
||||
SELECT kSteuerzone FROM dbo.tSteuerzone WHERE cName = @steuerZone
|
||||
)
|
||||
ORDER BY kSteuerklasse
|
||||
`;
|
||||
|
||||
export const LOAD_STEUERKLASSEN = `
|
||||
SELECT
|
||||
sk.kSteuerklasse,
|
||||
@@ -6,6 +17,8 @@ export const LOAD_STEUERKLASSEN = `
|
||||
sz.fSteuersatz
|
||||
FROM dbo.tSteuerklasse sk
|
||||
INNER JOIN dbo.tSteuersatz sz ON sz.kSteuerklasse = sk.kSteuerklasse
|
||||
WHERE sz.kSteuerzone = 1
|
||||
WHERE sz.kSteuerzone IN (
|
||||
SELECT kSteuerzone FROM dbo.tSteuerzone WHERE cName = @steuerZone
|
||||
)
|
||||
ORDER BY sk.kSteuerklasse
|
||||
`;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getPool } from '../../db/mssqlClient.js';
|
||||
import { LOAD_KAT_MAP } from '../../db/mssql/categories.js';
|
||||
import * as Q from '../../db/mssql/searchArticles.js';
|
||||
import { bruttoFromNetto, loadSteuersatz } from '../misc/getSteuer.js';
|
||||
|
||||
let katMapCache;
|
||||
|
||||
@@ -55,6 +56,22 @@ function buildKatAgg(recordset, katMap) {
|
||||
return katagg;
|
||||
}
|
||||
|
||||
function shapeArticle(row, steuersaetze) {
|
||||
const taxRow = steuersaetze.find((item) => item.kSteuerklasse === row.kSteuerklasse);
|
||||
const brutto = bruttoFromNetto(row.fVKNEtto, row.kSteuerklasse, steuersaetze);
|
||||
|
||||
return {
|
||||
katList: 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;
|
||||
@@ -120,12 +137,15 @@ function buildQuery(filter, categoryFilter) {
|
||||
WHERE `;
|
||||
}
|
||||
|
||||
export async function searchArticles(target, filter = ['']) {
|
||||
export async function searchArticles(target, filter = [''], offset = 0, maxCount = 10) {
|
||||
const trimmed = target?.toString().trim();
|
||||
if (!trimmed) {
|
||||
return { katagg: {}, recordset: [] };
|
||||
return { katagg: {}, 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);
|
||||
@@ -139,8 +159,17 @@ export async function searchArticles(target, filter = ['']) {
|
||||
}
|
||||
|
||||
const result = await request.query(sql);
|
||||
const katMap = await loadKatMap(pool);
|
||||
const [katMap, steuersaetze] = await Promise.all([
|
||||
loadKatMap(pool),
|
||||
loadSteuersatz(),
|
||||
]);
|
||||
const katagg = buildKatAgg(result.recordset, katMap);
|
||||
const length = result.recordset.length;
|
||||
const recordset = result.recordset
|
||||
.slice(start, start + count)
|
||||
.map((row) => shapeArticle(row, steuersaetze));
|
||||
|
||||
console.dir({ katagg, length, recordset }, { depth: null });
|
||||
|
||||
return { katagg, recordset: result.recordset };
|
||||
return { katagg, length, recordset };
|
||||
}
|
||||
|
||||
@@ -1,31 +1,56 @@
|
||||
import { getPool } from '../../db/mssqlClient.js';
|
||||
import { LOAD_STEUERKLASSEN } from '../../db/mssql/steuer.js';
|
||||
import { LOAD_STEUERSATZ, LOAD_STEUERKLASSEN } from '../../db/mssql/steuer.js';
|
||||
import env from '../../config/env.js';
|
||||
|
||||
let cache;
|
||||
let steuersatzCache;
|
||||
let steuerklassenCache;
|
||||
|
||||
/**
|
||||
* Loads JTL tax classes with domestic VAT rates (`tSteuerklasse` / `tSteuersatz`).
|
||||
* 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 (cache) {
|
||||
return cache;
|
||||
if (steuerklassenCache) {
|
||||
return steuerklassenCache;
|
||||
}
|
||||
|
||||
const pool = await getPool();
|
||||
const result = await pool.request().query(LOAD_STEUERKLASSEN);
|
||||
cache = result.recordset.map((row) => ({
|
||||
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 cache;
|
||||
return steuerklassenCache;
|
||||
}
|
||||
|
||||
export function bruttoFromNetto(netto, kSteuerklasse, steuerklassen) {
|
||||
const row = steuerklassen.find((item) => item.kSteuerklasse === kSteuerklasse);
|
||||
export function bruttoFromNetto(netto, kSteuerklasse, steuersaetze) {
|
||||
const row = steuersaetze.find((item) => item.kSteuerklasse === kSteuerklasse);
|
||||
if (row == null || netto == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user