Englische Texte, Kopiericon, Get-Route für suche

This commit is contained in:
2026-03-19 15:39:52 +01:00
parent 1ad3443433
commit e399019001
4 changed files with 205 additions and 5 deletions

View File

@@ -216,6 +216,7 @@
margin: 0 -15px;
padding-left: 15px;
padding-right: 15px;
align-items: flex-start;
}
.record-body .record-field:nth-child(even) {
background: rgba(0,0,0,0.04);
@@ -235,6 +236,23 @@
word-break: break-word;
white-space: pre-line;
margin-left: 16px;
flex: 1;
}
.copy-btn {
margin-left: 8px;
border: none;
background: transparent;
color: #666;
cursor: pointer;
font-size: 14px;
line-height: 1;
padding: 2px 4px;
border-radius: 4px;
}
.copy-btn:hover {
background: rgba(0, 0, 0, 0.08);
color: #222;
}
.price-age {
@@ -390,6 +408,17 @@
searchButton.addEventListener('click', performSearch);
resultsDiv.addEventListener('click', (e) => {
const copyBtn = e.target.closest('.copy-btn');
if (copyBtn) {
e.preventDefault();
const copyText = copyBtn.getAttribute('data-copy') || '';
copyToClipboard(copyText);
const original = copyBtn.textContent;
copyBtn.textContent = '✓';
setTimeout(() => { copyBtn.textContent = original; }, 700);
return;
}
const link = e.target.closest('.part-link');
if (!link) return;
e.preventDefault();
@@ -485,6 +514,8 @@
{ key: 'Teil', label: 'Teilenummer' },
{ key: 'Bez', label: 'Bezeichnung' },
{ key: 'Bez2', label: 'Bezeichnung 2' },
{ key: 'EngBez1', label: 'Englische Bezeichnung 1' },
{ key: 'EngBez2', label: 'Englische Bezeichnung 2' },
{ key: 'Ben8', label: 'Bezeichnung 3' },
{ key: 'Ben7', label: 'Englischer Text' },
{ key: 'Hersteller', label: 'Hersteller' },
@@ -533,40 +564,50 @@
if (!(key in record)) continue;
const value = record[key];
let displayValue;
let copyValue = '';
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>`;
copyValue = partNum;
} else if (key === 'PrsVK') {
const isVkTeil = record.VkTeil != null && Number(record.VkTeil) !== 0;
if (isVkTeil && value != null && String(value).trim() !== '') {
const valueText = String(value);
const priceText = highlightSearchTerm(String(value));
const priceTermStr = record.PrsVkTerm || record.PrsVKTerm;
let ageHtml = '';
let ageText = '';
if (priceTermStr) {
const priceDate = new Date(priceTermStr);
if (!isNaN(priceDate)) {
const now = new Date();
const diffMs = now.getTime() - priceDate.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
const ageText = `${diffDays} Tage alt`;
ageText = `${diffDays} Tage alt`;
const isOld = diffDays > 182; // > ca. 6 Monate
const ageClass = isOld ? 'price-age price-age--old' : 'price-age';
ageHtml = ` <span class="${ageClass}">(${escapeHtml(ageText)})</span>`;
}
}
displayValue = `${priceText}${ageHtml}`;
copyValue = ageText ? `${valueText} (${ageText})` : valueText;
} else {
displayValue = escapeHtml('kein Verkaufsteil');
copyValue = 'kein Verkaufsteil';
}
} else {
const plainValue = value === null || value === undefined ? '' : String(value);
displayValue = value === null || value === undefined ? '' : highlightSearchTerm(String(value));
copyValue = plainValue;
}
const safeCopyAttr = escapeHtml(copyValue).replace(/"/g, '&quot;');
html += `
<div class="record-field">
<span class="field-name">${escapeHtml(label)}:</span>
<span class="field-value">${displayValue}</span>
<button class="copy-btn" type="button" data-copy="${safeCopyAttr}" title="Zeile kopieren" aria-label="Zeile kopieren">⧉</button>
</div>`;
}
html += `
@@ -606,6 +647,22 @@
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
} catch (err) {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.focus();
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
}
// Health Check beim Laden
window.addEventListener('load', async () => {
try {

View File

@@ -216,6 +216,7 @@
margin: 0 -15px;
padding-left: 15px;
padding-right: 15px;
align-items: flex-start;
}
.record-body .record-field:nth-child(even) {
background: rgba(0,0,0,0.04);
@@ -235,6 +236,23 @@
word-break: break-word;
white-space: pre-line;
margin-left: 16px;
flex: 1;
}
.copy-btn {
margin-left: 8px;
border: none;
background: transparent;
color: #666;
cursor: pointer;
font-size: 14px;
line-height: 1;
padding: 2px 4px;
border-radius: 4px;
}
.copy-btn:hover {
background: rgba(0, 0, 0, 0.08);
color: #222;
}
.price-age {
@@ -390,6 +408,17 @@
searchButton.addEventListener('click', performSearch);
resultsDiv.addEventListener('click', (e) => {
const copyBtn = e.target.closest('.copy-btn');
if (copyBtn) {
e.preventDefault();
const copyText = copyBtn.getAttribute('data-copy') || '';
copyToClipboard(copyText);
const original = copyBtn.textContent;
copyBtn.textContent = '✓';
setTimeout(() => { copyBtn.textContent = original; }, 700);
return;
}
const link = e.target.closest('.part-link');
if (!link) return;
e.preventDefault();
@@ -484,9 +513,11 @@
const DISPLAY_COLUMNS = [
{ key: 'Teil', label: 'Teilenummer' },
{ key: 'Bez', label: 'Bezeichnung' },
{ key: 'Bez2', label: 'Bezeichnung 2' },
{ key: 'Bez2', label: 'Bezeichnung 2' },
{ key: 'Ben8', label: 'Bezeichnung 3' },
{ key: 'Ben7', label: 'Englischer Text' },
{ key: 'EngBez1', label: 'Englischer Text 1' },
{ key: 'EngBez2', label: 'Englischer Text 2' },
{ key: 'Ben7', label: 'Englischer Text 3' },
{ key: 'Hersteller', label: 'Hersteller' },
{ key: 'Text', label: 'Zusatztext' },
{ key: 'PrsVK', label: 'VK in €' },
@@ -533,40 +564,50 @@
if (!(key in record)) continue;
const value = record[key];
let displayValue;
let copyValue = '';
if (key === 'Ersatz' && value != null && String(value).trim() !== '') {
const partNum = String(value).trim();
const safePart = escapeHtml(partNum);
const safePartAttr = safePart.replace(/"/g, '&quot;');
displayValue = `<a href="#" class="part-link" data-part="${safePartAttr}">${safePart}</a>`;
copyValue = partNum;
} else if (key === 'PrsVK') {
const isVkTeil = record.VkTeil != null && Number(record.VkTeil) !== 0;
if (isVkTeil && value != null && String(value).trim() !== '') {
const valueText = String(value);
const priceText = highlightSearchTerm(String(value));
const priceTermStr = record.PrsVkTerm || record.PrsVKTerm;
let ageHtml = '';
let ageText = '';
if (priceTermStr) {
const priceDate = new Date(priceTermStr);
if (!isNaN(priceDate)) {
const now = new Date();
const diffMs = now.getTime() - priceDate.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
const ageText = `${diffDays} Tage alt`;
ageText = `${diffDays} Tage alt`;
const isOld = diffDays > 182; // > ca. 6 Monate
const ageClass = isOld ? 'price-age price-age--old' : 'price-age';
ageHtml = ` <span class="${ageClass}">(${escapeHtml(ageText)})</span>`;
}
}
displayValue = `${priceText}${ageHtml}`;
copyValue = ageText ? `${valueText} (${ageText})` : valueText;
} else {
displayValue = escapeHtml('kein Verkaufsteil');
copyValue = 'kein Verkaufsteil';
}
} else {
const plainValue = value === null || value === undefined ? '' : String(value);
displayValue = value === null || value === undefined ? '' : highlightSearchTerm(String(value));
copyValue = plainValue;
}
const safeCopyAttr = escapeHtml(copyValue).replace(/"/g, '&quot;');
html += `
<div class="record-field">
<span class="field-name">${escapeHtml(label)}:</span>
<span class="field-value">${displayValue}</span>
<button class="copy-btn" type="button" data-copy="${safeCopyAttr}" title="Zeile kopieren" aria-label="Zeile kopieren">⧉</button>
</div>`;
}
html += `
@@ -606,8 +647,42 @@
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
} catch (err) {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.focus();
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
}
// Suchbegriff/Status aus URL übernehmen (z.B. /?q=abc&status=aktiv)
function applySearchParamsFromUrl() {
const params = new URLSearchParams(window.location.search);
const initialSearch = (params.get('q') || params.get('search') || '').trim();
const initialStatus = (params.get('status') || '').trim();
if (initialSearch) {
searchInput.value = initialSearch;
}
if (['aktiv', 'pruefbar', 'inaktiv', ''].includes(initialStatus)) {
statusFilterSelect.value = initialStatus;
}
return initialSearch;
}
// Health Check beim Laden
window.addEventListener('load', async () => {
const initialSearch = applySearchParamsFromUrl();
try {
const response = await fetch('/api/health');
const data = await response.json();
@@ -617,6 +692,10 @@
} catch (error) {
console.error('Health check failed:', error);
}
if (initialSearch) {
await performSearch();
}
});
</script>
</body>

View File

@@ -21,6 +21,43 @@ app.use(express.static(path.join(__dirname, 'public')));
// API Routes
app.use('/api/search', searchRoutes);
// Direkte URL-Aufrufe auf die UI umleiten und Suchbegriff vorbelegen:
// - /search=meinbegriff -> /?q=meinbegriff
// - /search?q=meinbegriff oder /search?search=meinbegriff -> /?q=meinbegriff
app.get('/search=:searchTerm', (req, res) => {
const searchTerm = (req.params.searchTerm || '').trim();
const statusFilter = (req.query.status || '').trim();
if (!searchTerm) {
return res.redirect('/');
}
const redirectUrl = new URL(`http://localhost:${PORT}/`);
redirectUrl.searchParams.set('q', searchTerm);
if (statusFilter) {
redirectUrl.searchParams.set('status', statusFilter);
}
return res.redirect(`${redirectUrl.pathname}${redirectUrl.search}`);
});
app.get('/search', (req, res) => {
const searchTerm = (req.query.search || req.query.q || '').trim();
const statusFilter = (req.query.status || '').trim();
if (!searchTerm) {
return res.redirect('/');
}
const redirectUrl = new URL(`http://localhost:${PORT}/`);
redirectUrl.searchParams.set('q', searchTerm);
if (statusFilter) {
redirectUrl.searchParams.set('status', statusFilter);
}
return res.redirect(`${redirectUrl.pathname}${redirectUrl.search}`);
});
// Health check endpoint
app.get('/api/health', async (req, res) => {
try {

View File

@@ -27,6 +27,8 @@ const TEILE_OUTPUT_COLUMNS = [
'Teil',
'Bez',
'Bez2',
'EngBez1',
'EngBez2',
'Ben7',
'Ben8',
'Hersteller',
@@ -98,7 +100,14 @@ const fullTextSearch = async (searchTerm, statusFilter) => {
WHERE ek.[Teil] = t.Teil
AND tx.[Text] LIKE @${paramName} COLLATE SQL_Latin1_General_CP1_CI_AS
)`;
return `(${teileCond} OR ${textCond})`;
const tnbCond = `EXISTS (
SELECT 1
FROM TEXTE1 tx1 WITH (NOLOCK)
WHERE tx1.Id = N'TNB'
AND tx1.Schluessel = t.Teil
AND tx1.[Text] LIKE @${paramName} COLLATE SQL_Latin1_General_CP1_CI_AS
)`;
return `(${teileCond} OR ${textCond} OR ${tnbCond})`;
});
let whereClause = wordConditions.join(' AND ');
@@ -116,6 +125,8 @@ const fullTextSearch = async (searchTerm, statusFilter) => {
t.Teil,
t.Bez,
t.Bez2,
en.EngBez1,
en.EngBez2,
t.Ben7,
t.Ben8,
t.Ben43,
@@ -127,6 +138,22 @@ const fullTextSearch = async (searchTerm, statusFilter) => {
ts.Ersatz AS Ersatz,
ts.PrsVkTerm
FROM [${tableConfig.tableName}] t WITH (NOLOCK)
OUTER APPLY (
SELECT
LEFT(src.FullText, 30) AS EngBez1,
CASE
WHEN LEN(src.FullText) > 30 THEN SUBSTRING(src.FullText, 31, 30)
ELSE N''
END AS EngBez2
FROM (
SELECT TOP 1
ISNULL(tx1.[Text], N'') AS FullText
FROM TEXTE1 tx1 WITH (NOLOCK)
WHERE tx1.Id = N'TNB'
AND tx1.Schluessel = t.Teil
ORDER BY ISNULL(tx1.Zeile, 32767), tx1.ISN
) src
) en
OUTER APPLY (
SELECT TOP 1 MemoID
FROM EKATSS WITH (NOLOCK)