diff --git a/FabWorks.Api/Controllers/ExportsController.cs b/FabWorks.Api/Controllers/ExportsController.cs index e1d9a48..81cb8ea 100644 --- a/FabWorks.Api/Controllers/ExportsController.cs +++ b/FabWorks.Api/Controllers/ExportsController.cs @@ -246,6 +246,22 @@ namespace FabWorks.Api.Controllers }; } + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var record = await _db.ExportRecords + .Include(r => r.BomItems).ThenInclude(b => b.CutTemplate) + .Include(r => r.BomItems).ThenInclude(b => b.FormProgram) + .FirstOrDefaultAsync(r => r.Id == id); + + if (record == null) return NotFound(); + + _db.ExportRecords.Remove(record); + await _db.SaveChangesAsync(); + + return NoContent(); + } + [HttpGet("{id}/download-dxfs")] public async Task DownloadAllDxfs(int id) { diff --git a/FabWorks.Api/Program.cs b/FabWorks.Api/Program.cs index 8121da5..42e6c76 100644 --- a/FabWorks.Api/Program.cs +++ b/FabWorks.Api/Program.cs @@ -17,6 +17,10 @@ builder.Services.AddScoped(); var app = builder.Build(); app.UseDefaultFiles(); -app.UseStaticFiles(); +app.UseStaticFiles(new StaticFileOptions +{ + OnPrepareResponse = ctx => + ctx.Context.Response.Headers.Append("Cache-Control", "no-cache, no-store") +}); app.MapControllers(); app.Run(); diff --git a/FabWorks.Api/wwwroot/css/styles.css b/FabWorks.Api/wwwroot/css/styles.css index 8586003..eadff8d 100644 --- a/FabWorks.Api/wwwroot/css/styles.css +++ b/FabWorks.Api/wwwroot/css/styles.css @@ -384,6 +384,18 @@ tbody tr:last-child td { border-bottom: none; } color: var(--amber); } +.btn-red { + background: rgba(217, 45, 32, 0.08); + color: var(--red); + border-color: rgba(217, 45, 32, 0.25); +} + +.btn-red:hover { + background: rgba(217, 45, 32, 0.15); + border-color: rgba(217, 45, 32, 0.4); + color: var(--red); +} + .btn-sm { padding: 4px 10px; font-size: 12px; } /* ─── Search ─── */ diff --git a/FabWorks.Api/wwwroot/index.html b/FabWorks.Api/wwwroot/index.html index 0d6c7e3..4b1b4c4 100644 --- a/FabWorks.Api/wwwroot/index.html +++ b/FabWorks.Api/wwwroot/index.html @@ -46,11 +46,11 @@
- - - - - + + + + + diff --git a/FabWorks.Api/wwwroot/js/helpers.js b/FabWorks.Api/wwwroot/js/helpers.js index c0792e4..787225f 100644 --- a/FabWorks.Api/wwwroot/js/helpers.js +++ b/FabWorks.Api/wwwroot/js/helpers.js @@ -32,5 +32,19 @@ const api = { const r = await fetch(url); if (!r.ok) throw new Error(`${r.status} ${r.statusText}`); return r.json(); + }, + async del(url) { + const r = await fetch(url, { method: 'DELETE' }); + if (!r.ok) throw new Error(`${r.status} ${r.statusText}`); } }; + +async function deleteExport(id) { + if (!confirm('Delete this export record? This cannot be undone.')) return; + try { + await api.del(`/api/exports/${id}`); + router.dispatch(); + } catch (err) { + alert('Failed to delete: ' + err.message); + } +} diff --git a/FabWorks.Api/wwwroot/js/icons.js b/FabWorks.Api/wwwroot/js/icons.js index c81c93e..5af4ad1 100644 --- a/FabWorks.Api/wwwroot/js/icons.js +++ b/FabWorks.Api/wwwroot/js/icons.js @@ -9,6 +9,7 @@ const icons = { chevron: ``, laser: ``, bend: ``, + trash: ``, }; function fileIcon(name) { diff --git a/FabWorks.Api/wwwroot/js/pages.js b/FabWorks.Api/wwwroot/js/pages.js index c08d863..c89b136 100644 --- a/FabWorks.Api/wwwroot/js/pages.js +++ b/FabWorks.Api/wwwroot/js/pages.js @@ -29,87 +29,34 @@ const pages = { 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; - }); + setPage('Exports', `${data.items.length} exports`); - // 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(''); + 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 = ` -
-
Drawings
${uniqueDrawings}
-
Equipment
${uniqueEquip}
-
- ${groupsHtml}`; +
+ + + + + + + + + + + ${rows} +
#DrawingTitleItemsExported ByDate
+
`; } catch (err) { content.innerHTML = `
Error: ${esc(err.message)}
`; } @@ -171,6 +118,7 @@ const pages = { ${exp.pdfContentHash ? `${icons.download} PDF` : ''} ${dxfCount > 0 ? `${icons.download} All DXFs` : ''} + ${exp.bomItems?.length ? ` @@ -193,34 +141,115 @@ const pages = { } }, - async drawings() { + async drawings(params) { const actions = document.getElementById('topbar-actions'); const content = document.getElementById('page-content'); setPage('Drawings'); - actions.innerHTML = ''; + + const searchVal = (params && params.q) || ''; + actions.innerHTML = ` + `; + 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 numbers = await api.get('/api/exports/drawing-numbers'); - if (numbers.length === 0) { + 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; } - numbers.sort(); - setPage('Drawings', `${numbers.length} drawings`); + // 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; + }); - const cards = numbers.map((d, i) => ` -
-
${esc(d)}
-
Drawing
-
`).join(''); + // 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 ` +
+
+ ${icons.chevron} + ${esc(equip)} +
+ ${items.length} drawings + ${totalBom} items +
+
+
+ + + + + + + + + ${rows} +
DrawingTitleItemsExported ByLatest Export
+
+
`; + }).join(''); content.innerHTML = `
-
Total Drawings
${numbers.length}
+
Drawings
${uniqueDrawings}
+
Equipment
${uniqueEquip}
-
${cards}
`; + ${groupsHtml}`; } catch (err) { content.innerHTML = `
Error: ${esc(err.message)}
`; } diff --git a/FabWorks.Api/wwwroot/js/router.js b/FabWorks.Api/wwwroot/js/router.js index 5b5a311..66fdfc1 100644 --- a/FabWorks.Api/wwwroot/js/router.js +++ b/FabWorks.Api/wwwroot/js/router.js @@ -26,7 +26,7 @@ const router = { switch(page) { case 'exports': pages.exports(params); break; case 'export-detail': pages.exportDetail(id); break; - case 'drawings': pages.drawings(); break; + case 'drawings': pages.drawings(params); break; case 'drawing-detail': pages.drawingDetail(id, params); break; case 'files': pages.files(params); break; default: pages.exports(params);