Compare commits
23 Commits
e072919a59
...
feature/fa
| Author | SHA1 | Date | |
|---|---|---|---|
| bd3e7c2a36 | |||
| b9e84de7c0 | |||
| f6cd91f1b5 | |||
| 3554bb6110 | |||
| 4707e96359 | |||
| c5bd7fb4c8 | |||
| 13c61a82a4 | |||
| 444a077cbc | |||
| c4920f933d | |||
| b472729fda | |||
| 5d2948d563 | |||
| 71c65e0bf5 | |||
| 53aa23f762 | |||
| 036ab2a55a | |||
| f9e7ace35d | |||
| 622cbf1170 | |||
| 4a3f33db33 | |||
| 77d0157370 | |||
| 26e9233b30 | |||
| e59584a5c0 | |||
| dcc508d479 | |||
| 1266378b51 | |||
| 5de40ebafd |
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -245,3 +245,6 @@ ModelManifest.xml
|
||||
|
||||
# Test documents
|
||||
TestDocs/
|
||||
|
||||
# Claude Code local settings
|
||||
.claude/
|
||||
|
||||
Submodule EtchBendLines updated: 89d987f6c6...da4d3228b0
@@ -1,14 +1,12 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29123.88
|
||||
# Visual Studio Version 18
|
||||
VisualStudioVersion = 18.3.11512.155 d18.3
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExportDXF", "ExportDXF\ExportDXF.csproj", "{05F21D73-FD31-4E77-8D9B-41C86D4D8305}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EtchBendLines", "EtchBendLines\EtchBendLines\EtchBendLines.csproj", "{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "netDxf", "EtchBendLines\netDxf\netDxf\netDxf.csproj", "{785380E0-CEB9-4C34-82E5-60D0E33E848E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FabWorks.Core", "FabWorks.Core\FabWorks.Core.csproj", "{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FabWorks.Tests", "FabWorks.Tests\FabWorks.Tests.csproj", "{6DD89774-D86B-47E9-B982-2794BD95616A}"
|
||||
@@ -49,18 +47,6 @@ Global
|
||||
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|x64.Build.0 = Release|Any CPU
|
||||
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|x86.Build.0 = Release|Any CPU
|
||||
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Release|x64.Build.0 = Release|Any CPU
|
||||
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Release|x86.Build.0 = Release|Any CPU
|
||||
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ExportDXF
|
||||
@@ -13,6 +14,35 @@ namespace ExportDXF
|
||||
|
||||
public string Source { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The descriptive text after the equipment/drawing number (e.g. "Prox switch bracket for drive").
|
||||
/// </summary>
|
||||
public string Description
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(Source) || string.IsNullOrEmpty(EquipmentNo))
|
||||
return null;
|
||||
|
||||
// Strip equipment number (and optional drawing number) from the source to get the description
|
||||
var prefix = string.IsNullOrEmpty(DrawingNo)
|
||||
? EquipmentNo
|
||||
: EquipmentNo + " " + DrawingNo;
|
||||
|
||||
var desc = Source;
|
||||
if (desc.StartsWith(prefix, System.StringComparison.OrdinalIgnoreCase))
|
||||
desc = desc.Substring(prefix.Length);
|
||||
|
||||
// Remove file extension (e.g. ".SLDPRT")
|
||||
var ext = Path.GetExtension(desc);
|
||||
if (!string.IsNullOrEmpty(ext))
|
||||
desc = desc.Substring(0, desc.Length - ext.Length);
|
||||
|
||||
desc = desc.Trim();
|
||||
return string.IsNullOrEmpty(desc) ? null : desc;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (string.IsNullOrEmpty(DrawingNo))
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="PDFtoImage" Version="4.1.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
14
ExportDXF/Forms/MainForm.Designer.cs
generated
14
ExportDXF/Forms/MainForm.Designer.cs
generated
@@ -114,16 +114,16 @@ namespace ExportDXF.Forms
|
||||
logEventsDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
|
||||
logEventsDataGrid.Location = new System.Drawing.Point(6, 6);
|
||||
logEventsDataGrid.Name = "logEventsDataGrid";
|
||||
logEventsDataGrid.Size = new System.Drawing.Size(890, 440);
|
||||
logEventsDataGrid.Size = new System.Drawing.Size(890, 444);
|
||||
logEventsDataGrid.TabIndex = 0;
|
||||
//
|
||||
// bomTab
|
||||
//
|
||||
bomTab.Controls.Add(bomDataGrid);
|
||||
bomTab.Location = new System.Drawing.Point(4, 28);
|
||||
bomTab.Location = new System.Drawing.Point(4, 30);
|
||||
bomTab.Name = "bomTab";
|
||||
bomTab.Padding = new System.Windows.Forms.Padding(3);
|
||||
bomTab.Size = new System.Drawing.Size(902, 409);
|
||||
bomTab.Size = new System.Drawing.Size(902, 458);
|
||||
bomTab.TabIndex = 1;
|
||||
bomTab.Text = "Bill Of Materials";
|
||||
bomTab.UseVisualStyleBackColor = true;
|
||||
@@ -135,16 +135,16 @@ namespace ExportDXF.Forms
|
||||
bomDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
|
||||
bomDataGrid.Location = new System.Drawing.Point(6, 6);
|
||||
bomDataGrid.Name = "bomDataGrid";
|
||||
bomDataGrid.Size = new System.Drawing.Size(1281, 644);
|
||||
bomDataGrid.Size = new System.Drawing.Size(890, 444);
|
||||
bomDataGrid.TabIndex = 1;
|
||||
//
|
||||
// cutTemplatesTab
|
||||
//
|
||||
cutTemplatesTab.Controls.Add(cutTemplatesDataGrid);
|
||||
cutTemplatesTab.Location = new System.Drawing.Point(4, 28);
|
||||
cutTemplatesTab.Location = new System.Drawing.Point(4, 30);
|
||||
cutTemplatesTab.Name = "cutTemplatesTab";
|
||||
cutTemplatesTab.Padding = new System.Windows.Forms.Padding(3);
|
||||
cutTemplatesTab.Size = new System.Drawing.Size(902, 409);
|
||||
cutTemplatesTab.Size = new System.Drawing.Size(902, 458);
|
||||
cutTemplatesTab.TabIndex = 2;
|
||||
cutTemplatesTab.Text = "Cut Templates";
|
||||
cutTemplatesTab.UseVisualStyleBackColor = true;
|
||||
@@ -156,7 +156,7 @@ namespace ExportDXF.Forms
|
||||
cutTemplatesDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
|
||||
cutTemplatesDataGrid.Location = new System.Drawing.Point(6, 6);
|
||||
cutTemplatesDataGrid.Name = "cutTemplatesDataGrid";
|
||||
cutTemplatesDataGrid.Size = new System.Drawing.Size(1281, 644);
|
||||
cutTemplatesDataGrid.Size = new System.Drawing.Size(890, 447);
|
||||
cutTemplatesDataGrid.TabIndex = 2;
|
||||
//
|
||||
// equipmentBox
|
||||
|
||||
@@ -487,6 +487,9 @@ namespace ExportDXF.Forms
|
||||
equipmentBox.Text = drawingInfo.EquipmentNo;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(titleBox.Text) && !string.IsNullOrEmpty(drawingInfo.Description))
|
||||
titleBox.Text = drawingInfo.Description;
|
||||
|
||||
// Load drawings for the selected equipment, then set drawing number
|
||||
await UpdateDrawingDropdownAsync();
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@ using ExportDXF.Forms;
|
||||
using ExportDXF.Services;
|
||||
using System;
|
||||
using System.Configuration;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace ExportDXF
|
||||
@@ -44,6 +47,8 @@ namespace ExportDXF
|
||||
var partExporter = new PartExporter();
|
||||
var drawingExporter = new DrawingExporter();
|
||||
|
||||
EnsureApiRunning();
|
||||
|
||||
var httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(_apiBaseUrl),
|
||||
@@ -60,5 +65,52 @@ namespace ExportDXF
|
||||
|
||||
return new MainForm(solidWorksService, exportService, apiClient);
|
||||
}
|
||||
|
||||
private void EnsureApiRunning()
|
||||
{
|
||||
// Check if API is already responding
|
||||
using (var probe = new HttpClient { Timeout = TimeSpan.FromSeconds(2) })
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = probe.GetAsync(_apiBaseUrl + "/api/exports?take=1").Result;
|
||||
if (response.IsSuccessStatusCode)
|
||||
return; // already running
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// Find the API executable relative to this assembly
|
||||
var exeDir = AppContext.BaseDirectory;
|
||||
var apiExe = Path.GetFullPath(Path.Combine(exeDir, @"..\..\..\FabWorks.Api\bin\Debug\net8.0\FabWorks.Api.exe"));
|
||||
if (!File.Exists(apiExe))
|
||||
return; // can't find it, skip
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = apiExe,
|
||||
WorkingDirectory = Path.GetDirectoryName(apiExe),
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
Process.Start(startInfo);
|
||||
|
||||
// Wait up to 10 seconds for API to become ready
|
||||
using (var probe = new HttpClient { Timeout = TimeSpan.FromSeconds(2) })
|
||||
{
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
Thread.Sleep(500);
|
||||
try
|
||||
{
|
||||
var response = probe.GetAsync(_apiBaseUrl + "/api/exports?take=1").Result;
|
||||
if (response.IsSuccessStatusCode)
|
||||
return;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +207,6 @@ namespace ExportDXF.Services
|
||||
private async Task ExportDrawingAsync(ExportContext context, string drawingNumber, string tempDir)
|
||||
{
|
||||
LogProgress(context, "Active document is a Drawing");
|
||||
LogProgress(context, "Finding BOM tables...");
|
||||
|
||||
var drawing = context.ActiveDocument.NativeDocument as DrawingDoc;
|
||||
if (drawing == null)
|
||||
@@ -216,16 +215,6 @@ namespace ExportDXF.Services
|
||||
return;
|
||||
}
|
||||
|
||||
var items = _bomExtractor.ExtractFromDrawing(drawing, context.ProgressCallback);
|
||||
|
||||
if (items == null || items.Count == 0)
|
||||
{
|
||||
LogProgress(context, "Error: Bill of materials not found.", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
LogProgress(context, $"Found {items.Count} component(s)");
|
||||
|
||||
// Export drawing to PDF in temp dir
|
||||
_drawingExporter.ExportToPdf(drawing, tempDir, context);
|
||||
|
||||
@@ -239,7 +228,7 @@ namespace ExportDXF.Services
|
||||
if (pdfs.Length > 0)
|
||||
{
|
||||
var pdfTempPath = pdfs[0];
|
||||
var pdfHash = ContentHasher.ComputeFileHash(pdfTempPath);
|
||||
var pdfHash = ContentHasher.ComputePdfContentHash(pdfTempPath);
|
||||
|
||||
var uploadResult = await _apiClient.UploadPdfAsync(
|
||||
pdfTempPath,
|
||||
@@ -248,6 +237,9 @@ namespace ExportDXF.Services
|
||||
pdfHash,
|
||||
exportRecord?.Id);
|
||||
|
||||
if (exportRecord != null)
|
||||
await _apiClient.UpdatePdfHashAsync(exportRecord.Id, pdfHash);
|
||||
|
||||
if (uploadResult != null)
|
||||
{
|
||||
if (uploadResult.WasUnchanged)
|
||||
@@ -264,8 +256,74 @@ namespace ExportDXF.Services
|
||||
LogProgress(context, $"PDF upload error: {ex.Message}", LogLevel.Error);
|
||||
}
|
||||
|
||||
// Export parts to DXF and save BOM items
|
||||
await ExportItemsAsync(items, tempDir, context, exportRecord?.Id);
|
||||
// Extract BOM items from drawing tables
|
||||
LogProgress(context, "Finding BOM tables...");
|
||||
var items = _bomExtractor.ExtractFromDrawing(drawing, context.ProgressCallback);
|
||||
|
||||
if (items != null && items.Count > 0)
|
||||
{
|
||||
LogProgress(context, $"Found {items.Count} component(s)");
|
||||
await ExportItemsAsync(items, tempDir, context, exportRecord?.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No BOM table — fall back to exporting the part referenced by the drawing views
|
||||
LogProgress(context, "No BOM table found. Checking drawing views for referenced part...");
|
||||
|
||||
var (part, configuration) = GetReferencedPartFromViews(drawing);
|
||||
if (part == null)
|
||||
{
|
||||
LogProgress(context, "No referenced part found in drawing views.", LogLevel.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
LogProgress(context, $"Found referenced part, exporting as single part...");
|
||||
|
||||
var item = _partExporter.ExportSinglePart(part, tempDir, context);
|
||||
if (item != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(configuration))
|
||||
item.Configuration = configuration;
|
||||
|
||||
var existingItemNo = await FindExistingItemNoAsync(exportRecord?.Id, item.PartName, item.Configuration);
|
||||
item.ItemNo = existingItemNo ?? await GetNextItemNumberAsync(drawingNumber);
|
||||
|
||||
var bomItem = new BomItem
|
||||
{
|
||||
ExportRecordId = exportRecord?.Id ?? 0,
|
||||
ItemNo = item.ItemNo,
|
||||
PartNo = item.FileName ?? item.PartName ?? "",
|
||||
SortOrder = 0,
|
||||
Qty = item.Quantity,
|
||||
TotalQty = item.Quantity,
|
||||
Description = item.Description ?? "",
|
||||
PartName = item.PartName ?? "",
|
||||
ConfigurationName = item.Configuration ?? "",
|
||||
Material = item.Material ?? ""
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(item.LocalTempPath))
|
||||
{
|
||||
var uploadResult = await UploadDxfAsync(item, context);
|
||||
if (uploadResult != null)
|
||||
{
|
||||
bomItem.CutTemplate = new CutTemplate
|
||||
{
|
||||
DxfFilePath = uploadResult.StoredFilePath,
|
||||
ContentHash = item.ContentHash,
|
||||
Thickness = item.Thickness > 0 ? item.Thickness : null,
|
||||
KFactor = item.KFactor > 0 ? item.KFactor : null,
|
||||
DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
context.BomItemCallback?.Invoke(bomItem);
|
||||
|
||||
if (exportRecord != null)
|
||||
await SaveBomItemAsync(exportRecord.Id, bomItem, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -600,6 +658,24 @@ namespace ExportDXF.Services
|
||||
throw new ArgumentException("ProgressCallback cannot be null.", nameof(context));
|
||||
}
|
||||
|
||||
private (PartDoc part, string configuration) GetReferencedPartFromViews(DrawingDoc drawing)
|
||||
{
|
||||
var view = (IView)drawing.GetFirstView();
|
||||
// First view is the sheet itself — skip it
|
||||
view = (IView)view.GetNextView();
|
||||
|
||||
while (view != null)
|
||||
{
|
||||
var doc = view.ReferencedDocument;
|
||||
if (doc is PartDoc part)
|
||||
return (part, view.ReferencedConfiguration);
|
||||
|
||||
view = (IView)view.GetNextView();
|
||||
}
|
||||
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
private void LogProgress(ExportContext context, string message, LogLevel level = LogLevel.Info, string file = null)
|
||||
{
|
||||
context.ProgressCallback?.Invoke(message, level, file);
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace ExportDXF.Services
|
||||
|
||||
try
|
||||
{
|
||||
var fileName = GetSinglePartFileName(model, context.FilePrefix);
|
||||
var fileName = GetSinglePartFileName(model, context.Equipment);
|
||||
var savePath = Path.Combine(saveDirectory, fileName + ".dxf");
|
||||
|
||||
// Build result item with metadata
|
||||
@@ -139,7 +139,7 @@ namespace ExportDXF.Services
|
||||
|
||||
EnrichItemWithMetadata(item, model, part);
|
||||
|
||||
var fileName = GetItemFileName(item, context.FilePrefix);
|
||||
var fileName = GetItemFileName(item, context);
|
||||
var savePath = Path.Combine(saveDirectory, fileName + ".dxf");
|
||||
|
||||
var templateDrawing = context.GetOrCreateTemplateDrawing();
|
||||
@@ -234,7 +234,7 @@ namespace ExportDXF.Services
|
||||
var drawingModel = templateDrawing as ModelDoc2;
|
||||
drawingModel.SaveAs(savePath);
|
||||
|
||||
AddEtchLines(savePath);
|
||||
AddEtchLines(savePath, context);
|
||||
|
||||
context.ProgressCallback?.Invoke($"Saved to \"{savePath}\"", LogLevel.Info, partTitle);
|
||||
|
||||
@@ -301,36 +301,64 @@ namespace ExportDXF.Services
|
||||
drawing.DeleteSelection(false);
|
||||
}
|
||||
|
||||
private void AddEtchLines(string dxfPath)
|
||||
private void AddEtchLines(string dxfPath, ExportContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
var etcher = new EtchBendLines.Etcher();
|
||||
etcher.AddEtchLines(dxfPath);
|
||||
FixDegreeSymbol(dxfPath);
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Silently fail if etch lines can't be added
|
||||
context.ProgressCallback?.Invoke($"Etch lines failed: {ex.Message}", LogLevel.Warning, Path.GetFileName(dxfPath));
|
||||
}
|
||||
}
|
||||
|
||||
private string GetSinglePartFileName(ModelDoc2 model, string prefix)
|
||||
/// <summary>
|
||||
/// Workaround for ACadSharp encoding bug (no upstream fix as of v3.4.9).
|
||||
/// ACadSharp's DxfReader uses $DWGCODEPAGE (ANSI_1252) to decode text, but
|
||||
/// AC1018+ DXF files use UTF-8. The degree symbol ° (UTF-8: C2 B0) gets
|
||||
/// misread as two ANSI_1252 characters: Â (C2) and ° (B0).
|
||||
/// See: https://github.com/DomCR/ACadSharp/issues?q=encoding
|
||||
/// </summary>
|
||||
private static void FixDegreeSymbol(string path)
|
||||
{
|
||||
var text = System.IO.File.ReadAllText(path);
|
||||
if (text.Contains("\u00C2\u00B0"))
|
||||
{
|
||||
text = text.Replace("\u00C2\u00B0", "\u00B0");
|
||||
System.IO.File.WriteAllText(path, text);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetSinglePartFileName(ModelDoc2 model, string equipment)
|
||||
{
|
||||
var title = model.GetTitle().Replace(".SLDPRT", "");
|
||||
var config = model.ConfigurationManager.ActiveConfiguration.Name;
|
||||
var isDefaultConfig = string.Equals(config, "default", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return isDefaultConfig ? title : $"{title} [{config}]";
|
||||
var name = isDefaultConfig ? title : $"{title} [{config}]";
|
||||
return string.IsNullOrWhiteSpace(equipment) ? name : $"{equipment} {name}";
|
||||
}
|
||||
|
||||
private string GetItemFileName(Item item, string prefix)
|
||||
private string GetItemFileName(Item item, ExportContext context)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(context.DrawingNo))
|
||||
{
|
||||
// No drawing number: preserve part name, prefix with EquipmentNo
|
||||
var equipment = context.Equipment;
|
||||
return string.IsNullOrWhiteSpace(equipment)
|
||||
? item.PartName
|
||||
: $"{equipment} {item.PartName}";
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(item.ItemNo))
|
||||
return item.PartName;
|
||||
|
||||
prefix = prefix?.Replace("\"", "''") ?? string.Empty;
|
||||
var prefix = context.FilePrefix?.Replace("\"", "''") ?? string.Empty;
|
||||
var num = item.ItemNo.PadLeft(2, '0');
|
||||
// Expected format: {DrawingNo} PT{ItemNo}
|
||||
// Expected format: {EquipNo} {DrawingNo} PT{ItemNo}
|
||||
return string.IsNullOrWhiteSpace(prefix)
|
||||
? $"PT{num}"
|
||||
: $"{prefix} PT{num}";
|
||||
|
||||
@@ -1,26 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using ACadSharp.Entities;
|
||||
using ACadSharp.IO;
|
||||
using PDFtoImage;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace ExportDXF.Utilities
|
||||
{
|
||||
public static class ContentHasher
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes a SHA256 hash of DXF file content, skipping the HEADER section
|
||||
/// which contains timestamps that change on every save.
|
||||
/// Computes a SHA256 hash of DXF geometry, ignoring entity ordering,
|
||||
/// handle assignments, style names, and floating-point epsilon differences
|
||||
/// that SolidWorks changes between re-exports of identical geometry.
|
||||
/// Falls back to a raw file hash if ACadSharp parsing fails.
|
||||
/// </summary>
|
||||
public static string ComputeDxfContentHash(string filePath)
|
||||
{
|
||||
var text = File.ReadAllText(filePath);
|
||||
var contentStart = FindEndOfHeader(text);
|
||||
var content = contentStart >= 0 ? text.Substring(contentStart) : text;
|
||||
|
||||
using (var sha = SHA256.Create())
|
||||
try
|
||||
{
|
||||
var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(content));
|
||||
return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
|
||||
return ComputeGeometricHash(filePath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return ComputeFileHash(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a perceptual hash of a PDF by rendering page 1 to an image,
|
||||
/// so only visual changes affect the hash (metadata/timestamp changes are ignored).
|
||||
/// Falls back to a raw file hash if rendering fails.
|
||||
/// </summary>
|
||||
public static string ComputePdfContentHash(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var pdfStream = File.OpenRead(filePath))
|
||||
using (var pngStream = new MemoryStream())
|
||||
{
|
||||
Conversion.SavePng(pngStream, pdfStream, page: 0,
|
||||
options: new RenderOptions(Dpi: 150));
|
||||
pngStream.Position = 0;
|
||||
|
||||
using (var image = Image.Load<Rgba32>(pngStream))
|
||||
{
|
||||
var hash = ComputeDifferenceHash(image);
|
||||
return hash.ToString("x16");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return ComputeFileHash(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,81 +77,126 @@ namespace ExportDXF.Utilities
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the position immediately after the HEADER section's ENDSEC marker.
|
||||
/// DXF HEADER format:
|
||||
/// 0\nSECTION\n2\nHEADER\n...variables...\n0\nENDSEC\n
|
||||
/// Returns -1 if no HEADER section is found.
|
||||
/// DifferenceHash: resize to 9x8 grayscale, compare adjacent pixels.
|
||||
/// Produces a 64-bit hash. Implemented directly against ImageSharp 3.x API
|
||||
/// (CoenM.ImageHash uses the removed GetPixelRowSpan from ImageSharp 2.x).
|
||||
/// </summary>
|
||||
private static int FindEndOfHeader(string text)
|
||||
private static ulong ComputeDifferenceHash(Image<Rgba32> image)
|
||||
{
|
||||
// Find the HEADER section start
|
||||
var headerIndex = FindGroupCode(text, 0, "2", "HEADER");
|
||||
if (headerIndex < 0)
|
||||
return -1;
|
||||
// Resize to 9 wide x 8 tall for 8x8 = 64 bit comparisons
|
||||
image.Mutate(ctx => ctx.Resize(9, 8));
|
||||
|
||||
// Advance past the HEADER value line so pair scanning stays aligned
|
||||
var headerLineEnd = text.IndexOf('\n', headerIndex);
|
||||
if (headerLineEnd < 0)
|
||||
return -1;
|
||||
ulong hash = 0;
|
||||
int bit = 0;
|
||||
|
||||
// Find the ENDSEC that closes the HEADER section
|
||||
var pos = headerLineEnd + 1;
|
||||
while (pos < text.Length)
|
||||
for (int y = 0; y < 8; y++)
|
||||
{
|
||||
var endsecIndex = FindGroupCode(text, pos, "0", "ENDSEC");
|
||||
if (endsecIndex < 0)
|
||||
return -1;
|
||||
for (int x = 0; x < 8; x++)
|
||||
{
|
||||
var left = image[x, y];
|
||||
var right = image[x + 1, y];
|
||||
var leftGray = 0.299 * left.R + 0.587 * left.G + 0.114 * left.B;
|
||||
var rightGray = 0.299 * right.R + 0.587 * right.G + 0.114 * right.B;
|
||||
|
||||
// Move past the ENDSEC line
|
||||
var lineEnd = text.IndexOf('\n', endsecIndex);
|
||||
return lineEnd >= 0 ? lineEnd + 1 : text.Length;
|
||||
if (leftGray > rightGray)
|
||||
hash |= (1UL << bit);
|
||||
|
||||
bit++;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
return hash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a DXF group code pair (code line followed by value line) starting from the given position.
|
||||
/// Returns the position of the value line, or -1 if not found.
|
||||
/// </summary>
|
||||
private static int FindGroupCode(string text, int startIndex, string groupCode, string value)
|
||||
private static string ComputeGeometricHash(string filePath)
|
||||
{
|
||||
var pos = startIndex;
|
||||
while (pos < text.Length)
|
||||
using (var reader = new DxfReader(filePath))
|
||||
{
|
||||
// Skip whitespace/newlines to find the group code
|
||||
while (pos < text.Length && (text[pos] == '\r' || text[pos] == '\n' || text[pos] == ' '))
|
||||
pos++;
|
||||
var doc = reader.Read();
|
||||
var signatures = new List<string>();
|
||||
|
||||
if (pos >= text.Length)
|
||||
break;
|
||||
foreach (var entity in doc.Entities)
|
||||
{
|
||||
signatures.Add(GetEntitySignature(entity));
|
||||
}
|
||||
|
||||
// Read the group code line
|
||||
var codeLineEnd = text.IndexOf('\n', pos);
|
||||
if (codeLineEnd < 0)
|
||||
break;
|
||||
signatures.Sort(StringComparer.Ordinal);
|
||||
var combined = string.Join("\n", signatures);
|
||||
|
||||
var codeLine = text.Substring(pos, codeLineEnd - pos).Trim();
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(combined));
|
||||
return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move to the value line
|
||||
var valueStart = codeLineEnd + 1;
|
||||
if (valueStart >= text.Length)
|
||||
break;
|
||||
private static string GetEntitySignature(Entity entity)
|
||||
{
|
||||
var layer = entity.Layer?.Name ?? "";
|
||||
|
||||
var valueLineEnd = text.IndexOf('\n', valueStart);
|
||||
if (valueLineEnd < 0)
|
||||
valueLineEnd = text.Length;
|
||||
switch (entity)
|
||||
{
|
||||
case Line line:
|
||||
return GetLineSignature(line, layer);
|
||||
case Arc arc:
|
||||
return GetArcSignature(arc, layer);
|
||||
case Circle circle:
|
||||
return GetCircleSignature(circle, layer);
|
||||
case MText mtext:
|
||||
return GetMTextSignature(mtext, layer);
|
||||
default:
|
||||
return $"{entity.GetType().Name}|{layer}";
|
||||
}
|
||||
}
|
||||
|
||||
var valueLine = text.Substring(valueStart, valueLineEnd - valueStart).Trim();
|
||||
private static string GetLineSignature(Line line, string layer)
|
||||
{
|
||||
var p1 = FormatPoint(line.StartPoint.X, line.StartPoint.Y);
|
||||
var p2 = FormatPoint(line.EndPoint.X, line.EndPoint.Y);
|
||||
|
||||
if (codeLine == groupCode && string.Equals(valueLine, value, StringComparison.OrdinalIgnoreCase))
|
||||
return valueStart;
|
||||
|
||||
// Move to the next pair
|
||||
pos = valueLineEnd + 1;
|
||||
// Normalize endpoint order so direction doesn't affect the hash
|
||||
if (string.Compare(p1, p2, StringComparison.Ordinal) > 0)
|
||||
{
|
||||
var tmp = p1;
|
||||
p1 = p2;
|
||||
p2 = tmp;
|
||||
}
|
||||
|
||||
return -1;
|
||||
return $"LINE|{layer}|{p1}|{p2}";
|
||||
}
|
||||
|
||||
private static string GetArcSignature(Arc arc, string layer)
|
||||
{
|
||||
var center = FormatPoint(arc.Center.X, arc.Center.Y);
|
||||
var r = R(arc.Radius);
|
||||
var sa = R(arc.StartAngle);
|
||||
var ea = R(arc.EndAngle);
|
||||
return $"ARC|{layer}|{center}|{r}|{sa}|{ea}";
|
||||
}
|
||||
|
||||
private static string GetCircleSignature(Circle circle, string layer)
|
||||
{
|
||||
var center = FormatPoint(circle.Center.X, circle.Center.Y);
|
||||
var r = R(circle.Radius);
|
||||
return $"CIRCLE|{layer}|{center}|{r}";
|
||||
}
|
||||
|
||||
private static string GetMTextSignature(MText mtext, string layer)
|
||||
{
|
||||
var point = FormatPoint(mtext.InsertPoint.X, mtext.InsertPoint.Y);
|
||||
var text = mtext.Value ?? "";
|
||||
return $"MTEXT|{layer}|{point}|{text}";
|
||||
}
|
||||
|
||||
private static string R(double value)
|
||||
{
|
||||
return Math.Round(value, 4).ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static string FormatPoint(double x, double y)
|
||||
{
|
||||
return $"{R(x)},{R(y)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Globalization;
|
||||
using System.IO.Compression;
|
||||
using System.Numerics;
|
||||
using FabWorks.Api.DTOs;
|
||||
using FabWorks.Api.Services;
|
||||
using FabWorks.Core.Data;
|
||||
@@ -212,11 +214,57 @@ namespace FabWorks.Api.Controllers
|
||||
if (record == null) return NotFound();
|
||||
|
||||
record.PdfContentHash = request.PdfContentHash;
|
||||
|
||||
if (!string.IsNullOrEmpty(record.DrawingNumber) && !string.IsNullOrEmpty(request.PdfContentHash))
|
||||
{
|
||||
var (drawing, revision) = await ResolveDrawingAsync(record.DrawingNumber, record.Title, request.PdfContentHash);
|
||||
record.Drawing = drawing;
|
||||
record.DrawingRevision = revision;
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private async Task<(Drawing drawing, int revision)> ResolveDrawingAsync(string drawingNumber, string title, string pdfContentHash)
|
||||
{
|
||||
var drawing = await _db.Drawings
|
||||
.FirstOrDefaultAsync(d => d.DrawingNumber == drawingNumber);
|
||||
|
||||
// Get the highest revision recorded for this drawing across all exports
|
||||
var lastRevision = await _db.ExportRecords
|
||||
.Where(r => r.DrawingNumber == drawingNumber && r.DrawingRevision != null)
|
||||
.OrderByDescending(r => r.DrawingRevision)
|
||||
.Select(r => r.DrawingRevision)
|
||||
.FirstOrDefaultAsync() ?? 0;
|
||||
|
||||
if (drawing == null)
|
||||
{
|
||||
drawing = new Drawing
|
||||
{
|
||||
DrawingNumber = drawingNumber,
|
||||
Title = title,
|
||||
PdfContentHash = pdfContentHash
|
||||
};
|
||||
_db.Drawings.Add(drawing);
|
||||
return (drawing, 1);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
drawing.Title = title;
|
||||
|
||||
if (ArePerceptualHashesSimilar(drawing.PdfContentHash, pdfContentHash))
|
||||
{
|
||||
// Hash unchanged — keep same revision
|
||||
return (drawing, lastRevision == 0 ? 1 : lastRevision);
|
||||
}
|
||||
|
||||
// Hash changed — bump revision and update stored hash
|
||||
drawing.PdfContentHash = pdfContentHash;
|
||||
return (drawing, lastRevision + 1);
|
||||
}
|
||||
|
||||
[HttpGet("previous-cut-template")]
|
||||
public async Task<ActionResult<CutTemplateDto>> GetPreviousCutTemplate(
|
||||
[FromQuery] string drawingNumber,
|
||||
@@ -246,6 +294,22 @@ namespace FabWorks.Api.Controllers
|
||||
};
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> 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<IActionResult> DownloadAllDxfs(int id)
|
||||
{
|
||||
@@ -261,6 +325,37 @@ namespace FabWorks.Api.Controllers
|
||||
|
||||
if (dxfItems.Count == 0) return NotFound("No DXF files for this export.");
|
||||
|
||||
var zipName = $"{record.DrawingNumber ?? $"Export-{id}"} DXFs.zip";
|
||||
return BuildDxfZip(dxfItems, zipName);
|
||||
}
|
||||
|
||||
[HttpGet("download-dxfs")]
|
||||
public async Task<IActionResult> DownloadDxfsByDrawing([FromQuery] string drawingNumber)
|
||||
{
|
||||
if (string.IsNullOrEmpty(drawingNumber))
|
||||
return BadRequest("drawingNumber is required.");
|
||||
|
||||
var dxfItems = await _db.BomItems
|
||||
.Include(b => b.CutTemplate)
|
||||
.Where(b => b.ExportRecord.DrawingNumber == drawingNumber
|
||||
&& b.CutTemplate != null
|
||||
&& b.CutTemplate.ContentHash != null)
|
||||
.ToListAsync();
|
||||
|
||||
if (dxfItems.Count == 0) return NotFound("No DXF files for this drawing.");
|
||||
|
||||
// Deduplicate by content hash (keep latest)
|
||||
dxfItems = dxfItems
|
||||
.GroupBy(b => b.CutTemplate.ContentHash)
|
||||
.Select(g => g.Last())
|
||||
.ToList();
|
||||
|
||||
var zipName = $"{drawingNumber} DXFs.zip";
|
||||
return BuildDxfZip(dxfItems, zipName);
|
||||
}
|
||||
|
||||
private FileResult BuildDxfZip(List<BomItem> dxfItems, string zipName)
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
using (var zip = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
|
||||
{
|
||||
@@ -286,16 +381,41 @@ namespace FabWorks.Api.Controllers
|
||||
|
||||
var entry = zip.CreateEntry(fileName, CompressionLevel.Fastest);
|
||||
using var entryStream = entry.Open();
|
||||
await blobStream.CopyToAsync(entryStream);
|
||||
blobStream.CopyTo(entryStream);
|
||||
blobStream.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
ms.Position = 0;
|
||||
var zipName = $"{record.DrawingNumber ?? $"Export-{id}"} DXFs.zip";
|
||||
return File(ms, "application/zip", zipName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares two perceptual hashes using Hamming distance.
|
||||
/// Perceptual hashes (16 hex chars / 64 bits) are compared with a tolerance
|
||||
/// of up to 10 differing bits (~84% similarity). SHA256 fallback hashes
|
||||
/// (64 hex chars) use exact comparison.
|
||||
/// </summary>
|
||||
private static bool ArePerceptualHashesSimilar(string hash1, string hash2)
|
||||
{
|
||||
if (hash1 == hash2) return true;
|
||||
if (string.IsNullOrEmpty(hash1) || string.IsNullOrEmpty(hash2)) return false;
|
||||
|
||||
// Perceptual hashes are 16 hex chars (64-bit DifferenceHash)
|
||||
// SHA256 fallback hashes are 64 hex chars — require exact match
|
||||
if (hash1.Length != 16 || hash2.Length != 16)
|
||||
return false;
|
||||
|
||||
if (ulong.TryParse(hash1, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var h1) &&
|
||||
ulong.TryParse(hash2, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var h2))
|
||||
{
|
||||
var hammingDistance = BitOperations.PopCount(h1 ^ h2);
|
||||
return hammingDistance <= 10;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ExportDetailDto MapToDto(ExportRecord r) => new()
|
||||
{
|
||||
Id = r.Id,
|
||||
|
||||
@@ -38,6 +38,7 @@ namespace FabWorks.Api.Controllers
|
||||
c.DxfFilePath,
|
||||
c.ContentHash,
|
||||
c.Thickness,
|
||||
c.Revision,
|
||||
DrawingNumber = c.BomItem.ExportRecord.DrawingNumber,
|
||||
CreatedAt = c.BomItem.ExportRecord.ExportedAt
|
||||
});
|
||||
@@ -70,6 +71,7 @@ namespace FabWorks.Api.Controllers
|
||||
FileType = "dxf",
|
||||
DrawingNumber = c.DrawingNumber,
|
||||
Thickness = c.Thickness,
|
||||
Revision = c.Revision,
|
||||
CreatedAt = c.CreatedAt
|
||||
});
|
||||
}
|
||||
@@ -85,7 +87,8 @@ namespace FabWorks.Api.Controllers
|
||||
r.Id,
|
||||
r.DrawingNumber,
|
||||
r.PdfContentHash,
|
||||
r.ExportedAt
|
||||
r.ExportedAt,
|
||||
r.DrawingRevision
|
||||
});
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
@@ -113,6 +116,7 @@ namespace FabWorks.Api.Controllers
|
||||
ContentHash = r.PdfContentHash,
|
||||
FileType = "pdf",
|
||||
DrawingNumber = r.DrawingNumber,
|
||||
Revision = r.DrawingRevision,
|
||||
CreatedAt = r.ExportedAt
|
||||
});
|
||||
}
|
||||
@@ -179,6 +183,7 @@ namespace FabWorks.Api.Controllers
|
||||
public string FileType { get; set; }
|
||||
public string DrawingNumber { get; set; }
|
||||
public double? Thickness { get; set; }
|
||||
public int? Revision { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace FabWorks.Api.Controllers
|
||||
return BadRequest("No file uploaded.");
|
||||
|
||||
using var stream = file.OpenReadStream();
|
||||
var result = await _fileStorage.StoreDxfAsync(stream, equipment, drawingNo, itemNo, contentHash);
|
||||
var result = await _fileStorage.StoreDxfAsync(stream, equipment, drawingNo, itemNo, contentHash, file.FileName);
|
||||
|
||||
return Ok(new FileUploadResponse
|
||||
{
|
||||
|
||||
@@ -17,6 +17,10 @@ builder.Services.AddScoped<IFileStorageService, FileStorageService>();
|
||||
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();
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace FabWorks.Api.Services
|
||||
public interface IFileStorageService
|
||||
{
|
||||
string OutputFolder { get; }
|
||||
Task<FileUploadResult> StoreDxfAsync(Stream stream, string equipment, string drawingNo, string itemNo, string contentHash);
|
||||
Task<FileUploadResult> StoreDxfAsync(Stream stream, string equipment, string drawingNo, string itemNo, string contentHash, string originalFileName = null);
|
||||
Task<FileUploadResult> StorePdfAsync(Stream stream, string equipment, string drawingNo, string contentHash, int? exportRecordId = null);
|
||||
Stream OpenBlob(string contentHash, string extension);
|
||||
bool BlobExists(string contentHash, string extension);
|
||||
@@ -39,9 +39,9 @@ namespace FabWorks.Api.Services
|
||||
Directory.CreateDirectory(blobRoot);
|
||||
}
|
||||
|
||||
public async Task<FileUploadResult> StoreDxfAsync(Stream stream, string equipment, string drawingNo, string itemNo, string contentHash)
|
||||
public async Task<FileUploadResult> StoreDxfAsync(Stream stream, string equipment, string drawingNo, string itemNo, string contentHash, string originalFileName = null)
|
||||
{
|
||||
var fileName = BuildDxfFileName(drawingNo, equipment, itemNo);
|
||||
var fileName = BuildDxfFileName(drawingNo, equipment, itemNo, originalFileName);
|
||||
|
||||
// Look up previous hash by drawing number + item number
|
||||
var drawingNumber = BuildDrawingNumber(equipment, drawingNo);
|
||||
@@ -147,8 +147,16 @@ namespace FabWorks.Api.Services
|
||||
return drawingNo ?? "";
|
||||
}
|
||||
|
||||
private static string BuildDxfFileName(string drawingNo, string equipment, string itemNo)
|
||||
private static string BuildDxfFileName(string drawingNo, string equipment, string itemNo, string originalFileName = null)
|
||||
{
|
||||
// No drawing number: use the original filename from the client
|
||||
if (string.IsNullOrEmpty(drawingNo) && !string.IsNullOrEmpty(originalFileName))
|
||||
{
|
||||
return originalFileName.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase)
|
||||
? originalFileName
|
||||
: originalFileName + ".dxf";
|
||||
}
|
||||
|
||||
var drawingNumber = BuildDrawingNumber(equipment, drawingNo);
|
||||
var paddedItem = (itemNo ?? "").PadLeft(2, '0');
|
||||
if (!string.IsNullOrEmpty(drawingNumber) && !string.IsNullOrEmpty(itemNo))
|
||||
|
||||
@@ -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 ─── */
|
||||
@@ -723,6 +735,85 @@ tbody tr:last-child td { border-bottom: none; }
|
||||
.equip-group.collapsed .equip-body { display: none; }
|
||||
.equip-group.collapsed .equip-header { border-radius: 6px; }
|
||||
|
||||
/* ─── Modal ─── */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 200;
|
||||
animation: fadeIn 0.15s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||
|
||||
.modal-panel {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
||||
width: 90%;
|
||||
max-width: 640px;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: fadeSlideIn 0.2s ease forwards;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
font-family: var(--font-display);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.02em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.modal-header svg { width: 16px; height: 16px; }
|
||||
|
||||
.modal-body {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-green {
|
||||
background: var(--green-dim);
|
||||
color: var(--green);
|
||||
border-color: rgba(6, 118, 71, 0.25);
|
||||
}
|
||||
|
||||
.btn-green:hover {
|
||||
background: rgba(6, 118, 71, 0.15);
|
||||
border-color: rgba(6, 118, 71, 0.4);
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
/* ─── Toast ─── */
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--text);
|
||||
color: #fff;
|
||||
padding: 8px 20px;
|
||||
border-radius: 4px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
z-index: 300;
|
||||
animation: fadeSlideIn 0.2s ease, fadeOut 0.3s ease 2s forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeOut { to { opacity: 0; } }
|
||||
|
||||
/* ─── Responsive ─── */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar { display: none; }
|
||||
|
||||
@@ -46,11 +46,11 @@
|
||||
<div class="page-content" id="page-content"></div>
|
||||
</div>
|
||||
|
||||
<script src="js/icons.js"></script>
|
||||
<script src="js/helpers.js"></script>
|
||||
<script src="js/components.js"></script>
|
||||
<script src="js/pages.js"></script>
|
||||
<script src="js/router.js"></script>
|
||||
<script src="js/icons.js?v=3"></script>
|
||||
<script src="js/helpers.js?v=3"></script>
|
||||
<script src="js/components.js?v=3"></script>
|
||||
<script src="js/pages.js?v=3"></script>
|
||||
<script src="js/router.js?v=3"></script>
|
||||
<script>router.init();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -58,3 +58,98 @@ function toggleBomRow(id) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ const icons = {
|
||||
chevron: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>`,
|
||||
laser: `<svg viewBox="0 0 16 16" fill="none" stroke="var(--cyan)" stroke-width="1.2"><circle cx="8" cy="8" r="2"/><path d="M8 2v3M8 11v3M2 8h3M11 8h3" opacity="0.5"/></svg>`,
|
||||
bend: `<svg viewBox="0 0 16 16" fill="none" stroke="var(--amber)" stroke-width="1.2"><path d="M3 13V7a4 4 0 0 1 4-4h6"/><polyline points="10 6 13 3 10 0" transform="translate(0,2)"/></svg>`,
|
||||
trash: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>`,
|
||||
clipboard: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="2" width="6" height="4" rx="1"/><path d="M9 2H7a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-2"/><line x1="9" y1="12" x2="15" y2="12"/><line x1="9" y1="16" x2="15" y2="16"/></svg>`,
|
||||
check: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>`,
|
||||
close: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`,
|
||||
};
|
||||
|
||||
function fileIcon(name) {
|
||||
|
||||
@@ -29,6 +29,69 @@ const pages = {
|
||||
return;
|
||||
}
|
||||
|
||||
setPage('Exports', `${data.items.length} exports`);
|
||||
|
||||
const rows = data.items.map((e, i) => `
|
||||
<tr class="clickable" onclick="router.go('drawing-detail', {id: '${encodeURIComponent(e.drawingNumber)}', eid: '${e.id}'})" style="animation: fadeSlideIn 0.2s ease ${0.02 * Math.min(i, 25)}s forwards; opacity: 0">
|
||||
<td style="font-family:var(--font-mono);color:var(--text-dim);font-size:13px">${e.id}</td>
|
||||
<td><strong>${esc(e.drawingNumber) || '<span style="color:var(--text-dim)">\u2014</span>'}</strong></td>
|
||||
<td style="color:var(--text-secondary);font-size:13px">${esc(e.title) || ''}</td>
|
||||
<td><span class="badge badge-count">${e.bomItemCount}</span></td>
|
||||
<td style="color:var(--text-secondary)">${esc(e.exportedBy)}</td>
|
||||
<td style="font-family:var(--font-mono);font-size:13px;color:var(--text-secondary);white-space:nowrap">${fmtDate(e.exportedAt)}</td>
|
||||
<td><button class="btn btn-red btn-sm" onclick="event.stopPropagation();deleteExport(${e.id})">${icons.trash}</button></td>
|
||||
</tr>`).join('');
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="card animate-in">
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th style="width:50px">#</th>
|
||||
<th>Drawing</th>
|
||||
<th>Title</th>
|
||||
<th style="width:80px">Items</th>
|
||||
<th>Exported By</th>
|
||||
<th style="width:180px">Date</th>
|
||||
<th style="width:50px"></th>
|
||||
</tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
} catch (err) {
|
||||
content.innerHTML = `<div class="empty">Error: ${esc(err.message)}</div>`;
|
||||
}
|
||||
},
|
||||
|
||||
async drawings(params) {
|
||||
const actions = document.getElementById('topbar-actions');
|
||||
const content = document.getElementById('page-content');
|
||||
setPage('Drawings');
|
||||
|
||||
const searchVal = (params && params.q) || '';
|
||||
actions.innerHTML = `
|
||||
<div class="search-box">
|
||||
${icons.search}
|
||||
<input type="text" id="drawing-search" placeholder="Search drawing, part, user..." value="${esc(searchVal)}">
|
||||
</div>`;
|
||||
|
||||
content.innerHTML = `<div class="loading">Loading drawings</div>`;
|
||||
|
||||
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 = `<div class="empty">No drawings found.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Deduplicate: keep only the latest export per drawing number
|
||||
const seen = new Set();
|
||||
const unique = data.items.filter(e => {
|
||||
@@ -57,7 +120,7 @@ const pages = {
|
||||
|
||||
const uniqueEquip = sortedGroups.length;
|
||||
const uniqueDrawings = unique.length;
|
||||
setPage('Exports', `${uniqueDrawings} drawings / ${uniqueEquip} equipment`);
|
||||
setPage('Drawings', `${uniqueDrawings} drawings / ${uniqueEquip} equipment`);
|
||||
|
||||
const groupsHtml = sortedGroups.map(([equip, items], gi) => {
|
||||
const totalBom = items.reduce((s, e) => s + e.bomItemCount, 0);
|
||||
@@ -68,8 +131,7 @@ const pages = {
|
||||
const drawingPart = spaceIdx > 0 ? dn.substring(spaceIdx + 1) : dn;
|
||||
|
||||
return `
|
||||
<tr class="clickable" onclick="router.go('export-detail', {id: ${e.id}})" style="animation: fadeSlideIn 0.2s ease ${0.02 * i}s forwards; opacity: 0">
|
||||
<td style="font-family:var(--font-mono);color:var(--text-dim);font-size:13px">${e.id}</td>
|
||||
<tr class="clickable" onclick="router.go('drawing-detail', {id: '${encodeURIComponent(e.drawingNumber)}'})" style="animation: fadeSlideIn 0.2s ease ${0.02 * i}s forwards; opacity: 0">
|
||||
<td><strong>${esc(drawingPart) || '<span style="color:var(--text-dim)">\u2014</span>'}</strong></td>
|
||||
<td style="color:var(--text-secondary);font-size:13px">${esc(e.title) || ''}</td>
|
||||
<td><span class="badge badge-count">${e.bomItemCount}</span></td>
|
||||
@@ -84,19 +146,18 @@ const pages = {
|
||||
<span class="chevron-toggle open" id="equip-${esc(equip)}-icon">${icons.chevron}</span>
|
||||
<span class="equip-header-number">${esc(equip)}</span>
|
||||
<div class="equip-header-meta">
|
||||
<span class="equip-header-stat"><strong>${items.length}</strong> exports</span>
|
||||
<span class="equip-header-stat"><strong>${items.length}</strong> drawings</span>
|
||||
<span class="equip-header-stat"><strong>${totalBom}</strong> items</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="equip-body">
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th style="width:50px">#</th>
|
||||
<th>Drawing</th>
|
||||
<th>Title</th>
|
||||
<th style="width:80px">Items</th>
|
||||
<th>Exported By</th>
|
||||
<th style="width:180px">Date</th>
|
||||
<th style="width:180px">Latest Export</th>
|
||||
</tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
@@ -115,119 +176,9 @@ const pages = {
|
||||
}
|
||||
},
|
||||
|
||||
async exportDetail(id) {
|
||||
const actions = document.getElementById('topbar-actions');
|
||||
const content = document.getElementById('page-content');
|
||||
setPage('Loading...');
|
||||
actions.innerHTML = '';
|
||||
content.innerHTML = `<div class="loading">Loading export</div>`;
|
||||
|
||||
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 `
|
||||
<tr class="${hasDetails ? 'clickable' : ''}" ${hasDetails ? `onclick="toggleBomRow('${toggleId}')"` : ''} style="animation: fadeSlideIn 0.25s ease ${0.03 * i}s forwards; opacity: 0">
|
||||
<td style="width:32px">${hasDetails ? `<span class="chevron-toggle" id="${toggleId}-icon">${icons.chevron}</span>` : ''}</td>
|
||||
<td style="font-family:var(--font-mono);font-weight:600;color:var(--cyan)">${esc(b.itemNo)}</td>
|
||||
<td><strong>${esc(b.partName)}</strong></td>
|
||||
<td style="color:var(--text-secondary)">${esc(b.description)}</td>
|
||||
<td><span style="font-family:var(--font-mono);font-size:13px">${esc(b.material)}</span></td>
|
||||
<td style="font-family:var(--font-mono);text-align:center">${b.qty ?? ''}</td>
|
||||
<td style="font-family:var(--font-mono);text-align:center">${b.totalQty ?? ''}</td>
|
||||
<td>
|
||||
${b.cutTemplate ? `<span class="badge badge-cyan">${icons.laser} DXF</span>` : ''}
|
||||
${b.formProgram ? `<span class="badge badge-amber">${icons.bend} Form</span>` : ''}
|
||||
</td>
|
||||
</tr>
|
||||
${hasDetails ? `<tr class="bom-expand-row" id="${toggleId}" style="display:none"><td colspan="8">${renderBomDetails(b)}</td></tr>` : ''}`;
|
||||
}).join('');
|
||||
|
||||
content.innerHTML = `
|
||||
<a class="back-link" onclick="router.go('exports')">${icons.back} Back to exports</a>
|
||||
|
||||
<div class="card animate-in" style="margin-bottom:20px">
|
||||
<div class="card-header">Export Information</div>
|
||||
<div class="card-body">
|
||||
<div class="detail-grid">
|
||||
<div class="detail-field"><label>Drawing Number</label><div class="value">${esc(exp.drawingNumber) || '\u2014'}</div></div>
|
||||
${exp.title ? `<div class="detail-field"><label>Title</label><div class="value">${esc(exp.title)}</div></div>` : ''}
|
||||
<div class="detail-field"><label>Exported By</label><div class="value">${esc(exp.exportedBy)}</div></div>
|
||||
<div class="detail-field"><label>Date</label><div class="value mono">${fmtDate(exp.exportedAt)}</div></div>
|
||||
<div class="detail-field"><label>Source File</label><div class="value mono">${esc(exp.sourceFilePath)}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card animate-in">
|
||||
<div class="card-header">
|
||||
BOM Items
|
||||
<span class="badge badge-count">${exp.bomItems?.length || 0} items</span>
|
||||
<span style="margin-left:auto;display:flex;gap:6px">
|
||||
${exp.pdfContentHash ? `<a class="btn btn-amber btn-sm" href="/api/filebrowser/download?hash=${encodeURIComponent(exp.pdfContentHash)}&ext=pdf&name=${encodeURIComponent((exp.drawingNumber || 'drawing') + '.pdf')}">${icons.download} PDF</a>` : ''}
|
||||
${dxfCount > 0 ? `<a class="btn btn-cyan btn-sm" href="/api/exports/${exp.id}/download-dxfs">${icons.download} All DXFs</a>` : ''}
|
||||
</span>
|
||||
</div>
|
||||
${exp.bomItems?.length ? `
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th style="width:32px"></th>
|
||||
<th style="width:60px">Item</th>
|
||||
<th>Part Name</th>
|
||||
<th>Description</th>
|
||||
<th>Material</th>
|
||||
<th style="width:50px;text-align:center">Qty</th>
|
||||
<th style="width:55px;text-align:center">Total</th>
|
||||
<th style="width:120px">Data</th>
|
||||
</tr></thead>
|
||||
<tbody>${bomRows}</tbody>
|
||||
</table>` : '<div class="empty">No BOM items for this export.</div>'}
|
||||
</div>`;
|
||||
} catch (err) {
|
||||
content.innerHTML = `<div class="empty">Error: ${esc(err.message)}</div>`;
|
||||
}
|
||||
},
|
||||
|
||||
async drawings() {
|
||||
const actions = document.getElementById('topbar-actions');
|
||||
const content = document.getElementById('page-content');
|
||||
setPage('Drawings');
|
||||
actions.innerHTML = '';
|
||||
content.innerHTML = `<div class="loading">Loading drawings</div>`;
|
||||
|
||||
try {
|
||||
const numbers = await api.get('/api/exports/drawing-numbers');
|
||||
if (numbers.length === 0) {
|
||||
content.innerHTML = `<div class="empty">No drawings found.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
numbers.sort();
|
||||
setPage('Drawings', `${numbers.length} drawings`);
|
||||
|
||||
const cards = numbers.map((d, i) => `
|
||||
<div class="drawing-card" onclick="router.go('drawing-detail', {id: '${encodeURIComponent(d)}'})" style="animation: fadeSlideIn 0.3s ease ${0.025 * Math.min(i, 20)}s forwards; opacity: 0">
|
||||
<div class="drawing-card-title">${esc(d)}</div>
|
||||
<div class="drawing-card-sub">Drawing</div>
|
||||
</div>`).join('');
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card animate-in"><div class="stat-label">Total Drawings</div><div class="stat-value">${numbers.length}</div></div>
|
||||
</div>
|
||||
<div class="drawings-grid">${cards}</div>`;
|
||||
} catch (err) {
|
||||
content.innerHTML = `<div class="empty">Error: ${esc(err.message)}</div>`;
|
||||
}
|
||||
},
|
||||
|
||||
async drawingDetail(drawingEncoded) {
|
||||
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');
|
||||
@@ -244,12 +195,26 @@ const pages = {
|
||||
return;
|
||||
}
|
||||
|
||||
const allBom = [];
|
||||
exports.forEach(exp => {
|
||||
(exp.bomItems || []).forEach(b => {
|
||||
allBom.push({ ...b, exportId: exp.id, exportedAt: exp.exportedAt });
|
||||
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()];
|
||||
}
|
||||
|
||||
// Store for cut list modal
|
||||
window._currentBom = allBom;
|
||||
|
||||
const bomRows = allBom.map((b, i) => {
|
||||
const hasDetails = b.cutTemplate || b.formProgram;
|
||||
@@ -259,6 +224,7 @@ const pages = {
|
||||
<td style="width:32px">${hasDetails ? `<span class="chevron-toggle" id="${toggleId}-icon">${icons.chevron}</span>` : ''}</td>
|
||||
<td style="font-family:var(--font-mono);font-weight:600;color:var(--cyan)">${esc(b.itemNo)}</td>
|
||||
<td><strong>${esc(b.partName)}</strong></td>
|
||||
<td style="font-family:var(--font-mono);text-align:center;font-size:13px">${b.cutTemplate?.revision ?? ''}</td>
|
||||
<td style="color:var(--text-secondary)">${esc(b.description)}</td>
|
||||
<td><span style="font-family:var(--font-mono);font-size:13px">${esc(b.material)}</span></td>
|
||||
<td style="font-family:var(--font-mono);text-align:center">${b.qty ?? ''}</td>
|
||||
@@ -268,22 +234,47 @@ const pages = {
|
||||
${b.formProgram ? `<span class="badge badge-amber">${icons.bend} Form</span>` : ''}
|
||||
</td>
|
||||
</tr>
|
||||
${hasDetails ? `<tr class="bom-expand-row" id="${toggleId}" style="display:none"><td colspan="8">${renderBomDetails(b)}</td></tr>` : ''}`;
|
||||
${hasDetails ? `<tr class="bom-expand-row" id="${toggleId}" style="display:none"><td colspan="9">${renderBomDetails(b)}</td></tr>` : ''}`;
|
||||
}).join('');
|
||||
|
||||
content.innerHTML = `
|
||||
<a class="back-link" onclick="router.go('drawings')">${icons.back} Back to drawings</a>
|
||||
const backLink = singleExport
|
||||
? `<a class="back-link" onclick="router.go('exports')">${icons.back} Back to exports</a>`
|
||||
: `<a class="back-link" onclick="router.go('drawings')">${icons.back} Back to drawings</a>`;
|
||||
|
||||
<div class="stats-grid">
|
||||
const statsHtml = singleExport
|
||||
? `<div class="stats-grid">
|
||||
<div class="stat-card animate-in"><div class="stat-label">Exported By</div><div class="stat-value stat-sm">${esc(singleExport.exportedBy)}</div></div>
|
||||
<div class="stat-card animate-in"><div class="stat-label">BOM Items</div><div class="stat-value">${allBom.length}</div></div>
|
||||
<div class="stat-card animate-in"><div class="stat-label">Exported</div><div class="stat-value stat-sm">${fmtDate(singleExport.exportedAt)}</div></div>
|
||||
</div>`
|
||||
: `<div class="stats-grid">
|
||||
<div class="stat-card animate-in"><div class="stat-label">Exports</div><div class="stat-value">${exports.length}</div></div>
|
||||
<div class="stat-card animate-in"><div class="stat-label">BOM Items</div><div class="stat-value">${allBom.length}</div></div>
|
||||
<div class="stat-card animate-in"><div class="stat-label">Latest Export</div><div class="stat-value stat-sm">${fmtDate(exports[0].exportedAt)}</div></div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
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}
|
||||
|
||||
<div class="card animate-in">
|
||||
<div class="card-header">
|
||||
All BOM Items
|
||||
${bomHeader}
|
||||
<span class="badge badge-count">${allBom.length} items</span>
|
||||
<span style="margin-left:auto;display:flex;gap:6px">
|
||||
${dxfCount > 0 ? `<button class="btn btn-sm" onclick="showCutListModal(window._currentBom)">${icons.clipboard} Cut List</button>` : ''}
|
||||
${pdfHash ? `<a class="btn btn-amber btn-sm" href="/api/filebrowser/download?hash=${encodeURIComponent(pdfHash)}&ext=pdf&name=${pdfName}">${icons.download} PDF</a>` : ''}
|
||||
${dxfCount > 0 ? (singleExport
|
||||
? `<a class="btn btn-cyan btn-sm" href="/api/exports/${activeExport.id}/download-dxfs">${icons.download} All DXFs</a>`
|
||||
: `<a class="btn btn-cyan btn-sm" href="/api/exports/download-dxfs?drawingNumber=${encodeURIComponent(drawingNumber)}">${icons.download} All DXFs</a>`
|
||||
) : ''}
|
||||
</span>
|
||||
</div>
|
||||
${allBom.length ? `
|
||||
<table>
|
||||
@@ -291,6 +282,7 @@ const pages = {
|
||||
<th style="width:32px"></th>
|
||||
<th style="width:60px">Item</th>
|
||||
<th>Part Name</th>
|
||||
<th style="width:45px;text-align:center">Rev</th>
|
||||
<th>Description</th>
|
||||
<th>Material</th>
|
||||
<th style="width:50px;text-align:center">Qty</th>
|
||||
@@ -368,6 +360,7 @@ const pages = {
|
||||
<td><div class="file-name-cell">${ext === 'pdf' ? icons.filePdf : icons.fileDxf}<a href="/api/filebrowser/download?hash=${encodeURIComponent(f.contentHash)}&ext=${ext}&name=${encodeURIComponent(f.fileName)}">${esc(f.fileName)}</a></div></td>
|
||||
<td><span class="badge ${ext === 'dxf' ? 'badge-cyan' : 'badge-amber'}">${ext.toUpperCase()}</span></td>
|
||||
<td style="color:var(--text-secondary)">${esc(f.drawingNumber)}</td>
|
||||
<td style="font-family:var(--font-mono);font-size:13px;text-align:center;color:var(--text-secondary)">${f.revision != null ? f.revision : '\u2014'}</td>
|
||||
<td style="font-family:var(--font-mono);font-size:13px;color:var(--text-secondary)">${f.thickness != null ? f.thickness.toFixed(4) + '"' : '\u2014'}</td>
|
||||
<td style="font-family:var(--font-mono);font-size:13px;color:var(--text-secondary)">${fmtDate(f.createdAt)}</td>
|
||||
<td style="font-family:var(--font-mono);font-size:12px;color:var(--text-dim)">${esc(hashShort)}</td>
|
||||
@@ -384,6 +377,7 @@ const pages = {
|
||||
<th>Name</th>
|
||||
<th style="width:60px">Type</th>
|
||||
<th>Drawing</th>
|
||||
<th style="width:45px;text-align:center">Rev</th>
|
||||
<th style="width:90px">Thickness</th>
|
||||
<th style="width:170px">Date</th>
|
||||
<th style="width:100px">Hash</th>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
const router = {
|
||||
go(page, params = {}) {
|
||||
const hash = page + (params.id ? '/' + params.id : '') + (params.q ? '?q=' + encodeURIComponent(params.q) : '');
|
||||
const qParts = [];
|
||||
if (params.q) qParts.push('q=' + encodeURIComponent(params.q));
|
||||
if (params.eid) qParts.push('eid=' + encodeURIComponent(params.eid));
|
||||
const hash = page + (params.id ? '/' + params.id : '') + (qParts.length ? '?' + qParts.join('&') : '');
|
||||
location.hash = hash;
|
||||
},
|
||||
parse() {
|
||||
@@ -20,13 +23,11 @@ const router = {
|
||||
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 'drawings': pages.drawings(params); break;
|
||||
case 'drawing-detail': pages.drawingDetail(id, params); break;
|
||||
case 'files': pages.files(params); break;
|
||||
default: pages.exports(params);
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace FabWorks.Core.Data
|
||||
public DbSet<BomItem> BomItems { get; set; }
|
||||
public DbSet<CutTemplate> CutTemplates { get; set; }
|
||||
public DbSet<FormProgram> FormPrograms { get; set; }
|
||||
public DbSet<Drawing> Drawings { get; set; }
|
||||
|
||||
public FabWorksDbContext(DbContextOptions<FabWorksDbContext> options) : base(options) { }
|
||||
|
||||
@@ -32,6 +33,11 @@ namespace FabWorks.Core.Data
|
||||
.WithOne(b => b.ExportRecord)
|
||||
.HasForeignKey(b => b.ExportRecordId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasOne(e => e.Drawing)
|
||||
.WithMany(d => d.ExportRecords)
|
||||
.HasForeignKey(e => e.DrawingId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<BomItem>(entity =>
|
||||
@@ -74,6 +80,15 @@ namespace FabWorks.Core.Data
|
||||
entity.Property(e => e.LowerToolNames).HasMaxLength(500);
|
||||
entity.Property(e => e.SetupNotes).HasMaxLength(2000);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Drawing>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.DrawingNumber).HasMaxLength(100);
|
||||
entity.Property(e => e.Title).HasMaxLength(200);
|
||||
entity.Property(e => e.PdfContentHash).HasMaxLength(64);
|
||||
entity.HasIndex(e => e.DrawingNumber).IsUnique();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
325
FabWorks.Core/Migrations/20260220125334_AddDrawingEntity.Designer.cs
generated
Normal file
325
FabWorks.Core/Migrations/20260220125334_AddDrawingEntity.Designer.cs
generated
Normal file
@@ -0,0 +1,325 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FabWorks.Core.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FabWorks.Core.Migrations
|
||||
{
|
||||
[DbContext(typeof(FabWorksDbContext))]
|
||||
[Migration("20260220125334_AddDrawingEntity")]
|
||||
partial class AddDrawingEntity
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.11")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
|
||||
|
||||
b.Property<string>("ConfigurationName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<int>("ExportRecordId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ItemNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Material")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("PartName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("PartNo")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int?>("Qty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("TotalQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("ExportRecordId");
|
||||
|
||||
b.ToTable("BomItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BomItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("CutTemplateName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<double?>("DefaultBendRadius")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("DxfFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<double?>("KFactor")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int>("Revision")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<double?>("Thickness")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BomItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CutTemplates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DrawingNumber")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("PdfContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<int>("Revision")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DrawingNumber")
|
||||
.IsUnique()
|
||||
.HasFilter("[DrawingNumber] IS NOT NULL");
|
||||
|
||||
b.ToTable("Drawings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("DrawingId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("DrawingNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("DrawingNumber")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("EquipmentNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<DateTime>("ExportedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ExportedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("OutputFolder")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("PdfContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("SourceFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DrawingId");
|
||||
|
||||
b.ToTable("ExportRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BendCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("BomItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<double?>("KFactor")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("LowerToolNames")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("MaterialType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("ProgramFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("ProgramName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("SetupNotes")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<double?>("Thickness")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("UpperToolNames")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BomItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("FormPrograms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.ExportRecord", "ExportRecord")
|
||||
.WithMany("BomItems")
|
||||
.HasForeignKey("ExportRecordId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ExportRecord");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
|
||||
.WithOne("CutTemplate")
|
||||
.HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BomItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.Drawing", "Drawing")
|
||||
.WithMany("ExportRecords")
|
||||
.HasForeignKey("DrawingId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Drawing");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
|
||||
.WithOne("FormProgram")
|
||||
.HasForeignKey("FabWorks.Core.Models.FormProgram", "BomItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BomItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.Navigation("CutTemplate");
|
||||
|
||||
b.Navigation("FormProgram");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
|
||||
{
|
||||
b.Navigation("ExportRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Navigation("BomItems");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
75
FabWorks.Core/Migrations/20260220125334_AddDrawingEntity.cs
Normal file
75
FabWorks.Core/Migrations/20260220125334_AddDrawingEntity.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FabWorks.Core.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDrawingEntity : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "DrawingId",
|
||||
table: "ExportRecords",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Drawings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
DrawingNumber = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
|
||||
PdfContentHash = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
||||
Revision = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Drawings", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ExportRecords_DrawingId",
|
||||
table: "ExportRecords",
|
||||
column: "DrawingId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Drawings_DrawingNumber",
|
||||
table: "Drawings",
|
||||
column: "DrawingNumber",
|
||||
unique: true,
|
||||
filter: "[DrawingNumber] IS NOT NULL");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_ExportRecords_Drawings_DrawingId",
|
||||
table: "ExportRecords",
|
||||
column: "DrawingId",
|
||||
principalTable: "Drawings",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_ExportRecords_Drawings_DrawingId",
|
||||
table: "ExportRecords");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Drawings");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ExportRecords_DrawingId",
|
||||
table: "ExportRecords");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DrawingId",
|
||||
table: "ExportRecords");
|
||||
}
|
||||
}
|
||||
}
|
||||
325
FabWorks.Core/Migrations/20260220130029_SeedDrawingsFromExistingExports.Designer.cs
generated
Normal file
325
FabWorks.Core/Migrations/20260220130029_SeedDrawingsFromExistingExports.Designer.cs
generated
Normal file
@@ -0,0 +1,325 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FabWorks.Core.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FabWorks.Core.Migrations
|
||||
{
|
||||
[DbContext(typeof(FabWorksDbContext))]
|
||||
[Migration("20260220130029_SeedDrawingsFromExistingExports")]
|
||||
partial class SeedDrawingsFromExistingExports
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.11")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
|
||||
|
||||
b.Property<string>("ConfigurationName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<int>("ExportRecordId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ItemNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Material")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("PartName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("PartNo")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int?>("Qty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("TotalQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("ExportRecordId");
|
||||
|
||||
b.ToTable("BomItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BomItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("CutTemplateName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<double?>("DefaultBendRadius")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("DxfFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<double?>("KFactor")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int>("Revision")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<double?>("Thickness")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BomItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CutTemplates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DrawingNumber")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("PdfContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<int>("Revision")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DrawingNumber")
|
||||
.IsUnique()
|
||||
.HasFilter("[DrawingNumber] IS NOT NULL");
|
||||
|
||||
b.ToTable("Drawings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("DrawingId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("DrawingNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("DrawingNumber")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("EquipmentNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<DateTime>("ExportedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ExportedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("OutputFolder")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("PdfContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("SourceFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DrawingId");
|
||||
|
||||
b.ToTable("ExportRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BendCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("BomItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<double?>("KFactor")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("LowerToolNames")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("MaterialType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("ProgramFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("ProgramName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("SetupNotes")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<double?>("Thickness")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("UpperToolNames")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BomItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("FormPrograms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.ExportRecord", "ExportRecord")
|
||||
.WithMany("BomItems")
|
||||
.HasForeignKey("ExportRecordId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ExportRecord");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
|
||||
.WithOne("CutTemplate")
|
||||
.HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BomItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.Drawing", "Drawing")
|
||||
.WithMany("ExportRecords")
|
||||
.HasForeignKey("DrawingId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Drawing");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
|
||||
.WithOne("FormProgram")
|
||||
.HasForeignKey("FabWorks.Core.Models.FormProgram", "BomItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BomItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.Navigation("CutTemplate");
|
||||
|
||||
b.Navigation("FormProgram");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
|
||||
{
|
||||
b.Navigation("ExportRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Navigation("BomItems");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FabWorks.Core.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SeedDrawingsFromExistingExports : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Create Drawing records from existing ExportRecords (latest hash per DrawingNumber)
|
||||
migrationBuilder.Sql(@"
|
||||
INSERT INTO Drawings (DrawingNumber, Title, PdfContentHash, Revision)
|
||||
SELECT
|
||||
sub.DrawingNumber,
|
||||
sub.Title,
|
||||
sub.PdfContentHash,
|
||||
1
|
||||
FROM (
|
||||
SELECT
|
||||
e.DrawingNumber,
|
||||
e.Title,
|
||||
e.PdfContentHash,
|
||||
ROW_NUMBER() OVER (PARTITION BY e.DrawingNumber ORDER BY e.Id DESC) AS rn
|
||||
FROM ExportRecords e
|
||||
WHERE e.DrawingNumber IS NOT NULL
|
||||
AND e.PdfContentHash IS NOT NULL
|
||||
) sub
|
||||
WHERE sub.rn = 1;
|
||||
");
|
||||
|
||||
// Link ExportRecords to their Drawing
|
||||
migrationBuilder.Sql(@"
|
||||
UPDATE er
|
||||
SET er.DrawingId = d.Id
|
||||
FROM ExportRecords er
|
||||
INNER JOIN Drawings d ON d.DrawingNumber = er.DrawingNumber
|
||||
WHERE er.PdfContentHash IS NOT NULL;
|
||||
");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("UPDATE ExportRecords SET DrawingId = NULL;");
|
||||
migrationBuilder.Sql("DELETE FROM Drawings;");
|
||||
}
|
||||
}
|
||||
}
|
||||
325
FabWorks.Core/Migrations/20260220171747_MoveRevisionFromDrawingToExportRecord.Designer.cs
generated
Normal file
325
FabWorks.Core/Migrations/20260220171747_MoveRevisionFromDrawingToExportRecord.Designer.cs
generated
Normal file
@@ -0,0 +1,325 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FabWorks.Core.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FabWorks.Core.Migrations
|
||||
{
|
||||
[DbContext(typeof(FabWorksDbContext))]
|
||||
[Migration("20260220171747_MoveRevisionFromDrawingToExportRecord")]
|
||||
partial class MoveRevisionFromDrawingToExportRecord
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.11")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.Property<int>("ID")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
|
||||
|
||||
b.Property<string>("ConfigurationName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<int>("ExportRecordId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ItemNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Material")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("PartName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("PartNo")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int?>("Qty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("TotalQty")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("ID");
|
||||
|
||||
b.HasIndex("ExportRecordId");
|
||||
|
||||
b.ToTable("BomItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BomItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("CutTemplateName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<double?>("DefaultBendRadius")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("DxfFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<double?>("KFactor")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<int>("Revision")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<double?>("Thickness")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BomItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CutTemplates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DrawingNumber")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("PdfContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DrawingNumber")
|
||||
.IsUnique()
|
||||
.HasFilter("[DrawingNumber] IS NOT NULL");
|
||||
|
||||
b.ToTable("Drawings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("DrawingId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("DrawingNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("DrawingNumber")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int?>("DrawingRevision")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("EquipmentNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<DateTime>("ExportedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("ExportedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("OutputFolder")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("PdfContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("SourceFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DrawingId");
|
||||
|
||||
b.ToTable("ExportRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BendCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("BomItemId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<double?>("KFactor")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("LowerToolNames")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("MaterialType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("ProgramFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("ProgramName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("SetupNotes")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<double?>("Thickness")
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<string>("UpperToolNames")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BomItemId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("FormPrograms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.ExportRecord", "ExportRecord")
|
||||
.WithMany("BomItems")
|
||||
.HasForeignKey("ExportRecordId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ExportRecord");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
|
||||
.WithOne("CutTemplate")
|
||||
.HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BomItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.Drawing", "Drawing")
|
||||
.WithMany("ExportRecords")
|
||||
.HasForeignKey("DrawingId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Drawing");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
|
||||
.WithOne("FormProgram")
|
||||
.HasForeignKey("FabWorks.Core.Models.FormProgram", "BomItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("BomItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
|
||||
{
|
||||
b.Navigation("CutTemplate");
|
||||
|
||||
b.Navigation("FormProgram");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
|
||||
{
|
||||
b.Navigation("ExportRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Navigation("BomItems");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FabWorks.Core.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MoveRevisionFromDrawingToExportRecord : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Revision",
|
||||
table: "Drawings");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "DrawingRevision",
|
||||
table: "ExportRecords",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DrawingRevision",
|
||||
table: "ExportRecords");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Revision",
|
||||
table: "Drawings",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,6 +116,35 @@ namespace FabWorks.Core.Migrations
|
||||
b.ToTable("CutTemplates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DrawingNumber")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("PdfContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DrawingNumber")
|
||||
.IsUnique()
|
||||
.HasFilter("[DrawingNumber] IS NOT NULL");
|
||||
|
||||
b.ToTable("Drawings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -124,6 +153,9 @@ namespace FabWorks.Core.Migrations
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("DrawingId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("DrawingNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
@@ -132,6 +164,9 @@ namespace FabWorks.Core.Migrations
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int?>("DrawingRevision")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("EquipmentNo")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
@@ -161,6 +196,8 @@ namespace FabWorks.Core.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DrawingId");
|
||||
|
||||
b.ToTable("ExportRecords");
|
||||
});
|
||||
|
||||
@@ -242,6 +279,16 @@ namespace FabWorks.Core.Migrations
|
||||
b.Navigation("BomItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.Drawing", "Drawing")
|
||||
.WithMany("ExportRecords")
|
||||
.HasForeignKey("DrawingId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Drawing");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
|
||||
{
|
||||
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
|
||||
@@ -260,6 +307,11 @@ namespace FabWorks.Core.Migrations
|
||||
b.Navigation("FormProgram");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
|
||||
{
|
||||
b.Navigation("ExportRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
|
||||
{
|
||||
b.Navigation("BomItems");
|
||||
|
||||
14
FabWorks.Core/Models/Drawing.cs
Normal file
14
FabWorks.Core/Models/Drawing.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FabWorks.Core.Models
|
||||
{
|
||||
public class Drawing
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string DrawingNumber { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string PdfContentHash { get; set; }
|
||||
|
||||
public virtual ICollection<ExportRecord> ExportRecords { get; set; } = new List<ExportRecord>();
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,10 @@ namespace FabWorks.Core.Models
|
||||
public string ExportedBy { get; set; }
|
||||
public string PdfContentHash { get; set; }
|
||||
|
||||
public int? DrawingId { get; set; }
|
||||
public int? DrawingRevision { get; set; }
|
||||
public virtual Drawing Drawing { get; set; }
|
||||
|
||||
public virtual ICollection<BomItem> BomItems { get; set; } = new List<BomItem>();
|
||||
}
|
||||
}
|
||||
|
||||
32
docs/plans/2026-02-17-autofill-from-export-history-design.md
Normal file
32
docs/plans/2026-02-17-autofill-from-export-history-design.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Auto-fill Equipment/Drawing from Export History
|
||||
|
||||
## Problem
|
||||
|
||||
When a SolidWorks file is opened that doesn't match the `DrawingInfo` regex (e.g., `Conveyor Frame.sldasm` instead of `5028 A02 Conveyor.slddrw`), the equipment and drawing number dropdowns are left empty even though the file may have been exported before with known values.
|
||||
|
||||
## Decision
|
||||
|
||||
- **Lookup key:** SolidWorks source file path (`ActiveDocument.FilePath`)
|
||||
- **Storage:** Query the existing `ExportRecords` table (no new table or migration)
|
||||
- **Priority:** Database lookup first; fall back to title regex parse if no history found
|
||||
|
||||
## Design
|
||||
|
||||
### Modify `UpdateActiveDocumentDisplay()` in `MainForm.cs`
|
||||
|
||||
When the active document changes:
|
||||
|
||||
1. Query the DB for the most recent `ExportRecord` where `SourceFilePath` matches `activeDoc.FilePath` (case-insensitive)
|
||||
2. If found, parse the stored `DrawingNumber` via `DrawingInfo.Parse()` and auto-fill equipment/drawing dropdowns
|
||||
3. If not found, fall back to current behavior: `DrawingInfo.Parse(activeDoc.Title)`
|
||||
|
||||
### What doesn't change
|
||||
|
||||
- No schema changes, no new migration
|
||||
- Equipment/drawing dropdowns still populated with historical values in `InitializeDrawingDropdowns()`
|
||||
- Export flow untouched
|
||||
|
||||
### Error handling
|
||||
|
||||
- DB query wrapped in try/catch so failures don't break the UI
|
||||
- Case-insensitive path comparison (Windows paths are case-insensitive)
|
||||
1191
docs/plans/2026-02-17-fabworks-api.md
Normal file
1191
docs/plans/2026-02-17-fabworks-api.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user