const pages = {
async exports(params) {
const actions = document.getElementById('topbar-actions');
const content = document.getElementById('page-content');
setPage('Exports');
const searchVal = params.q || '';
actions.innerHTML = `
${icons.search}
`;
content.innerHTML = `Loading exports
`;
const searchInput = document.getElementById('export-search');
let debounce;
searchInput.addEventListener('input', () => {
clearTimeout(debounce);
debounce = setTimeout(() => router.go('exports', { q: searchInput.value }), 400);
});
try {
const searchQ = searchVal ? `&search=${encodeURIComponent(searchVal)}` : '';
const data = await api.get(`/api/exports?take=500${searchQ}`);
if (data.items.length === 0) {
content.innerHTML = `No exports found.
`;
return;
}
setPage('Exports', `${data.items.length} exports`);
const rows = data.items.map((e, i) => `
| ${e.id} |
${esc(e.drawingNumber) || '\u2014'} |
${esc(e.title) || ''} |
${e.bomItemCount} |
${esc(e.exportedBy)} |
${fmtDate(e.exportedAt)} |
|
`).join('');
content.innerHTML = `
| # |
Drawing |
Title |
Items |
Exported By |
Date |
|
${rows}
`;
} catch (err) {
content.innerHTML = `Error: ${esc(err.message)}
`;
}
},
async drawings(params) {
const actions = document.getElementById('topbar-actions');
const content = document.getElementById('page-content');
setPage('Drawings');
const searchVal = (params && params.q) || '';
actions.innerHTML = `
${icons.search}
`;
content.innerHTML = `Loading drawings
`;
const searchInput = document.getElementById('drawing-search');
let debounce;
searchInput.addEventListener('input', () => {
clearTimeout(debounce);
debounce = setTimeout(() => router.go('drawings', { q: searchInput.value }), 400);
});
try {
const searchQ = searchVal ? `&search=${encodeURIComponent(searchVal)}` : '';
const data = await api.get(`/api/exports?take=500${searchQ}`);
if (data.items.length === 0) {
content.innerHTML = `No drawings found.
`;
return;
}
// Deduplicate: keep only the latest export per drawing number
const seen = new Set();
const unique = data.items.filter(e => {
const dn = e.drawingNumber || '';
if (seen.has(dn)) return false;
seen.add(dn);
return true;
});
// Group by equipment number (first token of drawing number)
const groups = new Map();
unique.forEach(e => {
const dn = e.drawingNumber || '';
const spaceIdx = dn.indexOf(' ');
const equip = spaceIdx > 0 ? dn.substring(0, spaceIdx) : (dn || 'Other');
if (!groups.has(equip)) groups.set(equip, []);
groups.get(equip).push(e);
});
// Sort equipment groups by number descending (most recent equipment first)
const sortedGroups = [...groups.entries()].sort((a, b) => {
const numA = parseInt(a[0]) || 0;
const numB = parseInt(b[0]) || 0;
return numB - numA;
});
const uniqueEquip = sortedGroups.length;
const uniqueDrawings = unique.length;
setPage('Drawings', `${uniqueDrawings} drawings / ${uniqueEquip} equipment`);
const groupsHtml = sortedGroups.map(([equip, items], gi) => {
const totalBom = items.reduce((s, e) => s + e.bomItemCount, 0);
const rows = items.map((e, i) => {
const dn = e.drawingNumber || '';
const spaceIdx = dn.indexOf(' ');
const drawingPart = spaceIdx > 0 ? dn.substring(spaceIdx + 1) : dn;
return `
| ${esc(drawingPart) || '\u2014'} |
${esc(e.title) || ''} |
${e.bomItemCount} |
${esc(e.exportedBy)} |
${fmtDate(e.exportedAt)} |
`;
}).join('');
return `
| Drawing |
Title |
Items |
Exported By |
Latest Export |
${rows}
`;
}).join('');
content.innerHTML = `
Drawings
${uniqueDrawings}
${groupsHtml}`;
} catch (err) {
content.innerHTML = `Error: ${esc(err.message)}
`;
}
},
async drawingDetail(drawingEncoded, params) {
const drawingNumber = decodeURIComponent(drawingEncoded);
const exportId = params?.eid ? parseInt(params.eid) : null;
const actions = document.getElementById('topbar-actions');
const content = document.getElementById('page-content');
setPage(drawingNumber, 'drawing');
actions.innerHTML = '';
content.innerHTML = `Loading drawing
`;
try {
const exports = await api.get(`/api/exports/by-drawing?drawingNumber=${encodeURIComponent(drawingNumber)}`);
if (exports.length === 0) {
content.innerHTML = `
${icons.back} Back to drawings
No exports found for this drawing.
`;
return;
}
let allBom;
const singleExport = exportId ? exports.find(e => e.id === exportId) : null;
if (singleExport) {
// Viewing a specific export - show only its BOM items
allBom = (singleExport.bomItems || []).map(b => ({ ...b, exportId: singleExport.id, exportedAt: singleExport.exportedAt }));
} else {
// Viewing drawing overview - deduplicate by itemNo, keeping latest revision (exports are newest-first)
const bomByItem = new Map();
exports.forEach(exp => {
(exp.bomItems || []).forEach(b => {
if (!bomByItem.has(b.itemNo)) {
bomByItem.set(b.itemNo, { ...b, exportId: exp.id, exportedAt: exp.exportedAt });
}
});
});
allBom = [...bomByItem.values()];
}
const bomRows = allBom.map((b, i) => {
const hasDetails = b.cutTemplate || b.formProgram;
const toggleId = `dbom-${b.id}`;
return `
| ${hasDetails ? `${icons.chevron}` : ''} |
${esc(b.itemNo)} |
${esc(b.partName)} |
${b.cutTemplate?.revision ?? ''} |
${esc(b.description)} |
${esc(b.material)} |
${b.qty ?? ''} |
${b.totalQty ?? ''} |
${b.cutTemplate ? `${icons.laser} DXF` : ''}
${b.formProgram ? `${icons.bend} Form` : ''}
|
${hasDetails ? `| ${renderBomDetails(b)} |
` : ''}`;
}).join('');
const backLink = singleExport
? `${icons.back} Back to exports`
: `${icons.back} Back to drawings`;
const statsHtml = singleExport
? `
Exported By
${esc(singleExport.exportedBy)}
BOM Items
${allBom.length}
Exported
${fmtDate(singleExport.exportedAt)}
`
: `
BOM Items
${allBom.length}
Latest Export
${fmtDate(exports[0].exportedAt)}
`;
const bomHeader = singleExport ? 'BOM Items' : 'All BOM Items';
const activeExport = singleExport || exports[0];
const dxfCount = allBom.filter(b => b.cutTemplate?.contentHash).length;
const pdfHash = activeExport.pdfContentHash;
const pdfName = encodeURIComponent((drawingNumber || 'drawing') + '.pdf');
content.innerHTML = `
${backLink}
${statsHtml}
${allBom.length ? `
|
Item |
Part Name |
Rev |
Description |
Material |
Qty |
Total |
Data |
${bomRows}
` : '
No BOM items.
'}
`;
} catch (err) {
content.innerHTML = `Error: ${esc(err.message)}
`;
}
},
async files(params) {
const actions = document.getElementById('topbar-actions');
const content = document.getElementById('page-content');
setPage('Files');
const searchVal = params.q || '';
actions.innerHTML = `
`;
content.innerHTML = `Loading files
`;
const searchInput = document.getElementById('file-search');
const typeFilter = document.getElementById('file-type-filter');
let debounce;
const refresh = () => {
clearTimeout(debounce);
debounce = setTimeout(() => router.go('files', { q: searchInput.value + (typeFilter.value ? '&type=' + typeFilter.value : '') }), 400);
};
searchInput.addEventListener('input', refresh);
typeFilter.addEventListener('change', refresh);
// Parse search and type from combined param
let searchQ = searchVal;
let typeQ = '';
if (searchVal.includes('&type=')) {
const parts = searchVal.split('&type=');
searchQ = parts[0];
typeQ = parts[1] || '';
searchInput.value = searchQ;
typeFilter.value = typeQ;
}
try {
let url = '/api/filebrowser/files?';
if (searchQ) url += `search=${encodeURIComponent(searchQ)}&`;
if (typeQ) url += `type=${encodeURIComponent(typeQ)}&`;
const data = await api.get(url);
setPage('Files', `${data.total} files`);
if (data.files.length === 0) {
content.innerHTML = `No files found.
`;
return;
}
const rows = data.files.map((f, i) => {
const ext = f.fileType || f.fileName.split('.').pop().toLowerCase();
const hashShort = f.contentHash ? f.contentHash.substring(0, 12) : '';
return `
|
${ext.toUpperCase()} |
${esc(f.drawingNumber)} |
${f.thickness != null ? f.thickness.toFixed(4) + '"' : '\u2014'} |
${fmtDate(f.createdAt)} |
${esc(hashShort)} |
${icons.download}
|
`;
}).join('');
content.innerHTML = `
| Name |
Type |
Drawing |
Thickness |
Date |
Hash |
Actions |
${rows}
`;
} catch (err) {
content.innerHTML = `Error: ${esc(err.message)}
`;
}
}
};