Englische Texte, Kopiericon, Get-Route für suche
This commit is contained in:
@@ -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, '"');
|
||||
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 {
|
||||
|
||||
@@ -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();
|
||||
@@ -486,7 +515,9 @@
|
||||
{ key: 'Bez', label: 'Bezeichnung' },
|
||||
{ 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, '"');
|
||||
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, '"');
|
||||
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>
|
||||
|
||||
37
server.js
37
server.js
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user