- Move Revision from Drawing to ExportRecord so each export captures its own revision snapshot - Add Hamming distance comparison for perceptual hashes (tolerance of 10 bits) to avoid false revision bumps - Replace CoenM.ImageHash with inline DifferenceHash impl (compatible with ImageSharp 3.x) - Increase PDF render DPI from 72 to 150 for better hash fidelity - Add download-dxfs-by-drawing endpoint for cross-export DXF zip downloads - Prefix DXF filenames with equipment number when no drawing number is present - Pass original filename to storage service for standalone part exports - Auto-start FabWorks.Api from ExportDXF client if not already running - Add cut list modal with copy-to-clipboard in the web UI - Update PDF hash on existing export records after upload - Bump static asset cache versions to v3 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
156 lines
6.9 KiB
JavaScript
156 lines
6.9 KiB
JavaScript
/* ─── BOM Detail Expansion ─── */
|
|
function renderBomDetails(b) {
|
|
let html = '<div class="bom-expand-content">';
|
|
|
|
if (b.cutTemplate) {
|
|
const ct = b.cutTemplate;
|
|
const displayName = ct.dxfFilePath?.split(/[/\\]/).pop() || '';
|
|
html += `
|
|
<div class="bom-section-title">${icons.laser} Cut Template</div>
|
|
<div class="info-grid">
|
|
<div class="info-item"><span class="lbl">File</span><span class="val">${esc(displayName)}</span></div>
|
|
<div class="info-item"><span class="lbl">Thickness</span><span class="val">${fmtThickness(ct.thickness)}</span></div>
|
|
<div class="info-item"><span class="lbl">K-Factor</span><span class="val">${ct.kFactor != null ? ct.kFactor : '\u2014'}</span></div>
|
|
<div class="info-item"><span class="lbl">Bend Radius</span><span class="val">${ct.defaultBendRadius != null ? ct.defaultBendRadius.toFixed(4) + '"' : '\u2014'}</span></div>
|
|
</div>`;
|
|
|
|
if (ct.contentHash) {
|
|
html += `<div style="margin-top:10px">
|
|
<a class="btn btn-cyan btn-sm" href="/api/files/blob/${encodeURIComponent(ct.contentHash)}?ext=dxf&download=true&name=${encodeURIComponent(displayName)}" onclick="event.stopPropagation()">${icons.download} Download DXF</a>
|
|
<span style="font-family:var(--font-mono);font-size:13px;color:var(--text-dim);margin-left:8px">${esc(displayName)}</span>
|
|
</div>`;
|
|
}
|
|
}
|
|
|
|
if (b.formProgram) {
|
|
const fp = b.formProgram;
|
|
html += `
|
|
<div class="bom-section-title">${icons.bend} Form Program</div>
|
|
<div class="info-grid">
|
|
<div class="info-item"><span class="lbl">Program</span><span class="val">${esc(fp.programName)}</span></div>
|
|
<div class="info-item"><span class="lbl">Thickness</span><span class="val">${fmtThickness(fp.thickness)}</span></div>
|
|
<div class="info-item"><span class="lbl">Material</span><span class="val">${esc(fp.materialType)}</span></div>
|
|
<div class="info-item"><span class="lbl">K-Factor</span><span class="val">${fp.kFactor != null ? fp.kFactor : '\u2014'}</span></div>
|
|
<div class="info-item"><span class="lbl">Bends</span><span class="val">${fp.bendCount}</span></div>
|
|
<div class="info-item"><span class="lbl">Upper Tools</span><span class="val">${esc(fp.upperToolNames) || '\u2014'}</span></div>
|
|
<div class="info-item"><span class="lbl">Lower Tools</span><span class="val">${esc(fp.lowerToolNames) || '\u2014'}</span></div>
|
|
</div>
|
|
${fp.setupNotes ? `<div style="margin-top:8px;padding:8px 12px;background:var(--amber-dim);border-radius:4px;font-size:13px;font-family:var(--font-mono);color:var(--amber)"><span class="lbl">Setup Notes</span>${esc(fp.setupNotes)}</div>` : ''}`;
|
|
}
|
|
|
|
html += '</div>';
|
|
return html;
|
|
}
|
|
|
|
function toggleEquipGroup(id) {
|
|
const group = document.getElementById(id);
|
|
const icon = document.getElementById(id + '-icon');
|
|
if (!group) return;
|
|
group.classList.toggle('collapsed');
|
|
if (icon) icon.classList.toggle('open', !group.classList.contains('collapsed'));
|
|
}
|
|
|
|
function toggleBomRow(id) {
|
|
const row = document.getElementById(id);
|
|
const icon = document.getElementById(id + '-icon');
|
|
if (!row) return;
|
|
const visible = row.style.display !== 'none';
|
|
row.style.display = visible ? 'none' : '';
|
|
if (icon) icon.classList.toggle('open', !visible);
|
|
}
|
|
|
|
/* ─── Cut List Modal ─── */
|
|
function showCutListModal(bomItems) {
|
|
const cutItems = bomItems.filter(b => b.cutTemplate);
|
|
if (cutItems.length === 0) {
|
|
showToast('No cut templates found');
|
|
return;
|
|
}
|
|
|
|
const rows = cutItems.map(b => {
|
|
const ct = b.cutTemplate;
|
|
const name = ct.cutTemplateName || ct.dxfFilePath?.split(/[/\\]/).pop()?.replace(/\.dxf$/i, '') || b.partName || '';
|
|
const qty = b.qty ?? '';
|
|
return { name, qty };
|
|
});
|
|
|
|
const tableRows = rows.map((r, i) => `
|
|
<tr style="animation: fadeSlideIn 0.15s ease ${0.02 * i}s forwards; opacity: 0">
|
|
<td style="font-family:var(--font-mono);font-weight:600">${esc(r.name)}</td>
|
|
<td style="font-family:var(--font-mono);text-align:center">${r.qty}</td>
|
|
</tr>`).join('');
|
|
|
|
// Remove existing modal if any
|
|
const existing = document.getElementById('cut-list-modal');
|
|
if (existing) existing.remove();
|
|
|
|
const modal = document.createElement('div');
|
|
modal.id = 'cut-list-modal';
|
|
modal.className = 'modal-overlay';
|
|
modal.innerHTML = `
|
|
<div class="modal-panel">
|
|
<div class="modal-header">
|
|
<span>${icons.laser} Cut List</span>
|
|
<span class="badge badge-count">${cutItems.length} templates</span>
|
|
<span style="margin-left:auto;display:flex;gap:6px">
|
|
<button class="btn btn-cyan btn-sm" onclick="copyCutList()" id="copy-cut-list-btn">${icons.clipboard} Copy</button>
|
|
<button class="btn btn-sm" onclick="closeCutListModal()">${icons.close}</button>
|
|
</span>
|
|
</div>
|
|
<div class="modal-body">
|
|
<table>
|
|
<thead><tr>
|
|
<th>Name</th>
|
|
<th style="width:60px;text-align:center">Qty</th>
|
|
</tr></thead>
|
|
<tbody>${tableRows}</tbody>
|
|
</table>
|
|
</div>
|
|
</div>`;
|
|
|
|
document.body.appendChild(modal);
|
|
// Store data for copy
|
|
modal._cutData = rows;
|
|
// Close on backdrop click
|
|
modal.addEventListener('click', e => { if (e.target === modal) closeCutListModal(); });
|
|
// Close on Escape
|
|
modal._keyHandler = e => { if (e.key === 'Escape') closeCutListModal(); };
|
|
document.addEventListener('keydown', modal._keyHandler);
|
|
}
|
|
|
|
function closeCutListModal() {
|
|
const modal = document.getElementById('cut-list-modal');
|
|
if (!modal) return;
|
|
document.removeEventListener('keydown', modal._keyHandler);
|
|
modal.remove();
|
|
}
|
|
|
|
function copyCutList() {
|
|
const modal = document.getElementById('cut-list-modal');
|
|
if (!modal || !modal._cutData) return;
|
|
|
|
const text = modal._cutData.map(r => `${r.name}\t${r.qty}`).join('\n');
|
|
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
const btn = document.getElementById('copy-cut-list-btn');
|
|
if (btn) {
|
|
btn.innerHTML = `${icons.check} Copied!`;
|
|
btn.classList.remove('btn-cyan');
|
|
btn.classList.add('btn-green');
|
|
setTimeout(() => {
|
|
btn.innerHTML = `${icons.clipboard} Copy`;
|
|
btn.classList.remove('btn-green');
|
|
btn.classList.add('btn-cyan');
|
|
}, 2000);
|
|
}
|
|
});
|
|
}
|
|
|
|
function showToast(msg) {
|
|
const t = document.createElement('div');
|
|
t.className = 'toast';
|
|
t.textContent = msg;
|
|
document.body.appendChild(t);
|
|
setTimeout(() => t.remove(), 2500);
|
|
}
|