Daten via SQL (pw muss in .env eingetragen werden), Erweitert-Modus um nach anderen Feldern zu suchen
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import express from "express";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { fetchCustomersFromSql } from "./db.js";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -13,47 +13,75 @@ const config = {
|
||||
projectId: Number(process.env.REDMINE_PROJECT_ID) || 1,
|
||||
trackerId: Number(process.env.REDMINE_TRACKER_ID) || 3,
|
||||
customerFieldId: Number(process.env.CUSTOMER_FIELD_ID) || 5,
|
||||
customersFile: process.env.CUSTOMERS_FILE || path.join(__dirname, "data", "customers.csv"),
|
||||
customersRefreshMs: Number(process.env.CUSTOMERS_REFRESH_MS) || 5 * 60 * 1000,
|
||||
};
|
||||
|
||||
// Ohne API-Key läuft der Server im Trockenlauf: Tickets werden nur simuliert.
|
||||
const dryRun = config.redmineApiKey === "";
|
||||
|
||||
// --- Kundenstammdaten laden (CSV: Kundennummer;Firmenname) ---
|
||||
function loadCustomers(filePath) {
|
||||
const rawContent = fs.readFileSync(filePath, "utf8");
|
||||
return rawContent
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line !== "" && !line.startsWith("#"))
|
||||
.map((line) => {
|
||||
const [number, ...nameParts] = line.split(";");
|
||||
return { number: number.trim(), name: nameParts.join(";").trim() };
|
||||
})
|
||||
// --- Kundenstammdaten aus dem WAWIS-SQL-Server (nur lesend, siehe db.js) ---
|
||||
// gesperrte/inaktive Kunden werden für die Ticket-Anlage ausgeblendet
|
||||
let customers = [];
|
||||
let customersByNumber = new Map();
|
||||
|
||||
// Felder, die zusätzlich zu Nummer/Name über die erweiterte Suche durchsucht werden können
|
||||
const ADVANCED_SEARCH_FIELDS = ["kugru", "kg_id", "sydat", "datlv", "fibuk", "ku_id"];
|
||||
|
||||
function applyCustomers(rows) {
|
||||
customers = rows
|
||||
.filter((row) => row.baktiv === "ja" && row.bsper !== "ja")
|
||||
.map((row) => ({
|
||||
number: String(row.debnr).trim(),
|
||||
name: String(row.debez || "").trim(),
|
||||
kugru: row.kugru,
|
||||
kg_id: row.kg_id,
|
||||
sydat: row.sydat,
|
||||
datlv: row.datlv,
|
||||
fibuk: row.fibuk,
|
||||
ku_id: row.ku_id,
|
||||
}))
|
||||
.filter((customer) => customer.number !== "" && customer.name !== "");
|
||||
customersByNumber = new Map(customers.map((customer) => [customer.number, customer]));
|
||||
}
|
||||
|
||||
async function refreshCustomers() {
|
||||
const rows = await fetchCustomersFromSql();
|
||||
applyCustomers(rows);
|
||||
console.log(`${customers.length} aktive Kunden aus dem SQL-Server geladen`);
|
||||
}
|
||||
|
||||
let customers = [];
|
||||
try {
|
||||
customers = loadCustomers(config.customersFile);
|
||||
console.log(`${customers.length} Kunden aus ${config.customersFile} geladen`);
|
||||
await refreshCustomers();
|
||||
} catch (error) {
|
||||
console.error(`Kundendatei konnte nicht gelesen werden: ${error.message}`);
|
||||
console.error(`Kundendaten konnten nicht vom SQL-Server geladen werden: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const customersByNumber = new Map(customers.map((customer) => [customer.number, customer]));
|
||||
// Periodische Aktualisierung im Hintergrund; bei Fehler bleiben die alten Daten aktiv.
|
||||
setInterval(() => {
|
||||
refreshCustomers().catch((error) => {
|
||||
console.error(`Aktualisierung der Kundendaten fehlgeschlagen, alte Daten bleiben aktiv: ${error.message}`);
|
||||
});
|
||||
}, config.customersRefreshMs);
|
||||
|
||||
// --- Suche: Treffer am Wortanfang zuerst, dann Teilstring-Treffer ---
|
||||
function searchCustomers(query, limit) {
|
||||
const needle = query.toLocaleLowerCase("de-DE");
|
||||
// extraFields (aus ADVANCED_SEARCH_FIELDS) erweitert die durchsuchten Werte um Felder aus der Abfrage
|
||||
// Bei mehreren durch Leerzeichen getrennten Wörtern müssen alle Wörter im Treffer vorkommen (UND-Verknüpfung)
|
||||
function searchCustomers(query, extraFields, limit) {
|
||||
const words = query.toLocaleLowerCase("de-DE").split(/\s+/).filter(Boolean);
|
||||
const startMatches = [];
|
||||
const partialMatches = [];
|
||||
|
||||
for (const customer of customers) {
|
||||
const haystack = `${customer.number} ${customer.name}`.toLocaleLowerCase("de-DE");
|
||||
if (!haystack.includes(needle)) continue;
|
||||
const isStartMatch = haystack.startsWith(needle) || haystack.includes(` ${needle}`);
|
||||
const haystackParts = [customer.number, customer.name];
|
||||
for (const field of extraFields) {
|
||||
if (customer[field] !== undefined && customer[field] !== null) {
|
||||
haystackParts.push(String(customer[field]));
|
||||
}
|
||||
}
|
||||
const haystack = haystackParts.join(" ").toLocaleLowerCase("de-DE");
|
||||
if (!words.every((word) => haystack.includes(word))) continue;
|
||||
const isStartMatch = words.every((word) => haystack.startsWith(word) || haystack.includes(` ${word}`));
|
||||
if (isStartMatch) { startMatches.push(customer); } else { partialMatches.push(customer); }
|
||||
if (startMatches.length >= limit) break;
|
||||
}
|
||||
@@ -103,7 +131,12 @@ app.get("/api/customers", (request, response) => {
|
||||
if (query.length < 2) {
|
||||
return response.json({ results: [], total: customers.length });
|
||||
}
|
||||
const results = searchCustomers(query, 12);
|
||||
const extraFields = String(request.query.fields || "")
|
||||
.split(",")
|
||||
.map((field) => field.trim())
|
||||
.filter((field) => ADVANCED_SEARCH_FIELDS.includes(field));
|
||||
const matches = searchCustomers(query, extraFields, 12);
|
||||
const results = matches.map((customer) => ({ number: customer.number, name: customer.name }));
|
||||
return response.json({ results, total: customers.length });
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user