diff --git a/public/dev.html b/public/dev.html
index 7d530c0..95ec069 100644
--- a/public/dev.html
+++ b/public/dev.html
@@ -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 = `${safePart}`;
+ 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 = ` (${escapeHtml(ageText)})`;
}
}
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 += `
${escapeHtml(label)}:
${displayValue}
+
`;
}
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 {
diff --git a/public/index.html b/public/index.html
index 7d530c0..dfd0b09 100644
--- a/public/index.html
+++ b/public/index.html
@@ -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, '"');
displayValue = `${safePart}`;
+ 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 = ` (${escapeHtml(ageText)})`;
}
}
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 += `
${escapeHtml(label)}:
${displayValue}
+
`;
}
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();
+ }
});