Files
shape3d.it/public/assets/app.js
2026-01-24 02:04:46 +01:00

305 lines
9.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function () {
const API_BASE = '/api';
const printerSelect = document.getElementById('printerSelect');
const printerCompare = document.getElementById('printerCompare');
const matBody = document.getElementById('matBody');
const tableHead = document.getElementById('tableHead');
const statusEl = document.getElementById('status');
const errorBox = document.getElementById('errorBox');
const tooltipEl = document.getElementById('mmTooltip');
if (!printerSelect || !printerCompare || !matBody || !tableHead) {
return;
}
function setStatus(text) {
if (statusEl) {
statusEl.textContent = text;
}
}
function showError(msg) {
if (!errorBox) return;
errorBox.hidden = false;
errorBox.textContent = msg;
}
function clearError() {
if (!errorBox) return;
errorBox.hidden = true;
errorBox.textContent = '';
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
async function fetchJSON(path) {
const res = await fetch(API_BASE + path, { headers: { 'Accept': 'application/json' } });
if (!res.ok) throw new Error('HTTP ' + res.status + ' for ' + path);
return await res.json();
}
async function loadPrinters() {
try {
setStatus('Lade Drucker ...');
const data = await fetchJSON('/printers');
printerSelect.innerHTML = '';
printerCompare.innerHTML = '';
if (!Array.isArray(data) || data.length === 0) {
printerSelect.innerHTML = '<option value="">(keine Drucker gefunden)</option>';
setStatus('Keine Drucker gefunden');
return;
}
data.forEach(p => {
printerSelect.appendChild(new Option(p.name, p.id));
printerCompare.appendChild(new Option(p.name, p.id));
});
loadSinglePrinter(data[0].id);
setStatus('Drucker geladen');
} catch (err) {
console.error(err);
showError('Konnte Drucker nicht laden.');
setStatus('Fehler');
}
}
async function loadSinglePrinter(id) {
if (!id) return;
clearError();
setStatus('Lade Materialien ...');
try {
const data = await fetchJSON('/printer-materials?id=' + encodeURIComponent(id));
renderTable([data]);
setStatus('Fertig');
} catch (err) {
console.error(err);
showError('Konnte Materialien für den Drucker nicht laden.');
setStatus('Fehler');
}
}
async function loadMultiplePrinters(ids) {
if (!ids.length) return;
clearError();
setStatus('Lade Vergleich ...');
try {
const datasets = await Promise.all(
ids.map(id => fetchJSON('/printer-materials?id=' + encodeURIComponent(id)))
);
renderTable(datasets);
setStatus('Vergleich geladen');
} catch (err) {
console.error(err);
showError('Konnte einen der gewählten Drucker nicht laden.');
setStatus('Fehler');
}
}
function renderTable(datasets) {
const baseHead = [
'Material',
'Eigenschaften',
'Anwendung',
'Kinder <span aria-hidden="true">👶</span>',
'Emission <span aria-hidden="true">🌫️</span>'
].map(label => `<th>${label}</th>`).join('');
let printerCols = '';
datasets.forEach(ds => {
if (ds && ds.printer) {
printerCols += `<th data-printer="${escapeHtml(ds.printer.id)}">${escapeHtml(ds.printer.name)}</th>`;
}
});
tableHead.innerHTML = baseHead + printerCols;
const materials = (datasets[0] && datasets[0].materials) ? datasets[0].materials : [];
matBody.innerHTML = '';
if (!materials.length) {
const empty = document.createElement('tr');
empty.innerHTML = '<td colspan="12">Keine Materialien gefunden.</td>';
matBody.appendChild(empty);
return;
}
materials.forEach((m, idx) => {
const tr = document.createElement('tr');
tr.className = idx % 2 === 0 ? '' : 'is-alt';
const kid = m.kid_safety === 'safe'
? { icon: '🌿', text: 'sicher' }
: (m.kid_safety === 'limited' ? { icon: '🟡', text: 'eingeschränkt' } : { icon: '🔴', text: 'nicht geeignet' });
const em = m.emission === 'low'
? { icon: '✅', text: 'niedrig' }
: (m.emission === 'medium' ? { icon: '⚠️', text: 'mittel' } : { icon: '⛔', text: 'hoch' });
let html = '';
const detailPayload = {
tg: m.tg_celsius ?? '',
nozzle: m.nozzle_req ?? '',
plate: m.plate_req ?? '',
extra: m.extra_req ?? '',
emission: m.emission ?? ''
};
const detailAttr = escapeHtml(JSON.stringify(detailPayload));
html += `<td class="mm-row-toggle" data-details="${detailAttr}">` +
`<strong>${escapeHtml(m.code)}</strong>` +
`<div class="mm-sub">${escapeHtml(m.short_desc || '')}</div>` +
`<button type="button" class="mm-info-btn" data-tooltip="Mehr Infos anzeigen">Mehr Info</button>` +
`</td>`;
html += `<td>${escapeHtml(m.properties || '')}</td>`;
html += `<td>${escapeHtml(m.application || '')}</td>`;
html += `<td title="${escapeHtml(kid.text)}">${kid.icon}</td>`;
html += `<td title="${escapeHtml(em.text)}">${em.icon}</td>`;
datasets.forEach(ds => {
const printerId = ds && ds.printer ? ds.printer.id : '';
const match = ds.materials.find(x => x.id === m.id || x.code === m.code);
if (!match || !match.support_level) {
html += `<td data-printer="${escapeHtml(printerId)}"><span class="mm-tag">unbekannt</span></td>`;
} else {
let badge = '';
if (match.support_level === 'full') {
badge = '<span class="mm-tag ok">voll</span>';
} else if (match.support_level === 'partial') {
badge = '<span class="mm-tag warn">teilw.</span>';
} else if (match.support_level === 'with_addon') {
badge = '<span class="mm-tag addon">Zusatz</span>';
} else {
badge = '<span class="mm-tag no">nein</span>';
}
const note = match.partial_reason
? `<div class="mm-sub">${escapeHtml(match.partial_reason)}</div>`
: (match.extra_info ? `<div class="mm-sub">${escapeHtml(match.extra_info)}</div>` : '');
html += `<td data-printer="${escapeHtml(printerId)}">${badge}${note}</td>`;
}
});
tr.innerHTML = html;
matBody.appendChild(tr);
});
}
function setTooltip(content, x, y) {
if (!tooltipEl) return;
tooltipEl.textContent = content;
const padding = 12;
const maxX = window.innerWidth - tooltipEl.offsetWidth - padding;
const maxY = window.innerHeight - tooltipEl.offsetHeight - padding;
const left = Math.min(x + 14, maxX);
const top = Math.min(y + 14, maxY);
tooltipEl.style.left = Math.max(left, padding) + 'px';
tooltipEl.style.top = Math.max(top, padding) + 'px';
tooltipEl.classList.add('is-visible');
tooltipEl.setAttribute('aria-hidden', 'false');
}
function hideTooltip() {
if (!tooltipEl) return;
tooltipEl.classList.remove('is-visible');
tooltipEl.setAttribute('aria-hidden', 'true');
}
function buildDetailsHtml(details) {
return `
<button type="button" class="mm-details-close">Schließen</button>
<div><strong>Tg °C</strong>${escapeHtml(details.tg)}</div>
<div><strong>Düse</strong>${escapeHtml(details.nozzle)}</div>
<div><strong>Platte</strong>${escapeHtml(details.plate)}</div>
<div><strong>Zusatz</strong>${escapeHtml(details.extra)}</div>
<div><strong>Emission</strong>${escapeHtml(details.emission)}</div>
`;
}
function closeActiveDetails() {
const open = matBody.querySelector('tr.mm-details-row');
if (open) {
open.remove();
}
const active = matBody.querySelector('tr.is-active');
if (active) {
active.classList.remove('is-active');
}
}
matBody.addEventListener('mouseover', (e) => {
const cell = e.target.closest('[data-details]');
if (!cell) return;
let details;
try {
details = JSON.parse(cell.dataset.details || '{}');
} catch {
details = {};
}
const hoverText = `Tg °C: ${details.tg ?? ''} | Düse: ${details.nozzle ?? ''} | Platte: ${details.plate ?? ''} | Zusatz: ${details.extra ?? ''} | Emission: ${details.emission ?? ''}`;
setTooltip(hoverText, e.clientX, e.clientY);
});
matBody.addEventListener('mousemove', (e) => {
if (!tooltipEl || tooltipEl.getAttribute('aria-hidden') === 'true') return;
setTooltip(tooltipEl.textContent, e.clientX, e.clientY);
});
matBody.addEventListener('mouseout', (e) => {
if (e.target.closest('[data-details]')) {
hideTooltip();
}
});
matBody.addEventListener('click', (e) => {
if (e.target.closest('.mm-details-close')) {
closeActiveDetails();
return;
}
const cell = e.target.closest('[data-details]');
if (!cell) return;
const row = cell.closest('tr');
const isActive = row.classList.contains('is-active');
closeActiveDetails();
if (isActive) return;
let details;
try {
details = JSON.parse(cell.dataset.details || '{}');
} catch {
details = {};
}
const colCount = row.children.length;
const detailRow = document.createElement('tr');
detailRow.className = 'mm-details-row';
detailRow.innerHTML = `<td colspan="${colCount}"><div class="mm-details">${buildDetailsHtml(details)}</div></td>`;
row.classList.add('is-active');
row.parentNode.insertBefore(detailRow, row.nextSibling);
});
printerSelect.addEventListener('change', e => {
const id = e.target.value;
if (id) {
loadSinglePrinter(id);
printerCompare.selectedIndex = -1;
}
});
printerCompare.addEventListener('change', e => {
const ids = Array.from(e.target.selectedOptions).map(o => o.value);
if (ids.length) {
loadMultiplePrinters(ids);
}
});
loadPrinters();
})();