Änderungen in der Suche
This commit is contained in:
@@ -4,7 +4,7 @@ Bez2
|
||||
Ben7 (Englischer Text)
|
||||
Ben8 (Deutscher Text)
|
||||
Hersteller
|
||||
Text -> Text aus der tabelle TEXT durch TextId verknüpft
|
||||
Text -> MemoID aus EKATSS (EKATSS.TEIL = Teil), dann TEXT.TextId = MemoID
|
||||
PrsVK -> Aus Tabelle TSSAEF verknüpft durch Teil
|
||||
Ersatz (Ersetzte durch) -> Aus Tabelle TSSAEF, Spalte Ersatz
|
||||
|
||||
|
||||
@@ -208,14 +208,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
t.Ben8,
|
||||
t.Hersteller,
|
||||
t.Stat,
|
||||
t.VkTeil,
|
||||
txt.Text AS [Text],
|
||||
ts.PrsVk AS PrsVK,
|
||||
ts.Ersatz AS Ersatz
|
||||
FROM TEILE t WITH (NOLOCK)
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1 MemoID
|
||||
FROM EKATSS WITH (NOLOCK)
|
||||
WHERE [Teil] = t.Teil
|
||||
ORDER BY (SELECT NULL)
|
||||
) ek
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1 [Text]
|
||||
FROM TEXT WITH (NOLOCK)
|
||||
WHERE TextId = t.MemoID
|
||||
WHERE TextId = ek.MemoID
|
||||
ORDER BY LfdNr
|
||||
) txt
|
||||
OUTER APPLY (
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: #0066FF;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
@@ -70,6 +70,21 @@
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.search-filter {
|
||||
padding: 15px 12px;
|
||||
font-size: 16px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
min-width: 140px;
|
||||
}
|
||||
.search-filter:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
padding: 15px 40px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
@@ -198,6 +213,15 @@
|
||||
.record-field {
|
||||
display: flex;
|
||||
padding: 5px 0;
|
||||
margin: 0 -15px;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
.record-body .record-field:nth-child(even) {
|
||||
background: rgba(0,0,0,0.04);
|
||||
}
|
||||
.record-body .record-field:nth-child(odd) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.field-name {
|
||||
@@ -209,6 +233,16 @@
|
||||
.field-value {
|
||||
color: #333;
|
||||
word-break: break-word;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.field-value .part-link {
|
||||
color: #1565c0;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
.field-value .part-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
@@ -301,6 +335,12 @@
|
||||
placeholder="Suchbegriff eingeben..."
|
||||
autofocus
|
||||
>
|
||||
<select id="statusFilter" class="search-filter" title="Status-Filter">
|
||||
<option value="">Alle</option>
|
||||
<option value="aktiv">Nur Aktiv</option>
|
||||
<option value="pruefbar">Nur Prüfbar</option>
|
||||
<option value="inaktiv">Nur Inaktiv</option>
|
||||
</select>
|
||||
<button id="searchButton" class="search-button">Suchen</button>
|
||||
<button id="configButton" class="config-button" style="display: none;">Konfiguration</button>
|
||||
</div>
|
||||
@@ -310,10 +350,6 @@
|
||||
<div class="stat-value" id="totalMatches">0</div>
|
||||
<div class="stat-label">Treffer</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="tablesSearched">0</div>
|
||||
<div class="stat-label">Tabellen durchsucht</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="searchTime">0ms</div>
|
||||
<div class="stat-label">Suchzeit</div>
|
||||
@@ -329,6 +365,7 @@
|
||||
const searchButton = document.getElementById('searchButton');
|
||||
const resultsDiv = document.getElementById('results');
|
||||
const statsDiv = document.getElementById('stats');
|
||||
const statusFilterSelect = document.getElementById('statusFilter');
|
||||
let currentSearchTerm = '';
|
||||
|
||||
// Enter-Taste zum Suchen
|
||||
@@ -340,6 +377,19 @@
|
||||
|
||||
searchButton.addEventListener('click', performSearch);
|
||||
|
||||
resultsDiv.addEventListener('click', (e) => {
|
||||
const link = e.target.closest('.part-link');
|
||||
if (!link) return;
|
||||
e.preventDefault();
|
||||
const part = link.getAttribute('data-part');
|
||||
if (part) {
|
||||
searchInput.value = part;
|
||||
// Beim Klick auf "Ersetzt durch" immer auf "Alle" setzen
|
||||
statusFilterSelect.value = '';
|
||||
performSearch();
|
||||
}
|
||||
});
|
||||
|
||||
async function performSearch() {
|
||||
const searchTerm = searchInput.value.trim();
|
||||
|
||||
@@ -357,7 +407,11 @@
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/search?q=${encodeURIComponent(searchTerm)}`);
|
||||
const statusFilter = statusFilterSelect.value;
|
||||
const url = new URL('/api/search', window.location.origin);
|
||||
url.searchParams.set('q', searchTerm);
|
||||
if (statusFilter) url.searchParams.set('status', statusFilter);
|
||||
const response = await fetch(url.toString());
|
||||
const data = await response.json();
|
||||
|
||||
const endTime = performance.now();
|
||||
@@ -380,7 +434,6 @@
|
||||
// Statistiken anzeigen
|
||||
statsDiv.style.display = 'flex';
|
||||
document.getElementById('totalMatches').textContent = data.totalMatches;
|
||||
document.getElementById('tablesSearched').textContent = data.tablesSearched;
|
||||
document.getElementById('searchTime').textContent = searchTime + 'ms';
|
||||
|
||||
// Ergebnisse anzeigen
|
||||
@@ -405,7 +458,7 @@
|
||||
<span class="table-count">${tableResult.matchCount} Treffer</span>
|
||||
</div>
|
||||
<div class="records">
|
||||
${tableResult.records.map(record => renderRecord(record)).join('')}
|
||||
${tableResult.records.slice().sort((a, b) => getStatusSortOrder(a.Stat) - getStatusSortOrder(b.Stat)).map(record => renderRecord(record)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -420,8 +473,8 @@
|
||||
{ key: 'Teil', label: 'Teilenummer' },
|
||||
{ key: 'Bez', label: 'Bezeichnung' },
|
||||
{ key: 'Bez2', label: 'Bezeichnung 2' },
|
||||
{ key: 'Ben7', label: 'Englischer Text' },
|
||||
{ key: 'Ben8', label: 'Deutscher Text' },
|
||||
{ key: 'Ben8', label: 'Bezeichnung 3' },
|
||||
{ key: 'Ben7', label: 'Eng 3' },
|
||||
{ key: 'Hersteller', label: 'Hersteller' },
|
||||
{ key: 'Text', label: 'Zusatztext' },
|
||||
{ key: 'PrsVK', label: 'Verkaufspreis in €' },
|
||||
@@ -437,6 +490,14 @@
|
||||
return 'record--aktiv'; // "" oder leer = aktiv
|
||||
}
|
||||
|
||||
/** Sortierreihenfolge: 0 = aktiv (zuerst), 1 = prüfbar, 2 = Rest */
|
||||
function getStatusSortOrder(stat) {
|
||||
const s = (stat == null ? '' : String(stat).trim());
|
||||
if (s === '') return 0; // aktiv
|
||||
if (s === 'L') return 1; // prüfbar
|
||||
return 2; // inaktiv, löschbar, pseudoteil
|
||||
}
|
||||
|
||||
function getStatusLabel(stat) {
|
||||
const s = (stat == null ? '' : String(stat).trim());
|
||||
if (s === 'i') return 'Inaktiv';
|
||||
@@ -458,7 +519,20 @@
|
||||
for (const { key, label } of DISPLAY_COLUMNS) {
|
||||
if (!(key in record)) continue;
|
||||
const value = record[key];
|
||||
const displayValue = value === null || value === undefined ? '' : highlightSearchTerm(String(value));
|
||||
let displayValue;
|
||||
if (key === 'Ersatz' && value != null && String(value).trim() !== '') {
|
||||
const partNum = String(value).trim();
|
||||
const safePart = escapeHtml(partNum);
|
||||
const safePartAttr = safePart.replace(/"/g, '"');
|
||||
displayValue = `<a href="#" class="part-link" data-part="${safePartAttr}">${safePart}</a>`;
|
||||
} else if (key === 'PrsVK') {
|
||||
const isVkTeil = record.VkTeil != null && Number(record.VkTeil) !== 0;
|
||||
displayValue = isVkTeil && value != null && String(value).trim() !== ''
|
||||
? highlightSearchTerm(String(value))
|
||||
: escapeHtml('kein Verkaufsteil');
|
||||
} else {
|
||||
displayValue = value === null || value === undefined ? '' : highlightSearchTerm(String(value));
|
||||
}
|
||||
html += `
|
||||
<div class="record-field">
|
||||
<span class="field-name">${escapeHtml(label)}:</span>
|
||||
|
||||
@@ -9,6 +9,7 @@ const { fullTextSearch, getAvailableTables, getTableColumns, SEARCH_CONFIG } = r
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const searchTerm = req.query.q;
|
||||
const statusFilter = req.query.status; // '' | 'aktiv' | 'pruefbar' | 'inaktiv'
|
||||
|
||||
if (!searchTerm) {
|
||||
return res.status(400).json({
|
||||
@@ -17,7 +18,7 @@ router.get('/', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
const results = await fullTextSearch(searchTerm);
|
||||
const results = await fullTextSearch(searchTerm, statusFilter);
|
||||
|
||||
const totalMatches = results.reduce((sum, table) => sum + table.matchCount, 0);
|
||||
const tableErrors = results.filter(table => table && table.error);
|
||||
|
||||
@@ -21,7 +21,7 @@ const TEILE_SEARCH_COLUMNS = [
|
||||
];
|
||||
|
||||
// Ausgabe-Spalten für Suchergebnisse (siehe Spalten.txt)
|
||||
// Text aus TEXT (TEILE.MemoID = TEXT.TextId), PrsVK + Ersatz aus TSSAEF (Teil = Teil)
|
||||
// Text aus TEXT: MemoID aus EKATSS (EKATSS.TEIL = Teil), dann TEXT.TextId = MemoID; PrsVK + Ersatz aus TSSAEF (Teil = Teil)
|
||||
const TEILE_OUTPUT_COLUMNS = [
|
||||
'Teil',
|
||||
'Bez',
|
||||
@@ -68,9 +68,10 @@ const buildSingleWordCondition = (columns, paramName, tableAlias = '') => {
|
||||
* - Optimierte Query-Struktur mit TOP für bessere Performance
|
||||
*
|
||||
* @param {string} searchTerm - Der zu suchende Begriff (mehrere Wörter möglich, getrennt durch Leerzeichen)
|
||||
* @param {string} [statusFilter] - Optional: 'aktiv' | 'pruefbar' | 'inaktiv' (leer/undefined = alle)
|
||||
* @returns {Promise<Array>} Array mit Suchergebnissen
|
||||
*/
|
||||
const fullTextSearch = async (searchTerm) => {
|
||||
const fullTextSearch = async (searchTerm, statusFilter) => {
|
||||
if (!searchTerm || searchTerm.trim() === '') {
|
||||
throw new Error('Suchbegriff darf nicht leer sein');
|
||||
}
|
||||
@@ -82,16 +83,32 @@ const fullTextSearch = async (searchTerm) => {
|
||||
|
||||
const pool = await getConnection();
|
||||
|
||||
// TEILE: Feste 8 Spalten mit JOINs auf TEXT (MemoID=TextId) und TSSAEF (Teil)
|
||||
// TEILE: Ausgabe-Spalten mit JOINs auf EKATSS (MemoID), TEXT (TextId=MemoID), TSSAEF (Teil)
|
||||
const searchPromises = SEARCH_CONFIG.map(async (tableConfig) => {
|
||||
try {
|
||||
// Pro Wort: (Spalte1 LIKE @searchWord0 OR ... OR SpalteN LIKE @searchWord0), alle mit AND verknüpft
|
||||
// Pro Wort: Treffer in TEILE-Spalten ODER im Zusatztext (TEXT über EKATSS); alle Wörter mit AND
|
||||
const wordConditions = words.map((_, i) => {
|
||||
const paramName = `searchWord${i}`;
|
||||
const cond = buildSingleWordCondition(tableConfig.columns, paramName, 't');
|
||||
return `(${cond})`;
|
||||
const teileCond = buildSingleWordCondition(tableConfig.columns, paramName, 't');
|
||||
const textCond = `EXISTS (
|
||||
SELECT 1
|
||||
FROM EKATSS ek WITH (NOLOCK)
|
||||
INNER JOIN [TEXT] tx WITH (NOLOCK) ON tx.TextId = ek.MemoID
|
||||
WHERE ek.[Teil] = t.Teil
|
||||
AND tx.[Text] LIKE @${paramName} COLLATE SQL_Latin1_General_CP1_CI_AS
|
||||
)`;
|
||||
return `(${teileCond} OR ${textCond})`;
|
||||
});
|
||||
const whereClause = wordConditions.join(' AND ');
|
||||
let whereClause = wordConditions.join(' AND ');
|
||||
|
||||
// Status-Filter: aktiv = leer/NULL, pruefbar = L, inaktiv = i
|
||||
if (statusFilter === 'aktiv') {
|
||||
whereClause += ` AND (t.Stat IS NULL OR LTRIM(RTRIM(ISNULL(t.Stat, N''))) = N'')`;
|
||||
} else if (statusFilter === 'pruefbar') {
|
||||
whereClause += ` AND t.Stat = @statusFilter`;
|
||||
} else if (statusFilter === 'inaktiv') {
|
||||
whereClause += ` AND t.Stat = @statusFilter`;
|
||||
}
|
||||
|
||||
const query = `
|
||||
SELECT TOP 100
|
||||
@@ -102,15 +119,22 @@ const fullTextSearch = async (searchTerm) => {
|
||||
t.Ben8,
|
||||
t.Hersteller,
|
||||
t.Stat,
|
||||
t.VkTeil,
|
||||
txt.Text AS [Text],
|
||||
ts.PrsVk AS PrsVK,
|
||||
ts.Ersatz AS Ersatz
|
||||
FROM [${tableConfig.tableName}] t WITH (NOLOCK)
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1 [Text]
|
||||
FROM [TEXT] WITH (NOLOCK)
|
||||
WHERE TextId = COALESCE(NULLIF(RTRIM(LTRIM(t.MemoID)), N''), N'MEM-' + t.Teil)
|
||||
ORDER BY LfdNr
|
||||
SELECT TOP 1 MemoID
|
||||
FROM EKATSS WITH (NOLOCK)
|
||||
WHERE [Teil] = t.Teil
|
||||
ORDER BY (SELECT NULL)
|
||||
) ek
|
||||
OUTER APPLY (
|
||||
SELECT
|
||||
STRING_AGG(tx.[Text], CHAR(10)) WITHIN GROUP (ORDER BY tx.LfdNr) AS [Text]
|
||||
FROM [TEXT] tx WITH (NOLOCK)
|
||||
WHERE tx.TextId = ek.MemoID
|
||||
) txt
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1 PrsVk, Ersatz
|
||||
@@ -125,6 +149,11 @@ const fullTextSearch = async (searchTerm) => {
|
||||
words.forEach((word, i) => {
|
||||
request.input(`searchWord${i}`, sql.NVarChar, `%${word}%`);
|
||||
});
|
||||
if (statusFilter === 'pruefbar') {
|
||||
request.input('statusFilter', sql.NVarChar(10), 'L');
|
||||
} else if (statusFilter === 'inaktiv') {
|
||||
request.input('statusFilter', sql.NVarChar(10), 'i');
|
||||
}
|
||||
|
||||
const result = await request.query(query);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user