diff --git a/FabWorks.Api/wwwroot/css/styles.css b/FabWorks.Api/wwwroot/css/styles.css new file mode 100644 index 0000000..9dd9643 --- /dev/null +++ b/FabWorks.Api/wwwroot/css/styles.css @@ -0,0 +1,737 @@ +:root { + --bg-deep: #0a0e14; + --bg: #0d1117; + --surface: #151b23; + --surface-raised: #1c2128; + --border: #2a313a; + --border-subtle: #21262d; + --text: #e6edf3; + --text-secondary: #7d8590; + --text-dim: #484f58; + --cyan: #00d4ff; + --cyan-dim: rgba(0, 212, 255, 0.15); + --cyan-glow: rgba(0, 212, 255, 0.3); + --amber: #f0883e; + --amber-dim: rgba(240, 136, 62, 0.15); + --green: #3fb950; + --green-dim: rgba(63, 185, 80, 0.12); + --red: #f85149; + --sidebar-w: 64px; + --font-display: 'Outfit', sans-serif; + --font-body: 'IBM Plex Sans', sans-serif; + --font-mono: 'IBM Plex Mono', monospace; +} + +* { box-sizing: border-box; margin: 0; padding: 0; } + +body { + font-family: var(--font-body); + background: var(--bg); + color: var(--text); + display: flex; + min-height: 100vh; + overflow-x: hidden; +} + +/* Blueprint grid background */ +body::before { + content: ''; + position: fixed; + inset: 0; + background-image: + linear-gradient(rgba(0, 212, 255, 0.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(0, 212, 255, 0.03) 1px, transparent 1px); + background-size: 48px 48px; + pointer-events: none; + z-index: 0; +} + +/* ─── Sidebar ─── */ +.sidebar { + width: var(--sidebar-w); + background: var(--bg-deep); + border-right: 1px solid var(--border-subtle); + display: flex; + flex-direction: column; + align-items: center; + position: fixed; + top: 0; left: 0; bottom: 0; + z-index: 50; + padding-top: 8px; +} + +.sidebar-brand { + width: 40px; height: 40px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 24px; + position: relative; +} + +.sidebar-brand::after { + content: ''; + position: absolute; + bottom: -12px; + left: 8px; right: 8px; + height: 1px; + background: var(--border); +} + +.sidebar-brand svg { + width: 26px; height: 26px; + color: var(--cyan); + filter: drop-shadow(0 0 6px rgba(0, 212, 255, 0.4)); +} + +.sidebar-nav { + display: flex; + flex-direction: column; + gap: 4px; + padding-top: 16px; + width: 100%; +} + +.nav-item { + display: flex; + align-items: center; + justify-content: center; + width: 44px; height: 44px; + margin: 0 auto; + color: var(--text-dim); + text-decoration: none; + cursor: pointer; + border-radius: 8px; + transition: all 0.2s; + position: relative; +} + +.nav-item:hover { + color: var(--text-secondary); + background: var(--surface); +} + +.nav-item.active { + color: var(--cyan); + background: var(--cyan-dim); +} + +.nav-item.active::before { + content: ''; + position: absolute; + left: -10px; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 20px; + background: var(--cyan); + border-radius: 0 2px 2px 0; + box-shadow: 0 0 8px var(--cyan-glow); +} + +.nav-item svg { width: 20px; height: 20px; } + +.nav-tooltip { + position: absolute; + left: calc(100% + 12px); + top: 50%; + transform: translateY(-50%); + background: var(--surface-raised); + border: 1px solid var(--border); + color: var(--text); + padding: 4px 10px; + border-radius: 4px; + font-size: 12px; + font-family: var(--font-body); + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity 0.15s; + z-index: 100; +} + +.nav-item:hover .nav-tooltip { opacity: 1; } + +/* ─── Main ─── */ +.main { + margin-left: var(--sidebar-w); + flex: 1; + display: flex; + flex-direction: column; + min-height: 100vh; + position: relative; + z-index: 1; +} + +.topbar { + background: rgba(13, 17, 23, 0.85); + backdrop-filter: blur(12px); + border-bottom: 1px solid var(--border-subtle); + padding: 0 32px; + height: 56px; + display: flex; + align-items: center; + justify-content: space-between; + position: sticky; + top: 0; + z-index: 40; +} + +.topbar-left { + display: flex; + align-items: center; + gap: 12px; +} + +.topbar h2 { + font-family: var(--font-display); + font-size: 16px; + font-weight: 600; + letter-spacing: -0.01em; +} + +.topbar-tag { + font-family: var(--font-mono); + font-size: 10px; + color: var(--cyan); + background: var(--cyan-dim); + padding: 2px 8px; + border-radius: 3px; + letter-spacing: 0.5px; + text-transform: uppercase; +} + +.page-content { + padding: 28px 32px; + flex: 1; +} + +/* ─── Animations ─── */ +@keyframes fadeSlideIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes pulseGlow { + 0%, 100% { box-shadow: 0 0 4px var(--cyan-glow); } + 50% { box-shadow: 0 0 12px var(--cyan-glow); } +} + +.animate-in { + animation: fadeSlideIn 0.3s ease forwards; + opacity: 0; +} + +.animate-in:nth-child(1) { animation-delay: 0.04s; } +.animate-in:nth-child(2) { animation-delay: 0.08s; } +.animate-in:nth-child(3) { animation-delay: 0.12s; } +.animate-in:nth-child(4) { animation-delay: 0.16s; } + +/* ─── Cards ─── */ +.card { + background: var(--surface); + border: 1px solid var(--border-subtle); + border-radius: 6px; + overflow: hidden; +} + +.card-header { + padding: 14px 18px; + border-bottom: 1px solid var(--border-subtle); + font-family: var(--font-display); + font-weight: 600; + font-size: 13px; + letter-spacing: 0.02em; + display: flex; + align-items: center; + justify-content: space-between; + text-transform: uppercase; + color: var(--text-secondary); +} + +.card-body { padding: 18px; } + +/* ─── Stats ─── */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 12px; + margin-bottom: 24px; +} + +.stat-card { + background: var(--surface); + border: 1px solid var(--border-subtle); + border-radius: 6px; + padding: 18px 20px; + position: relative; + overflow: hidden; +} + +.stat-card::before { + content: ''; + position: absolute; + top: 0; left: 0; + width: 100%; height: 2px; + background: linear-gradient(90deg, var(--cyan), transparent); + opacity: 0.6; +} + +.stat-label { + font-family: var(--font-mono); + font-size: 10px; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 1.5px; +} + +.stat-value { + font-family: var(--font-display); + font-size: 32px; + font-weight: 700; + margin-top: 4px; + color: var(--text); + letter-spacing: -0.02em; +} + +.stat-value.stat-sm { + font-size: 14px; + font-weight: 500; + font-family: var(--font-mono); +} + +/* ─── Tables ─── */ +table { width: 100%; border-collapse: collapse; } + +th { + text-align: left; + padding: 10px 16px; + background: var(--bg); + border-bottom: 1px solid var(--border); + font-family: var(--font-mono); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 1.2px; + color: var(--text-dim); + font-weight: 500; + white-space: nowrap; +} + +td { + padding: 11px 16px; + border-bottom: 1px solid var(--border-subtle); + font-size: 13px; +} + +tbody tr { transition: background 0.1s; } +tbody tr:hover td { background: rgba(0, 212, 255, 0.03); } +tbody tr:last-child td { border-bottom: none; } + +/* ─── Badges ─── */ +.badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 3px; + font-family: var(--font-mono); + font-size: 10px; + font-weight: 500; + letter-spacing: 0.5px; + text-transform: uppercase; +} + +.badge svg { width: 12px; height: 12px; flex-shrink: 0; } +.badge-cyan { background: var(--cyan-dim); color: var(--cyan); } +.badge-amber { background: var(--amber-dim); color: var(--amber); } +.badge-green { background: var(--green-dim); color: var(--green); } +.badge-count { + background: var(--surface-raised); + color: var(--text-secondary); + border: 1px solid var(--border); +} + +/* ─── Buttons ─── */ +.btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border-radius: 4px; + font-family: var(--font-body); + font-size: 12px; + font-weight: 500; + cursor: pointer; + text-decoration: none; + border: 1px solid var(--border); + background: var(--surface-raised); + color: var(--text-secondary); + transition: all 0.15s; + white-space: nowrap; +} + +.btn:hover { + background: var(--surface); + color: var(--text); + border-color: var(--text-dim); +} + +.btn svg { width: 13px; height: 13px; } + +.btn-cyan { + background: var(--cyan-dim); + color: var(--cyan); + border-color: rgba(0, 212, 255, 0.2); +} + +.btn-cyan:hover { + background: rgba(0, 212, 255, 0.2); + border-color: rgba(0, 212, 255, 0.4); + color: var(--cyan); +} + +.btn-sm { padding: 3px 8px; font-size: 11px; } + +/* ─── Search ─── */ +.search-box { + display: flex; + align-items: center; + gap: 8px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 4px; + padding: 0 12px; + height: 34px; + width: 300px; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.search-box:focus-within { + border-color: var(--cyan); + box-shadow: 0 0 0 1px var(--cyan-dim); +} + +.search-box svg { + width: 14px; height: 14px; + color: var(--text-dim); + flex-shrink: 0; +} + +.search-box input { + border: none; + outline: none; + font-family: var(--font-body); + font-size: 13px; + width: 100%; + background: transparent; + color: var(--text); +} + +.search-box input::placeholder { color: var(--text-dim); } + +/* ─── Clickable ─── */ +.clickable { cursor: pointer; } + +/* ─── Detail sections ─── */ +.detail-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 20px; +} + +.detail-field label { + display: block; + font-family: var(--font-mono); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 1.2px; + color: var(--text-dim); + margin-bottom: 4px; +} + +.detail-field .value { + font-size: 14px; + font-weight: 500; + word-break: break-all; +} + +.detail-field .value.mono { + font-family: var(--font-mono); + font-size: 12px; + color: var(--text-secondary); +} + +/* ─── Back link ─── */ +.back-link { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--text-dim); + text-decoration: none; + font-size: 12px; + cursor: pointer; + margin-bottom: 20px; + font-family: var(--font-mono); + text-transform: uppercase; + letter-spacing: 0.5px; + transition: color 0.15s; +} + +.back-link:hover { color: var(--cyan); } +.back-link svg { width: 14px; height: 14px; } + +/* ─── BOM Expansion ─── */ +.bom-expand-row td { + padding: 0 !important; + background: var(--bg) !important; +} + +.bom-expand-content { + padding: 16px 16px 16px 48px; + border-left: 2px solid var(--cyan-dim); + margin-left: 16px; +} + +.bom-expand-content .info-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 6px 24px; +} + +.bom-expand-content .info-item { + font-size: 12px; + padding: 2px 0; +} + +.bom-expand-content .info-item .lbl { + color: var(--text-dim); + font-family: var(--font-mono); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-right: 6px; +} + +.bom-expand-content .info-item .val { + font-family: var(--font-mono); + color: var(--text); +} + +.bom-section-title { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1.5px; + color: var(--cyan); + margin: 14px 0 8px; + display: flex; + align-items: center; + gap: 8px; +} + +.bom-section-title svg { width: 14px; height: 14px; flex-shrink: 0; } +.bom-section-title:first-child { margin-top: 0; } + +.bom-section-title::after { + content: ''; + flex: 1; + height: 1px; + background: var(--border-subtle); +} + +/* ─── File Browser ─── */ +.breadcrumb { + display: flex; + align-items: center; + gap: 6px; + font-family: var(--font-mono); + font-size: 12px; + margin-bottom: 16px; + flex-wrap: wrap; + padding: 8px 14px; + background: var(--surface); + border: 1px solid var(--border-subtle); + border-radius: 4px; +} + +.breadcrumb a { + color: var(--cyan); + text-decoration: none; + cursor: pointer; + transition: opacity 0.15s; +} + +.breadcrumb a:hover { opacity: 0.7; } +.breadcrumb .sep { color: var(--text-dim); font-size: 10px; } +.breadcrumb .current { color: var(--text); font-weight: 500; } + +.file-name-cell { + display: flex; + align-items: center; + gap: 10px; +} + +.file-name-cell svg { width: 18px; height: 18px; flex-shrink: 0; } + +.file-name-cell a { + color: var(--text); + text-decoration: none; + cursor: pointer; + transition: color 0.15s; +} + +.file-name-cell a:hover { color: var(--cyan); } + +/* ─── Loading / Empty ─── */ +.loading, .empty { + text-align: center; + padding: 60px 24px; + color: var(--text-dim); + font-size: 13px; + font-family: var(--font-mono); +} + +.loading::before { + content: ''; + display: block; + width: 24px; + height: 24px; + border: 2px solid var(--border); + border-top-color: var(--cyan); + border-radius: 50%; + animation: spin 0.8s linear infinite; + margin: 0 auto 12px; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* ─── Chevron toggle ─── */ +.chevron-toggle { + display: inline-flex; + width: 18px; height: 18px; + align-items: center; + justify-content: center; + transition: transform 0.2s; + color: var(--text-dim); +} + +.chevron-toggle.open { + transform: rotate(90deg); + color: var(--cyan); +} + +/* ─── Drawing cards grid ─── */ +.drawings-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 12px; +} + +.drawing-card { + background: var(--surface); + border: 1px solid var(--border-subtle); + border-radius: 6px; + padding: 18px 20px; + cursor: pointer; + transition: all 0.2s; + position: relative; +} + +.drawing-card:hover { + border-color: var(--cyan); + background: rgba(0, 212, 255, 0.03); + box-shadow: 0 0 0 1px var(--cyan-dim); +} + +.drawing-card-title { + font-family: var(--font-display); + font-size: 15px; + font-weight: 600; + margin-bottom: 2px; +} + +.drawing-card-sub { + font-family: var(--font-mono); + font-size: 10px; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 1px; +} + +/* ─── Equipment Groups ─── */ +.equip-group { + margin-bottom: 16px; +} + +.equip-group:last-child { margin-bottom: 0; } + +.equip-header { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 16px; + background: var(--surface); + border: 1px solid var(--border-subtle); + border-radius: 6px 6px 0 0; + cursor: pointer; + transition: all 0.15s; + user-select: none; +} + +.equip-header:hover { background: rgba(0, 212, 255, 0.03); } + +.equip-header .chevron-toggle { flex-shrink: 0; } + +.equip-header-title { + font-family: var(--font-display); + font-size: 15px; + font-weight: 600; +} + +.equip-header-number { + font-family: var(--font-mono); + font-size: 13px; + color: var(--cyan); + font-weight: 600; +} + +.equip-header-meta { + margin-left: auto; + display: flex; + align-items: center; + gap: 12px; +} + +.equip-header-stat { + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-dim); +} + +.equip-header-stat strong { + color: var(--text-secondary); +} + +.equip-body { + border: 1px solid var(--border-subtle); + border-top: none; + border-radius: 0 0 6px 6px; + overflow: hidden; +} + +.equip-body table { margin: 0; } + +.equip-group.collapsed .equip-body { display: none; } +.equip-group.collapsed .equip-header { border-radius: 6px; } + +/* ─── Responsive ─── */ +@media (max-width: 768px) { + .sidebar { display: none; } + .main { margin-left: 0; } + .search-box { width: 100%; } + .topbar { padding: 0 16px; } + .page-content { padding: 16px; } +} diff --git a/FabWorks.Api/wwwroot/index.html b/FabWorks.Api/wwwroot/index.html new file mode 100644 index 0000000..0d6c7e3 --- /dev/null +++ b/FabWorks.Api/wwwroot/index.html @@ -0,0 +1,56 @@ + + + + + + FabWorks + + + + + + + + +
+
+
+

Exports

+ +
+
+
+
+
+ + + + + + + + + diff --git a/FabWorks.Api/wwwroot/js/components.js b/FabWorks.Api/wwwroot/js/components.js new file mode 100644 index 0000000..de03a79 --- /dev/null +++ b/FabWorks.Api/wwwroot/js/components.js @@ -0,0 +1,60 @@ +/* ─── BOM Detail Expansion ─── */ +function renderBomDetails(b) { + let html = '
'; + + if (b.cutTemplate) { + const ct = b.cutTemplate; + const displayName = ct.dxfFilePath?.split(/[/\\]/).pop() || ''; + html += ` +
${icons.laser} Cut Template
+
+
File${esc(displayName)}
+
Thickness${fmtThickness(ct.thickness)}
+
K-Factor${ct.kFactor != null ? ct.kFactor : '\u2014'}
+
Bend Radius${ct.defaultBendRadius != null ? ct.defaultBendRadius.toFixed(4) + '"' : '\u2014'}
+
`; + + if (ct.contentHash) { + html += `
+ ${icons.download} Download DXF + ${esc(displayName)} +
`; + } + } + + if (b.formProgram) { + const fp = b.formProgram; + html += ` +
${icons.bend} Form Program
+
+
Program${esc(fp.programName)}
+
Thickness${fmtThickness(fp.thickness)}
+
Material${esc(fp.materialType)}
+
K-Factor${fp.kFactor != null ? fp.kFactor : '\u2014'}
+
Bends${fp.bendCount}
+
Upper Tools${esc(fp.upperToolNames) || '\u2014'}
+
Lower Tools${esc(fp.lowerToolNames) || '\u2014'}
+
+ ${fp.setupNotes ? `
Setup Notes${esc(fp.setupNotes)}
` : ''}`; + } + + html += '
'; + 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); +} diff --git a/FabWorks.Api/wwwroot/js/helpers.js b/FabWorks.Api/wwwroot/js/helpers.js new file mode 100644 index 0000000..c0792e4 --- /dev/null +++ b/FabWorks.Api/wwwroot/js/helpers.js @@ -0,0 +1,36 @@ +function fmtSize(b) { + if (!b) return '0 B'; + const k = 1024, s = ['B','KB','MB','GB']; + const i = Math.floor(Math.log(b) / Math.log(k)); + return parseFloat((b / Math.pow(k, i)).toFixed(1)) + ' ' + s[i]; +} + +function fmtDate(d) { + if (!d) return ''; + const dt = new Date(d); + return dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) + + ' ' + dt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); +} + +function fmtThickness(t) { + if (t == null) return '\u2014'; + return `${t.toFixed(4)}"`; +} + +function esc(s) { + return s ? s.replace(//g,'>').replace(/"/g,'"').replace(/'/g,''') : ''; +} + +function setPage(title, tag = '') { + document.getElementById('page-title').textContent = title; + document.getElementById('page-tag').textContent = tag; + document.getElementById('page-tag').style.display = tag ? '' : 'none'; +} + +const api = { + async get(url) { + const r = await fetch(url); + if (!r.ok) throw new Error(`${r.status} ${r.statusText}`); + return r.json(); + } +}; diff --git a/FabWorks.Api/wwwroot/js/icons.js b/FabWorks.Api/wwwroot/js/icons.js new file mode 100644 index 0000000..97a1bda --- /dev/null +++ b/FabWorks.Api/wwwroot/js/icons.js @@ -0,0 +1,19 @@ +const icons = { + search: ``, + folder: ``, + fileDxf: ``, + filePdf: ``, + fileGeneric: ``, + download: ``, + back: ``, + chevron: ``, + laser: ``, + bend: ``, +}; + +function fileIcon(name) { + const ext = name.split('.').pop().toLowerCase(); + if (ext === 'dxf') return icons.fileDxf; + if (ext === 'pdf') return icons.filePdf; + return icons.fileGeneric; +} diff --git a/FabWorks.Api/wwwroot/js/pages.js b/FabWorks.Api/wwwroot/js/pages.js new file mode 100644 index 0000000..f4c99d6 --- /dev/null +++ b/FabWorks.Api/wwwroot/js/pages.js @@ -0,0 +1,396 @@ +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 = ` + `; + + 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; + } + + // 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('Exports', `${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 ` + + ${e.id} + ${esc(drawingPart) || '\u2014'} + ${esc(e.title) || ''} + ${e.bomItemCount} + ${esc(e.exportedBy)} + ${fmtDate(e.exportedAt)} + `; + }).join(''); + + return ` +
+
+ ${icons.chevron} + ${esc(equip)} +
+ ${items.length} exports + ${totalBom} items +
+
+
+ + + + + + + + + + ${rows} +
#DrawingTitleItemsExported ByDate
+
+
`; + }).join(''); + + content.innerHTML = ` +
+
Drawings
${uniqueDrawings}
+
Equipment
${uniqueEquip}
+
+ ${groupsHtml}`; + } catch (err) { + content.innerHTML = `
Error: ${esc(err.message)}
`; + } + }, + + async exportDetail(id) { + const actions = document.getElementById('topbar-actions'); + const content = document.getElementById('page-content'); + setPage('Loading...'); + actions.innerHTML = ''; + content.innerHTML = `
Loading export
`; + + try { + const exp = await api.get(`/api/exports/${id}`); + setPage(exp.drawingNumber || `Export #${exp.id}`, 'export detail'); + + const dxfCount = (exp.bomItems || []).filter(b => b.cutTemplate?.contentHash).length; + + const bomRows = (exp.bomItems || []).map((b, i) => { + const hasDetails = b.cutTemplate || b.formProgram; + const toggleId = `bom-${b.id}`; + return ` + + ${hasDetails ? `${icons.chevron}` : ''} + ${esc(b.itemNo)} + ${esc(b.partName)} + ${esc(b.description)} + ${esc(b.material)} + ${b.qty ?? ''} + ${b.totalQty ?? ''} + + ${b.cutTemplate ? `${icons.laser} DXF` : ''} + ${b.formProgram ? `${icons.bend} Form` : ''} + + + ${hasDetails ? `${renderBomDetails(b)}` : ''}`; + }).join(''); + + content.innerHTML = ` + ${icons.back} Back to exports + +
+
Export Information
+
+
+
${esc(exp.drawingNumber) || '\u2014'}
+ ${exp.title ? `
${esc(exp.title)}
` : ''} +
${esc(exp.exportedBy)}
+
${fmtDate(exp.exportedAt)}
+
${esc(exp.sourceFilePath)}
+
+
+
+ +
+
+ BOM Items + ${exp.bomItems?.length || 0} items + ${dxfCount > 0 ? `${icons.download} Download All DXFs` : ''} +
+ ${exp.bomItems?.length ? ` + + + + + + + + + + + + ${bomRows} +
ItemPart NameDescriptionMaterialQtyTotalData
` : '
No BOM items for this export.
'} +
`; + } catch (err) { + content.innerHTML = `
Error: ${esc(err.message)}
`; + } + }, + + async drawings() { + const actions = document.getElementById('topbar-actions'); + const content = document.getElementById('page-content'); + setPage('Drawings'); + actions.innerHTML = ''; + content.innerHTML = `
Loading drawings
`; + + try { + const numbers = await api.get('/api/exports/drawing-numbers'); + if (numbers.length === 0) { + content.innerHTML = `
No drawings found.
`; + return; + } + + numbers.sort(); + setPage('Drawings', `${numbers.length} drawings`); + + const cards = numbers.map((d, i) => ` +
+
${esc(d)}
+
Drawing
+
`).join(''); + + content.innerHTML = ` +
+
Total Drawings
${numbers.length}
+
+
${cards}
`; + } catch (err) { + content.innerHTML = `
Error: ${esc(err.message)}
`; + } + }, + + async drawingDetail(drawingEncoded) { + const drawingNumber = decodeURIComponent(drawingEncoded); + 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; + } + + const allBom = []; + exports.forEach(exp => { + (exp.bomItems || []).forEach(b => { + allBom.push({ ...b, exportId: exp.id, exportedAt: exp.exportedAt }); + }); + }); + + 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)} + ${esc(b.description)} + ${esc(b.material)} + ${b.qty ?? ''} + ${b.totalQty ?? ''} + + ${b.cutTemplate ? `${icons.laser} DXF` : ''} + ${b.formProgram ? `${icons.bend} Form` : ''} + + + ${hasDetails ? `${renderBomDetails(b)}` : ''}`; + }).join(''); + + content.innerHTML = ` + ${icons.back} Back to drawings + +
+
Exports
${exports.length}
+
BOM Items
${allBom.length}
+
Latest Export
${fmtDate(exports[0].exportedAt)}
+
+ +
+
+ All BOM Items + ${allBom.length} items +
+ ${allBom.length ? ` + + + + + + + + + + + + ${bomRows} +
ItemPart NameDescriptionMaterialQtyTotalData
` : '
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 === 'pdf' ? icons.filePdf : icons.fileDxf}${esc(f.fileName)}
+ ${ext.toUpperCase()} + ${esc(f.drawingNumber)} + ${f.thickness != null ? f.thickness.toFixed(4) + '"' : '\u2014'} + ${fmtDate(f.createdAt)} + ${esc(hashShort)} + + ${icons.download} + + `; + }).join(''); + + content.innerHTML = ` +
+ + + + + + + + + + + ${rows} +
NameTypeDrawingThicknessDateHashActions
+
`; + } catch (err) { + content.innerHTML = `
Error: ${esc(err.message)}
`; + } + } +}; diff --git a/FabWorks.Api/wwwroot/js/router.js b/FabWorks.Api/wwwroot/js/router.js new file mode 100644 index 0000000..5b5a311 --- /dev/null +++ b/FabWorks.Api/wwwroot/js/router.js @@ -0,0 +1,35 @@ +const router = { + go(page, params = {}) { + const hash = page + (params.id ? '/' + params.id : '') + (params.q ? '?q=' + encodeURIComponent(params.q) : ''); + location.hash = hash; + }, + parse() { + const h = location.hash.slice(1) || 'exports'; + const [path, qs] = h.split('?'); + const parts = path.split('/'); + const params = {}; + if (qs) qs.split('&').forEach(p => { const [k,v] = p.split('='); params[k] = decodeURIComponent(v); }); + return { page: parts[0], id: parts[1], params }; + }, + init() { + window.addEventListener('hashchange', () => this.dispatch()); + this.dispatch(); + }, + dispatch() { + const { page, id, params } = this.parse(); + document.querySelectorAll('.nav-item').forEach(el => { + el.classList.toggle('active', + el.dataset.page === page || + (page === 'export-detail' && el.dataset.page === 'exports') || + (page === 'drawing-detail' && el.dataset.page === 'drawings')); + }); + switch(page) { + case 'exports': pages.exports(params); break; + case 'export-detail': pages.exportDetail(id); break; + case 'drawings': pages.drawings(); break; + case 'drawing-detail': pages.drawingDetail(id, params); break; + case 'files': pages.files(params); break; + default: pages.exports(params); + } + } +};